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

mendersoftware / mender-server / 1925715791

14 Jul 2025 02:01PM UTC coverage: 65.487% (-0.02%) from 65.504%
1925715791

Pull #790

gitlab-ci

bahaa-ghazal
feat(deployments): Implement new v2 GET `/artifacts` endpoint

Ticket: MEN-8181
Changelog: Title
Signed-off-by: Bahaa Aldeen Ghazal <bahaa.ghazal@northern.tech>
Pull Request #790: feat(deployments): Implement new v2 GET `/artifacts` endpoint

145 of 237 new or added lines in 7 files covered. (61.18%)

129 existing lines in 3 files now uncovered.

32534 of 49680 relevant lines covered (65.49%)

1.38 hits per line

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

67.35
/backend/services/deployments/model/image.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 model
16

17
import (
18
        "context"
19
        "io"
20
        "path"
21
        "time"
22

23
        validation "github.com/go-ozzo/ozzo-validation/v4"
24
        "github.com/go-ozzo/ozzo-validation/v4/is"
25
        "github.com/pkg/errors"
26
        "go.mongodb.org/mongo-driver/bson"
27
        "go.mongodb.org/mongo-driver/bson/bsontype"
28

29
        "github.com/mendersoftware/mender-server/pkg/identity"
30
        "github.com/mendersoftware/mender-server/pkg/mongo/doc"
31
)
32

33
const (
34
        ArtifactFileSuffix = ".mender"
35
        MaximumPerPage     = 500
36
)
37

38
var (
39
        StorageKeyImageProvidesIdxKey   = "meta_artifact.provides_idx.key"
40
        StorageKeyImageProvidesIdxValue = "meta_artifact.provides_idx.value"
41
)
42

43
type ProvidesIdx map[string]string
44

45
func ImagePathFromContext(ctx context.Context, id string) string {
2✔
46
        imgPath := id
2✔
47
        if idty := identity.FromContext(ctx); idty != nil {
4✔
48
                imgPath = path.Join(idty.Tenant, id)
2✔
49
        }
2✔
50
        return imgPath
2✔
51
}
52

53
// Information provided by the user
54
type ImageMeta struct {
55
        // Image description
56
        Description string `json:"description,omitempty" valid:"length(1|4096),optional"`
57
}
58

59
// Creates new, empty ImageMeta
60
func NewImageMeta() *ImageMeta {
1✔
61
        return &ImageMeta{}
1✔
62
}
1✔
63

64
// Validate checks structure according to valid tags.
65
func (s ImageMeta) Validate() error {
3✔
66
        return validation.ValidateStruct(&s,
3✔
67
                validation.Field(&s.Description, lengthLessThan4096),
3✔
68
        )
3✔
69
}
3✔
70

71
// Structure with artifact version information
72
type ArtifactInfo struct {
73
        // Mender artifact format - the only possible value is "mender"
74
        //Format string `json:"format" valid:"string,equal("mender"),required"`
75
        Format string `json:"format" valid:"required"`
76

77
        // Mender artifact format version
78
        //Version uint `json:"version" valid:"uint,equal(1),required"`
79
        Version uint `json:"version" valid:"required"`
80
}
81

82
func (ai ArtifactInfo) Validate() error {
3✔
83
        return validation.ValidateStruct(&ai,
3✔
84
                validation.Field(&ai.Format, validation.Required),
3✔
85
                validation.Field(&ai.Version, validation.In(uint(1), uint(2), uint(3))),
3✔
86
        )
3✔
87
}
3✔
88

89
// Information provided by the Mender Artifact header
90
type ArtifactMeta struct {
91
        // artifact_name from artifact file
92
        Name string `json:"name" bson:"name" valid:"length(1|4096),required"`
93

94
        // Compatible device types for the application
95
        //nolint:lll
96
        DeviceTypesCompatible []string `json:"device_types_compatible" bson:"device_types_compatible" valid:"length(1|4096),required"`
97

98
        // Artifact version info
99
        Info *ArtifactInfo `json:"info"`
100

101
        // Flag that indicates if artifact is signed or not
102
        Signed bool `json:"signed" bson:"signed"`
103

104
        // List of updates
105
        Updates []Update `json:"updates" valid:"-"`
106

107
        // Provides is a map of artifact_provides used
108
        // for checking artifact (version 3) dependencies.
109
        //nolint: lll
110
        Provides map[string]string `json:"artifact_provides,omitempty" bson:"provides,omitempty" valid:"-"`
111

112
        // ProvidesIdx is special representation of provides
113
        // which makes possible to index and query using provides.
114
        ProvidesIdx ProvidesIdx `json:"-" bson:"provides_idx,omitempty"`
115

116
        // Depends is a map[string]interface{} (JSON) of artifact_depends used
117
        // for checking/validate against artifact (version 3) provides.
118
        Depends map[string]interface{} `json:"artifact_depends,omitempty" bson:"depends" valid:"-"`
119

120
        // ClearsProvides is a list of strings (JSON) of clears_artifact_provides used
121
        // for clearing already-installed artifact (version 3) provides.
122
        //nolint:lll
123
        ClearsProvides []string `json:"clears_artifact_provides,omitempty" bson:"clears_provides,omitempty" valid:"-"`
124
}
125

126
// MarshalBSON transparently creates depends_idx field on bson.Marshal
127
func (am ArtifactMeta) MarshalBSON() ([]byte, error) {
×
128
        if err := am.Validate(); err != nil {
×
129
                return nil, err
×
130
        }
×
131
        dependsIdx, err := doc.UnwindMap(am.Depends)
×
132
        if err != nil {
×
133
                return nil, err
×
134
        }
×
135
        doc := doc.DocumentFromStruct(am, bson.E{
×
136
                Key: "depends_idx", Value: dependsIdx,
×
137
        })
×
138
        return bson.Marshal(doc)
×
139
}
140

141
// MarshalBSONValue transparently creates depends_idx field on bson.MarshalValue
142
// which is called if ArtifactMeta is marshaled as an embedded document.
143
func (am ArtifactMeta) MarshalBSONValue() (bsontype.Type, []byte, error) {
2✔
144
        if err := am.Validate(); err != nil {
2✔
145
                return bson.TypeNull, nil, err
×
146
        }
×
147
        dependsIdx, err := doc.UnwindMap(am.Depends)
2✔
148
        if err != nil {
2✔
149
                return bson.TypeNull, nil, err
×
150
        }
×
151
        doc := doc.DocumentFromStruct(am, bson.E{
2✔
152
                Key: "depends_idx", Value: dependsIdx,
2✔
153
        })
2✔
154
        return bson.MarshalValue(doc)
2✔
155
}
156

157
// Validate checks structure according to valid tags.
158
func (am *ArtifactMeta) Validate() error {
3✔
159
        if am.Depends == nil {
4✔
160
                am.Depends = make(map[string]interface{})
1✔
161
        }
1✔
162
        am.Depends["device_type"] = am.DeviceTypesCompatible
3✔
163

3✔
164
        return validation.ValidateStruct(am,
3✔
165
                validation.Field(&am.Name, validation.Required, lengthIn1To4096),
3✔
166
                validation.Field(&am.DeviceTypesCompatible,
3✔
167
                        validation.Required,
3✔
168
                        lengthIn0To200,
3✔
169
                        validation.Each(lengthIn1To4096),
3✔
170
                ),
3✔
171
                validation.Field(&am.Info),
3✔
172
        )
3✔
173
}
174

175
func NewArtifactMeta() *ArtifactMeta {
3✔
176
        return &ArtifactMeta{}
3✔
177
}
3✔
178

179
// Image YOCTO image with user application
180
type Image struct {
181
        // Image ID
182
        Id string `json:"id" bson:"_id" valid:"uuidv4,required"`
183

184
        // User provided field set
185
        *ImageMeta `bson:"meta"`
186

187
        // Field set provided with yocto image
188
        *ArtifactMeta `bson:"meta_artifact"`
189

190
        // Artifact total size
191
        Size int64 `json:"size" bson:"size" valid:"-"`
192

193
        // Last modification time, including image upload time
194
        Modified *time.Time `json:"modified" valid:"-"`
195
}
196

197
func (img Image) MarshalBSON() (b []byte, err error) {
2✔
198
        return bson.Marshal(doc.DocumentFromStruct(img))
2✔
199
}
2✔
200

201
func (img Image) MarshalBSONValue() (bsontype.Type, []byte, error) {
2✔
202
        return bson.MarshalValue(doc.DocumentFromStruct(img))
2✔
203
}
2✔
204

205
// Validate checks structure according to valid tags.
206
func (s Image) Validate() error {
3✔
207
        return validation.ValidateStruct(&s,
3✔
208
                validation.Field(&s.Id, validation.Required, is.UUID),
3✔
209
                validation.Field(&s.ImageMeta),
3✔
210
                validation.Field(&s.ArtifactMeta),
3✔
211
        )
3✔
212
}
3✔
213

214
// NewImage creates new software image object.
215
func NewImage(
216
        id string,
217
        metaConstructor *ImageMeta,
218
        metaArtifactConstructor *ArtifactMeta,
219
        artifactSize int64) *Image {
3✔
220

3✔
221
        now := time.Now()
3✔
222

3✔
223
        return &Image{
3✔
224
                ImageMeta:    metaConstructor,
3✔
225
                ArtifactMeta: metaArtifactConstructor,
3✔
226
                Modified:     &now,
3✔
227
                Id:           id,
3✔
228
                Size:         artifactSize,
3✔
229
        }
3✔
230
}
3✔
231

232
// SetModified set last modification time for the image.
233
func (s *Image) SetModified(time time.Time) {
×
234
        s.Modified = &time
×
235
}
×
236

237
type ReadCounter interface {
238
        io.Reader
239
        // Count returns the number of bytes read.
240
        Count() int64
241
}
242

243
// MultipartUploadMsg is a structure with fields extracted from the multipart/form-data form
244
// send in the artifact upload request
245
type MultipartUploadMsg struct {
246
        // user metadata constructor
247
        MetaConstructor *ImageMeta
248
        // ArtifactID contains the artifact ID
249
        ArtifactID string
250
        // reader pointing to the beginning of the artifact data
251
        ArtifactReader io.Reader
252
}
253

254
// MultipartGenerateImageMsg is a structure with fields extracted from the multipart/form-data
255
// form sent in the artifact generation request
256
type MultipartGenerateImageMsg struct {
257
        Name                  string    `json:"name"`
258
        Description           string    `json:"description"`
259
        DeviceTypesCompatible []string  `json:"device_types_compatible"`
260
        Type                  string    `json:"type"`
261
        Args                  string    `json:"args"`
262
        ArtifactID            string    `json:"artifact_id"`
263
        GetArtifactURI        string    `json:"get_artifact_uri"`
264
        DeleteArtifactURI     string    `json:"delete_artifact_uri"`
265
        TenantID              string    `json:"tenant_id"`
266
        Token                 string    `json:"token"`
267
        FileReader            io.Reader `json:"-"`
268
}
269

270
func (msg MultipartGenerateImageMsg) Validate() error {
2✔
271
        if err := validation.ValidateStruct(&msg,
2✔
272
                validation.Field(&msg.Name, validation.Required),
2✔
273
                validation.Field(&msg.DeviceTypesCompatible, validation.Required),
2✔
274
                validation.Field(&msg.Type, validation.Required),
2✔
275
        ); err != nil {
2✔
276
                return err
×
277
        }
×
278
        // Somehow FileReader is not covered by "required" rule.
279
        if msg.FileReader == nil {
2✔
280
                return errors.New("missing 'file' section")
×
281
        }
×
282
        return nil
2✔
283
}
284

285
type provideInternal struct {
286
        Key   string
287
        Value string
288
}
289

290
func (p ProvidesIdx) MarshalBSONValue() (bsontype.Type, []byte, error) {
2✔
291
        attrs := make([]provideInternal, len(p))
2✔
292
        i := 0
2✔
293
        for k, v := range p {
4✔
294
                attrs[i].Key = k
2✔
295
                attrs[i].Value = v
2✔
296
                i++
2✔
297
        }
2✔
298
        return bson.MarshalValue(attrs)
2✔
299
}
300

301
func (p *ProvidesIdx) UnmarshalBSONValue(t bsontype.Type, b []byte) error {
2✔
302
        raw := bson.Raw(b)
2✔
303
        elems, err := raw.Elements()
2✔
304
        if err != nil {
2✔
305
                return err
×
306
        }
×
307
        *p = make(ProvidesIdx, len(elems))
2✔
308
        var tmp provideInternal
2✔
309
        for _, elem := range elems {
4✔
310
                err = elem.Value().Unmarshal(&tmp)
2✔
311
                if err != nil {
2✔
312
                        return err
×
313
                }
×
314
                (*p)[tmp.Key] = tmp.Value
2✔
315
        }
316

317
        return nil
2✔
318
}
319

320
type ImageFilter struct {
321
        ExactNames   []string
322
        NamePrefixes []string
323
        Description  string
324
        DeviceType   string
325
        Page         int
326
        PerPage      int
327
        Sort         string
328
        Limit        int
329
}
330

NEW
331
func (filter ImageFilter) Validate() error {
×
NEW
332
        if len(filter.ExactNames) > 0 && len(filter.NamePrefixes) > 0 {
×
NEW
333
                return errors.New("cannot filter by both exact names and name prefixes")
×
NEW
334
        }
×
NEW
335
        return validation.ValidateStruct(&filter,
×
NEW
336
                validation.Field(&filter.ExactNames,
×
NEW
337
                        validation.Length(0, 100),
×
NEW
338
                        validation.Each(lengthLessThan4096)),
×
NEW
339

×
NEW
340
                validation.Field(&filter.NamePrefixes,
×
NEW
341
                        validation.Length(0, 1),
×
NEW
342
                        validation.Each(lengthLessThan4096)),
×
NEW
343

×
NEW
344
                validation.Field(&filter.Description, lengthLessThan4096),
×
NEW
345
                validation.Field(&filter.DeviceType, lengthLessThan4096),
×
NEW
346

×
NEW
347
                validation.Field(&filter.Page, validation.Min(1)),
×
NEW
348
                validation.Field(&filter.PerPage, validation.Min(1), validation.Max(MaximumPerPage)),
×
NEW
349

×
NEW
350
                // validation.Field(&filter.Sort, validation.In("name", "modified")),
×
NEW
351
        )
×
352
}
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