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

mendersoftware / mender-artifact / 2313947309

09 Feb 2026 09:46AM UTC coverage: 76.335% (+76.3%) from 0.0%
2313947309

Pull #782

gitlab-ci

vpodzime
chore: Deprecate the --device-type CLI option

Mark --device-type as deprecated in help text and print a warning when
it is used, directing users to use --compatible-types instead.

Ticket: MEN-9010
Changelog: title

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: Vratislav Podzimek <vratislav.podzimek+auto-signed@northern.tech>
Pull Request #782: Add --compatible-types as an alias for --device-type and deprecate the latter

47 of 58 new or added lines in 2 files covered. (81.03%)

49 existing lines in 1 file now uncovered.

6119 of 8016 relevant lines covered (76.33%)

140.8 hits per line

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

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

15
package cli
16

17
import (
18
        "bufio"
19
        "context"
20
        "encoding/json"
21
        "fmt"
22
        "os"
23
        "regexp"
24
        "strings"
25

26
        "io"
27

28
        "github.com/pkg/errors"
29
        "github.com/urfave/cli"
30

31
        "github.com/mendersoftware/mender-artifact/artifact"
32
        "github.com/mendersoftware/mender-artifact/artifact/stage"
33
        "github.com/mendersoftware/mender-artifact/awriter"
34
        "github.com/mendersoftware/mender-artifact/cli/util"
35
        "github.com/mendersoftware/mender-artifact/handlers"
36
        "github.com/mendersoftware/mender-artifact/utils"
37
)
38

39
func writeRootfsImageChecksum(rootfsFilename string,
40
        typeInfo *artifact.TypeInfoV3, legacy bool) (err error) {
203✔
41
        chk := artifact.NewWriterChecksum(io.Discard)
203✔
42
        payload, err := os.Open(rootfsFilename)
203✔
43
        if err != nil {
205✔
44
                return cli.NewExitError(
2✔
45
                        fmt.Sprintf("Failed to open the payload file: %q", rootfsFilename),
2✔
46
                        1,
2✔
47
                )
2✔
48
        }
2✔
49
        if _, err = io.Copy(chk, payload); err != nil {
201✔
50
                return cli.NewExitError("Failed to generate the checksum for the payload", 1)
×
51
        }
×
52
        checksum := string(chk.Checksum())
201✔
53

201✔
54
        checksumKey := "rootfs-image.checksum"
201✔
55
        if legacy {
203✔
56
                checksumKey = "rootfs_image_checksum"
2✔
57
        }
2✔
58

59
        Log.Debugf("Adding the `%s`: %q to Artifact provides", checksumKey, checksum)
201✔
60
        if typeInfo == nil {
201✔
61
                return errors.New("Type-info is unitialized")
×
62
        }
×
63
        if typeInfo.ArtifactProvides == nil {
203✔
64
                t, err := artifact.NewTypeInfoProvides(map[string]string{checksumKey: checksum})
2✔
65
                if err != nil {
2✔
66
                        return errors.Wrapf(err, "%s", "Failed to write the "+"`"+checksumKey+"` provides")
×
67
                }
×
68
                typeInfo.ArtifactProvides = t
2✔
69
        } else {
199✔
70
                typeInfo.ArtifactProvides[checksumKey] = checksum
199✔
71
        }
199✔
72
        return nil
201✔
73
}
74

75
// getCompatibleDevices returns the device types from either --device-type or --compatible-types
76
// flag. Both flags are mutually exclusive and at least one must be provided.
77
func getCompatibleDevices(c *cli.Context) []string {
141✔
78
        deviceTypes := c.StringSlice("device-type")
141✔
79
        if len(deviceTypes) > 0 {
282✔
80
                return deviceTypes
141✔
81
        }
141✔
NEW
82
        return c.StringSlice("compatible-types")
×
83
}
84

85
func validateInput(c *cli.Context) error {
100✔
86
        // Version 2 and 3 validation.
100✔
87
        fileMissing := false
100✔
88
        if c.Command.Name != "bootstrap-artifact" {
196✔
89
                if len(c.String("file")) == 0 {
96✔
90
                        fileMissing = true
×
91
                }
×
92
        }
93

94
        // Check mutual exclusivity of --device-type and --compatible-types
95
        hasDeviceType := len(c.StringSlice("device-type")) > 0
100✔
96
        hasCompatibleTypes := len(c.StringSlice("compatible-types")) > 0
100✔
97
        if hasDeviceType {
198✔
98
                Log.Warn("--device-type is deprecated and will be removed in future versions, " +
98✔
99
                        "use --compatible-types instead")
98✔
100
        }
98✔
101
        if hasDeviceType && hasCompatibleTypes {
100✔
NEW
102
                return cli.NewExitError(
×
NEW
103
                        "`device-type` and `compatible-types` are mutually exclusive",
×
NEW
104
                        errArtifactInvalidParameters,
×
NEW
105
                )
×
NEW
106
        }
×
107

108
        if !hasDeviceType && !hasCompatibleTypes ||
100✔
109
                len(c.String("artifact-name")) == 0 || fileMissing {
102✔
110
                return cli.NewExitError(
2✔
111
                        "must provide `device-type` or `compatible-types`, `artifact-name` and `file`",
2✔
112
                        errArtifactInvalidParameters,
2✔
113
                )
2✔
114
        }
2✔
115
        if len(strings.Fields(c.String("artifact-name"))) > 1 {
100✔
116
                // check for whitespace in artifact-name
2✔
117
                return cli.NewExitError(
2✔
118
                        "whitespace is not allowed in the artifact-name",
2✔
119
                        errArtifactInvalidParameters,
2✔
120
                )
2✔
121
        }
2✔
122
        return nil
96✔
123
}
124

125
func createRootfsFromSSH(c *cli.Context) (string, error) {
×
126
        _, err := utils.GetBinaryPath("blkid")
×
127
        if err != nil {
×
128
                Log.Warnf("Skipping running fsck on the Artifact: %v", errBlkidNotFound)
×
129
        }
×
130
        rootfsFilename, err := getDeviceSnapshot(c)
×
131
        if err != nil {
×
132
                return rootfsFilename, cli.NewExitError("SSH error: "+err.Error(), 1)
×
133
        }
×
134

135
        // check for blkid and get filesystem type
136
        fstype, err := imgFilesystemType(rootfsFilename)
×
137
        if err != nil {
×
138
                if err == errBlkidNotFound {
×
139
                        return rootfsFilename, nil
×
140
                }
×
141
                return rootfsFilename, cli.NewExitError(
×
142
                        "imgFilesystemType error: "+err.Error(),
×
143
                        errArtifactCreate,
×
144
                )
×
145
        }
146

147
        // run fsck
148
        switch fstype {
×
149
        case fat:
×
150
                err = runFsck(rootfsFilename, "vfat")
×
151
        case ext:
×
152
                err = runFsck(rootfsFilename, "ext4")
×
153
        case unsupported:
×
154
                err = errors.New("createRootfsFromSSH: unsupported filesystem")
×
155

156
        }
157
        if err != nil {
×
158
                return rootfsFilename, cli.NewExitError("runFsck error: "+err.Error(), errArtifactCreate)
×
159
        }
×
160

161
        return rootfsFilename, nil
×
162
}
163

164
func makeEmptyUpdates(ctx *cli.Context) (*awriter.Updates, error) {
4✔
165
        handler := handlers.NewBootstrapArtifact()
4✔
166

4✔
167
        dataFiles := make([](*handlers.DataFile), 0)
4✔
168
        if err := handler.SetUpdateFiles(dataFiles); err != nil {
4✔
169
                return nil, cli.NewExitError(
×
170
                        err,
×
171
                        1,
×
172
                )
×
173
        }
×
174

175
        upd := &awriter.Updates{
4✔
176
                Updates: []handlers.Composer{handler},
4✔
177
        }
4✔
178
        return upd, nil
4✔
179
}
180

181
func writeBootstrapArtifact(c *cli.Context) error {
4✔
182
        comp, err := artifact.NewCompressorFromId(c.GlobalString("compression"))
4✔
183
        if err != nil {
4✔
184
                return cli.NewExitError(
×
185
                        "compressor '"+c.GlobalString("compression")+"' is not supported: "+err.Error(),
×
186
                        1,
×
187
                )
×
188
        }
×
189

190
        if err := validateInput(c); err != nil {
4✔
191
                Log.Error(err.Error())
×
192
                return err
×
193
        }
×
194

195
        // set the default name
196
        name := "artifact.mender"
4✔
197
        if len(c.String("output-path")) > 0 {
8✔
198
                name = c.String("output-path")
4✔
199
        }
4✔
200
        version := c.Int("version")
4✔
201

4✔
202
        Log.Debugf("creating bootstrap artifact [%s], version: %d", name, version)
4✔
203

4✔
204
        var w io.Writer
4✔
205
        if name == "-" {
4✔
206
                w = os.Stdout
×
207
        } else {
4✔
208
                f, err := os.Create(name)
4✔
209
                if err != nil {
4✔
210
                        return cli.NewExitError(
×
211
                                "can not create bootstrap artifact file: "+err.Error(),
×
212
                                errArtifactCreate,
×
213
                        )
×
214
                }
×
215
                defer f.Close()
4✔
216
                w = f
4✔
217
        }
218

219
        aw, err := artifactWriter(c, comp, w, version)
4✔
220
        if err != nil {
4✔
221
                return cli.NewExitError(err.Error(), 1)
×
222
        }
×
223

224
        compatibleDevices := getCompatibleDevices(c)
4✔
225
        depends := artifact.ArtifactDepends{
4✔
226
                ArtifactName:      c.StringSlice("artifact-name-depends"),
4✔
227
                CompatibleDevices: compatibleDevices,
4✔
228
                ArtifactGroup:     c.StringSlice("depends-groups"),
4✔
229
        }
4✔
230

4✔
231
        provides := artifact.ArtifactProvides{
4✔
232
                ArtifactName:  c.String("artifact-name"),
4✔
233
                ArtifactGroup: c.String("provides-group"),
4✔
234
        }
4✔
235

4✔
236
        upd, err := makeEmptyUpdates(c)
4✔
237
        if err != nil {
4✔
238
                return err
×
239
        }
×
240

241
        typeInfoV3, _, err := makeTypeInfo(c)
4✔
242
        if err != nil {
4✔
243
                return err
×
244
        }
×
245

246
        if !c.Bool("no-progress") {
8✔
247
                ctx, cancel := context.WithCancel(context.Background())
4✔
248
                go reportProgress(ctx, aw.State)
4✔
249
                defer cancel()
4✔
250
                aw.ProgressWriter = utils.NewProgressWriter()
4✔
251
        }
4✔
252

253
        err = aw.WriteArtifact(
4✔
254
                &awriter.WriteArtifactArgs{
4✔
255
                        Format:     "mender",
4✔
256
                        Version:    version,
4✔
257
                        Devices:    compatibleDevices,
4✔
258
                        Name:       c.String("artifact-name"),
4✔
259
                        Updates:    upd,
4✔
260
                        Scripts:    nil,
4✔
261
                        Depends:    &depends,
4✔
262
                        Provides:   &provides,
4✔
263
                        TypeInfoV3: typeInfoV3,
4✔
264
                        Bootstrap:  true,
4✔
265
                })
4✔
266
        if err != nil {
4✔
267
                return cli.NewExitError(err.Error(), 1)
×
268
        }
×
269
        return nil
4✔
270
}
271

272
func writeRootfs(c *cli.Context) error {
96✔
273
        comp, err := artifact.NewCompressorFromId(c.GlobalString("compression"))
96✔
274
        if err != nil {
96✔
275
                return cli.NewExitError(
×
276
                        "compressor '"+c.GlobalString("compression")+"' is not supported: "+err.Error(),
×
277
                        1,
×
278
                )
×
279
        }
×
280

281
        if err := validateInput(c); err != nil {
100✔
282
                Log.Error(err.Error())
4✔
283
                return err
4✔
284
        }
4✔
285

286
        // set the default name
287
        name := "artifact.mender"
92✔
288
        if len(c.String("output-path")) > 0 {
184✔
289
                name = c.String("output-path")
92✔
290
        }
92✔
291
        version := c.Int("version")
92✔
292
        var showprovides map[string]string
92✔
293

92✔
294
        Log.Debugf("creating artifact [%s], version: %d", name, version)
92✔
295
        rootfsFilename := c.String("file")
92✔
296
        if strings.HasPrefix(rootfsFilename, "ssh://") {
92✔
297
                rootfsFilename, err = createRootfsFromSSH(c)
×
298
                defer os.Remove(rootfsFilename)
×
299
                if err != nil {
×
300
                        return cli.NewExitError(err.Error(), errArtifactCreate)
×
301
                }
×
302
                showprovides, err = showProvides(c)
×
303
                if err != nil {
×
304
                        return cli.NewExitError(err.Error(), errArtifactCreate)
×
305
                }
×
306
        }
307

308
        var h handlers.Composer
92✔
309
        switch version {
92✔
310
        case 2:
2✔
311
                h = handlers.NewRootfsV2(rootfsFilename)
2✔
312
        case 3:
84✔
313
                h = handlers.NewRootfsV3(rootfsFilename)
84✔
314
        default:
6✔
315
                return cli.NewExitError(
6✔
316
                        fmt.Sprintf("Artifact version %d is not supported", version),
6✔
317
                        errArtifactUnsupportedVersion,
6✔
318
                )
6✔
319
        }
320

321
        upd := &awriter.Updates{
86✔
322
                Updates: []handlers.Composer{h},
86✔
323
        }
86✔
324

86✔
325
        var w io.Writer
86✔
326
        if name == "-" {
86✔
327
                w = os.Stdout
×
328
        } else {
86✔
329
                f, err := os.Create(name)
86✔
330
                if err != nil {
86✔
331
                        return cli.NewExitError(
×
332
                                "can not create artifact file: "+err.Error(),
×
333
                                errArtifactCreate,
×
334
                        )
×
335
                }
×
336
                defer f.Close()
86✔
337
                w = f
86✔
338
        }
339

340
        aw, err := artifactWriter(c, comp, w, version)
86✔
341
        if err != nil {
88✔
342
                return cli.NewExitError(err.Error(), 1)
2✔
343
        }
2✔
344

345
        scr, err := scripts(c.StringSlice("script"))
84✔
346
        if err != nil {
86✔
347
                return cli.NewExitError(err.Error(), 1)
2✔
348
        }
2✔
349

350
        compatibleDevices := getCompatibleDevices(c)
82✔
351
        depends := artifact.ArtifactDepends{
82✔
352
                ArtifactName:      c.StringSlice("artifact-name-depends"),
82✔
353
                CompatibleDevices: compatibleDevices,
82✔
354
                ArtifactGroup:     c.StringSlice("depends-groups"),
82✔
355
        }
82✔
356

82✔
357
        provides := artifact.ArtifactProvides{
82✔
358
                ArtifactName:  c.String("artifact-name"),
82✔
359
                ArtifactGroup: c.String("provides-group"),
82✔
360
        }
82✔
361

82✔
362
        typeInfoV3, _, err := makeTypeInfo(c)
82✔
363
        if err != nil {
82✔
364
                return err
×
365
        }
×
366
        for k, v := range showprovides {
82✔
367
                _, exist := typeInfoV3.ArtifactProvides[k]
×
368
                if !exist {
×
369
                        typeInfoV3.ArtifactProvides[k] = v
×
370
                }
×
371
        }
372

373
        if !c.Bool("no-checksum-provide") {
160✔
374
                legacy := c.Bool("legacy-rootfs-image-checksum")
78✔
375
                if err = writeRootfsImageChecksum(rootfsFilename, typeInfoV3, legacy); err != nil {
78✔
376
                        return cli.NewExitError(
×
377
                                errors.Wrap(err, "Failed to write the `rootfs-image.checksum` to the artifact"),
×
378
                                1,
×
379
                        )
×
380
                }
×
381
        }
382
        if !c.Bool("no-progress") {
164✔
383
                ctx, cancel := context.WithCancel(context.Background())
82✔
384
                go reportProgress(ctx, aw.State)
82✔
385
                defer cancel()
82✔
386
                aw.ProgressWriter = utils.NewProgressWriter()
82✔
387
        }
82✔
388

389
        err = aw.WriteArtifact(
82✔
390
                &awriter.WriteArtifactArgs{
82✔
391
                        Format:     "mender",
82✔
392
                        Version:    version,
82✔
393
                        Devices:    compatibleDevices,
82✔
394
                        Name:       c.String("artifact-name"),
82✔
395
                        Updates:    upd,
82✔
396
                        Scripts:    scr,
82✔
397
                        Depends:    &depends,
82✔
398
                        Provides:   &provides,
82✔
399
                        TypeInfoV3: typeInfoV3,
82✔
400
                })
82✔
401
        if err != nil {
82✔
402
                return cli.NewExitError(err.Error(), 1)
×
403
        }
×
404

405
        return checkArtifactSizeLimits(name, c)
82✔
406
}
407

408
func reportProgress(c context.Context, state chan string) {
141✔
409
        fmt.Fprintln(os.Stderr, "Writing Artifact...")
141✔
410
        str := fmt.Sprintf("%-20s\t", <-state)
141✔
411
        fmt.Fprint(os.Stderr, str)
141✔
412
        for {
844✔
413
                select {
703✔
414
                case str = <-state:
563✔
415
                        if str == stage.Data {
704✔
416
                                fmt.Fprintf(os.Stderr, "\033[1;32m\u2713\033[0m\n")
141✔
417
                                fmt.Fprintln(os.Stderr, "Payload")
141✔
418
                        } else {
564✔
419
                                fmt.Fprintf(os.Stderr, "\033[1;32m\u2713\033[0m\n")
423✔
420
                                str = fmt.Sprintf("%-20s\t", str)
423✔
421
                                fmt.Fprint(os.Stderr, str)
423✔
422
                        }
423✔
423
                case <-c.Done():
141✔
424
                        return
141✔
425
                }
426
        }
427
}
428

429
func artifactWriter(c *cli.Context, comp artifact.Compressor, w io.Writer,
430
        ver int) (*awriter.Writer, error) {
145✔
431
        privateKey, err := getKey(c)
145✔
432
        if err != nil {
147✔
433
                return nil, err
2✔
434
        }
2✔
435
        if privateKey != nil {
153✔
436
                if ver == 0 {
10✔
437
                        // check if we are having correct version
×
438
                        return nil, errors.New("can not use signed artifact with version 0")
×
439
                }
×
440
                return awriter.NewWriterSigned(w, comp, privateKey), nil
10✔
441
        }
442
        return awriter.NewWriter(w, comp), nil
133✔
443
}
444

445
func makeUpdates(ctx *cli.Context) (*awriter.Updates, error) {
55✔
446
        version := ctx.Int("version")
55✔
447

55✔
448
        var handler, augmentHandler handlers.Composer
55✔
449
        switch version {
55✔
450
        case 2:
×
451
                return nil, cli.NewExitError(
×
452
                        "Module images need at least artifact format version 3",
×
453
                        errArtifactInvalidParameters)
×
454
        case 3:
55✔
455
                handler = handlers.NewModuleImage(ctx.String("type"))
55✔
456
        default:
×
457
                return nil, cli.NewExitError(
×
458
                        fmt.Sprintf("unsupported artifact version: %v", version),
×
459
                        errArtifactUnsupportedVersion,
×
460
                )
×
461
        }
462

463
        dataFiles := make([](*handlers.DataFile), 0, len(ctx.StringSlice("file")))
55✔
464
        for _, file := range ctx.StringSlice("file") {
113✔
465
                dataFiles = append(dataFiles, &handlers.DataFile{Name: file})
58✔
466
        }
58✔
467
        if err := handler.SetUpdateFiles(dataFiles); err != nil {
55✔
468
                return nil, cli.NewExitError(
×
469
                        err,
×
470
                        1,
×
471
                )
×
472
        }
×
473

474
        upd := &awriter.Updates{
55✔
475
                Updates: []handlers.Composer{handler},
55✔
476
        }
55✔
477

55✔
478
        if ctx.String("augment-type") != "" {
59✔
479
                augmentHandler = handlers.NewAugmentedModuleImage(handler, ctx.String("augment-type"))
4✔
480
                dataFiles = make([](*handlers.DataFile), 0, len(ctx.StringSlice("augment-file")))
4✔
481
                for _, file := range ctx.StringSlice("augment-file") {
8✔
482
                        dataFiles = append(dataFiles, &handlers.DataFile{Name: file})
4✔
483
                }
4✔
484
                if err := augmentHandler.SetUpdateAugmentFiles(dataFiles); err != nil {
4✔
485
                        return nil, cli.NewExitError(
×
486
                                err,
×
487
                                1,
×
488
                        )
×
489
                }
×
490
                upd.Augments = []handlers.Composer{augmentHandler}
4✔
491
        }
492

493
        return upd, nil
55✔
494
}
495

496
// makeTypeInfo returns the type-info provides and depends and the augmented
497
// type-info provides and depends, or nil.
498
func makeTypeInfo(ctx *cli.Context) (*artifact.TypeInfoV3, *artifact.TypeInfoV3, error) {
141✔
499
        // Make key value pairs from the type-info fields supplied on command
141✔
500
        // line.
141✔
501
        var keyValues *map[string]string
141✔
502

141✔
503
        var typeInfoDepends artifact.TypeInfoDepends
141✔
504
        keyValues, err := extractKeyValues(ctx.StringSlice("depends"))
141✔
505
        if err != nil {
141✔
506
                return nil, nil, err
×
507
        } else if keyValues != nil {
171✔
508
                if typeInfoDepends, err = artifact.NewTypeInfoDepends(*keyValues); err != nil {
30✔
509
                        return nil, nil, err
×
510
                }
×
511
        }
512

513
        var typeInfoProvides artifact.TypeInfoProvides
141✔
514
        keyValues, err = extractKeyValues(ctx.StringSlice("provides"))
141✔
515
        if err != nil {
141✔
516
                return nil, nil, err
×
517
        } else if keyValues != nil {
173✔
518
                if typeInfoProvides, err = artifact.NewTypeInfoProvides(*keyValues); err != nil {
32✔
519
                        return nil, nil, err
×
520
                }
×
521
        }
522
        typeInfoProvides = applySoftwareVersionToTypeInfoProvides(ctx, typeInfoProvides)
141✔
523

141✔
524
        var augmentTypeInfoDepends artifact.TypeInfoDepends
141✔
525
        keyValues, err = extractKeyValues(ctx.StringSlice("augment-depends"))
141✔
526
        if err != nil {
141✔
527
                return nil, nil, err
×
528
        } else if keyValues != nil {
145✔
529
                if augmentTypeInfoDepends, err = artifact.NewTypeInfoDepends(*keyValues); err != nil {
4✔
530
                        return nil, nil, err
×
531
                }
×
532
        }
533

534
        var augmentTypeInfoProvides artifact.TypeInfoProvides
141✔
535
        keyValues, err = extractKeyValues(ctx.StringSlice("augment-provides"))
141✔
536
        if err != nil {
141✔
537
                return nil, nil, err
×
538
        } else if keyValues != nil {
145✔
539
                if augmentTypeInfoProvides, err = artifact.NewTypeInfoProvides(*keyValues); err != nil {
4✔
540
                        return nil, nil, err
×
541
                }
×
542
        }
543

544
        clearsArtifactProvides, err := makeClearsArtifactProvides(ctx)
141✔
545
        if err != nil {
141✔
546
                return nil, nil, err
×
547
        }
×
548

549
        var typeInfo *string
141✔
550
        if ctx.Command.Name != "bootstrap-artifact" {
278✔
551
                typeFlag := ctx.String("type")
137✔
552
                typeInfo = &typeFlag
137✔
553
        }
137✔
554
        typeInfoV3 := &artifact.TypeInfoV3{
141✔
555
                Type:                   typeInfo,
141✔
556
                ArtifactDepends:        typeInfoDepends,
141✔
557
                ArtifactProvides:       typeInfoProvides,
141✔
558
                ClearsArtifactProvides: clearsArtifactProvides,
141✔
559
        }
141✔
560

141✔
561
        if ctx.String("augment-type") == "" {
278✔
562
                // Non-augmented artifact
137✔
563
                if len(ctx.StringSlice("augment-file")) != 0 ||
137✔
564
                        len(ctx.StringSlice("augment-depends")) != 0 ||
137✔
565
                        len(ctx.StringSlice("augment-provides")) != 0 ||
137✔
566
                        ctx.String("augment-meta-data") != "" {
137✔
567

×
568
                        err = errors.New("Must give --augment-type argument if making augmented artifact")
×
569
                        fmt.Println(err.Error())
×
570
                        return nil, nil, err
×
571
                }
×
572
                return typeInfoV3, nil, nil
137✔
573
        }
574

575
        augmentType := ctx.String("augment-type")
4✔
576
        augmentTypeInfoV3 := &artifact.TypeInfoV3{
4✔
577
                Type:             &augmentType,
4✔
578
                ArtifactDepends:  augmentTypeInfoDepends,
4✔
579
                ArtifactProvides: augmentTypeInfoProvides,
4✔
580
        }
4✔
581

4✔
582
        return typeInfoV3, augmentTypeInfoV3, nil
4✔
583
}
584

585
func getSoftwareVersion(
586
        artifactName,
587
        softwareFilesystem,
588
        softwareName,
589
        softwareNameDefault,
590
        softwareVersion string,
591
        noDefaultSoftwareVersion bool,
592
) map[string]string {
155✔
593
        result := map[string]string{}
155✔
594
        softwareVersionName := "rootfs-image"
155✔
595
        if softwareFilesystem != "" {
171✔
596
                softwareVersionName = softwareFilesystem
16✔
597
        }
16✔
598
        if !noDefaultSoftwareVersion {
282✔
599
                if softwareName == "" {
242✔
600
                        softwareName = softwareNameDefault
115✔
601
                }
115✔
602
                if softwareVersion == "" {
242✔
603
                        softwareVersion = artifactName
115✔
604
                }
115✔
605
        }
606
        if softwareName != "" {
210✔
607
                softwareVersionName += fmt.Sprintf(".%s", softwareName)
55✔
608
        }
55✔
609
        if softwareVersionName != "" && softwareVersion != "" {
286✔
610
                result[softwareVersionName+".version"] = softwareVersion
131✔
611
        }
131✔
612
        return result
155✔
613
}
614

615
// applySoftwareVersionToTypeInfoProvides returns a new mapping, enriched with provides
616
// for the software version; the mapping provided as argument is not modified
617
func applySoftwareVersionToTypeInfoProvides(
618
        ctx *cli.Context,
619
        typeInfoProvides artifact.TypeInfoProvides,
620
) artifact.TypeInfoProvides {
141✔
621
        result := make(map[string]string)
141✔
622
        for key, value := range typeInfoProvides {
205✔
623
                result[key] = value
64✔
624
        }
64✔
625
        artifactName := ctx.String("artifact-name")
141✔
626
        softwareFilesystem := ctx.String(softwareFilesystemFlag)
141✔
627
        softwareName := ctx.String(softwareNameFlag)
141✔
628
        softwareNameDefault := ""
141✔
629
        if ctx.Command.Name == "module-image" {
196✔
630
                softwareNameDefault = ctx.String("type")
55✔
631
        }
55✔
632
        if ctx.Command.Name == "bootstrap-artifact" {
145✔
633
                return result
4✔
634
        }
4✔
635
        softwareVersion := ctx.String(softwareVersionFlag)
137✔
636
        noDefaultSoftwareVersion := ctx.Bool(noDefaultSoftwareVersionFlag)
137✔
637
        if softwareVersionMapping := getSoftwareVersion(
137✔
638
                artifactName,
137✔
639
                softwareFilesystem,
137✔
640
                softwareName,
137✔
641
                softwareNameDefault,
137✔
642
                softwareVersion,
137✔
643
                noDefaultSoftwareVersion,
137✔
644
        ); len(softwareVersionMapping) > 0 {
254✔
645
                for key, value := range softwareVersionMapping {
234✔
646
                        if result[key] == "" || softwareVersionOverridesProvides(ctx, key) {
230✔
647
                                result[key] = value
113✔
648
                        }
113✔
649
                }
650
        }
651
        return result
137✔
652
}
653

654
func softwareVersionOverridesProvides(ctx *cli.Context, key string) bool {
6✔
655
        mainCtx := ctx.Parent().Parent()
6✔
656
        cmdLine := strings.Join(mainCtx.Args(), " ")
6✔
657

6✔
658
        var providesVersion string = `(-p|--provides)(\s+|=)` + regexp.QuoteMeta(key) + ":"
6✔
659
        reProvidesVersion := regexp.MustCompile(providesVersion)
6✔
660
        providesIndexes := reProvidesVersion.FindAllStringIndex(cmdLine, -1)
6✔
661

6✔
662
        var softareVersion string = "--software-(name|version|filesystem)"
6✔
663
        reSoftwareVersion := regexp.MustCompile(softareVersion)
6✔
664
        softwareIndexes := reSoftwareVersion.FindAllStringIndex(cmdLine, -1)
6✔
665

6✔
666
        if len(providesIndexes) == 0 {
6✔
667
                return true
×
668
        } else if len(softwareIndexes) == 0 {
8✔
669
                return false
2✔
670
        } else {
6✔
671
                return softwareIndexes[len(softwareIndexes)-1][0] >
4✔
672
                        providesIndexes[len(providesIndexes)-1][0]
4✔
673
        }
4✔
674
}
675

676
func makeClearsArtifactProvides(ctx *cli.Context) ([]string, error) {
141✔
677
        list := ctx.StringSlice(clearsProvidesFlag)
141✔
678

141✔
679
        if ctx.Bool(noDefaultClearsProvidesFlag) ||
141✔
680
                ctx.Bool(noDefaultSoftwareVersionFlag) ||
141✔
681
                ctx.Command.Name == "bootstrap-artifact" {
173✔
682
                return list, nil
32✔
683
        }
32✔
684

685
        var softwareFilesystem string
109✔
686
        if ctx.IsSet("software-filesystem") {
117✔
687
                softwareFilesystem = ctx.String("software-filesystem")
8✔
688
        } else {
109✔
689
                softwareFilesystem = "rootfs-image"
101✔
690
        }
101✔
691

692
        var softwareName string
109✔
693
        if len(ctx.String("software-name")) > 0 {
117✔
694
                softwareName = ctx.String("software-name") + "."
8✔
695
        } else if ctx.Command.Name == "rootfs-image" {
177✔
696
                softwareName = ""
68✔
697
                // "rootfs_image_checksum" is included for legacy
68✔
698
                // reasons. Previously, "rootfs_image_checksum" was the name
68✔
699
                // given to the checksum, but new artifacts follow the new dot
68✔
700
                // separated scheme, "rootfs-image.checksum", which also has the
68✔
701
                // correct dash instead of the incorrect underscore.
68✔
702
                //
68✔
703
                // "artifact_group" is included as a sane default for
68✔
704
                // rootfs-image updates. A standard rootfs-image update should
68✔
705
                // clear the group if it does not have one.
68✔
706
                if softwareFilesystem == "rootfs-image" {
134✔
707
                        list = append(list, "artifact_group", "rootfs_image_checksum")
66✔
708
                }
66✔
709
        } else if ctx.Command.Name == "module-image" {
66✔
710
                softwareName = ctx.String("type") + "."
33✔
711
        } else {
33✔
712
                return nil, errors.New(
×
713
                        "Unknown write command in makeClearsArtifactProvides(), this is a bug.",
×
714
                )
×
715
        }
×
716

717
        defaultCap := fmt.Sprintf("%s.%s*", softwareFilesystem, softwareName)
109✔
718
        for _, cap := range list {
247✔
719
                if defaultCap == cap {
140✔
720
                        // Avoid adding it twice if the default is the same as a
2✔
721
                        // specified provide.
2✔
722
                        goto dontAdd
2✔
723
                }
724
        }
725
        list = append(list, defaultCap)
107✔
726

107✔
727
dontAdd:
107✔
728
        return list, nil
109✔
729
}
730

731
func makeMetaData(ctx *cli.Context) (map[string]interface{}, map[string]interface{}, error) {
96✔
732
        var metaData map[string]interface{}
96✔
733
        var augmentMetaData map[string]interface{}
96✔
734

96✔
735
        if len(ctx.String("meta-data")) > 0 {
117✔
736
                file, err := os.Open(ctx.String("meta-data"))
21✔
737
                if err != nil {
21✔
738
                        return metaData, augmentMetaData, cli.NewExitError(err, errArtifactInvalidParameters)
×
739
                }
×
740
                defer file.Close()
21✔
741
                dec := json.NewDecoder(file)
21✔
742
                err = dec.Decode(&metaData)
21✔
743
                if err != nil {
21✔
744
                        return metaData, augmentMetaData, cli.NewExitError(err, errArtifactInvalidParameters)
×
745
                }
×
746
        }
747

748
        if len(ctx.String("augment-meta-data")) > 0 {
100✔
749
                file, err := os.Open(ctx.String("augment-meta-data"))
4✔
750
                if err != nil {
4✔
751
                        return metaData, augmentMetaData, cli.NewExitError(err, errArtifactInvalidParameters)
×
752
                }
×
753
                defer file.Close()
4✔
754
                dec := json.NewDecoder(file)
4✔
755
                err = dec.Decode(&augmentMetaData)
4✔
756
                if err != nil {
4✔
757
                        return metaData, augmentMetaData, cli.NewExitError(err, errArtifactInvalidParameters)
×
758
                }
×
759
        }
760

761
        return metaData, augmentMetaData, nil
96✔
762
}
763

764
func writeModuleImage(ctx *cli.Context) error {
57✔
765
        comp, err := artifact.NewCompressorFromId(ctx.GlobalString("compression"))
57✔
766
        if err != nil {
57✔
767
                return cli.NewExitError(
×
768
                        "compressor '"+ctx.GlobalString("compression")+"' is not supported: "+err.Error(),
×
769
                        1,
×
770
                )
×
771
        }
×
772

773
        // set the default name
774
        name := "artifact.mender"
57✔
775
        if len(ctx.String("output-path")) > 0 {
114✔
776
                name = ctx.String("output-path")
57✔
777
        }
57✔
778
        version := ctx.Int("version")
57✔
779

57✔
780
        if version == 1 {
57✔
781
                return cli.NewExitError("Mender-Artifact version 1 is not supported", 1)
×
782
        }
×
783

784
        // Check mutual exclusivity of --device-type and --compatible-types
785
        hasDeviceType := len(ctx.StringSlice("device-type")) > 0
57✔
786
        hasCompatibleTypes := len(ctx.StringSlice("compatible-types")) > 0
57✔
787
        if hasDeviceType {
112✔
788
                Log.Warn("--device-type is deprecated and will be removed in future versions, " +
55✔
789
                        "use --compatible-types instead")
55✔
790
        }
55✔
791
        if hasDeviceType && hasCompatibleTypes {
57✔
NEW
792
                return cli.NewExitError(
×
NEW
793
                        "`device-type` and `compatible-types` are mutually exclusive",
×
NEW
794
                        errArtifactInvalidParameters,
×
NEW
795
                )
×
NEW
796
        }
×
797

798
        // Either device-type or compatible-types flag is required
799
        if !hasDeviceType && !hasCompatibleTypes {
59✔
800
                return cli.NewExitError("The `device-type` or `compatible-types` flag is required", 1)
2✔
801
        }
2✔
802

803
        upd, err := makeUpdates(ctx)
55✔
804
        if err != nil {
55✔
805
                return err
×
806
        }
×
807

808
        var w io.Writer
55✔
809
        if name == "-" {
55✔
810
                w = os.Stdout
×
811
        } else {
55✔
812
                f, err := os.Create(name)
55✔
813
                if err != nil {
55✔
814
                        return cli.NewExitError(
×
815
                                "can not create artifact file: "+err.Error(),
×
816
                                errArtifactCreate,
×
817
                        )
×
818
                }
×
819
                defer f.Close()
55✔
820
                w = f
55✔
821
        }
822

823
        aw, err := artifactWriter(ctx, comp, w, version)
55✔
824
        if err != nil {
55✔
825
                return cli.NewExitError(err.Error(), 1)
×
826
        }
×
827

828
        scr, err := scripts(ctx.StringSlice("script"))
55✔
829
        if err != nil {
55✔
830
                return cli.NewExitError(err.Error(), 1)
×
831
        }
×
832

833
        compatibleDevices := getCompatibleDevices(ctx)
55✔
834
        depends := artifact.ArtifactDepends{
55✔
835
                ArtifactName:      ctx.StringSlice("artifact-name-depends"),
55✔
836
                CompatibleDevices: compatibleDevices,
55✔
837
                ArtifactGroup:     ctx.StringSlice("depends-groups"),
55✔
838
        }
55✔
839

55✔
840
        provides := artifact.ArtifactProvides{
55✔
841
                ArtifactName:  ctx.String("artifact-name"),
55✔
842
                ArtifactGroup: ctx.String("provides-group"),
55✔
843
        }
55✔
844

55✔
845
        typeInfoV3, augmentTypeInfoV3, err := makeTypeInfo(ctx)
55✔
846
        if err != nil {
55✔
847
                return err
×
848
        }
×
849

850
        metaData, augmentMetaData, err := makeMetaData(ctx)
55✔
851
        if err != nil {
55✔
852
                return err
×
853
        }
×
854

855
        if !ctx.Bool("no-progress") {
110✔
856
                ctx, cancel := context.WithCancel(context.Background())
55✔
857
                go reportProgress(ctx, aw.State)
55✔
858
                defer cancel()
55✔
859
                aw.ProgressWriter = utils.NewProgressWriter()
55✔
860
        }
55✔
861

862
        err = aw.WriteArtifact(
55✔
863
                &awriter.WriteArtifactArgs{
55✔
864
                        Format:            "mender",
55✔
865
                        Version:           version,
55✔
866
                        Devices:           compatibleDevices,
55✔
867
                        Name:              ctx.String("artifact-name"),
55✔
868
                        Updates:           upd,
55✔
869
                        Scripts:           scr,
55✔
870
                        Depends:           &depends,
55✔
871
                        Provides:          &provides,
55✔
872
                        TypeInfoV3:        typeInfoV3,
55✔
873
                        MetaData:          metaData,
55✔
874
                        AugmentTypeInfoV3: augmentTypeInfoV3,
55✔
875
                        AugmentMetaData:   augmentMetaData,
55✔
876
                })
55✔
877
        if err != nil {
55✔
878
                return cli.NewExitError(err.Error(), 1)
×
879
        }
×
880

881
        return checkArtifactSizeLimits(name, ctx)
55✔
882
}
883

884
func extractKeyValues(params []string) (*map[string]string, error) {
728✔
885
        var keyValues *map[string]string
728✔
886
        if len(params) > 0 {
804✔
887
                keyValues = &map[string]string{}
76✔
888
                for _, arg := range params {
232✔
889
                        split := strings.SplitN(arg, ":", 2)
156✔
890
                        if len(split) != 2 {
156✔
891
                                return nil, cli.NewExitError(
×
892
                                        fmt.Sprintf("argument must have a delimiting colon: %s", arg),
×
893
                                        errArtifactInvalidParameters)
×
894
                        }
×
895
                        (*keyValues)[split[0]] = split[1]
156✔
896
                }
897
        }
898
        return keyValues, nil
728✔
899
}
900

901
// SSH to remote host and dump rootfs snapshot to a local temporary file.
902
func getDeviceSnapshot(c *cli.Context) (filePath string, err error) {
×
903
        filePath = ""
×
904
        const sshConnectedToken = "Initializing snapshot..."
×
905
        ctx, cancel := context.WithCancel(context.Background())
×
906
        defer cancel()
×
907

×
908
        // Create tempfile for storing the snapshot
×
909
        f, err := os.CreateTemp("", "rootfs.tmp")
×
910
        if err != nil {
×
911
                return
×
912
        }
×
913

914
        defer removeOnPanic(f.Name())
×
915
        defer f.Close()
×
916
        // // First echo to stdout such that we know when ssh connection is
×
917
        // // established (password prompt is written to /dev/tty directly,
×
918
        // // and hence impossible to detect).
×
919
        // // When user id is 0 do not bother with sudo.
×
920
        snapshotArgs := `'[ $(id -u) -eq 0 ] || sudo_cmd="sudo -S"` +
×
921
                `; if which mender-snapshot 1> /dev/null` +
×
922
                `; then $sudo_cmd /bin/sh -c "echo ` + sshConnectedToken + `; mender-snapshot dump" | cat` +
×
923
                `; elif which mender 1> /dev/null` +
×
924
                `; then $sudo_cmd /bin/sh -c "echo ` + sshConnectedToken + `; mender snapshot dump" | cat` +
×
925
                `; else echo "Mender not found: Please check that Mender is installed" >&2 &&` +
×
926
                `exit 1; fi'`
×
927

×
928
        command, err := util.StartSSHCommand(c,
×
929
                ctx,
×
930
                cancel,
×
931
                snapshotArgs,
×
932
                sshConnectedToken,
×
933
        )
×
934
        defer func() {
×
935
                if cleanupErr := command.WaitForEchoRestore(); cleanupErr != nil {
×
936
                        filePath = ""
×
937
                        err = cleanupErr
×
938
                }
×
939
        }()
940

941
        if err != nil {
×
942
                return
×
943
        }
×
944

945
        _, err = recvSnapshot(f, command.Stdout)
×
946
        if err != nil {
×
947
                _ = command.Cmd.Process.Kill()
×
948
                return
×
949
        }
×
950

951
        err = command.EndSSHCommand()
×
952
        if err != nil {
×
953
                return
×
954
        }
×
955

956
        filePath = f.Name()
×
957
        return
×
958
}
959

960
func showProvides(c *cli.Context) (providesMap map[string]string, err error) {
×
961
        const sshConnectedToken = "Initializing show-provides..."
×
962
        ctx, cancel := context.WithCancel(context.Background())
×
963
        defer cancel()
×
964
        providesMap = nil
×
965
        tmpMap := make(map[string]string)
×
966

×
967
        providesArgs := `'[ $(id -u) -eq 0 ] || sudo_cmd="sudo -S"` +
×
968
                `; if which mender-update 1> /dev/null` +
×
969
                `; then $sudo_cmd /bin/sh -c "echo ` + sshConnectedToken + `;mender-update show-provides"` +
×
970
                `; elif which mender 1> /dev/null` +
×
971
                `; then $sudo_cmd /bin/sh -c "echo ` + sshConnectedToken + `;mender show-provides"` +
×
972
                `; else echo "Mender not found: Please check that Mender is installed" >&2 &&` +
×
973
                ` exit 1; fi'`
×
974

×
975
        command, err := util.StartSSHCommand(c,
×
976
                ctx,
×
977
                cancel,
×
978
                providesArgs,
×
979
                sshConnectedToken,
×
980
        )
×
981
        defer func() {
×
982
                if cleanupErr := command.WaitForEchoRestore(); cleanupErr != nil {
×
983
                        providesMap = nil
×
984
                        err = cleanupErr
×
985
                }
×
986
        }()
987

988
        if err != nil {
×
989
                return
×
990
        }
×
991

992
        scanner := bufio.NewScanner(command.Stdout)
×
993
        for scanner.Scan() {
×
994
                line := scanner.Text()
×
995
                if strings.HasPrefix(line, "rootfs-image.") {
×
996
                        parts := strings.SplitN(line, "=", 2)
×
997
                        if len(parts) == 2 {
×
998
                                tmpMap[parts[0]] = parts[1]
×
999
                        }
×
1000
                }
1001
        }
1002

1003
        err = command.EndSSHCommand()
×
1004
        if err != nil {
×
1005
                return
×
1006
        }
×
1007
        providesMap = tmpMap
×
1008
        return
×
1009
}
1010

1011
// Performs the same operation as io.Copy while at the same time prining
1012
// the number of bytes written at any time.
1013
func recvSnapshot(dst io.Writer, src io.Reader) (int64, error) {
×
1014
        buf := make([]byte, 1024*1024*32)
×
1015
        var written int64
×
1016
        for {
×
1017
                nr, err := src.Read(buf)
×
1018
                if err == io.EOF {
×
1019
                        fmt.Println()
×
1020
                        break
×
1021
                } else if err != nil {
×
1022
                        return written, errors.Wrap(err,
×
1023
                                "Error receiving snapshot from device")
×
1024
                }
×
1025
                nw, err := dst.Write(buf[:nr])
×
1026
                if err != nil {
×
1027
                        return written, errors.Wrap(err,
×
1028
                                "Error storing snapshot locally")
×
1029
                } else if nw < nr {
×
1030
                        return written, io.ErrShortWrite
×
1031
                }
×
1032
                written += int64(nw)
×
1033
        }
1034
        return written, nil
×
1035
}
1036

1037
func checkArtifactSizeLimits(name string, c *cli.Context) error {
137✔
1038
        if name != "-" {
274✔
1039
                if err := CheckArtifactSize(name, c); err != nil {
143✔
1040
                        return cli.NewExitError(err.Error(), errArtifactCreate)
6✔
1041
                }
6✔
1042
        } else {
×
1043
                // Inform user that size limits don't apply to stdout
×
1044
                if c.String("max-artifact-size") != "" || c.String("warn-artifact-size") != "" {
×
1045
                        Log.Info("Note: Artifact size limits are not enforced when writing to stdout")
×
1046
                }
×
1047
        }
1048
        return nil
131✔
1049
}
1050

1051
func removeOnPanic(filename string) {
×
1052
        if r := recover(); r != nil {
×
1053
                err := os.Remove(filename)
×
1054
                if err != nil {
×
1055
                        switch v := r.(type) {
×
1056
                        case string:
×
1057
                                err = errors.Wrap(errors.New(v), err.Error())
×
1058
                                panic(err)
×
1059
                        case error:
×
1060
                                err = errors.Wrap(v, err.Error())
×
1061
                                panic(err)
×
1062
                        default:
×
1063
                                panic(r)
×
1064
                        }
1065
                }
1066
                panic(r)
×
1067
        }
1068
}
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