• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kubevirt / kubevirt / 71566407-af62-4e37-9e87-5d67388e004d

24 Jun 2026 07:30PM UTC coverage: 71.962% (-0.007%) from 71.969%
71566407-af62-4e37-9e87-5d67388e004d

push

prow

web-flow
Merge pull request #18032 from 0xFelix/oci-export-phase2

Add OCI export support for VirtualMachineTemplates

432 of 589 new or added lines in 10 files covered. (73.34%)

669 existing lines in 8 files now uncovered.

81649 of 113462 relevant lines covered (71.96%)

468.97 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

76.82
/pkg/virtctl/vmexport/vmexport.go
1
/*
2
 * This file is part of the KubeVirt project
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 *
16
 * Copyright The KubeVirt Authors.
17
 *
18
 */
19

20
package vmexport
21

22
import (
23
        "compress/gzip"
24
        "context"
25
        "crypto/tls"
26
        "crypto/x509"
27
        "fmt"
28
        "io"
29
        "log"
30
        "net/http"
31
        "net/url"
32
        "os"
33
        "strconv"
34
        "strings"
35
        "time"
36

37
        "github.com/cheggaaa/pb/v3"
38
        "github.com/spf13/cobra"
39
        k8sv1 "k8s.io/api/core/v1"
40
        k8serrors "k8s.io/apimachinery/pkg/api/errors"
41
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
42
        "k8s.io/apimachinery/pkg/labels"
43
        "k8s.io/apimachinery/pkg/runtime/schema"
44
        "k8s.io/apimachinery/pkg/util/httpstream"
45
        "k8s.io/client-go/tools/portforward"
46
        "k8s.io/client-go/transport/spdy"
47
        kubectlutil "k8s.io/kubectl/pkg/util"
48

49
        virtv1 "kubevirt.io/api/core/v1"
50
        exportv1 "kubevirt.io/api/export/v1"
51
        snapshotv1 "kubevirt.io/api/snapshot/v1beta1"
52
        "kubevirt.io/client-go/kubecli"
53
        templateapi "kubevirt.io/virt-template-api/core"
54

55
        virtwait "kubevirt.io/kubevirt/pkg/apimachinery/wait"
56
        "kubevirt.io/kubevirt/pkg/pointer"
57
        "kubevirt.io/kubevirt/pkg/util"
58
        "kubevirt.io/kubevirt/pkg/virtctl/clientconfig"
59
        "kubevirt.io/kubevirt/pkg/virtctl/templates"
60
)
61

62
const (
63
        // Available vmexport functions
64
        CREATE   = "create"
65
        DELETE   = "delete"
66
        DOWNLOAD = "download"
67

68
        // Available vmexport flags
69
        OUTPUT_FLAG            = "--output"
70
        VOLUME_FLAG            = "--volume"
71
        VM_FLAG                = "--vm"
72
        SNAPSHOT_FLAG          = "--snapshot"
73
        INSECURE_FLAG          = "--insecure"
74
        KEEP_FLAG              = "--keep-vme"
75
        DELETE_FLAG            = "--delete-vme"
76
        FORMAT_FLAG            = "--format"
77
        PVC_FLAG               = "--pvc"
78
        VMTEMPLATE_FLAG        = "--vmtemplate"
79
        TTL_FLAG               = "--ttl"
80
        MANIFEST_FLAG          = "--manifest"
81
        OUTPUT_FORMAT_FLAG     = "--manifest-output-format"
82
        SERVICE_URL_FLAG       = "--service-url"
83
        INCLUDE_SECRET_FLAG    = "--include-secret"
84
        PORT_FORWARD_FLAG      = "--port-forward"
85
        LOCAL_PORT_FLAG        = "--local-port"
86
        RETRY_FLAG             = "--retry"
87
        LABELS_FLAG            = "--labels"
88
        ANNOTATIONS_FLAG       = "--annotations"
89
        READINESS_TIMEOUT_FLAG = "--readiness-timeout"
90

91
        // Possible output format for manifests
92
        OUTPUT_FORMAT_JSON = "json"
93
        OUTPUT_FORMAT_YAML = "yaml"
94

95
        // Possible output format for volumes
96
        GZIP_FORMAT = "gzip"
97
        RAW_FORMAT  = "raw"
98
        OCI_FORMAT  = "oci"
99

100
        ACCEPT           = "Accept"
101
        APPLICATION_YAML = "application/yaml"
102
        APPLICATION_JSON = "application/json"
103

104
        // processingWaitInterval is the time interval used to wait for a virtualMachineExport to be ready
105
        processingWaitInterval = 2 * time.Second
106
        // DefaultProcessingWaitTotal is the default maximum time used to wait for a virtualMachineExport to be ready
107
        DefaultProcessingWaitTotal = 2 * time.Minute
108

109
        // exportTokenHeader is the http header used to download the exported volume using the secret token
110
        exportTokenHeader = "x-kubevirt-export-token"
111
        // secretTokenKey is the entry used to store the token in the virtualMachineExport secret
112
        secretTokenKey = "token"
113

114
        // ErrRequiredFlag serves as error message when a mandatory flag is missing
115
        ErrRequiredFlag = "need to specify the '%s' flag when using '%s'"
116
        // ErrIncompatibleFlag serves as error message when an incompatible flag is used
117
        ErrIncompatibleFlag = "the '%s' flag is incompatible with '%s'"
118
        // ErrRequiredExportType serves as error message when no export kind is provided
119
        ErrRequiredExportType = "need to specify export kind when attempting to create a VirtualMachineExport [--pvc|--vm|--snapshot|--vmtemplate]"
120
        // ErrIncompatibleExportType serves as error message when an export kind is provided with an incompatible argument
121
        ErrIncompatibleExportType = "should not specify export kind"
122
        // ErrIncompatibleExportTypeManifest serves as error message when a PVC kind is defined when getting manifest
123
        ErrIncompatibleExportTypeManifest = "cannot get manifest for PVC export"
124
        // ErrInvalidValue ensures that the value provided in a flag is one of the acceptable values
125
        ErrInvalidValue = "%s is not a valid value, acceptable values are %s"
126

127
        // progressBarCycle is a const used to store the cycle displayed in the progress bar when downloading the exported volume
128
        progressBarCycle = `"[___________________]" "[==>________________]" "[====>______________]" "[======>____________]" "[========>__________]" "[==========>________]" "[============>______]" "[==============>____]" "[================>__]" "[==================>]"`
129
)
130

131
var (
132
        // Flags
133
        vm                   string
134
        snapshot             string
135
        pvc                  string
136
        vmtemplate           string
137
        outputFile           string
138
        insecure             bool
139
        keepVme              bool
140
        deleteVme            bool
141
        shouldCreate         bool
142
        includeSecret        bool
143
        exportManifest       bool
144
        portForward          bool
145
        format               string
146
        localPort            string
147
        serviceUrl           string
148
        volumeName           string
149
        ttl                  string
150
        manifestOutputFormat string
151
        downloadRetries      int
152
        resourceLabels       []string
153
        resourceAnnotations  []string
154
        readinessTimeout     string
155
)
156

157
type VMExportInfo struct {
158
        ShouldCreate     bool
159
        Insecure         bool
160
        KeepVme          bool
161
        DeleteVme        bool
162
        IncludeSecret    bool
163
        ExportManifest   bool
164
        Decompress       bool
165
        PortForward      bool
166
        LocalPort        string
167
        OutputFile       string
168
        OutputWriter     io.Writer
169
        VolumeName       string
170
        Namespace        string
171
        Name             string
172
        OutputFormat     string
173
        ServiceURL       string
174
        ExportSource     k8sv1.TypedLocalObjectReference
175
        TTL              metav1.Duration
176
        DownloadRetries  int
177
        ReadinessTimeout time.Duration
178
        Labels           map[string]string
179
        Annotations      map[string]string
180
}
181

182
type command struct {
183
        cmd *cobra.Command
184
}
185

186
// WaitForVirtualMachineExportFn allows overriding the function to wait for the export object to be ready (useful for unit testing)
187
var WaitForVirtualMachineExportFn = WaitForVirtualMachineExport
188

189
// GetHTTPClientFn allows overriding the default http client (useful for unit testing)
190
var GetHTTPClientFn = GetHTTPClient
191

192
// HandleHTTPGetRequestFn allows overriding the default http GET request handler (useful for unit testing)
193
var HandleHTTPGetRequestFn = HandleHTTPGetRequest
194

195
// RunPortForwardFn allows overriding the default port-forwarder (useful for unit testing)
196
var RunPortForwardFn = RunPortForward
197

198
var exportFunction func(client kubecli.KubevirtClient, vmeInfo *VMExportInfo) error
199

200
// TODO Should use cmd.Printf and cmd.SetOut
201
var printToOutput = fmt.Printf
202

203
// usage provides several valid usage examples of vmexport
204
func usage() string {
968✔
205
        usage := `# Create a VirtualMachineExport to export a volume from a virtual machine:
968✔
206
        {{ProgramName}} vmexport create vm1-export --vm=vm1
968✔
207

968✔
208
        # Create a VirtualMachineExport to export a volume from a virtual machine snapshot
968✔
209
        {{ProgramName}} vmexport create snap1-export --snapshot=snap1
968✔
210

968✔
211
        # Create a VirtualMachineExport to export a volume from a PVC
968✔
212
        {{ProgramName}} vmexport create pvc1-export --pvc=pvc1
968✔
213

968✔
214
        # Delete a VirtualMachineExport resource
968✔
215
        {{ProgramName}} vmexport delete snap1-export
968✔
216

968✔
217
        # Download a volume from an already existing VirtualMachineExport (--volume is optional when only one volume is available)
968✔
218
        {{ProgramName}} vmexport download vm1-export --volume=volume1 --output=disk.img.gz
968✔
219

968✔
220
        # Download a volume as before but through local port 5410
968✔
221
        {{ProgramName}} vmexport download vm1-export --volume=volume1 --output=disk.img.gz --port-forward --local-port=5410
968✔
222

968✔
223
        # Create a VirtualMachineExport and download the requested volume from it
968✔
224
        {{ProgramName}} vmexport download vm1-export --vm=vm1 --volume=volume1 --output=disk.img.gz
968✔
225

968✔
226
        # Create a VirtualMachineExport and get the VirtualMachine manifest in Yaml format
968✔
227
        {{ProgramName}} vmexport download vm1-export --vm=vm1 --manifest
968✔
228

968✔
229
        # Get the VirtualMachine manifest in Yaml format from an existing VirtualMachineExport including CDI header secret
968✔
230
        {{ProgramName}} vmexport download existing-export --include-secret --manifest`
968✔
231
        return usage
968✔
232
}
968✔
233

234
// NewVirtualMachineExportCommand returns a cobra.Command to handle the export process
235
func NewVirtualMachineExportCommand() *cobra.Command {
968✔
236
        c := command{}
968✔
237
        cmd := &cobra.Command{
968✔
238
                Use:     "vmexport",
968✔
239
                Short:   "Export a VM volume.",
968✔
240
                Example: usage(),
968✔
241
                Args:    cobra.ExactArgs(2),
968✔
242
                RunE:    c.run,
968✔
243
        }
968✔
244

968✔
245
        shouldCreate = false
968✔
246

968✔
247
        cmd.Flags().StringVar(&vm, "vm", "", "Sets VirtualMachine as vmexport kind and specifies the vm name.")
968✔
248
        cmd.Flags().StringVar(&snapshot, "snapshot", "", "Sets VirtualMachineSnapshot as vmexport kind and specifies the snapshot name.")
968✔
249
        cmd.Flags().StringVar(&pvc, "pvc", "", "Sets PersistentVolumeClaim as vmexport kind and specifies the PVC name.")
968✔
250
        cmd.Flags().StringVar(&vmtemplate, "vmtemplate", "", "Sets VirtualMachineTemplate as vmexport kind and specifies the template name.")
968✔
251
        cmd.MarkFlagsMutuallyExclusive("vm", "snapshot", "pvc", "vmtemplate")
968✔
252
        cmd.Flags().StringVar(&outputFile, "output", "", "Specifies the output path of the volume to be downloaded.")
968✔
253
        cmd.Flags().StringVar(&volumeName, "volume", "", "Specifies the volume to be downloaded.")
968✔
254
        cmd.Flags().StringVar(&format, "format", "", "Used to specify the format of the downloaded image. There's two options: gzip (default) and raw.")
968✔
255
        cmd.Flags().BoolVar(&insecure, "insecure", false, "When used with the 'download' option, specifies that the http request should be insecure.")
968✔
256
        cmd.Flags().BoolVar(&keepVme, "keep-vme", false, "When used with the 'download' option, specifies that the vmexport object should always be retained after the download finishes.")
968✔
257
        cmd.Flags().BoolVar(&deleteVme, "delete-vme", false, "When used with the 'download' option, specifies that the vmexport object should always be deleted after the download finishes.")
968✔
258
        cmd.MarkFlagsMutuallyExclusive("keep-vme", "delete-vme")
968✔
259
        cmd.Flags().StringVar(&ttl, "ttl", "", "The time after the export was created that it is eligible to be automatically deleted, defaults to 2 hours by the server side if not specified")
968✔
260
        cmd.Flags().StringVar(&manifestOutputFormat, "manifest-output-format", "", "Manifest output format, defaults to Yaml. Valid options are yaml or json")
968✔
261
        cmd.Flags().StringVar(&serviceUrl, "service-url", "", "Specify service url to use in the returned manifest, instead of the external URL in the Virtual Machine export status. This is useful for NodePorts or if you don't have an external URL configured")
968✔
262
        cmd.Flags().BoolVar(&portForward, "port-forward", false, "Configures port-forwarding on a random port. Useful to download without proper ingress/route configuration")
968✔
263
        cmd.Flags().StringVar(&localPort, "local-port", "0", "Defines the specific port to be used in port-forward.")
968✔
264
        cmd.Flags().IntVar(&downloadRetries, "retry", 0, "When export server returns a transient error, we retry this number of times before giving up")
968✔
265
        cmd.Flags().BoolVar(&includeSecret, "include-secret", false, "When used with manifest and set to true include a secret that contains proper headers for CDI to import using the manifest")
968✔
266
        cmd.Flags().BoolVar(&exportManifest, "manifest", false, "Instead of downloading a volume, retrieve the VM manifest")
968✔
267
        cmd.Flags().StringSliceVar(&resourceLabels, "labels", nil, "Specify custom labels to VM export object and its associated pod")
968✔
268
        cmd.Flags().StringSliceVar(&resourceAnnotations, "annotations", nil, "Specify custom annotations to VM export object and its associated pod")
968✔
269
        cmd.Flags().StringVar(&readinessTimeout, "readiness-timeout", "", "Specify maximum wait for VM export object to be ready")
968✔
270
        cmd.SetUsageTemplate(templates.UsageTemplate())
968✔
271

968✔
272
        return cmd
968✔
273
}
968✔
274

275
// run serves as entrypoint for the vmexport command
276
func (c *command) run(cmd *cobra.Command, args []string) error {
61✔
277
        c.cmd = cmd
61✔
278

61✔
279
        var vmeInfo VMExportInfo
61✔
280
        if err := c.parseExportArguments(args, &vmeInfo); err != nil {
75✔
281
                return err
14✔
282
        }
14✔
283
        // If writing to a file, the OutputWriter will also be a Closer
284
        if closer, ok := vmeInfo.OutputWriter.(io.Closer); ok && vmeInfo.OutputFile != "" {
75✔
285
                defer util.CloseIOAndCheckErr(closer, nil)
28✔
286
        }
28✔
287

288
        virtClient, namespace, _, err := clientconfig.ClientAndNamespaceFromContext(cmd.Context())
47✔
289
        if err != nil {
47✔
290
                return err
×
291
        }
×
292
        vmeInfo.Namespace = namespace
47✔
293

47✔
294
        // Finally, run the vmexport function (create|delete|download)
47✔
295
        if err := exportFunction(virtClient, &vmeInfo); err != nil {
62✔
296
                return err
15✔
297
        }
15✔
298

299
        return nil
32✔
300
}
301

302
// parseExportArguments parses and validates vmexport arguments and flags. These arguments should always be:
303
//  1. The vmexport function (create|delete|download)
304
//  2. The VirtualMachineExport name
305
func (c *command) parseExportArguments(args []string, vmeInfo *VMExportInfo) error {
61✔
306
        funcName := strings.ToLower(args[0])
61✔
307

61✔
308
        // Assign the appropiate vmexport function and make sure the used flags are compatible
61✔
309
        switch funcName {
61✔
310
        case CREATE:
12✔
311
                exportFunction = CreateVirtualMachineExport
12✔
312
                if err := handleCreateFlags(); err != nil {
14✔
313
                        return err
2✔
314
                }
2✔
315
        case DELETE:
6✔
316
                exportFunction = DeleteVirtualMachineExport
6✔
317
                if err := handleDeleteFlags(); err != nil {
9✔
318
                        return err
3✔
319
                }
3✔
320
        case DOWNLOAD:
43✔
321
                exportFunction = DownloadVirtualMachineExport
43✔
322
                if err := handleDownloadFlags(); err != nil {
52✔
323
                        return err
9✔
324
                }
9✔
325
        default:
×
326
                return fmt.Errorf("invalid function '%s'", funcName)
×
327
        }
328

329
        // VirtualMachineExport name
330
        vmeInfo.Name = args[1]
47✔
331

47✔
332
        // We store the flags in a struct to avoid relying on global variables
47✔
333
        if err := c.initVMExportInfo(vmeInfo); err != nil {
47✔
334
                return err
×
335
        }
×
336

337
        return nil
47✔
338
}
339

340
func (c *command) initVMExportInfo(vmeInfo *VMExportInfo) error {
47✔
341
        vmeInfo.ExportSource = getExportSource()
47✔
342
        // User wants the output in a file, create
47✔
343
        if outputFile != "" && outputFile != "-" {
75✔
344
                vmeInfo.OutputFile = outputFile
28✔
345
                output, err := os.Create(vmeInfo.OutputFile)
28✔
346
                if err != nil {
28✔
347
                        return err
×
348
                }
×
349
                vmeInfo.OutputWriter = output
28✔
350
        } else {
19✔
351
                vmeInfo.OutputWriter = c.cmd.OutOrStdout()
19✔
352
                vmeInfo.OutputFile = ""
19✔
353
                // Setting standard printer to output into stderr. We can then output
19✔
354
                // the volume into stdout without any interfering prints.
19✔
355
                printToOutput = func(format string, a ...interface{}) (int, error) {
73✔
356
                        return fmt.Fprintf(os.Stderr, format, a...)
54✔
357
                }
54✔
358
        }
359
        // If raw format is specified, we'll attempt to download and decompress a gzipped volume
360
        if format == RAW_FORMAT {
49✔
361
                vmeInfo.Decompress = true
2✔
362
        }
2✔
363
        vmeInfo.DownloadRetries = downloadRetries
47✔
364
        vmeInfo.ShouldCreate = shouldCreate
47✔
365
        vmeInfo.Insecure = insecure
47✔
366
        vmeInfo.KeepVme = keepVme
47✔
367
        vmeInfo.DeleteVme = deleteVme
47✔
368
        vmeInfo.VolumeName = volumeName
47✔
369
        vmeInfo.ServiceURL = serviceUrl
47✔
370
        vmeInfo.OutputFormat = manifestOutputFormat
47✔
371
        vmeInfo.IncludeSecret = includeSecret
47✔
372
        vmeInfo.ExportManifest = exportManifest
47✔
373
        if portForward {
50✔
374
                vmeInfo.PortForward = portForward
3✔
375
                vmeInfo.Insecure = true
3✔
376
                // Defaults to 0, which will be replaced by a random available port
3✔
377
                vmeInfo.LocalPort = localPort
3✔
378
                if vmeInfo.ServiceURL == "" {
6✔
379
                        vmeInfo.ServiceURL = fmt.Sprintf("127.0.0.1:%s", vmeInfo.LocalPort)
3✔
380
                }
3✔
381
        }
382
        vmeInfo.TTL = metav1.Duration{}
47✔
383
        if ttl != "" {
48✔
384
                duration, err := time.ParseDuration(ttl)
1✔
385
                if err != nil {
1✔
386
                        return err
×
387
                }
×
388
                vmeInfo.TTL = metav1.Duration{Duration: duration}
1✔
389
        }
390
        vmeInfo.ReadinessTimeout = DefaultProcessingWaitTotal
47✔
391
        if readinessTimeout != "" {
48✔
392
                duration, err := time.ParseDuration(readinessTimeout)
1✔
393
                if err != nil {
1✔
394
                        return err
×
395
                }
×
396
                vmeInfo.ReadinessTimeout = duration
1✔
397
        }
398

399
        vmeInfo.Labels = convertSliceToMap(resourceLabels)
47✔
400
        vmeInfo.Annotations = convertSliceToMap(resourceAnnotations)
47✔
401

47✔
402
        return nil
47✔
403
}
404

405
// Convert a slice of "key=value" strings to a map
406
func convertSliceToMap(slice []string) map[string]string {
94✔
407
        mapResult := make(map[string]string)
94✔
408
        for _, item := range slice {
96✔
409
                parts := strings.SplitN(item, "=", 2)
2✔
410
                if len(parts) == 2 {
4✔
411
                        mapResult[parts[0]] = parts[1]
2✔
412
                }
2✔
413
        }
414
        return mapResult
94✔
415
}
416

417
// getVirtualMachineExport serves as a wrapper to get the VirtualMachineExport object
418
func getVirtualMachineExport(client kubecli.KubevirtClient, vmeInfo *VMExportInfo) (*exportv1.VirtualMachineExport, error) {
75✔
419
        vmexport, err := client.VirtualMachineExport(vmeInfo.Namespace).Get(context.TODO(), vmeInfo.Name, metav1.GetOptions{})
75✔
420
        if err != nil {
93✔
421
                if k8serrors.IsNotFound(err) {
36✔
422
                        return nil, nil
18✔
423
                }
18✔
424
                return nil, err
×
425
        }
426

427
        return vmexport, nil
57✔
428
}
429

430
// CreateVirtualMachineExport serves as a wrapper to create the virtualMachineExport object and, if needed, do error handling
431
func CreateVirtualMachineExport(client kubecli.KubevirtClient, vmeInfo *VMExportInfo) error {
29✔
432
        vmexport, err := getVirtualMachineExport(client, vmeInfo)
29✔
433
        if err != nil {
29✔
434
                return err
×
435
        }
×
436
        if vmexport != nil {
41✔
437
                return fmt.Errorf("VirtualMachineExport '%s/%s' already exists", vmeInfo.Namespace, vmeInfo.Name)
12✔
438
        }
12✔
439

440
        secretRef := getExportSecretName(vmeInfo.Name)
17✔
441
        vmexport = &exportv1.VirtualMachineExport{
17✔
442
                ObjectMeta: metav1.ObjectMeta{
17✔
443
                        Name:        vmeInfo.Name,
17✔
444
                        Namespace:   vmeInfo.Namespace,
17✔
445
                        Labels:      vmeInfo.Labels,
17✔
446
                        Annotations: vmeInfo.Annotations,
17✔
447
                },
17✔
448
                Spec: exportv1.VirtualMachineExportSpec{
17✔
449
                        TokenSecretRef: &secretRef,
17✔
450
                        Source:         vmeInfo.ExportSource,
17✔
451
                },
17✔
452
        }
17✔
453
        if vmeInfo.TTL.Duration > 0 {
18✔
454
                vmexport.Spec.TTLDuration = &vmeInfo.TTL
1✔
455
        }
1✔
456

457
        vmexport, err = client.VirtualMachineExport(vmeInfo.Namespace).Create(context.TODO(), vmexport, metav1.CreateOptions{})
17✔
458
        if err != nil {
17✔
459
                return err
×
460
        }
×
461

462
        // Generate/get secret to be used with the vmexport
463
        _, err = getOrCreateTokenSecret(client, vmexport)
17✔
464
        if err != nil {
17✔
465
                return err
×
466
        }
×
467

468
        printToOutput("VirtualMachineExport '%s/%s' created successfully\n", vmeInfo.Namespace, vmeInfo.Name)
17✔
469
        return nil
17✔
470
}
471

472
// DeleteVirtualMachineExport serves as a wrapper to delete the virtualMachineExport object
473
func DeleteVirtualMachineExport(client kubecli.KubevirtClient, vmeInfo *VMExportInfo) error {
12✔
474
        if err := client.VirtualMachineExport(vmeInfo.Namespace).Delete(context.TODO(), vmeInfo.Name, metav1.DeleteOptions{}); err != nil {
14✔
475
                if !k8serrors.IsNotFound(err) {
2✔
476
                        return err
×
477
                }
×
478
                printToOutput("VirtualMachineExport '%s/%s' does not exist\n", vmeInfo.Namespace, vmeInfo.Name)
2✔
479
                return nil
2✔
480
        }
481

482
        printToOutput("VirtualMachineExport '%s/%s' deleted successfully\n", vmeInfo.Namespace, vmeInfo.Name)
10✔
483
        return nil
10✔
484
}
485

486
// DownloadVirtualMachineExport handles the process of downloading the requested volume from a VirtualMachineExport object
487
func DownloadVirtualMachineExport(client kubecli.KubevirtClient, vmeInfo *VMExportInfo) error {
40✔
488
        for attempt := 0; attempt <= vmeInfo.DownloadRetries; attempt++ {
83✔
489
                succeeded, err := downloadVirtualMachineExport(client, vmeInfo)
43✔
490
                if err != nil {
55✔
491
                        return err
12✔
492
                }
12✔
493
                if succeeded {
57✔
494
                        return nil
26✔
495
                }
26✔
496
                if attempt < vmeInfo.DownloadRetries {
8✔
497
                        printToOutput("Retrying...\n")
3✔
498
                        time.Sleep(2 * time.Second)
3✔
499
                }
3✔
500
        }
501
        return fmt.Errorf("retry count reached, exiting unsuccessfully")
2✔
502
}
503

504
func downloadVirtualMachineExport(client kubecli.KubevirtClient, vmeInfo *VMExportInfo) (bool, error) {
43✔
505
        if vmeInfo.ShouldCreate {
62✔
506
                if err := CreateVirtualMachineExport(client, vmeInfo); err != nil {
30✔
507
                        if errExportAlreadyExists(err) {
22✔
508
                                // Don't delete VMExports that already exist unless specified explicitely
11✔
509
                                vmeInfo.KeepVme = true
11✔
510
                        } else {
11✔
511
                                return false, err
×
512
                        }
×
513
                }
514
        }
515

516
        if shouldDeleteVMExport(vmeInfo) {
52✔
517
                defer DeleteVirtualMachineExport(client, vmeInfo)
9✔
518
        }
9✔
519

520
        if vmeInfo.PortForward {
49✔
521
                stopChan, err := setupPortForward(client, vmeInfo)
6✔
522
                if err != nil {
8✔
523
                        return false, err
2✔
524
                }
2✔
525
                defer close(stopChan)
4✔
526
        }
527

528
        // Wait for the vmexport object to be ready
529
        if err := WaitForVirtualMachineExportFn(client, vmeInfo, processingWaitInterval, vmeInfo.ReadinessTimeout); err != nil {
43✔
530
                return false, err
2✔
531
        }
2✔
532

533
        vmexport, err := getVirtualMachineExport(client, vmeInfo)
39✔
534
        if err != nil {
39✔
535
                return false, err
×
536
        }
×
537
        if vmexport == nil {
40✔
538
                return false, fmt.Errorf("unable to get '%s/%s' VirtualMachineExport", vmeInfo.Namespace, vmeInfo.Name)
1✔
539
        }
1✔
540

541
        // Grab the VM Manifest and display it.
542
        if vmeInfo.ExportManifest {
44✔
543
                return getVirtualMachineManifest(client, vmexport, vmeInfo)
6✔
544
        }
6✔
545

546
        // Download the exported volume
547
        return downloadVolume(client, vmexport, vmeInfo)
32✔
548
}
549

550
func printRequestBody(client kubecli.KubevirtClient, vmexport *exportv1.VirtualMachineExport, vmeInfo *VMExportInfo, manifestUrl string, headers map[string]string) (bool, error) {
7✔
551
        resp, err := HandleHTTPGetRequestFn(client, vmexport, manifestUrl, vmeInfo.Insecure, vmeInfo.ServiceURL, headers)
7✔
552
        if err != nil {
7✔
553
                return false, err
×
554
        }
×
555
        defer resp.Body.Close()
7✔
556

7✔
557
        // Check server response
7✔
558
        if resp.StatusCode != http.StatusOK {
8✔
559
                printToOutput("Bad status: %s\n", resp.Status)
1✔
560
                return false, nil
1✔
561
        }
1✔
562
        bodyAll, err := io.ReadAll(resp.Body)
6✔
563
        if err != nil {
6✔
564
                return false, err
×
565
        }
×
566
        fmt.Fprintf(vmeInfo.OutputWriter, "%s", bodyAll)
6✔
567
        return true, nil
6✔
568
}
569

570
func getVirtualMachineManifest(client kubecli.KubevirtClient, vmexport *exportv1.VirtualMachineExport, vmeInfo *VMExportInfo) (bool, error) {
6✔
571
        manifestMap, err := GetManifestUrlsFromVirtualMachineExport(vmexport, vmeInfo)
6✔
572
        if err != nil {
6✔
573
                return false, err
×
574
        }
×
575
        headers := make(map[string]string)
6✔
576
        headers[ACCEPT] = APPLICATION_YAML
6✔
577
        if strings.ToLower(vmeInfo.OutputFormat) == OUTPUT_FORMAT_JSON {
7✔
578
                headers[ACCEPT] = APPLICATION_JSON
1✔
579
        }
1✔
580
        succeeded, err := printRequestBody(client, vmexport, vmeInfo, manifestMap[exportv1.AllManifests], headers)
6✔
581
        if err != nil || !succeeded {
7✔
582
                return false, err
1✔
583
        }
1✔
584
        if vmeInfo.IncludeSecret {
6✔
585
                return printRequestBody(client, vmexport, vmeInfo, manifestMap[exportv1.AuthHeader], headers)
1✔
586
        }
1✔
587
        return true, nil
4✔
588
}
589

590
// downloadVolume handles the process of downloading the requested volume from a VirtualMachineExport
591
func downloadVolume(client kubecli.KubevirtClient, vmexport *exportv1.VirtualMachineExport, vmeInfo *VMExportInfo) (bool, error) {
32✔
592
        var downloadUrl string
32✔
593
        var err error
32✔
594

32✔
595
        if format == OCI_FORMAT {
34✔
596
                switch vmexport.Spec.Source.Kind {
2✔
597
                case "VirtualMachine", "VirtualMachineSnapshot", "VirtualMachineTemplate":
2✔
NEW
598
                default:
×
NEW
599
                        return false, fmt.Errorf("OCI export is not supported for %q sources", vmexport.Spec.Source.Kind)
×
600
                }
601
                manifestUrls, err := GetManifestUrlsFromVirtualMachineExport(vmexport, vmeInfo)
2✔
602
                if err != nil {
2✔
603
                        return false, err
×
604
                }
×
605
                ociUrl, ok := manifestUrls[exportv1.OCI]
2✔
606
                if !ok {
3✔
607
                        return false, fmt.Errorf("OCI export format not available for '%s/%s'", vmexport.Namespace, vmexport.Name)
1✔
608
                }
1✔
609
                downloadUrl = ociUrl
1✔
610
                vmeInfo.Decompress = false
1✔
611
        } else {
30✔
612
                downloadUrl, err = GetUrlFromVirtualMachineExport(vmexport, vmeInfo)
30✔
613
                if err != nil {
35✔
614
                        return false, err
5✔
615
                }
5✔
616
        }
617

618
        resp, err := HandleHTTPGetRequestFn(client, vmexport, downloadUrl, vmeInfo.Insecure, vmeInfo.ServiceURL, nil)
26✔
619
        if err != nil {
27✔
620
                return false, err
1✔
621
        }
1✔
622
        defer resp.Body.Close()
25✔
623

25✔
624
        // Check server response
25✔
625
        if resp.StatusCode != http.StatusOK {
29✔
626
                printToOutput("Bad status: %s\n", resp.Status)
4✔
627
                return false, nil
4✔
628
        }
4✔
629

630
        // Lastly, copy the file to the expected output
631
        if err := copyFileWithProgressBar(vmeInfo.OutputWriter, resp, vmeInfo.Decompress); err != nil {
21✔
632
                return false, err
×
633
        }
×
634

635
        printToOutput("Download finished successfully\n")
21✔
636

21✔
637
        return true, nil
21✔
638
}
639

640
// shouldDeleteVMExport decides wether we should retain or delete a VMExport after a download. If delete/retain are not explicitly specified,
641
// the vmexport will be deleted when is created in the same instance as the download, retained otherwise.
642
func shouldDeleteVMExport(vmeInfo *VMExportInfo) bool {
43✔
643
        return !vmeInfo.ExportManifest && (vmeInfo.DeleteVme || (vmeInfo.ShouldCreate && !vmeInfo.KeepVme))
43✔
644
}
43✔
645

646
func replaceUrlWithServiceUrl(manifestUrl string, vmeInfo *VMExportInfo) (string, error) {
29✔
647
        // Replace internal URL with specified URL
29✔
648
        manUrl, err := url.Parse(manifestUrl)
29✔
649
        if err != nil {
29✔
650
                return "", err
×
651
        }
×
652
        if vmeInfo.ServiceURL != "" {
33✔
653
                manUrl.Host = vmeInfo.ServiceURL
4✔
654
        }
4✔
655
        return manUrl.String(), nil
29✔
656
}
657

658
// GetUrlFromVirtualMachineExport inspects the VirtualMachineExport status to fetch the extected URL
659
func GetUrlFromVirtualMachineExport(vmexport *exportv1.VirtualMachineExport, vmeInfo *VMExportInfo) (string, error) {
33✔
660
        var (
33✔
661
                downloadUrl string
33✔
662
                err         error
33✔
663
                format      exportv1.VirtualMachineExportVolumeFormat
33✔
664
                links       *exportv1.VirtualMachineExportLink
33✔
665
        )
33✔
666

33✔
667
        if vmeInfo.ServiceURL == "" && vmexport.Status.Links != nil && vmexport.Status.Links.External != nil {
62✔
668
                links = vmexport.Status.Links.External
29✔
669
        } else if vmexport.Status.Links != nil && vmexport.Status.Links.Internal != nil {
37✔
670
                links = vmexport.Status.Links.Internal
4✔
671
        }
4✔
672
        if links == nil || len(links.Volumes) <= 0 {
34✔
673
                return "", fmt.Errorf("unable to access the volume info from '%s/%s' VirtualMachineExport", vmexport.Namespace, vmexport.Name)
1✔
674
        }
1✔
675
        volumeNumber := len(links.Volumes)
32✔
676
        if volumeNumber > 1 && vmeInfo.VolumeName == "" {
33✔
677
                return "", fmt.Errorf("detected more than one downloadable volume in '%s/%s' VirtualMachineExport: Select the expected volume using the --volume flag", vmexport.Namespace, vmexport.Name)
1✔
678
        }
1✔
679
        for _, exportVolume := range links.Volumes {
63✔
680
                // Access the requested volume
32✔
681
                if volumeNumber == 1 || exportVolume.Name == vmeInfo.VolumeName {
62✔
682
                        for _, format = range exportVolume.Formats {
61✔
683
                                if format.Format == exportv1.KubeVirtGz || format.Format == exportv1.ArchiveGz || format.Format == exportv1.KubeVirtRaw {
60✔
684
                                        downloadUrl, err = replaceUrlWithServiceUrl(format.Url, vmeInfo)
29✔
685
                                        if err != nil {
29✔
686
                                                return "", err
×
687
                                        }
×
688
                                }
689
                                // By default, we always attempt to find and get the compressed file URL,
690
                                // so we only break the loop when one is found.
691
                                if format.Format == exportv1.KubeVirtGz || format.Format == exportv1.ArchiveGz {
46✔
692
                                        break
15✔
693
                                }
694
                        }
695
                }
696
        }
697

698
        // No need to decompress file if format is not gzip
699
        if format.Format == exportv1.KubeVirtRaw {
43✔
700
                vmeInfo.Decompress = false
12✔
701
        }
12✔
702

703
        if downloadUrl == "" {
35✔
704
                return "", fmt.Errorf("unable to get a valid URL from '%s/%s' VirtualMachineExport", vmexport.Namespace, vmexport.Name)
4✔
705
        }
4✔
706

707
        return downloadUrl, nil
27✔
708
}
709

710
// GetManifestUrlsFromVirtualMachineExport retrieves the manifest URLs from VirtualMachineExport status
711
func GetManifestUrlsFromVirtualMachineExport(vmexport *exportv1.VirtualMachineExport, vmeInfo *VMExportInfo) (map[exportv1.ExportManifestType]string, error) {
8✔
712
        res := make(map[exportv1.ExportManifestType]string, 0)
8✔
713
        if vmeInfo.ServiceURL == "" {
16✔
714
                if vmexport.Status.Links == nil || vmexport.Status.Links.External == nil || len(vmexport.Status.Links.External.Manifests) == 0 {
8✔
715
                        return nil, fmt.Errorf("unable to access the manifest info from '%s/%s' VirtualMachineExport", vmexport.Namespace, vmexport.Name)
×
716
                }
×
717

718
                for _, manifest := range vmexport.Status.Links.External.Manifests {
19✔
719
                        res[manifest.Type] = manifest.Url
11✔
720
                }
11✔
721
        } else {
×
722
                if vmexport.Status.Links == nil || vmexport.Status.Links.Internal == nil || len(vmexport.Status.Links.Internal.Manifests) == 0 {
×
723
                        return nil, fmt.Errorf("unable to access the manifest info from '%s/%s' VirtualMachineExport", vmexport.Namespace, vmexport.Name)
×
724
                }
×
725

726
                for _, manifest := range vmexport.Status.Links.Internal.Manifests {
×
727
                        // Replace internal URL with specified URL
×
728
                        manUrl, err := url.Parse(manifest.Url)
×
729
                        if err != nil {
×
730
                                return nil, err
×
731
                        }
×
732
                        manUrl.Host = vmeInfo.ServiceURL
×
733
                        res[manifest.Type] = manUrl.String()
×
734
                }
735
        }
736
        return res, nil
8✔
737
}
738

739
// WaitForVirtualMachineExport waits for the VirtualMachineExport status and external links to be ready
740
func WaitForVirtualMachineExport(client kubecli.KubevirtClient, vmeInfo *VMExportInfo, interval, timeout time.Duration) error {
1✔
741
        err := virtwait.PollImmediately(interval, timeout, func(_ context.Context) (bool, error) {
2✔
742
                vmexport, err := getVirtualMachineExport(client, vmeInfo)
1✔
743
                if err != nil {
1✔
744
                        return false, err
×
745
                }
×
746

747
                if vmexport == nil {
1✔
748
                        printToOutput("couldn't get VM Export %s, waiting for it to be created...\n", vmeInfo.Name)
×
749
                        return false, nil
×
750
                }
×
751

752
                if vmexport.Status == nil {
1✔
753
                        return false, nil
×
754
                }
×
755

756
                if vmexport.Status.Phase != exportv1.Ready {
2✔
757
                        printToOutput("waiting for VM Export %s status to be ready...\n", vmeInfo.Name)
1✔
758
                        return false, nil
1✔
759
                }
1✔
760

761
                if vmeInfo.ServiceURL == "" {
×
762
                        if vmexport.Status.Links == nil || vmexport.Status.Links.External == nil {
×
763
                                printToOutput("waiting for VM Export %s external links to be available...\n", vmeInfo.Name)
×
764
                                return false, nil
×
765
                        }
×
766
                } else {
×
767
                        if vmexport.Status.Links == nil || vmexport.Status.Links.Internal == nil {
×
768
                                printToOutput("waiting for VM Export %s internal links to be available...\n", vmeInfo.Name)
×
769
                                return false, nil
×
770
                        }
×
771
                }
772
                return true, nil
×
773
        })
774

775
        return err
1✔
776
}
777

778
// HandleHTTPGetRequest generates the GET request with proper certificate handling
779
func HandleHTTPGetRequest(client kubecli.KubevirtClient, vmexport *exportv1.VirtualMachineExport, downloadUrl string, insecure bool, exportURL string, headers map[string]string) (*http.Response, error) {
29✔
780
        token, err := getTokenFromSecret(client, vmexport)
29✔
781
        if err != nil {
30✔
782
                return nil, err
1✔
783
        }
1✔
784

785
        var cert string
28✔
786
        // Create new certPool and append our external SSL certificate
28✔
787
        if exportURL == "" {
56✔
788
                cert = vmexport.Status.Links.External.Cert
28✔
789
        } else {
28✔
790
                cert = vmexport.Status.Links.Internal.Cert
×
791
        }
×
792
        roots := x509.NewCertPool()
28✔
793
        roots.AppendCertsFromPEM([]byte(cert))
28✔
794
        transport := &http.Transport{
28✔
795
                TLSClientConfig: &tls.Config{RootCAs: roots},
28✔
796
        }
28✔
797
        httpClient := GetHTTPClientFn(transport, insecure)
28✔
798

28✔
799
        // Generate and do the request
28✔
800
        req, _ := http.NewRequest("GET", downloadUrl, nil)
28✔
801
        for k, v := range headers {
35✔
802
                req.Header.Set(k, v)
7✔
803
        }
7✔
804
        req.Header.Set(exportTokenHeader, token)
28✔
805

28✔
806
        return httpClient.Do(req)
28✔
807
}
808

809
// GetHTTPClient assigns the default, non-mocked HTTP client
810
func GetHTTPClient(transport *http.Transport, insecure bool) *http.Client {
×
811
        if insecure {
×
812
                transport = &http.Transport{
×
813
                        TLSClientConfig: &tls.Config{
×
814
                                InsecureSkipVerify: insecure,
×
815
                        },
×
816
                }
×
817
        }
×
818

819
        client := &http.Client{Transport: transport}
×
820
        return client
×
821
}
822

823
// copyFileWithProgressBar serves as a wrapper to copy the file with a progress bar
824
func copyFileWithProgressBar(output io.Writer, resp *http.Response, decompress bool) error {
21✔
825
        var rd io.Reader
21✔
826
        barTemplate := fmt.Sprintf(`{{ "Downloading file:" }} {{counters . }} {{ cycle . %s }} {{speed . }}`, progressBarCycle)
21✔
827

21✔
828
        // start bar based on our template
21✔
829
        bar := pb.ProgressBarTemplate(barTemplate).Start(0)
21✔
830
        defer bar.Finish()
21✔
831
        barRd := bar.NewProxyReader(resp.Body)
21✔
832
        rd = barRd
21✔
833
        bar.Start()
21✔
834

21✔
835
        if decompress {
23✔
836
                // Create a new gzip reader
2✔
837
                gzipReader, err := gzip.NewReader(barRd)
2✔
838
                if err != nil {
2✔
839
                        return err
×
840
                }
×
841
                defer gzipReader.Close()
2✔
842
                rd = gzipReader
2✔
843
                printToOutput("Decompressing image:\n")
2✔
844
        }
845

846
        _, err := io.Copy(output, rd)
21✔
847
        return err
21✔
848
}
849

850
// getOrCreateTokenSecret obtains a token secret to be used along with the virtualMachineExport
851
func getOrCreateTokenSecret(client kubecli.KubevirtClient, vmexport *exportv1.VirtualMachineExport) (*k8sv1.Secret, error) {
17✔
852
        // Securely randomize a 20 char string to be used as a token
17✔
853
        token, err := util.GenerateVMExportToken()
17✔
854
        if err != nil {
17✔
855
                return nil, err
×
856
        }
×
857

858
        ownerRef := metav1.NewControllerRef(vmexport, schema.GroupVersionKind{
17✔
859
                Group:   exportv1.SchemeGroupVersion.Group,
17✔
860
                Version: exportv1.SchemeGroupVersion.Version,
17✔
861
                Kind:    "VirtualMachineExport",
17✔
862
        })
17✔
863
        // This requires more RBAC on certain k8s distributions and isn't really needed
17✔
864
        ownerRef.BlockOwnerDeletion = pointer.P(false)
17✔
865
        secret := &k8sv1.Secret{
17✔
866
                ObjectMeta: metav1.ObjectMeta{
17✔
867
                        Name:      getExportSecretName(vmexport.Name),
17✔
868
                        Namespace: vmexport.Namespace,
17✔
869
                        OwnerReferences: []metav1.OwnerReference{
17✔
870
                                *ownerRef,
17✔
871
                        },
17✔
872
                },
17✔
873
                Type: k8sv1.SecretTypeOpaque,
17✔
874
                Data: map[string][]byte{
17✔
875
                        secretTokenKey: []byte(token),
17✔
876
                },
17✔
877
        }
17✔
878

17✔
879
        secret, err = client.CoreV1().Secrets(vmexport.Namespace).Create(context.Background(), secret, metav1.CreateOptions{})
17✔
880
        if err != nil && !k8serrors.IsAlreadyExists(err) {
17✔
881
                return nil, err
×
882
        }
×
883

884
        return secret, nil
17✔
885
}
886

887
// getTokenFromSecret extracts the token from the secret specified on the virtualMachineExport
888
func getTokenFromSecret(client kubecli.KubevirtClient, vmexport *exportv1.VirtualMachineExport) (string, error) {
29✔
889
        secretName := ""
29✔
890
        if vmexport.Status != nil && vmexport.Status.TokenSecretRef != nil {
58✔
891
                secretName = *vmexport.Status.TokenSecretRef
29✔
892
        }
29✔
893

894
        secret, err := client.CoreV1().Secrets(vmexport.Namespace).Get(context.Background(), secretName, metav1.GetOptions{})
29✔
895
        if err != nil {
30✔
896
                return "", err
1✔
897
        }
1✔
898

899
        token := secret.Data[secretTokenKey]
28✔
900
        return string(token), nil
28✔
901
}
902

903
// getExportSource creates and assembles the object that'll be used as a source in the virtualMachineExport
904
func getExportSource() k8sv1.TypedLocalObjectReference {
47✔
905
        var exportSource k8sv1.TypedLocalObjectReference
47✔
906
        if vm != "" {
55✔
907
                exportSource = k8sv1.TypedLocalObjectReference{
8✔
908
                        APIGroup: &virtv1.SchemeGroupVersion.Group,
8✔
909
                        Kind:     "VirtualMachine",
8✔
910
                        Name:     vm,
8✔
911
                }
8✔
912
        }
8✔
913
        if snapshot != "" {
49✔
914
                exportSource = k8sv1.TypedLocalObjectReference{
2✔
915
                        APIGroup: &snapshotv1.SchemeGroupVersion.Group,
2✔
916
                        Kind:     "VirtualMachineSnapshot",
2✔
917
                        Name:     snapshot,
2✔
918
                }
2✔
919
        }
2✔
920
        if pvc != "" {
59✔
921
                exportSource = k8sv1.TypedLocalObjectReference{
12✔
922
                        APIGroup: &k8sv1.SchemeGroupVersion.Group,
12✔
923
                        Kind:     "PersistentVolumeClaim",
12✔
924
                        Name:     pvc,
12✔
925
                }
12✔
926
        }
12✔
927
        if vmtemplate != "" {
48✔
928
                apiGroup := templateapi.GroupName
1✔
929
                exportSource = k8sv1.TypedLocalObjectReference{
1✔
930
                        APIGroup: &apiGroup,
1✔
931
                        Kind:     "VirtualMachineTemplate",
1✔
932
                        Name:     vmtemplate,
1✔
933
                }
1✔
934
        }
1✔
935

936
        return exportSource
47✔
937
}
938

939
// handleCreateFlags ensures that only compatible flag combinations are used with 'create'
940
func handleCreateFlags() error {
12✔
941
        if vm == "" && snapshot == "" && pvc == "" && vmtemplate == "" {
13✔
942
                return fmt.Errorf(ErrRequiredExportType)
1✔
943
        }
1✔
944

945
        if outputFile != "" {
11✔
946
                return fmt.Errorf(ErrIncompatibleFlag, OUTPUT_FLAG, CREATE)
×
947
        }
×
948
        if volumeName != "" {
11✔
949
                return fmt.Errorf(ErrIncompatibleFlag, VOLUME_FLAG, CREATE)
×
950
        }
×
951
        if insecure {
12✔
952
                return fmt.Errorf(ErrIncompatibleFlag, INSECURE_FLAG, CREATE)
1✔
953
        }
1✔
954
        if keepVme {
10✔
955
                return fmt.Errorf(ErrIncompatibleFlag, KEEP_FLAG, CREATE)
×
956
        }
×
957
        if deleteVme {
10✔
958
                return fmt.Errorf(ErrIncompatibleFlag, DELETE_FLAG, CREATE)
×
959
        }
×
960
        if portForward {
10✔
961
                return fmt.Errorf(ErrIncompatibleFlag, PORT_FORWARD_FLAG, CREATE)
×
962
        }
×
963
        if localPort != "0" {
10✔
964
                return fmt.Errorf(ErrIncompatibleFlag, LOCAL_PORT_FLAG, CREATE)
×
965
        }
×
966
        if format != "" {
10✔
967
                return fmt.Errorf(ErrIncompatibleFlag, FORMAT_FLAG, CREATE)
×
968
        }
×
969
        if serviceUrl != "" {
10✔
970
                return fmt.Errorf(ErrIncompatibleFlag, SERVICE_URL_FLAG, CREATE)
×
971
        }
×
972
        if downloadRetries != 0 {
10✔
973
                return fmt.Errorf(ErrIncompatibleFlag, RETRY_FLAG, CREATE)
×
974
        }
×
975

976
        return nil
10✔
977
}
978

979
// handleDeleteFlags ensures that only compatible flag combinations are used with 'delete'
980
func handleDeleteFlags() error {
6✔
981
        if vm != "" || snapshot != "" || pvc != "" || vmtemplate != "" {
8✔
982
                return fmt.Errorf(ErrIncompatibleExportType)
2✔
983
        }
2✔
984

985
        if outputFile != "" {
4✔
986
                return fmt.Errorf(ErrIncompatibleFlag, OUTPUT_FLAG, DELETE)
×
987
        }
×
988
        if volumeName != "" {
4✔
989
                return fmt.Errorf(ErrIncompatibleFlag, VOLUME_FLAG, DELETE)
×
990
        }
×
991
        if insecure {
5✔
992
                return fmt.Errorf(ErrIncompatibleFlag, INSECURE_FLAG, DELETE)
1✔
993
        }
1✔
994
        if keepVme {
3✔
995
                return fmt.Errorf(ErrIncompatibleFlag, KEEP_FLAG, DELETE)
×
996
        }
×
997
        if deleteVme {
3✔
998
                return fmt.Errorf(ErrIncompatibleFlag, DELETE_FLAG, DELETE)
×
999
        }
×
1000
        if portForward {
3✔
1001
                return fmt.Errorf(ErrIncompatibleFlag, PORT_FORWARD_FLAG, DELETE)
×
1002
        }
×
1003
        if localPort != "0" {
3✔
1004
                return fmt.Errorf(ErrIncompatibleFlag, LOCAL_PORT_FLAG, DELETE)
×
1005
        }
×
1006
        if format != "" {
3✔
1007
                return fmt.Errorf(ErrIncompatibleFlag, FORMAT_FLAG, DELETE)
×
1008
        }
×
1009
        if serviceUrl != "" {
3✔
1010
                return fmt.Errorf(ErrIncompatibleFlag, SERVICE_URL_FLAG, DELETE)
×
1011
        }
×
1012
        if downloadRetries != 0 {
3✔
1013
                return fmt.Errorf(ErrIncompatibleFlag, RETRY_FLAG, DELETE)
×
1014
        }
×
1015
        if readinessTimeout != "" {
3✔
1016
                return fmt.Errorf(ErrIncompatibleFlag, READINESS_TIMEOUT_FLAG, DELETE)
×
1017
        }
×
1018
        if len(resourceLabels) > 0 {
3✔
1019
                return fmt.Errorf(ErrIncompatibleFlag, LABELS_FLAG, DELETE)
×
1020
        }
×
1021
        if len(resourceAnnotations) > 0 {
3✔
1022
                return fmt.Errorf(ErrIncompatibleFlag, ANNOTATIONS_FLAG, DELETE)
×
1023
        }
×
1024

1025
        return nil
3✔
1026
}
1027

1028
// handleDownloadFlags ensures that only compatible flag combinations are used with 'download'
1029
func handleDownloadFlags() error {
43✔
1030
        // We assume that the vmexport should be created if a source has been specified
43✔
1031
        if hasSource := vm != "" || snapshot != "" || pvc != "" || vmtemplate != ""; hasSource {
59✔
1032
                shouldCreate = true
16✔
1033
        }
16✔
1034

1035
        if portForward {
47✔
1036
                port, err := strconv.Atoi(localPort)
4✔
1037
                if err != nil || port < 0 || port > 65535 {
5✔
1038
                        return fmt.Errorf(ErrInvalidValue, LOCAL_PORT_FLAG, "valid port numbers")
1✔
1039
                }
1✔
1040
        }
1041

1042
        if format != "" && format != GZIP_FORMAT && format != RAW_FORMAT && format != OCI_FORMAT {
43✔
1043
                return fmt.Errorf(ErrInvalidValue, FORMAT_FLAG, "gzip/raw/oci")
1✔
1044
        }
1✔
1045

1046
        if format == OCI_FORMAT {
46✔
1047
                if volumeName != "" {
6✔
1048
                        return fmt.Errorf(ErrIncompatibleFlag, VOLUME_FLAG, FORMAT_FLAG+"=oci")
1✔
1049
                }
1✔
1050
                if exportManifest {
5✔
1051
                        return fmt.Errorf(ErrIncompatibleFlag, MANIFEST_FLAG, FORMAT_FLAG+"=oci")
1✔
1052
                }
1✔
1053
                if pvc != "" {
4✔
1054
                        return fmt.Errorf(ErrIncompatibleFlag, PVC_FLAG, FORMAT_FLAG+"=oci")
1✔
1055
                }
1✔
1056
        }
1057

1058
        if downloadRetries < 0 {
38✔
1059
                return fmt.Errorf(ErrInvalidValue, RETRY_FLAG, "positive integers")
×
1060
        }
×
1061

1062
        if exportManifest {
47✔
1063
                if volumeName != "" {
10✔
1064
                        return fmt.Errorf(ErrIncompatibleFlag, VOLUME_FLAG, MANIFEST_FLAG)
1✔
1065
                }
1✔
1066

1067
                manifestOutputFormat = strings.ToLower(manifestOutputFormat)
8✔
1068
                if manifestOutputFormat != OUTPUT_FORMAT_JSON && manifestOutputFormat != OUTPUT_FORMAT_YAML && manifestOutputFormat != "" {
9✔
1069
                        return fmt.Errorf(ErrInvalidValue, OUTPUT_FORMAT_FLAG, "json/yaml")
1✔
1070
                }
1✔
1071

1072
                if pvc != "" {
8✔
1073
                        return fmt.Errorf(ErrIncompatibleFlag, PVC_FLAG, MANIFEST_FLAG)
1✔
1074
                }
1✔
1075
        }
1076
        if !exportManifest && outputFile == "" {
36✔
1077
                return fmt.Errorf("warning: Binary output can mess up your terminal. Use '%s -' to output into stdout anyway or consider '%s <FILE>' to save to a file", OUTPUT_FLAG, OUTPUT_FLAG)
1✔
1078
        }
1✔
1079

1080
        return nil
34✔
1081
}
1082

1083
// getExportSecretName builds the name of the token secret based on the virtualMachineExport object
1084
func getExportSecretName(vmexportName string) string {
34✔
1085
        return fmt.Sprintf("secret-%s", vmexportName)
34✔
1086
}
34✔
1087

1088
// errExportAlreadyExists is used to the determine if an error happened when creating an already existing vmexport
1089
func errExportAlreadyExists(err error) bool {
11✔
1090
        return strings.Contains(err.Error(), "VirtualMachineExport") && strings.Contains(err.Error(), "already exists")
11✔
1091
}
11✔
1092

1093
// Port-forward functions
1094

1095
// translateServicePortToTargetPort tranlates the specified port to be used with the service's pod
1096
func translateServicePortToTargetPort(localPort string, remotePort string, svc k8sv1.Service, pod k8sv1.Pod) ([]string, error) {
5✔
1097
        ports := []string{}
5✔
1098
        portnum, err := strconv.Atoi(remotePort)
5✔
1099
        if err != nil {
5✔
1100
                return ports, err
×
1101
        }
×
1102
        containerPort, err := kubectlutil.LookupContainerPortNumberByServicePort(svc, pod, int32(portnum))
5✔
1103
        if err != nil {
6✔
1104
                // can't resolve a named port, or Service did not declare this port, return an error
1✔
1105
                return ports, err
1✔
1106
        }
1✔
1107

1108
        // convert the resolved target port back to a string
1109
        remotePort = strconv.Itoa(int(containerPort))
4✔
1110
        if localPort != remotePort {
8✔
1111
                return append(ports, fmt.Sprintf("%s:%s", localPort, remotePort)), nil
4✔
1112
        }
4✔
1113

1114
        return append(ports, remotePort), nil
×
1115
}
1116

1117
// waitForExportServiceToBeReady waits until the vmexport service is ready for port-forwarding
1118
func waitForExportServiceToBeReady(client kubecli.KubevirtClient, vmeInfo *VMExportInfo, interval, timeout time.Duration) (*k8sv1.Service, error) {
6✔
1119
        service := &k8sv1.Service{}
6✔
1120
        err := virtwait.PollImmediately(interval, timeout, func(ctx context.Context) (bool, error) {
12✔
1121
                vmexport, err := getVirtualMachineExport(client, vmeInfo)
6✔
1122
                if err != nil || vmexport == nil {
6✔
1123
                        return false, err
×
1124
                }
×
1125

1126
                if vmexport.Status == nil || vmexport.Status.Phase != exportv1.Ready {
6✔
1127
                        printToOutput("waiting for VM Export %s status to be ready...\n", vmeInfo.Name)
×
1128
                        return false, nil
×
1129
                }
×
1130

1131
                serviceName := vmexport.Status.ServiceName
6✔
1132
                if serviceName == "" {
6✔
1133
                        printToOutput("waiting for VM Export %s service name to be set...\n", vmeInfo.Name)
×
1134
                        return false, nil
×
1135
                }
×
1136

1137
                service, err = client.CoreV1().Services(vmeInfo.Namespace).Get(ctx, serviceName, metav1.GetOptions{})
6✔
1138
                if err != nil {
6✔
1139
                        if k8serrors.IsNotFound(err) {
×
1140
                                printToOutput("waiting for service %s to be ready before port-forwarding...\n", serviceName)
×
1141
                                return false, nil
×
1142
                        }
×
1143
                        return false, err
×
1144
                }
1145
                printToOutput("service %s is ready for port-forwarding\n", service.Name)
6✔
1146
                return true, nil
6✔
1147
        })
1148
        return service, err
6✔
1149
}
1150

1151
// setupPortForward runs a port-forward after initializing all required arguments
1152
func setupPortForward(client kubecli.KubevirtClient, vmeInfo *VMExportInfo) (chan struct{}, error) {
6✔
1153
        // Wait for the vmexport object to be ready
6✔
1154
        service, err := waitForExportServiceToBeReady(client, vmeInfo, processingWaitInterval, vmeInfo.ReadinessTimeout)
6✔
1155
        if err != nil {
6✔
1156
                return nil, err
×
1157
        }
×
1158

1159
        // Extract the target pod selector from the service
1160
        podSelector := labels.SelectorFromSet(service.Spec.Selector)
6✔
1161

6✔
1162
        // List the pods matching the selector
6✔
1163
        podList, err := client.CoreV1().Pods(vmeInfo.Namespace).List(context.Background(), metav1.ListOptions{LabelSelector: podSelector.String()})
6✔
1164
        if err != nil {
6✔
1165
                return nil, fmt.Errorf("failed to list pods: %v", err)
×
1166
        }
×
1167

1168
        // Pick the first pod to forward the port
1169
        if len(podList.Items) == 0 {
7✔
1170
                return nil, fmt.Errorf("no pods found for the service %s", service.Name)
1✔
1171
        }
1✔
1172

1173
        // Set up the port forwarding ports
1174
        ports, err := translateServicePortToTargetPort(vmeInfo.LocalPort, "443", *service, podList.Items[0])
5✔
1175
        if err != nil {
6✔
1176
                return nil, err
1✔
1177
        }
1✔
1178

1179
        stopChan := make(chan struct{}, 1)
4✔
1180
        readyChan := make(chan struct{})
4✔
1181
        portChan := make(chan uint16)
4✔
1182
        go RunPortForwardFn(client, podList.Items[0], vmeInfo.Namespace, ports, stopChan, readyChan, portChan)
4✔
1183

4✔
1184
        // Wait for the port forwarding to be ready
4✔
1185
        select {
4✔
1186
        case <-readyChan:
4✔
1187
                printToOutput("Port forwarding is ready.\n")
4✔
1188
                // Using 0 allows listening on a random available port.
4✔
1189
                // Now we need to find out which port was used
4✔
1190
                if vmeInfo.LocalPort == "0" {
7✔
1191
                        localPort := <-portChan
3✔
1192
                        close(portChan)
3✔
1193
                        vmeInfo.ServiceURL = fmt.Sprintf("127.0.0.1:%d", localPort)
3✔
1194
                }
3✔
1195
        case <-time.After(30 * time.Second):
×
1196
                return nil, fmt.Errorf("timeout waiting for port forwarding to be ready")
×
1197
        }
1198
        return stopChan, nil
4✔
1199
}
1200

1201
// RunPortForward is the actual function that runs the port-forward. Meant to be run concurrently
1202
func RunPortForward(client kubecli.KubevirtClient, pod k8sv1.Pod, namespace string, ports []string, stopChan, readyChan chan struct{}, portChan chan uint16) error {
×
1203
        // Create a port forwarding request
×
1204
        req := client.CoreV1().RESTClient().Post().
×
1205
                Resource("pods").
×
1206
                Name(pod.Name).
×
1207
                Namespace(namespace).
×
1208
                SubResource("portforward")
×
1209

×
1210
        // Set up the port forwarding options
×
1211
        spdyTransport, upgrader, err := spdy.RoundTripperFor(client.Config())
×
1212
        if err != nil {
×
1213
                log.Fatalf("Failed to set up transport: %v", err)
×
1214
        }
×
1215
        spdyDialer := spdy.NewDialer(upgrader, &http.Client{Transport: spdyTransport}, "POST", req.URL())
×
1216
        wsDialer, err := portforward.NewSPDYOverWebsocketDialer(req.URL(), client.Config())
×
1217
        if err != nil {
×
1218
                log.Fatalf("Failed to set up websocket transport: %v", err)
×
1219
        }
×
1220
        dialer := portforward.NewFallbackDialer(wsDialer, spdyDialer, httpstream.IsUpgradeFailure)
×
1221

×
1222
        // Start port-forwarding
×
1223
        fw, err := portforward.New(dialer, ports, stopChan, readyChan, os.Stderr, os.Stderr)
×
1224
        if err != nil {
×
1225
                log.Fatalf("Failed to setup port forward: %v", err)
×
1226
        }
×
1227
        slicedPorts := strings.Split(ports[0], ":")
×
1228
        if len(slicedPorts) == 2 && slicedPorts[0] == "0" {
×
1229
                // If the local port is 0, then the port-forwarder will pick a random available port.
×
1230
                // We need to send this port number back to the caller.
×
1231
                go func() {
×
1232
                        <-readyChan
×
1233
                        forwardedPorts, err := fw.GetPorts()
×
1234
                        if err != nil {
×
1235
                                log.Fatalf("Failed to get forwarded ports: %v", err)
×
1236
                        }
×
1237
                        portChan <- forwardedPorts[0].Local
×
1238
                }()
1239
        }
1240
        return fw.ForwardPorts()
×
1241
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc