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

kubevirt / kubevirt / 37199505-f7fc-48df-8efe-d8ccfaf333fc

05 Jun 2026 03:58AM UTC coverage: 71.676% (+0.03%) from 71.651%
37199505-f7fc-48df-8efe-d8ccfaf333fc

push

prow

web-flow
Merge pull request #17944 from 0xFelix/oci-export-phase1

Add OCI artifact export for VirtualMachines

336 of 444 new or added lines in 9 files covered. (75.68%)

2 existing lines in 2 files now uncovered.

78948 of 110145 relevant lines covered (71.68%)

589.85 hits per line

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

77.04
/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/client-go/tools/portforward"
45
        "k8s.io/client-go/transport/spdy"
46
        kubectlutil "k8s.io/kubectl/pkg/util"
47

48
        virtv1 "kubevirt.io/api/core/v1"
49
        exportv1 "kubevirt.io/api/export/v1"
50
        snapshotv1 "kubevirt.io/api/snapshot/v1beta1"
51
        "kubevirt.io/client-go/kubecli"
52

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

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

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

88
        // Possible output format for manifests
89
        OUTPUT_FORMAT_JSON = "json"
90
        OUTPUT_FORMAT_YAML = "yaml"
91

92
        // Possible output format for volumes
93
        GZIP_FORMAT = "gzip"
94
        RAW_FORMAT  = "raw"
95
        OCI_FORMAT  = "oci"
96

97
        ACCEPT           = "Accept"
98
        APPLICATION_YAML = "application/yaml"
99
        APPLICATION_JSON = "application/json"
100

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

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

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

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

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

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

178
type command struct {
179
        cmd *cobra.Command
180
}
181

182
// WaitForVirtualMachineExportFn allows overriding the function to wait for the export object to be ready (useful for unit testing)
183
var WaitForVirtualMachineExportFn = WaitForVirtualMachineExport
184

185
// GetHTTPClientFn allows overriding the default http client (useful for unit testing)
186
var GetHTTPClientFn = GetHTTPClient
187

188
// HandleHTTPGetRequestFn allows overriding the default http GET request handler (useful for unit testing)
189
var HandleHTTPGetRequestFn = HandleHTTPGetRequest
190

191
// RunPortForwardFn allows overriding the default port-forwarder (useful for unit testing)
192
var RunPortForwardFn = RunPortForward
193

194
var exportFunction func(client kubecli.KubevirtClient, vmeInfo *VMExportInfo) error
195

196
// TODO Should use cmd.Printf and cmd.SetOut
197
var printToOutput = fmt.Printf
198

199
// usage provides several valid usage examples of vmexport
200
func usage() string {
963✔
201
        usage := `# Create a VirtualMachineExport to export a volume from a virtual machine:
963✔
202
        {{ProgramName}} vmexport create vm1-export --vm=vm1
963✔
203

963✔
204
        # Create a VirtualMachineExport to export a volume from a virtual machine snapshot
963✔
205
        {{ProgramName}} vmexport create snap1-export --snapshot=snap1
963✔
206

963✔
207
        # Create a VirtualMachineExport to export a volume from a PVC
963✔
208
        {{ProgramName}} vmexport create pvc1-export --pvc=pvc1
963✔
209

963✔
210
        # Delete a VirtualMachineExport resource
963✔
211
        {{ProgramName}} vmexport delete snap1-export
963✔
212

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

963✔
216
        # Download a volume as before but through local port 5410
963✔
217
        {{ProgramName}} vmexport download vm1-export --volume=volume1 --output=disk.img.gz --port-forward --local-port=5410
963✔
218

963✔
219
        # Create a VirtualMachineExport and download the requested volume from it
963✔
220
        {{ProgramName}} vmexport download vm1-export --vm=vm1 --volume=volume1 --output=disk.img.gz
963✔
221

963✔
222
        # Create a VirtualMachineExport and get the VirtualMachine manifest in Yaml format
963✔
223
        {{ProgramName}} vmexport download vm1-export --vm=vm1 --manifest
963✔
224

963✔
225
        # Get the VirtualMachine manifest in Yaml format from an existing VirtualMachineExport including CDI header secret
963✔
226
        {{ProgramName}} vmexport download existing-export --include-secret --manifest`
963✔
227
        return usage
963✔
228
}
963✔
229

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

963✔
241
        shouldCreate = false
963✔
242

963✔
243
        cmd.Flags().StringVar(&vm, "vm", "", "Sets VirtualMachine as vmexport kind and specifies the vm name.")
963✔
244
        cmd.Flags().StringVar(&snapshot, "snapshot", "", "Sets VirtualMachineSnapshot as vmexport kind and specifies the snapshot name.")
963✔
245
        cmd.Flags().StringVar(&pvc, "pvc", "", "Sets PersistentVolumeClaim as vmexport kind and specifies the PVC name.")
963✔
246
        cmd.MarkFlagsMutuallyExclusive("vm", "snapshot", "pvc")
963✔
247
        cmd.Flags().StringVar(&outputFile, "output", "", "Specifies the output path of the volume to be downloaded.")
963✔
248
        cmd.Flags().StringVar(&volumeName, "volume", "", "Specifies the volume to be downloaded.")
963✔
249
        cmd.Flags().StringVar(&format, "format", "", "Used to specify the format of the downloaded image. There's two options: gzip (default) and raw.")
963✔
250
        cmd.Flags().BoolVar(&insecure, "insecure", false, "When used with the 'download' option, specifies that the http request should be insecure.")
963✔
251
        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.")
963✔
252
        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.")
963✔
253
        cmd.MarkFlagsMutuallyExclusive("keep-vme", "delete-vme")
963✔
254
        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")
963✔
255
        cmd.Flags().StringVar(&manifestOutputFormat, "manifest-output-format", "", "Manifest output format, defaults to Yaml. Valid options are yaml or json")
963✔
256
        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")
963✔
257
        cmd.Flags().BoolVar(&portForward, "port-forward", false, "Configures port-forwarding on a random port. Useful to download without proper ingress/route configuration")
963✔
258
        cmd.Flags().StringVar(&localPort, "local-port", "0", "Defines the specific port to be used in port-forward.")
963✔
259
        cmd.Flags().IntVar(&downloadRetries, "retry", 0, "When export server returns a transient error, we retry this number of times before giving up")
963✔
260
        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")
963✔
261
        cmd.Flags().BoolVar(&exportManifest, "manifest", false, "Instead of downloading a volume, retrieve the VM manifest")
963✔
262
        cmd.Flags().StringSliceVar(&resourceLabels, "labels", nil, "Specify custom labels to VM export object and its associated pod")
963✔
263
        cmd.Flags().StringSliceVar(&resourceAnnotations, "annotations", nil, "Specify custom annotations to VM export object and its associated pod")
963✔
264
        cmd.Flags().StringVar(&readinessTimeout, "readiness-timeout", "", "Specify maximum wait for VM export object to be ready")
963✔
265
        cmd.SetUsageTemplate(templates.UsageTemplate())
963✔
266

963✔
267
        return cmd
963✔
268
}
963✔
269

270
// run serves as entrypoint for the vmexport command
271
func (c *command) run(cmd *cobra.Command, args []string) error {
59✔
272
        c.cmd = cmd
59✔
273

59✔
274
        var vmeInfo VMExportInfo
59✔
275
        if err := c.parseExportArguments(args, &vmeInfo); err != nil {
72✔
276
                return err
13✔
277
        }
13✔
278
        // If writing to a file, the OutputWriter will also be a Closer
279
        if closer, ok := vmeInfo.OutputWriter.(io.Closer); ok && vmeInfo.OutputFile != "" {
74✔
280
                defer util.CloseIOAndCheckErr(closer, nil)
28✔
281
        }
28✔
282

283
        virtClient, namespace, _, err := clientconfig.ClientAndNamespaceFromContext(cmd.Context())
46✔
284
        if err != nil {
46✔
285
                return err
×
286
        }
×
287
        vmeInfo.Namespace = namespace
46✔
288

46✔
289
        // Finally, run the vmexport function (create|delete|download)
46✔
290
        if err := exportFunction(virtClient, &vmeInfo); err != nil {
61✔
291
                return err
15✔
292
        }
15✔
293

294
        return nil
31✔
295
}
296

297
// parseExportArguments parses and validates vmexport arguments and flags. These arguments should always be:
298
//  1. The vmexport function (create|delete|download)
299
//  2. The VirtualMachineExport name
300
func (c *command) parseExportArguments(args []string, vmeInfo *VMExportInfo) error {
59✔
301
        funcName := strings.ToLower(args[0])
59✔
302

59✔
303
        // Assign the appropiate vmexport function and make sure the used flags are compatible
59✔
304
        switch funcName {
59✔
305
        case CREATE:
11✔
306
                exportFunction = CreateVirtualMachineExport
11✔
307
                if err := handleCreateFlags(); err != nil {
13✔
308
                        return err
2✔
309
                }
2✔
310
        case DELETE:
5✔
311
                exportFunction = DeleteVirtualMachineExport
5✔
312
                if err := handleDeleteFlags(); err != nil {
7✔
313
                        return err
2✔
314
                }
2✔
315
        case DOWNLOAD:
43✔
316
                exportFunction = DownloadVirtualMachineExport
43✔
317
                if err := handleDownloadFlags(); err != nil {
52✔
318
                        return err
9✔
319
                }
9✔
320
        default:
×
321
                return fmt.Errorf("invalid function '%s'", funcName)
×
322
        }
323

324
        // VirtualMachineExport name
325
        vmeInfo.Name = args[1]
46✔
326

46✔
327
        // We store the flags in a struct to avoid relying on global variables
46✔
328
        if err := c.initVMExportInfo(vmeInfo); err != nil {
46✔
329
                return err
×
330
        }
×
331

332
        return nil
46✔
333
}
334

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

394
        vmeInfo.Labels = convertSliceToMap(resourceLabels)
46✔
395
        vmeInfo.Annotations = convertSliceToMap(resourceAnnotations)
46✔
396

46✔
397
        return nil
46✔
398
}
399

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

412
// getVirtualMachineExport serves as a wrapper to get the VirtualMachineExport object
413
func getVirtualMachineExport(client kubecli.KubevirtClient, vmeInfo *VMExportInfo) (*exportv1.VirtualMachineExport, error) {
74✔
414
        vmexport, err := client.VirtualMachineExport(vmeInfo.Namespace).Get(context.TODO(), vmeInfo.Name, metav1.GetOptions{})
74✔
415
        if err != nil {
91✔
416
                if k8serrors.IsNotFound(err) {
34✔
417
                        return nil, nil
17✔
418
                }
17✔
419
                return nil, err
×
420
        }
421

422
        return vmexport, nil
57✔
423
}
424

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

435
        secretRef := getExportSecretName(vmeInfo.Name)
16✔
436
        vmexport = &exportv1.VirtualMachineExport{
16✔
437
                ObjectMeta: metav1.ObjectMeta{
16✔
438
                        Name:        vmeInfo.Name,
16✔
439
                        Namespace:   vmeInfo.Namespace,
16✔
440
                        Labels:      vmeInfo.Labels,
16✔
441
                        Annotations: vmeInfo.Annotations,
16✔
442
                },
16✔
443
                Spec: exportv1.VirtualMachineExportSpec{
16✔
444
                        TokenSecretRef: &secretRef,
16✔
445
                        Source:         vmeInfo.ExportSource,
16✔
446
                },
16✔
447
        }
16✔
448
        if vmeInfo.TTL.Duration > 0 {
17✔
449
                vmexport.Spec.TTLDuration = &vmeInfo.TTL
1✔
450
        }
1✔
451

452
        vmexport, err = client.VirtualMachineExport(vmeInfo.Namespace).Create(context.TODO(), vmexport, metav1.CreateOptions{})
16✔
453
        if err != nil {
16✔
454
                return err
×
455
        }
×
456

457
        // Generate/get secret to be used with the vmexport
458
        _, err = getOrCreateTokenSecret(client, vmexport)
16✔
459
        if err != nil {
16✔
460
                return err
×
461
        }
×
462

463
        printToOutput("VirtualMachineExport '%s/%s' created successfully\n", vmeInfo.Namespace, vmeInfo.Name)
16✔
464
        return nil
16✔
465
}
466

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

477
        printToOutput("VirtualMachineExport '%s/%s' deleted successfully\n", vmeInfo.Namespace, vmeInfo.Name)
10✔
478
        return nil
10✔
479
}
480

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

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

511
        if shouldDeleteVMExport(vmeInfo) {
52✔
512
                defer DeleteVirtualMachineExport(client, vmeInfo)
9✔
513
        }
9✔
514

515
        if vmeInfo.PortForward {
49✔
516
                stopChan, err := setupPortForward(client, vmeInfo)
6✔
517
                if err != nil {
8✔
518
                        return false, err
2✔
519
                }
2✔
520
                defer close(stopChan)
4✔
521
        }
522

523
        // Wait for the vmexport object to be ready
524
        if err := WaitForVirtualMachineExportFn(client, vmeInfo, processingWaitInterval, vmeInfo.ReadinessTimeout); err != nil {
43✔
525
                return false, err
2✔
526
        }
2✔
527

528
        vmexport, err := getVirtualMachineExport(client, vmeInfo)
39✔
529
        if err != nil {
39✔
530
                return false, err
×
531
        }
×
532
        if vmexport == nil {
40✔
533
                return false, fmt.Errorf("unable to get '%s/%s' VirtualMachineExport", vmeInfo.Namespace, vmeInfo.Name)
1✔
534
        }
1✔
535

536
        // Grab the VM Manifest and display it.
537
        if vmeInfo.ExportManifest {
44✔
538
                return getVirtualMachineManifest(client, vmexport, vmeInfo)
6✔
539
        }
6✔
540

541
        // Download the exported volume
542
        return downloadVolume(client, vmexport, vmeInfo)
32✔
543
}
544

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

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

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

585
// downloadVolume handles the process of downloading the requested volume from a VirtualMachineExport
586
func downloadVolume(client kubecli.KubevirtClient, vmexport *exportv1.VirtualMachineExport, vmeInfo *VMExportInfo) (bool, error) {
32✔
587
        var downloadUrl string
32✔
588
        var err error
32✔
589

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

612
        resp, err := HandleHTTPGetRequestFn(client, vmexport, downloadUrl, vmeInfo.Insecure, vmeInfo.ServiceURL, nil)
26✔
613
        if err != nil {
27✔
614
                return false, err
1✔
615
        }
1✔
616
        defer resp.Body.Close()
25✔
617

25✔
618
        // Check server response
25✔
619
        if resp.StatusCode != http.StatusOK {
29✔
620
                printToOutput("Bad status: %s\n", resp.Status)
4✔
621
                return false, nil
4✔
622
        }
4✔
623

624
        // Lastly, copy the file to the expected output
625
        if err := copyFileWithProgressBar(vmeInfo.OutputWriter, resp, vmeInfo.Decompress); err != nil {
21✔
626
                return false, err
×
627
        }
×
628

629
        printToOutput("Download finished successfully\n")
21✔
630

21✔
631
        return true, nil
21✔
632
}
633

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

640
func replaceUrlWithServiceUrl(manifestUrl string, vmeInfo *VMExportInfo) (string, error) {
29✔
641
        // Replace internal URL with specified URL
29✔
642
        manUrl, err := url.Parse(manifestUrl)
29✔
643
        if err != nil {
29✔
644
                return "", err
×
645
        }
×
646
        if vmeInfo.ServiceURL != "" {
33✔
647
                manUrl.Host = vmeInfo.ServiceURL
4✔
648
        }
4✔
649
        return manUrl.String(), nil
29✔
650
}
651

652
// GetUrlFromVirtualMachineExport inspects the VirtualMachineExport status to fetch the extected URL
653
func GetUrlFromVirtualMachineExport(vmexport *exportv1.VirtualMachineExport, vmeInfo *VMExportInfo) (string, error) {
33✔
654
        var (
33✔
655
                downloadUrl string
33✔
656
                err         error
33✔
657
                format      exportv1.VirtualMachineExportVolumeFormat
33✔
658
                links       *exportv1.VirtualMachineExportLink
33✔
659
        )
33✔
660

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

692
        // No need to decompress file if format is not gzip
693
        if format.Format == exportv1.KubeVirtRaw {
43✔
694
                vmeInfo.Decompress = false
12✔
695
        }
12✔
696

697
        if downloadUrl == "" {
35✔
698
                return "", fmt.Errorf("unable to get a valid URL from '%s/%s' VirtualMachineExport", vmexport.Namespace, vmexport.Name)
4✔
699
        }
4✔
700

701
        return downloadUrl, nil
27✔
702
}
703

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

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

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

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

741
                if vmexport == nil {
1✔
742
                        printToOutput("couldn't get VM Export %s, waiting for it to be created...\n", vmeInfo.Name)
×
743
                        return false, nil
×
744
                }
×
745

746
                if vmexport.Status == nil {
1✔
747
                        return false, nil
×
748
                }
×
749

750
                if vmexport.Status.Phase != exportv1.Ready {
2✔
751
                        printToOutput("waiting for VM Export %s status to be ready...\n", vmeInfo.Name)
1✔
752
                        return false, nil
1✔
753
                }
1✔
754

755
                if vmeInfo.ServiceURL == "" {
×
756
                        if vmexport.Status.Links == nil || vmexport.Status.Links.External == nil {
×
757
                                printToOutput("waiting for VM Export %s external links to be available...\n", vmeInfo.Name)
×
758
                                return false, nil
×
759
                        }
×
760
                } else {
×
761
                        if vmexport.Status.Links == nil || vmexport.Status.Links.Internal == nil {
×
762
                                printToOutput("waiting for VM Export %s internal links to be available...\n", vmeInfo.Name)
×
763
                                return false, nil
×
764
                        }
×
765
                }
766
                return true, nil
×
767
        })
768

769
        return err
1✔
770
}
771

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

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

28✔
793
        // Generate and do the request
28✔
794
        req, _ := http.NewRequest("GET", downloadUrl, nil)
28✔
795
        for k, v := range headers {
35✔
796
                req.Header.Set(k, v)
7✔
797
        }
7✔
798
        req.Header.Set(exportTokenHeader, token)
28✔
799

28✔
800
        return httpClient.Do(req)
28✔
801
}
802

803
// GetHTTPClient assigns the default, non-mocked HTTP client
804
func GetHTTPClient(transport *http.Transport, insecure bool) *http.Client {
×
805
        if insecure {
×
806
                transport = &http.Transport{
×
807
                        TLSClientConfig: &tls.Config{
×
808
                                InsecureSkipVerify: insecure,
×
809
                        },
×
810
                }
×
811
        }
×
812

813
        client := &http.Client{Transport: transport}
×
814
        return client
×
815
}
816

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

21✔
822
        // start bar based on our template
21✔
823
        bar := pb.ProgressBarTemplate(barTemplate).Start(0)
21✔
824
        defer bar.Finish()
21✔
825
        barRd := bar.NewProxyReader(resp.Body)
21✔
826
        rd = barRd
21✔
827
        bar.Start()
21✔
828

21✔
829
        if decompress {
23✔
830
                // Create a new gzip reader
2✔
831
                gzipReader, err := gzip.NewReader(barRd)
2✔
832
                if err != nil {
2✔
833
                        return err
×
834
                }
×
835
                defer gzipReader.Close()
2✔
836
                rd = gzipReader
2✔
837
                printToOutput("Decompressing image:\n")
2✔
838
        }
839

840
        _, err := io.Copy(output, rd)
21✔
841
        return err
21✔
842
}
843

844
// getOrCreateTokenSecret obtains a token secret to be used along with the virtualMachineExport
845
func getOrCreateTokenSecret(client kubecli.KubevirtClient, vmexport *exportv1.VirtualMachineExport) (*k8sv1.Secret, error) {
16✔
846
        // Securely randomize a 20 char string to be used as a token
16✔
847
        token, err := util.GenerateVMExportToken()
16✔
848
        if err != nil {
16✔
849
                return nil, err
×
850
        }
×
851

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

16✔
873
        secret, err = client.CoreV1().Secrets(vmexport.Namespace).Create(context.Background(), secret, metav1.CreateOptions{})
16✔
874
        if err != nil && !k8serrors.IsAlreadyExists(err) {
16✔
875
                return nil, err
×
876
        }
×
877

878
        return secret, nil
16✔
879
}
880

881
// getTokenFromSecret extracts the token from the secret specified on the virtualMachineExport
882
func getTokenFromSecret(client kubecli.KubevirtClient, vmexport *exportv1.VirtualMachineExport) (string, error) {
29✔
883
        secretName := ""
29✔
884
        if vmexport.Status != nil && vmexport.Status.TokenSecretRef != nil {
58✔
885
                secretName = *vmexport.Status.TokenSecretRef
29✔
886
        }
29✔
887

888
        secret, err := client.CoreV1().Secrets(vmexport.Namespace).Get(context.Background(), secretName, metav1.GetOptions{})
29✔
889
        if err != nil {
30✔
890
                return "", err
1✔
891
        }
1✔
892

893
        token := secret.Data[secretTokenKey]
28✔
894
        return string(token), nil
28✔
895
}
896

897
// getExportSource creates and assembles the object that'll be used as a source in the virtualMachineExport
898
func getExportSource() k8sv1.TypedLocalObjectReference {
46✔
899
        var exportSource k8sv1.TypedLocalObjectReference
46✔
900
        if vm != "" {
54✔
901
                exportSource = k8sv1.TypedLocalObjectReference{
8✔
902
                        APIGroup: &virtv1.SchemeGroupVersion.Group,
8✔
903
                        Kind:     "VirtualMachine",
8✔
904
                        Name:     vm,
8✔
905
                }
8✔
906
        }
8✔
907
        if snapshot != "" {
48✔
908
                exportSource = k8sv1.TypedLocalObjectReference{
2✔
909
                        APIGroup: &snapshotv1.SchemeGroupVersion.Group,
2✔
910
                        Kind:     "VirtualMachineSnapshot",
2✔
911
                        Name:     snapshot,
2✔
912
                }
2✔
913
        }
2✔
914
        if pvc != "" {
58✔
915
                exportSource = k8sv1.TypedLocalObjectReference{
12✔
916
                        APIGroup: &k8sv1.SchemeGroupVersion.Group,
12✔
917
                        Kind:     "PersistentVolumeClaim",
12✔
918
                        Name:     pvc,
12✔
919
                }
12✔
920
        }
12✔
921

922
        return exportSource
46✔
923
}
924

925
// handleCreateFlags ensures that only compatible flag combinations are used with 'create'
926
func handleCreateFlags() error {
11✔
927
        if vm == "" && snapshot == "" && pvc == "" {
12✔
928
                return fmt.Errorf(ErrRequiredExportType)
1✔
929
        }
1✔
930

931
        if outputFile != "" {
10✔
932
                return fmt.Errorf(ErrIncompatibleFlag, OUTPUT_FLAG, CREATE)
×
933
        }
×
934
        if volumeName != "" {
10✔
935
                return fmt.Errorf(ErrIncompatibleFlag, VOLUME_FLAG, CREATE)
×
936
        }
×
937
        if insecure {
11✔
938
                return fmt.Errorf(ErrIncompatibleFlag, INSECURE_FLAG, CREATE)
1✔
939
        }
1✔
940
        if keepVme {
9✔
941
                return fmt.Errorf(ErrIncompatibleFlag, KEEP_FLAG, CREATE)
×
942
        }
×
943
        if deleteVme {
9✔
944
                return fmt.Errorf(ErrIncompatibleFlag, DELETE_FLAG, CREATE)
×
945
        }
×
946
        if portForward {
9✔
947
                return fmt.Errorf(ErrIncompatibleFlag, PORT_FORWARD_FLAG, CREATE)
×
948
        }
×
949
        if localPort != "0" {
9✔
950
                return fmt.Errorf(ErrIncompatibleFlag, LOCAL_PORT_FLAG, CREATE)
×
951
        }
×
952
        if format != "" {
9✔
953
                return fmt.Errorf(ErrIncompatibleFlag, FORMAT_FLAG, CREATE)
×
954
        }
×
955
        if serviceUrl != "" {
9✔
956
                return fmt.Errorf(ErrIncompatibleFlag, SERVICE_URL_FLAG, CREATE)
×
957
        }
×
958
        if downloadRetries != 0 {
9✔
959
                return fmt.Errorf(ErrIncompatibleFlag, RETRY_FLAG, CREATE)
×
960
        }
×
961

962
        return nil
9✔
963
}
964

965
// handleDeleteFlags ensures that only compatible flag combinations are used with 'delete'
966
func handleDeleteFlags() error {
5✔
967
        if vm != "" || snapshot != "" || pvc != "" {
6✔
968
                return fmt.Errorf(ErrIncompatibleExportType)
1✔
969
        }
1✔
970

971
        if outputFile != "" {
4✔
972
                return fmt.Errorf(ErrIncompatibleFlag, OUTPUT_FLAG, DELETE)
×
973
        }
×
974
        if volumeName != "" {
4✔
975
                return fmt.Errorf(ErrIncompatibleFlag, VOLUME_FLAG, DELETE)
×
976
        }
×
977
        if insecure {
5✔
978
                return fmt.Errorf(ErrIncompatibleFlag, INSECURE_FLAG, DELETE)
1✔
979
        }
1✔
980
        if keepVme {
3✔
981
                return fmt.Errorf(ErrIncompatibleFlag, KEEP_FLAG, DELETE)
×
982
        }
×
983
        if deleteVme {
3✔
984
                return fmt.Errorf(ErrIncompatibleFlag, DELETE_FLAG, DELETE)
×
985
        }
×
986
        if portForward {
3✔
987
                return fmt.Errorf(ErrIncompatibleFlag, PORT_FORWARD_FLAG, DELETE)
×
988
        }
×
989
        if localPort != "0" {
3✔
990
                return fmt.Errorf(ErrIncompatibleFlag, LOCAL_PORT_FLAG, DELETE)
×
991
        }
×
992
        if format != "" {
3✔
993
                return fmt.Errorf(ErrIncompatibleFlag, FORMAT_FLAG, DELETE)
×
994
        }
×
995
        if serviceUrl != "" {
3✔
996
                return fmt.Errorf(ErrIncompatibleFlag, SERVICE_URL_FLAG, DELETE)
×
997
        }
×
998
        if downloadRetries != 0 {
3✔
999
                return fmt.Errorf(ErrIncompatibleFlag, RETRY_FLAG, DELETE)
×
1000
        }
×
1001
        if readinessTimeout != "" {
3✔
1002
                return fmt.Errorf(ErrIncompatibleFlag, READINESS_TIMEOUT_FLAG, DELETE)
×
1003
        }
×
1004
        if len(resourceLabels) > 0 {
3✔
1005
                return fmt.Errorf(ErrIncompatibleFlag, LABELS_FLAG, DELETE)
×
1006
        }
×
1007
        if len(resourceAnnotations) > 0 {
3✔
1008
                return fmt.Errorf(ErrIncompatibleFlag, ANNOTATIONS_FLAG, DELETE)
×
1009
        }
×
1010

1011
        return nil
3✔
1012
}
1013

1014
// handleDownloadFlags ensures that only compatible flag combinations are used with 'download'
1015
func handleDownloadFlags() error {
43✔
1016
        // We assume that the vmexport should be created if a source has been specified
43✔
1017
        if hasSource := vm != "" || snapshot != "" || pvc != ""; hasSource {
59✔
1018
                shouldCreate = true
16✔
1019
        }
16✔
1020

1021
        if portForward {
47✔
1022
                port, err := strconv.Atoi(localPort)
4✔
1023
                if err != nil || port < 0 || port > 65535 {
5✔
1024
                        return fmt.Errorf(ErrInvalidValue, LOCAL_PORT_FLAG, "valid port numbers")
1✔
1025
                }
1✔
1026
        }
1027

1028
        if format != "" && format != GZIP_FORMAT && format != RAW_FORMAT && format != OCI_FORMAT {
43✔
1029
                return fmt.Errorf(ErrInvalidValue, FORMAT_FLAG, "gzip/raw/oci")
1✔
1030
        }
1✔
1031

1032
        if format == OCI_FORMAT {
46✔
1033
                if volumeName != "" {
6✔
1034
                        return fmt.Errorf(ErrIncompatibleFlag, VOLUME_FLAG, FORMAT_FLAG+"=oci")
1✔
1035
                }
1✔
1036
                if exportManifest {
5✔
1037
                        return fmt.Errorf(ErrIncompatibleFlag, MANIFEST_FLAG, FORMAT_FLAG+"=oci")
1✔
1038
                }
1✔
1039
                if pvc != "" {
4✔
1040
                        return fmt.Errorf(ErrIncompatibleFlag, PVC_FLAG, FORMAT_FLAG+"=oci")
1✔
1041
                }
1✔
1042
        }
1043

1044
        if downloadRetries < 0 {
38✔
1045
                return fmt.Errorf(ErrInvalidValue, RETRY_FLAG, "positive integers")
×
1046
        }
×
1047

1048
        if exportManifest {
47✔
1049
                if volumeName != "" {
10✔
1050
                        return fmt.Errorf(ErrIncompatibleFlag, VOLUME_FLAG, MANIFEST_FLAG)
1✔
1051
                }
1✔
1052

1053
                manifestOutputFormat = strings.ToLower(manifestOutputFormat)
8✔
1054
                if manifestOutputFormat != OUTPUT_FORMAT_JSON && manifestOutputFormat != OUTPUT_FORMAT_YAML && manifestOutputFormat != "" {
9✔
1055
                        return fmt.Errorf(ErrInvalidValue, OUTPUT_FORMAT_FLAG, "json/yaml")
1✔
1056
                }
1✔
1057

1058
                if pvc != "" {
8✔
1059
                        return fmt.Errorf(ErrIncompatibleFlag, PVC_FLAG, MANIFEST_FLAG)
1✔
1060
                }
1✔
1061
        }
1062
        if !exportManifest && outputFile == "" {
36✔
1063
                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✔
1064
        }
1✔
1065

1066
        return nil
34✔
1067
}
1068

1069
// getExportSecretName builds the name of the token secret based on the virtualMachineExport object
1070
func getExportSecretName(vmexportName string) string {
32✔
1071
        return fmt.Sprintf("secret-%s", vmexportName)
32✔
1072
}
32✔
1073

1074
// errExportAlreadyExists is used to the determine if an error happened when creating an already existing vmexport
1075
func errExportAlreadyExists(err error) bool {
11✔
1076
        return strings.Contains(err.Error(), "VirtualMachineExport") && strings.Contains(err.Error(), "already exists")
11✔
1077
}
11✔
1078

1079
// Port-forward functions
1080

1081
// translateServicePortToTargetPort tranlates the specified port to be used with the service's pod
1082
func translateServicePortToTargetPort(localPort string, remotePort string, svc k8sv1.Service, pod k8sv1.Pod) ([]string, error) {
5✔
1083
        ports := []string{}
5✔
1084
        portnum, err := strconv.Atoi(remotePort)
5✔
1085
        if err != nil {
5✔
1086
                return ports, err
×
1087
        }
×
1088
        containerPort, err := kubectlutil.LookupContainerPortNumberByServicePort(svc, pod, int32(portnum))
5✔
1089
        if err != nil {
6✔
1090
                // can't resolve a named port, or Service did not declare this port, return an error
1✔
1091
                return ports, err
1✔
1092
        }
1✔
1093

1094
        // convert the resolved target port back to a string
1095
        remotePort = strconv.Itoa(int(containerPort))
4✔
1096
        if localPort != remotePort {
8✔
1097
                return append(ports, fmt.Sprintf("%s:%s", localPort, remotePort)), nil
4✔
1098
        }
4✔
1099

1100
        return append(ports, remotePort), nil
×
1101
}
1102

1103
// waitForExportServiceToBeReady waits until the vmexport service is ready for port-forwarding
1104
func waitForExportServiceToBeReady(client kubecli.KubevirtClient, vmeInfo *VMExportInfo, interval, timeout time.Duration) (*k8sv1.Service, error) {
6✔
1105
        service := &k8sv1.Service{}
6✔
1106
        err := virtwait.PollImmediately(interval, timeout, func(ctx context.Context) (bool, error) {
12✔
1107
                vmexport, err := getVirtualMachineExport(client, vmeInfo)
6✔
1108
                if err != nil || vmexport == nil {
6✔
1109
                        return false, err
×
1110
                }
×
1111

1112
                if vmexport.Status == nil || vmexport.Status.Phase != exportv1.Ready {
6✔
1113
                        printToOutput("waiting for VM Export %s status to be ready...\n", vmeInfo.Name)
×
1114
                        return false, nil
×
1115
                }
×
1116

1117
                serviceName := vmexport.Status.ServiceName
6✔
1118
                if serviceName == "" {
6✔
1119
                        printToOutput("waiting for VM Export %s service name to be set...\n", vmeInfo.Name)
×
1120
                        return false, nil
×
1121
                }
×
1122

1123
                service, err = client.CoreV1().Services(vmeInfo.Namespace).Get(ctx, serviceName, metav1.GetOptions{})
6✔
1124
                if err != nil {
6✔
1125
                        if k8serrors.IsNotFound(err) {
×
1126
                                printToOutput("waiting for service %s to be ready before port-forwarding...\n", serviceName)
×
1127
                                return false, nil
×
1128
                        }
×
1129
                        return false, err
×
1130
                }
1131
                printToOutput("service %s is ready for port-forwarding\n", service.Name)
6✔
1132
                return true, nil
6✔
1133
        })
1134
        return service, err
6✔
1135
}
1136

1137
// setupPortForward runs a port-forward after initializing all required arguments
1138
func setupPortForward(client kubecli.KubevirtClient, vmeInfo *VMExportInfo) (chan struct{}, error) {
6✔
1139
        // Wait for the vmexport object to be ready
6✔
1140
        service, err := waitForExportServiceToBeReady(client, vmeInfo, processingWaitInterval, vmeInfo.ReadinessTimeout)
6✔
1141
        if err != nil {
6✔
1142
                return nil, err
×
1143
        }
×
1144

1145
        // Extract the target pod selector from the service
1146
        podSelector := labels.SelectorFromSet(service.Spec.Selector)
6✔
1147

6✔
1148
        // List the pods matching the selector
6✔
1149
        podList, err := client.CoreV1().Pods(vmeInfo.Namespace).List(context.Background(), metav1.ListOptions{LabelSelector: podSelector.String()})
6✔
1150
        if err != nil {
6✔
1151
                return nil, fmt.Errorf("failed to list pods: %v", err)
×
1152
        }
×
1153

1154
        // Pick the first pod to forward the port
1155
        if len(podList.Items) == 0 {
7✔
1156
                return nil, fmt.Errorf("no pods found for the service %s", service.Name)
1✔
1157
        }
1✔
1158

1159
        // Set up the port forwarding ports
1160
        ports, err := translateServicePortToTargetPort(vmeInfo.LocalPort, "443", *service, podList.Items[0])
5✔
1161
        if err != nil {
6✔
1162
                return nil, err
1✔
1163
        }
1✔
1164

1165
        stopChan := make(chan struct{}, 1)
4✔
1166
        readyChan := make(chan struct{})
4✔
1167
        portChan := make(chan uint16)
4✔
1168
        go RunPortForwardFn(client, podList.Items[0], vmeInfo.Namespace, ports, stopChan, readyChan, portChan)
4✔
1169

4✔
1170
        // Wait for the port forwarding to be ready
4✔
1171
        select {
4✔
1172
        case <-readyChan:
4✔
1173
                printToOutput("Port forwarding is ready.\n")
4✔
1174
                // Using 0 allows listening on a random available port.
4✔
1175
                // Now we need to find out which port was used
4✔
1176
                if vmeInfo.LocalPort == "0" {
7✔
1177
                        localPort := <-portChan
3✔
1178
                        close(portChan)
3✔
1179
                        vmeInfo.ServiceURL = fmt.Sprintf("127.0.0.1:%d", localPort)
3✔
1180
                }
3✔
1181
        case <-time.After(30 * time.Second):
×
1182
                return nil, fmt.Errorf("timeout waiting for port forwarding to be ready")
×
1183
        }
1184
        return stopChan, nil
4✔
1185
}
1186

1187
// RunPortForward is the actual function that runs the port-forward. Meant to be run concurrently
1188
func RunPortForward(client kubecli.KubevirtClient, pod k8sv1.Pod, namespace string, ports []string, stopChan, readyChan chan struct{}, portChan chan uint16) error {
×
1189
        // Create a port forwarding request
×
1190
        req := client.CoreV1().RESTClient().Post().
×
1191
                Resource("pods").
×
1192
                Name(pod.Name).
×
1193
                Namespace(namespace).
×
1194
                SubResource("portforward")
×
1195

×
1196
        // Set up the port forwarding options
×
1197
        transport, upgrader, err := spdy.RoundTripperFor(client.Config())
×
1198
        if err != nil {
×
1199
                log.Fatalf("Failed to set up transport: %v", err)
×
1200
        }
×
1201
        dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, "POST", req.URL())
×
1202

×
1203
        // Start port-forwarding
×
1204
        fw, err := portforward.New(dialer, ports, stopChan, readyChan, os.Stderr, os.Stderr)
×
1205
        if err != nil {
×
1206
                log.Fatalf("Failed to setup port forward: %v", err)
×
1207
        }
×
1208
        slicedPorts := strings.Split(ports[0], ":")
×
1209
        if len(slicedPorts) == 2 && slicedPorts[0] == "0" {
×
1210
                // If the local port is 0, then the port-forwarder will pick a random available port.
×
1211
                // We need to send this port number back to the caller.
×
1212
                go func() {
×
1213
                        <-readyChan
×
1214
                        forwardedPorts, err := fw.GetPorts()
×
1215
                        if err != nil {
×
1216
                                log.Fatalf("Failed to get forwarded ports: %v", err)
×
1217
                        }
×
1218
                        portChan <- forwardedPorts[0].Local
×
1219
                }()
1220
        }
1221
        return fw.ForwardPorts()
×
1222
}
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