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

mendersoftware / mender-artifact / 2085792213

07 Oct 2025 02:02PM UTC coverage: 76.248% (+0.2%) from 76.088%
2085792213

Pull #754

gitlab-ci

lluiscampos
fixup! test: Add cli `write` tests for Artifact size limits
Pull Request #754: MEN-8567: Warn and optionally fail on large Artifact sizes

111 of 129 new or added lines in 3 files covered. (86.05%)

2 existing lines in 1 file now uncovered.

6093 of 7991 relevant lines covered (76.25%)

141.33 hits per line

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

61.03
/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
func validateInput(c *cli.Context) error {
98✔
76
        // Version 2 and 3 validation.
98✔
77
        fileMissing := false
98✔
78
        if c.Command.Name != "bootstrap-artifact" {
192✔
79
                if len(c.String("file")) == 0 {
94✔
80
                        fileMissing = true
×
81
                }
×
82
        }
83
        if len(c.StringSlice("device-type")) == 0 ||
98✔
84
                len(c.String("artifact-name")) == 0 || fileMissing {
98✔
85
                return cli.NewExitError(
×
86
                        "must provide `device-type`, `artifact-name` and `file`",
×
87
                        errArtifactInvalidParameters,
×
88
                )
×
89
        }
×
90
        if len(strings.Fields(c.String("artifact-name"))) > 1 {
100✔
91
                // check for whitespace in artifact-name
2✔
92
                return cli.NewExitError(
2✔
93
                        "whitespace is not allowed in the artifact-name",
2✔
94
                        errArtifactInvalidParameters,
2✔
95
                )
2✔
96
        }
2✔
97
        return nil
96✔
98
}
99

100
func createRootfsFromSSH(c *cli.Context) (string, error) {
×
101
        _, err := utils.GetBinaryPath("blkid")
×
102
        if err != nil {
×
103
                Log.Warnf("Skipping running fsck on the Artifact: %v", errBlkidNotFound)
×
104
        }
×
105
        rootfsFilename, err := getDeviceSnapshot(c)
×
106
        if err != nil {
×
107
                return rootfsFilename, cli.NewExitError("SSH error: "+err.Error(), 1)
×
108
        }
×
109

110
        // check for blkid and get filesystem type
111
        fstype, err := imgFilesystemType(rootfsFilename)
×
112
        if err != nil {
×
113
                if err == errBlkidNotFound {
×
114
                        return rootfsFilename, nil
×
115
                }
×
116
                return rootfsFilename, cli.NewExitError(
×
117
                        "imgFilesystemType error: "+err.Error(),
×
118
                        errArtifactCreate,
×
119
                )
×
120
        }
121

122
        // run fsck
123
        switch fstype {
×
124
        case fat:
×
125
                err = runFsck(rootfsFilename, "vfat")
×
126
        case ext:
×
127
                err = runFsck(rootfsFilename, "ext4")
×
128
        case unsupported:
×
129
                err = errors.New("createRootfsFromSSH: unsupported filesystem")
×
130

131
        }
132
        if err != nil {
×
133
                return rootfsFilename, cli.NewExitError("runFsck error: "+err.Error(), errArtifactCreate)
×
134
        }
×
135

136
        return rootfsFilename, nil
×
137
}
138

139
func makeEmptyUpdates(ctx *cli.Context) (*awriter.Updates, error) {
4✔
140
        handler := handlers.NewBootstrapArtifact()
4✔
141

4✔
142
        dataFiles := make([](*handlers.DataFile), 0)
4✔
143
        if err := handler.SetUpdateFiles(dataFiles); err != nil {
4✔
144
                return nil, cli.NewExitError(
×
145
                        err,
×
146
                        1,
×
147
                )
×
148
        }
×
149

150
        upd := &awriter.Updates{
4✔
151
                Updates: []handlers.Composer{handler},
4✔
152
        }
4✔
153
        return upd, nil
4✔
154
}
155

156
func writeBootstrapArtifact(c *cli.Context) error {
4✔
157
        comp, err := artifact.NewCompressorFromId(c.GlobalString("compression"))
4✔
158
        if err != nil {
4✔
159
                return cli.NewExitError(
×
160
                        "compressor '"+c.GlobalString("compression")+"' is not supported: "+err.Error(),
×
161
                        1,
×
162
                )
×
163
        }
×
164

165
        if err := validateInput(c); err != nil {
4✔
166
                Log.Error(err.Error())
×
167
                return err
×
168
        }
×
169

170
        // set the default name
171
        name := "artifact.mender"
4✔
172
        if len(c.String("output-path")) > 0 {
8✔
173
                name = c.String("output-path")
4✔
174
        }
4✔
175
        version := c.Int("version")
4✔
176

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

4✔
179
        var w io.Writer
4✔
180
        if name == "-" {
4✔
181
                w = os.Stdout
×
182
        } else {
4✔
183
                f, err := os.Create(name)
4✔
184
                if err != nil {
4✔
185
                        return cli.NewExitError(
×
186
                                "can not create bootstrap artifact file: "+err.Error(),
×
187
                                errArtifactCreate,
×
188
                        )
×
189
                }
×
190
                defer f.Close()
4✔
191
                w = f
4✔
192
        }
193

194
        aw, err := artifactWriter(c, comp, w, version)
4✔
195
        if err != nil {
4✔
196
                return cli.NewExitError(err.Error(), 1)
×
197
        }
×
198

199
        depends := artifact.ArtifactDepends{
4✔
200
                ArtifactName:      c.StringSlice("artifact-name-depends"),
4✔
201
                CompatibleDevices: c.StringSlice("device-type"),
4✔
202
                ArtifactGroup:     c.StringSlice("depends-groups"),
4✔
203
        }
4✔
204

4✔
205
        provides := artifact.ArtifactProvides{
4✔
206
                ArtifactName:  c.String("artifact-name"),
4✔
207
                ArtifactGroup: c.String("provides-group"),
4✔
208
        }
4✔
209

4✔
210
        upd, err := makeEmptyUpdates(c)
4✔
211
        if err != nil {
4✔
212
                return err
×
213
        }
×
214

215
        typeInfoV3, _, err := makeTypeInfo(c)
4✔
216
        if err != nil {
4✔
217
                return err
×
218
        }
×
219

220
        if !c.Bool("no-progress") {
8✔
221
                ctx, cancel := context.WithCancel(context.Background())
4✔
222
                go reportProgress(ctx, aw.State)
4✔
223
                defer cancel()
4✔
224
                aw.ProgressWriter = utils.NewProgressWriter()
4✔
225
        }
4✔
226

227
        err = aw.WriteArtifact(
4✔
228
                &awriter.WriteArtifactArgs{
4✔
229
                        Format:     "mender",
4✔
230
                        Version:    version,
4✔
231
                        Devices:    c.StringSlice("device-type"),
4✔
232
                        Name:       c.String("artifact-name"),
4✔
233
                        Updates:    upd,
4✔
234
                        Scripts:    nil,
4✔
235
                        Depends:    &depends,
4✔
236
                        Provides:   &provides,
4✔
237
                        TypeInfoV3: typeInfoV3,
4✔
238
                        Bootstrap:  true,
4✔
239
                })
4✔
240
        if err != nil {
4✔
241
                return cli.NewExitError(err.Error(), 1)
×
242
        }
×
243
        return nil
4✔
244
}
245

246
func writeRootfs(c *cli.Context) error {
94✔
247
        comp, err := artifact.NewCompressorFromId(c.GlobalString("compression"))
94✔
248
        if err != nil {
94✔
249
                return cli.NewExitError(
×
250
                        "compressor '"+c.GlobalString("compression")+"' is not supported: "+err.Error(),
×
251
                        1,
×
252
                )
×
253
        }
×
254

255
        if err := validateInput(c); err != nil {
96✔
256
                Log.Error(err.Error())
2✔
257
                return err
2✔
258
        }
2✔
259

260
        // set the default name
261
        name := "artifact.mender"
92✔
262
        if len(c.String("output-path")) > 0 {
184✔
263
                name = c.String("output-path")
92✔
264
        }
92✔
265
        version := c.Int("version")
92✔
266
        var showprovides map[string]string
92✔
267

92✔
268
        Log.Debugf("creating artifact [%s], version: %d", name, version)
92✔
269
        rootfsFilename := c.String("file")
92✔
270
        if strings.HasPrefix(rootfsFilename, "ssh://") {
92✔
271
                rootfsFilename, err = createRootfsFromSSH(c)
×
272
                defer os.Remove(rootfsFilename)
×
273
                if err != nil {
×
274
                        return cli.NewExitError(err.Error(), errArtifactCreate)
×
275
                }
×
276
                showprovides, err = showProvides(c)
×
277
                if err != nil {
×
278
                        return cli.NewExitError(err.Error(), errArtifactCreate)
×
279
                }
×
280
        }
281

282
        var h handlers.Composer
92✔
283
        switch version {
92✔
284
        case 2:
2✔
285
                h = handlers.NewRootfsV2(rootfsFilename)
2✔
286
        case 3:
84✔
287
                h = handlers.NewRootfsV3(rootfsFilename)
84✔
288
        default:
6✔
289
                return cli.NewExitError(
6✔
290
                        fmt.Sprintf("Artifact version %d is not supported", version),
6✔
291
                        errArtifactUnsupportedVersion,
6✔
292
                )
6✔
293
        }
294

295
        upd := &awriter.Updates{
86✔
296
                Updates: []handlers.Composer{h},
86✔
297
        }
86✔
298

86✔
299
        var w io.Writer
86✔
300
        if name == "-" {
86✔
301
                w = os.Stdout
×
302
        } else {
86✔
303
                f, err := os.Create(name)
86✔
304
                if err != nil {
86✔
305
                        return cli.NewExitError(
×
306
                                "can not create artifact file: "+err.Error(),
×
307
                                errArtifactCreate,
×
308
                        )
×
309
                }
×
310
                defer f.Close()
86✔
311
                w = f
86✔
312
        }
313

314
        aw, err := artifactWriter(c, comp, w, version)
86✔
315
        if err != nil {
88✔
316
                return cli.NewExitError(err.Error(), 1)
2✔
317
        }
2✔
318

319
        scr, err := scripts(c.StringSlice("script"))
84✔
320
        if err != nil {
86✔
321
                return cli.NewExitError(err.Error(), 1)
2✔
322
        }
2✔
323

324
        depends := artifact.ArtifactDepends{
82✔
325
                ArtifactName:      c.StringSlice("artifact-name-depends"),
82✔
326
                CompatibleDevices: c.StringSlice("device-type"),
82✔
327
                ArtifactGroup:     c.StringSlice("depends-groups"),
82✔
328
        }
82✔
329

82✔
330
        provides := artifact.ArtifactProvides{
82✔
331
                ArtifactName:  c.String("artifact-name"),
82✔
332
                ArtifactGroup: c.String("provides-group"),
82✔
333
        }
82✔
334

82✔
335
        typeInfoV3, _, err := makeTypeInfo(c)
82✔
336
        if err != nil {
82✔
337
                return err
×
338
        }
×
339
        for k, v := range showprovides {
82✔
340
                _, exist := typeInfoV3.ArtifactProvides[k]
×
341
                if !exist {
×
342
                        typeInfoV3.ArtifactProvides[k] = v
×
343
                }
×
344
        }
345

346
        if !c.Bool("no-checksum-provide") {
160✔
347
                legacy := c.Bool("legacy-rootfs-image-checksum")
78✔
348
                if err = writeRootfsImageChecksum(rootfsFilename, typeInfoV3, legacy); err != nil {
78✔
349
                        return cli.NewExitError(
×
350
                                errors.Wrap(err, "Failed to write the `rootfs-image.checksum` to the artifact"),
×
351
                                1,
×
352
                        )
×
353
                }
×
354
        }
355
        if !c.Bool("no-progress") {
164✔
356
                ctx, cancel := context.WithCancel(context.Background())
82✔
357
                go reportProgress(ctx, aw.State)
82✔
358
                defer cancel()
82✔
359
                aw.ProgressWriter = utils.NewProgressWriter()
82✔
360
        }
82✔
361

362
        err = aw.WriteArtifact(
82✔
363
                &awriter.WriteArtifactArgs{
82✔
364
                        Format:     "mender",
82✔
365
                        Version:    version,
82✔
366
                        Devices:    c.StringSlice("device-type"),
82✔
367
                        Name:       c.String("artifact-name"),
82✔
368
                        Updates:    upd,
82✔
369
                        Scripts:    scr,
82✔
370
                        Depends:    &depends,
82✔
371
                        Provides:   &provides,
82✔
372
                        TypeInfoV3: typeInfoV3,
82✔
373
                })
82✔
374
        if err != nil {
82✔
375
                return cli.NewExitError(err.Error(), 1)
×
376
        }
×
377

378
        // Check artifact size if output is a file (not stdout)
379
        if name != "-" {
164✔
380
                if err := CheckArtifactSize(name, c); err != nil {
86✔
381
                        return cli.NewExitError(err.Error(), errArtifactCreate)
4✔
382
                }
4✔
NEW
383
        } else {
×
NEW
384
                // Inform user that size limits don't apply to stdout
×
NEW
385
                if c.String("max-payload-size") != "" || c.String("warn-payload-size") != "" {
×
NEW
386
                        Log.Info("Note: Payload size limits are not enforced when writing to stdout")
×
NEW
387
                }
×
388
        }
389

390
        return nil
78✔
391
}
392

393
func reportProgress(c context.Context, state chan string) {
141✔
394
        fmt.Fprintln(os.Stderr, "Writing Artifact...")
141✔
395
        str := fmt.Sprintf("%-20s\t", <-state)
141✔
396
        fmt.Fprint(os.Stderr, str)
141✔
397
        for {
844✔
398
                select {
703✔
399
                case str = <-state:
563✔
400
                        if str == stage.Data {
704✔
401
                                fmt.Fprintf(os.Stderr, "\033[1;32m\u2713\033[0m\n")
141✔
402
                                fmt.Fprintln(os.Stderr, "Payload")
141✔
403
                        } else {
564✔
404
                                fmt.Fprintf(os.Stderr, "\033[1;32m\u2713\033[0m\n")
423✔
405
                                str = fmt.Sprintf("%-20s\t", str)
423✔
406
                                fmt.Fprint(os.Stderr, str)
423✔
407
                        }
423✔
408
                case <-c.Done():
141✔
409
                        return
141✔
410
                }
411
        }
412
}
413

414
func artifactWriter(c *cli.Context, comp artifact.Compressor, w io.Writer,
415
        ver int) (*awriter.Writer, error) {
145✔
416
        privateKey, err := getKey(c)
145✔
417
        if err != nil {
147✔
418
                return nil, err
2✔
419
        }
2✔
420
        if privateKey != nil {
153✔
421
                if ver == 0 {
10✔
422
                        // check if we are having correct version
×
423
                        return nil, errors.New("can not use signed artifact with version 0")
×
424
                }
×
425
                return awriter.NewWriterSigned(w, comp, privateKey), nil
10✔
426
        }
427
        return awriter.NewWriter(w, comp), nil
133✔
428
}
429

430
func makeUpdates(ctx *cli.Context) (*awriter.Updates, error) {
55✔
431
        version := ctx.Int("version")
55✔
432

55✔
433
        var handler, augmentHandler handlers.Composer
55✔
434
        switch version {
55✔
435
        case 2:
×
436
                return nil, cli.NewExitError(
×
437
                        "Module images need at least artifact format version 3",
×
438
                        errArtifactInvalidParameters)
×
439
        case 3:
55✔
440
                handler = handlers.NewModuleImage(ctx.String("type"))
55✔
441
        default:
×
442
                return nil, cli.NewExitError(
×
443
                        fmt.Sprintf("unsupported artifact version: %v", version),
×
444
                        errArtifactUnsupportedVersion,
×
445
                )
×
446
        }
447

448
        dataFiles := make([](*handlers.DataFile), 0, len(ctx.StringSlice("file")))
55✔
449
        for _, file := range ctx.StringSlice("file") {
113✔
450
                dataFiles = append(dataFiles, &handlers.DataFile{Name: file})
58✔
451
        }
58✔
452
        if err := handler.SetUpdateFiles(dataFiles); err != nil {
55✔
453
                return nil, cli.NewExitError(
×
454
                        err,
×
455
                        1,
×
456
                )
×
457
        }
×
458

459
        upd := &awriter.Updates{
55✔
460
                Updates: []handlers.Composer{handler},
55✔
461
        }
55✔
462

55✔
463
        if ctx.String("augment-type") != "" {
59✔
464
                augmentHandler = handlers.NewAugmentedModuleImage(handler, ctx.String("augment-type"))
4✔
465
                dataFiles = make([](*handlers.DataFile), 0, len(ctx.StringSlice("augment-file")))
4✔
466
                for _, file := range ctx.StringSlice("augment-file") {
8✔
467
                        dataFiles = append(dataFiles, &handlers.DataFile{Name: file})
4✔
468
                }
4✔
469
                if err := augmentHandler.SetUpdateAugmentFiles(dataFiles); err != nil {
4✔
470
                        return nil, cli.NewExitError(
×
471
                                err,
×
472
                                1,
×
473
                        )
×
474
                }
×
475
                upd.Augments = []handlers.Composer{augmentHandler}
4✔
476
        }
477

478
        return upd, nil
55✔
479
}
480

481
// makeTypeInfo returns the type-info provides and depends and the augmented
482
// type-info provides and depends, or nil.
483
func makeTypeInfo(ctx *cli.Context) (*artifact.TypeInfoV3, *artifact.TypeInfoV3, error) {
141✔
484
        // Make key value pairs from the type-info fields supplied on command
141✔
485
        // line.
141✔
486
        var keyValues *map[string]string
141✔
487

141✔
488
        var typeInfoDepends artifact.TypeInfoDepends
141✔
489
        keyValues, err := extractKeyValues(ctx.StringSlice("depends"))
141✔
490
        if err != nil {
141✔
491
                return nil, nil, err
×
492
        } else if keyValues != nil {
171✔
493
                if typeInfoDepends, err = artifact.NewTypeInfoDepends(*keyValues); err != nil {
30✔
494
                        return nil, nil, err
×
495
                }
×
496
        }
497

498
        var typeInfoProvides artifact.TypeInfoProvides
141✔
499
        keyValues, err = extractKeyValues(ctx.StringSlice("provides"))
141✔
500
        if err != nil {
141✔
501
                return nil, nil, err
×
502
        } else if keyValues != nil {
173✔
503
                if typeInfoProvides, err = artifact.NewTypeInfoProvides(*keyValues); err != nil {
32✔
504
                        return nil, nil, err
×
505
                }
×
506
        }
507
        typeInfoProvides = applySoftwareVersionToTypeInfoProvides(ctx, typeInfoProvides)
141✔
508

141✔
509
        var augmentTypeInfoDepends artifact.TypeInfoDepends
141✔
510
        keyValues, err = extractKeyValues(ctx.StringSlice("augment-depends"))
141✔
511
        if err != nil {
141✔
512
                return nil, nil, err
×
513
        } else if keyValues != nil {
145✔
514
                if augmentTypeInfoDepends, err = artifact.NewTypeInfoDepends(*keyValues); err != nil {
4✔
515
                        return nil, nil, err
×
516
                }
×
517
        }
518

519
        var augmentTypeInfoProvides artifact.TypeInfoProvides
141✔
520
        keyValues, err = extractKeyValues(ctx.StringSlice("augment-provides"))
141✔
521
        if err != nil {
141✔
522
                return nil, nil, err
×
523
        } else if keyValues != nil {
145✔
524
                if augmentTypeInfoProvides, err = artifact.NewTypeInfoProvides(*keyValues); err != nil {
4✔
525
                        return nil, nil, err
×
526
                }
×
527
        }
528

529
        clearsArtifactProvides, err := makeClearsArtifactProvides(ctx)
141✔
530
        if err != nil {
141✔
531
                return nil, nil, err
×
532
        }
×
533

534
        var typeInfo *string
141✔
535
        if ctx.Command.Name != "bootstrap-artifact" {
278✔
536
                typeFlag := ctx.String("type")
137✔
537
                typeInfo = &typeFlag
137✔
538
        }
137✔
539
        typeInfoV3 := &artifact.TypeInfoV3{
141✔
540
                Type:                   typeInfo,
141✔
541
                ArtifactDepends:        typeInfoDepends,
141✔
542
                ArtifactProvides:       typeInfoProvides,
141✔
543
                ClearsArtifactProvides: clearsArtifactProvides,
141✔
544
        }
141✔
545

141✔
546
        if ctx.String("augment-type") == "" {
278✔
547
                // Non-augmented artifact
137✔
548
                if len(ctx.StringSlice("augment-file")) != 0 ||
137✔
549
                        len(ctx.StringSlice("augment-depends")) != 0 ||
137✔
550
                        len(ctx.StringSlice("augment-provides")) != 0 ||
137✔
551
                        ctx.String("augment-meta-data") != "" {
137✔
552

×
553
                        err = errors.New("Must give --augment-type argument if making augmented artifact")
×
554
                        fmt.Println(err.Error())
×
555
                        return nil, nil, err
×
556
                }
×
557
                return typeInfoV3, nil, nil
137✔
558
        }
559

560
        augmentType := ctx.String("augment-type")
4✔
561
        augmentTypeInfoV3 := &artifact.TypeInfoV3{
4✔
562
                Type:             &augmentType,
4✔
563
                ArtifactDepends:  augmentTypeInfoDepends,
4✔
564
                ArtifactProvides: augmentTypeInfoProvides,
4✔
565
        }
4✔
566

4✔
567
        return typeInfoV3, augmentTypeInfoV3, nil
4✔
568
}
569

570
func getSoftwareVersion(
571
        artifactName,
572
        softwareFilesystem,
573
        softwareName,
574
        softwareNameDefault,
575
        softwareVersion string,
576
        noDefaultSoftwareVersion bool,
577
) map[string]string {
155✔
578
        result := map[string]string{}
155✔
579
        softwareVersionName := "rootfs-image"
155✔
580
        if softwareFilesystem != "" {
171✔
581
                softwareVersionName = softwareFilesystem
16✔
582
        }
16✔
583
        if !noDefaultSoftwareVersion {
282✔
584
                if softwareName == "" {
242✔
585
                        softwareName = softwareNameDefault
115✔
586
                }
115✔
587
                if softwareVersion == "" {
242✔
588
                        softwareVersion = artifactName
115✔
589
                }
115✔
590
        }
591
        if softwareName != "" {
210✔
592
                softwareVersionName += fmt.Sprintf(".%s", softwareName)
55✔
593
        }
55✔
594
        if softwareVersionName != "" && softwareVersion != "" {
286✔
595
                result[softwareVersionName+".version"] = softwareVersion
131✔
596
        }
131✔
597
        return result
155✔
598
}
599

600
// applySoftwareVersionToTypeInfoProvides returns a new mapping, enriched with provides
601
// for the software version; the mapping provided as argument is not modified
602
func applySoftwareVersionToTypeInfoProvides(
603
        ctx *cli.Context,
604
        typeInfoProvides artifact.TypeInfoProvides,
605
) artifact.TypeInfoProvides {
141✔
606
        result := make(map[string]string)
141✔
607
        for key, value := range typeInfoProvides {
205✔
608
                result[key] = value
64✔
609
        }
64✔
610
        artifactName := ctx.String("artifact-name")
141✔
611
        softwareFilesystem := ctx.String(softwareFilesystemFlag)
141✔
612
        softwareName := ctx.String(softwareNameFlag)
141✔
613
        softwareNameDefault := ""
141✔
614
        if ctx.Command.Name == "module-image" {
196✔
615
                softwareNameDefault = ctx.String("type")
55✔
616
        }
55✔
617
        if ctx.Command.Name == "bootstrap-artifact" {
145✔
618
                return result
4✔
619
        }
4✔
620
        softwareVersion := ctx.String(softwareVersionFlag)
137✔
621
        noDefaultSoftwareVersion := ctx.Bool(noDefaultSoftwareVersionFlag)
137✔
622
        if softwareVersionMapping := getSoftwareVersion(
137✔
623
                artifactName,
137✔
624
                softwareFilesystem,
137✔
625
                softwareName,
137✔
626
                softwareNameDefault,
137✔
627
                softwareVersion,
137✔
628
                noDefaultSoftwareVersion,
137✔
629
        ); len(softwareVersionMapping) > 0 {
254✔
630
                for key, value := range softwareVersionMapping {
234✔
631
                        if result[key] == "" || softwareVersionOverridesProvides(ctx, key) {
230✔
632
                                result[key] = value
113✔
633
                        }
113✔
634
                }
635
        }
636
        return result
137✔
637
}
638

639
func softwareVersionOverridesProvides(ctx *cli.Context, key string) bool {
6✔
640
        mainCtx := ctx.Parent().Parent()
6✔
641
        cmdLine := strings.Join(mainCtx.Args(), " ")
6✔
642

6✔
643
        var providesVersion string = `(-p|--provides)(\s+|=)` + regexp.QuoteMeta(key) + ":"
6✔
644
        reProvidesVersion := regexp.MustCompile(providesVersion)
6✔
645
        providesIndexes := reProvidesVersion.FindAllStringIndex(cmdLine, -1)
6✔
646

6✔
647
        var softareVersion string = "--software-(name|version|filesystem)"
6✔
648
        reSoftwareVersion := regexp.MustCompile(softareVersion)
6✔
649
        softwareIndexes := reSoftwareVersion.FindAllStringIndex(cmdLine, -1)
6✔
650

6✔
651
        if len(providesIndexes) == 0 {
6✔
652
                return true
×
653
        } else if len(softwareIndexes) == 0 {
8✔
654
                return false
2✔
655
        } else {
6✔
656
                return softwareIndexes[len(softwareIndexes)-1][0] >
4✔
657
                        providesIndexes[len(providesIndexes)-1][0]
4✔
658
        }
4✔
659
}
660

661
func makeClearsArtifactProvides(ctx *cli.Context) ([]string, error) {
141✔
662
        list := ctx.StringSlice(clearsProvidesFlag)
141✔
663

141✔
664
        if ctx.Bool(noDefaultClearsProvidesFlag) ||
141✔
665
                ctx.Bool(noDefaultSoftwareVersionFlag) ||
141✔
666
                ctx.Command.Name == "bootstrap-artifact" {
173✔
667
                return list, nil
32✔
668
        }
32✔
669

670
        var softwareFilesystem string
109✔
671
        if ctx.IsSet("software-filesystem") {
117✔
672
                softwareFilesystem = ctx.String("software-filesystem")
8✔
673
        } else {
109✔
674
                softwareFilesystem = "rootfs-image"
101✔
675
        }
101✔
676

677
        var softwareName string
109✔
678
        if len(ctx.String("software-name")) > 0 {
117✔
679
                softwareName = ctx.String("software-name") + "."
8✔
680
        } else if ctx.Command.Name == "rootfs-image" {
177✔
681
                softwareName = ""
68✔
682
                // "rootfs_image_checksum" is included for legacy
68✔
683
                // reasons. Previously, "rootfs_image_checksum" was the name
68✔
684
                // given to the checksum, but new artifacts follow the new dot
68✔
685
                // separated scheme, "rootfs-image.checksum", which also has the
68✔
686
                // correct dash instead of the incorrect underscore.
68✔
687
                //
68✔
688
                // "artifact_group" is included as a sane default for
68✔
689
                // rootfs-image updates. A standard rootfs-image update should
68✔
690
                // clear the group if it does not have one.
68✔
691
                if softwareFilesystem == "rootfs-image" {
134✔
692
                        list = append(list, "artifact_group", "rootfs_image_checksum")
66✔
693
                }
66✔
694
        } else if ctx.Command.Name == "module-image" {
66✔
695
                softwareName = ctx.String("type") + "."
33✔
696
        } else {
33✔
697
                return nil, errors.New(
×
698
                        "Unknown write command in makeClearsArtifactProvides(), this is a bug.",
×
699
                )
×
700
        }
×
701

702
        defaultCap := fmt.Sprintf("%s.%s*", softwareFilesystem, softwareName)
109✔
703
        for _, cap := range list {
247✔
704
                if defaultCap == cap {
140✔
705
                        // Avoid adding it twice if the default is the same as a
2✔
706
                        // specified provide.
2✔
707
                        goto dontAdd
2✔
708
                }
709
        }
710
        list = append(list, defaultCap)
107✔
711

107✔
712
dontAdd:
107✔
713
        return list, nil
109✔
714
}
715

716
func makeMetaData(ctx *cli.Context) (map[string]interface{}, map[string]interface{}, error) {
96✔
717
        var metaData map[string]interface{}
96✔
718
        var augmentMetaData map[string]interface{}
96✔
719

96✔
720
        if len(ctx.String("meta-data")) > 0 {
117✔
721
                file, err := os.Open(ctx.String("meta-data"))
21✔
722
                if err != nil {
21✔
723
                        return metaData, augmentMetaData, cli.NewExitError(err, errArtifactInvalidParameters)
×
724
                }
×
725
                defer file.Close()
21✔
726
                dec := json.NewDecoder(file)
21✔
727
                err = dec.Decode(&metaData)
21✔
728
                if err != nil {
21✔
729
                        return metaData, augmentMetaData, cli.NewExitError(err, errArtifactInvalidParameters)
×
730
                }
×
731
        }
732

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

746
        return metaData, augmentMetaData, nil
96✔
747
}
748

749
func writeModuleImage(ctx *cli.Context) error {
55✔
750
        comp, err := artifact.NewCompressorFromId(ctx.GlobalString("compression"))
55✔
751
        if err != nil {
55✔
752
                return cli.NewExitError(
×
753
                        "compressor '"+ctx.GlobalString("compression")+"' is not supported: "+err.Error(),
×
754
                        1,
×
755
                )
×
756
        }
×
757

758
        // set the default name
759
        name := "artifact.mender"
55✔
760
        if len(ctx.String("output-path")) > 0 {
110✔
761
                name = ctx.String("output-path")
55✔
762
        }
55✔
763
        version := ctx.Int("version")
55✔
764

55✔
765
        if version == 1 {
55✔
766
                return cli.NewExitError("Mender-Artifact version 1 is not supported", 1)
×
767
        }
×
768

769
        // The device-type flag is required
770
        if len(ctx.StringSlice("device-type")) == 0 {
55✔
771
                return cli.NewExitError("The `device-type` flag is required", 1)
×
772
        }
×
773

774
        upd, err := makeUpdates(ctx)
55✔
775
        if err != nil {
55✔
776
                return err
×
777
        }
×
778

779
        var w io.Writer
55✔
780
        if name == "-" {
55✔
781
                w = os.Stdout
×
782
        } else {
55✔
783
                f, err := os.Create(name)
55✔
784
                if err != nil {
55✔
785
                        return cli.NewExitError(
×
786
                                "can not create artifact file: "+err.Error(),
×
787
                                errArtifactCreate,
×
788
                        )
×
789
                }
×
790
                defer f.Close()
55✔
791
                w = f
55✔
792
        }
793

794
        aw, err := artifactWriter(ctx, comp, w, version)
55✔
795
        if err != nil {
55✔
796
                return cli.NewExitError(err.Error(), 1)
×
797
        }
×
798

799
        scr, err := scripts(ctx.StringSlice("script"))
55✔
800
        if err != nil {
55✔
801
                return cli.NewExitError(err.Error(), 1)
×
802
        }
×
803

804
        depends := artifact.ArtifactDepends{
55✔
805
                ArtifactName:      ctx.StringSlice("artifact-name-depends"),
55✔
806
                CompatibleDevices: ctx.StringSlice("device-type"),
55✔
807
                ArtifactGroup:     ctx.StringSlice("depends-groups"),
55✔
808
        }
55✔
809

55✔
810
        provides := artifact.ArtifactProvides{
55✔
811
                ArtifactName:  ctx.String("artifact-name"),
55✔
812
                ArtifactGroup: ctx.String("provides-group"),
55✔
813
        }
55✔
814

55✔
815
        typeInfoV3, augmentTypeInfoV3, err := makeTypeInfo(ctx)
55✔
816
        if err != nil {
55✔
817
                return err
×
818
        }
×
819

820
        metaData, augmentMetaData, err := makeMetaData(ctx)
55✔
821
        if err != nil {
55✔
822
                return err
×
823
        }
×
824

825
        if !ctx.Bool("no-progress") {
110✔
826
                ctx, cancel := context.WithCancel(context.Background())
55✔
827
                go reportProgress(ctx, aw.State)
55✔
828
                defer cancel()
55✔
829
                aw.ProgressWriter = utils.NewProgressWriter()
55✔
830
        }
55✔
831

832
        err = aw.WriteArtifact(
55✔
833
                &awriter.WriteArtifactArgs{
55✔
834
                        Format:            "mender",
55✔
835
                        Version:           version,
55✔
836
                        Devices:           ctx.StringSlice("device-type"),
55✔
837
                        Name:              ctx.String("artifact-name"),
55✔
838
                        Updates:           upd,
55✔
839
                        Scripts:           scr,
55✔
840
                        Depends:           &depends,
55✔
841
                        Provides:          &provides,
55✔
842
                        TypeInfoV3:        typeInfoV3,
55✔
843
                        MetaData:          metaData,
55✔
844
                        AugmentTypeInfoV3: augmentTypeInfoV3,
55✔
845
                        AugmentMetaData:   augmentMetaData,
55✔
846
                })
55✔
847
        if err != nil {
55✔
848
                return cli.NewExitError(err.Error(), 1)
×
849
        }
×
850

851
        // Check artifact size if output is a file (not stdout)
852
        if name != "-" {
110✔
853
                if err := CheckArtifactSize(name, ctx); err != nil {
57✔
854
                        return cli.NewExitError(err.Error(), errArtifactCreate)
2✔
855
                }
2✔
NEW
856
        } else {
×
NEW
857
                // Inform user that size limits don't apply to stdout
×
NEW
858
                if ctx.String("max-payload-size") != "" || ctx.String("warn-payload-size") != "" {
×
NEW
859
                        Log.Info("Note: Payload size limits are not enforced when writing to stdout")
×
NEW
860
                }
×
861
        }
862

863
        return nil
53✔
864
}
865

866
func extractKeyValues(params []string) (*map[string]string, error) {
728✔
867
        var keyValues *map[string]string
728✔
868
        if len(params) > 0 {
804✔
869
                keyValues = &map[string]string{}
76✔
870
                for _, arg := range params {
232✔
871
                        split := strings.SplitN(arg, ":", 2)
156✔
872
                        if len(split) != 2 {
156✔
873
                                return nil, cli.NewExitError(
×
874
                                        fmt.Sprintf("argument must have a delimiting colon: %s", arg),
×
875
                                        errArtifactInvalidParameters)
×
876
                        }
×
877
                        (*keyValues)[split[0]] = split[1]
156✔
878
                }
879
        }
880
        return keyValues, nil
728✔
881
}
882

883
// SSH to remote host and dump rootfs snapshot to a local temporary file.
884
func getDeviceSnapshot(c *cli.Context) (filePath string, err error) {
×
885
        filePath = ""
×
886
        const sshConnectedToken = "Initializing snapshot..."
×
887
        ctx, cancel := context.WithCancel(context.Background())
×
888
        defer cancel()
×
889

×
890
        // Create tempfile for storing the snapshot
×
891
        f, err := os.CreateTemp("", "rootfs.tmp")
×
892
        if err != nil {
×
893
                return
×
894
        }
×
895

896
        defer removeOnPanic(f.Name())
×
897
        defer f.Close()
×
898
        // // First echo to stdout such that we know when ssh connection is
×
899
        // // established (password prompt is written to /dev/tty directly,
×
900
        // // and hence impossible to detect).
×
901
        // // When user id is 0 do not bother with sudo.
×
902
        snapshotArgs := `'[ $(id -u) -eq 0 ] || sudo_cmd="sudo -S"` +
×
903
                `; if which mender-snapshot 1> /dev/null` +
×
904
                `; then $sudo_cmd /bin/sh -c "echo ` + sshConnectedToken + `; mender-snapshot dump" | cat` +
×
905
                `; elif which mender 1> /dev/null` +
×
906
                `; then $sudo_cmd /bin/sh -c "echo ` + sshConnectedToken + `; mender snapshot dump" | cat` +
×
907
                `; else echo "Mender not found: Please check that Mender is installed" >&2 &&` +
×
908
                `exit 1; fi'`
×
909

×
910
        command, err := util.StartSSHCommand(c,
×
911
                ctx,
×
912
                cancel,
×
913
                snapshotArgs,
×
914
                sshConnectedToken,
×
915
        )
×
916
        defer func() {
×
917
                if cleanupErr := command.WaitForEchoRestore(); cleanupErr != nil {
×
918
                        filePath = ""
×
919
                        err = cleanupErr
×
920
                }
×
921
        }()
922

923
        if err != nil {
×
924
                return
×
925
        }
×
926

927
        _, err = recvSnapshot(f, command.Stdout)
×
928
        if err != nil {
×
929
                _ = command.Cmd.Process.Kill()
×
930
                return
×
931
        }
×
932

933
        err = command.EndSSHCommand()
×
934
        if err != nil {
×
935
                return
×
936
        }
×
937

938
        filePath = f.Name()
×
939
        return
×
940
}
941

942
func showProvides(c *cli.Context) (providesMap map[string]string, err error) {
×
943
        const sshConnectedToken = "Initializing show-provides..."
×
944
        ctx, cancel := context.WithCancel(context.Background())
×
945
        defer cancel()
×
946
        providesMap = nil
×
947
        tmpMap := make(map[string]string)
×
948

×
949
        providesArgs := `'[ $(id -u) -eq 0 ] || sudo_cmd="sudo -S"` +
×
950
                `; if which mender-update 1> /dev/null` +
×
951
                `; then $sudo_cmd /bin/sh -c "echo ` + sshConnectedToken + `;mender-update show-provides"` +
×
952
                `; elif which mender 1> /dev/null` +
×
953
                `; then $sudo_cmd /bin/sh -c "echo ` + sshConnectedToken + `;mender show-provides"` +
×
954
                `; else echo "Mender not found: Please check that Mender is installed" >&2 &&` +
×
955
                ` exit 1; fi'`
×
956

×
957
        command, err := util.StartSSHCommand(c,
×
958
                ctx,
×
959
                cancel,
×
960
                providesArgs,
×
961
                sshConnectedToken,
×
962
        )
×
963
        defer func() {
×
964
                if cleanupErr := command.WaitForEchoRestore(); cleanupErr != nil {
×
965
                        providesMap = nil
×
966
                        err = cleanupErr
×
967
                }
×
968
        }()
969

970
        if err != nil {
×
971
                return
×
972
        }
×
973

974
        scanner := bufio.NewScanner(command.Stdout)
×
975
        for scanner.Scan() {
×
976
                line := scanner.Text()
×
977
                if strings.HasPrefix(line, "rootfs-image.") {
×
978
                        parts := strings.SplitN(line, "=", 2)
×
979
                        if len(parts) == 2 {
×
980
                                tmpMap[parts[0]] = parts[1]
×
981
                        }
×
982
                }
983
        }
984

985
        err = command.EndSSHCommand()
×
986
        if err != nil {
×
987
                return
×
988
        }
×
989
        providesMap = tmpMap
×
990
        return
×
991
}
992

993
// Performs the same operation as io.Copy while at the same time prining
994
// the number of bytes written at any time.
995
func recvSnapshot(dst io.Writer, src io.Reader) (int64, error) {
×
996
        buf := make([]byte, 1024*1024*32)
×
997
        var written int64
×
998
        for {
×
999
                nr, err := src.Read(buf)
×
1000
                if err == io.EOF {
×
1001
                        fmt.Println()
×
1002
                        break
×
1003
                } else if err != nil {
×
1004
                        return written, errors.Wrap(err,
×
1005
                                "Error receiving snapshot from device")
×
1006
                }
×
1007
                nw, err := dst.Write(buf[:nr])
×
1008
                if err != nil {
×
1009
                        return written, errors.Wrap(err,
×
1010
                                "Error storing snapshot locally")
×
1011
                } else if nw < nr {
×
1012
                        return written, io.ErrShortWrite
×
1013
                }
×
1014
                written += int64(nw)
×
1015
        }
1016
        return written, nil
×
1017
}
1018

1019
func removeOnPanic(filename string) {
×
1020
        if r := recover(); r != nil {
×
1021
                err := os.Remove(filename)
×
1022
                if err != nil {
×
1023
                        switch v := r.(type) {
×
1024
                        case string:
×
1025
                                err = errors.Wrap(errors.New(v), err.Error())
×
1026
                                panic(err)
×
1027
                        case error:
×
1028
                                err = errors.Wrap(v, err.Error())
×
1029
                                panic(err)
×
1030
                        default:
×
1031
                                panic(r)
×
1032
                        }
1033
                }
1034
                panic(r)
×
1035
        }
1036
}
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

© 2025 Coveralls, Inc