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

mendersoftware / deployments / 1280684078

06 May 2024 08:13PM UTC coverage: 79.689% (+0.08%) from 79.606%
1280684078

Pull #1018

gitlab-ci

tranchitella
feat: return 409 on conflicts where creating a deployment with the same parameters as an already existing and active deployment

Changelog: none
Ticket: MEN-6622

Signed-off-by: Fabio Tranchitella <fabio.tranchitella@northern.tech>
Pull Request #1018: feat: prevent the creation of deployments if there is already an active deployment with the same constructor parameters

61 of 66 new or added lines in 7 files covered. (92.42%)

12 existing lines in 2 files now uncovered.

8102 of 10167 relevant lines covered (79.69%)

34.63 hits per line

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

85.52
/model/deployment.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
        "crypto/sha256"
19
        "encoding/json"
20
        "fmt"
21
        "time"
22

23
        "github.com/pkg/errors"
24
        "go.mongodb.org/mongo-driver/bson"
25

26
        validation "github.com/go-ozzo/ozzo-validation/v4"
27
        "github.com/go-ozzo/ozzo-validation/v4/is"
28
        "github.com/google/uuid"
29
)
30

31
// Errors
32
var (
33
        ErrInvalidDeviceID                      = errors.New("Invalid device ID")
34
        ErrInvalidDeploymentDefinition          = errors.New("Invalid deployments definition")
35
        ErrInvalidDeploymentDefinitionNoDevices = errors.New(
36
                "Invalid deployments definition: provide list of devices or set all_devices flag",
37
        )
38
        ErrInvalidDeploymentDefinitionConflict = errors.New(
39
                "Invalid deployments definition: list of devices provided togheter with all_devices flag",
40
        )
41
        ErrInvalidDeploymentToGroupDefinitionConflict = errors.New(
42
                "The deployment for group constructor should have neither list of devices" +
43
                        " nor all_devices flag set",
44
        )
45
)
46

47
type DeploymentStatus string
48
type DeploymentType string
49

50
const (
51
        DeploymentStatusFinished   DeploymentStatus = "finished"
52
        DeploymentStatusInProgress DeploymentStatus = "inprogress"
53
        DeploymentStatusPending    DeploymentStatus = "pending"
54

55
        DeploymentTypeSoftware      DeploymentType = "software"
56
        DeploymentTypeConfiguration DeploymentType = "configuration"
57
)
58

59
func (stat DeploymentStatus) Validate() error {
×
60
        return validation.In(
×
61
                DeploymentStatusFinished,
×
62
                DeploymentStatusInProgress,
×
63
                DeploymentStatusPending,
×
64
        ).Validate(stat)
×
65
}
×
66

67
func (typ DeploymentType) Validate() error {
×
68
        return validation.In(DeploymentTypeSoftware,
×
69
                DeploymentTypeConfiguration).Validate(typ)
×
70
}
×
71

72
// DeploymentConstructor represent input data needed for creating new Deployment (they differ in
73
// fields)
74
type DeploymentConstructor struct {
75
        // Deployment name, required
76
        Name string `json:"name,omitempty"`
77

78
        // Artifact name to be installed required, associated with image
79
        ArtifactName string `json:"artifact_name,omitempty"`
80

81
        // List of device id's targeted for deployments, required
82
        Devices []string `json:"devices,omitempty" bson:"-"`
83

84
        // When set to true deployment will be created for all currently accepted devices
85
        AllDevices bool `json:"all_devices,omitempty" bson:"-"`
86

87
        // ForceInstallation forces the installation of the artifact and disables the
88
        // `already-installed` check
89
        ForceInstallation bool `json:"force_installation,omitempty" bson:"force_installation"`
90

91
        // When set the deployment will be created for all accepted devices from a given group
92
        Group string `json:"-" bson:"-"`
93
}
94

95
// Validate checks structure according to valid tags
96
// TODO: Add custom validator to check devices array content (such us UUID formatting)
97
func (c DeploymentConstructor) Validate() error {
16✔
98
        return validation.ValidateStruct(&c,
16✔
99
                validation.Field(&c.Name, validation.Required, lengthIn1To4096),
16✔
100
                validation.Field(&c.ArtifactName, validation.Required, lengthIn1To4096),
16✔
101
                validation.Field(&c.Devices, validation.Each(validation.Required)),
16✔
102
        )
16✔
103
}
16✔
104

105
func (c DeploymentConstructor) ValidateNew() error {
14✔
106
        if err := c.Validate(); err != nil {
19✔
107
                return err
5✔
108
        }
5✔
109

110
        if len(c.Group) == 0 {
18✔
111
                if len(c.Devices) == 0 && !c.AllDevices {
11✔
112
                        return ErrInvalidDeploymentDefinitionNoDevices
3✔
113
                }
3✔
114
                if len(c.Devices) > 0 && c.AllDevices {
8✔
115
                        return ErrInvalidDeploymentDefinitionConflict
2✔
116
                }
2✔
117
        } else {
2✔
118
                if len(c.Devices) > 0 || c.AllDevices {
3✔
119
                        return ErrInvalidDeploymentToGroupDefinitionConflict
1✔
120
                }
1✔
121
        }
122
        return nil
5✔
123
}
124

125
func (c DeploymentConstructor) Checksum() string {
6✔
126
        json, err := json.Marshal(c)
6✔
127
        if err == nil {
12✔
128
                return fmt.Sprintf("%x", sha256.Sum256(json))
6✔
129
        }
6✔
NEW
130
        return ""
×
131
}
132

133
type DeploymentStatistics struct {
134
        Status    Stats `json:"status" bson:"-"`
135
        TotalSize int   `json:"total_size" bson:"total_size"`
136
}
137

138
type Deployment struct {
139
        // User provided field set
140
        *DeploymentConstructor
141

142
        // Set the DeploymentConstructor checksum
143
        DeploymentConstructorChecksum string `json:"-" bson:"deploymentconstructor_checksum,omitempty"`
144

145
        // Auto set on create, required
146
        Created *time.Time `json:"created"`
147

148
        // Finished deployment time
149
        Finished *time.Time `json:"finished,omitempty"`
150

151
        // Deployment id, required
152
        Id string `json:"id" bson:"_id"`
153

154
        // List of artifact id's targeted for deployments, optional
155
        Artifacts []string `json:"artifacts,omitempty" bson:"artifacts"`
156

157
        // Aggregated device status counters.
158
        // Initialized with the "pending" counter set to total device count for deployment.
159
        // Individual counter incremented/decremented according to device status updates.
160
        Stats Stats `json:"-"`
161

162
        Statistics DeploymentStatistics `json:"statistics,omitempty" bson:"statistics,omitempty"`
163

164
        // Status is the overall deployment status
165
        Status DeploymentStatus `json:"status" bson:"status"`
166

167
        // Active is true for unfinished deployments
168
        Active bool `json:"-" bson:"active"`
169

170
        // Number of devices being part of the deployment
171
        DeviceCount *int `json:"device_count" bson:"device_count"`
172

173
        // Total number of devices targeted
174
        MaxDevices int `json:"max_devices,omitempty" bson:"max_devices"`
175

176
        // device groups
177
        Groups []string `json:"groups,omitempty" bson:"groups"`
178

179
        // list of devices
180
        DeviceList []string `json:"-" bson:"device_list"`
181

182
        // deployment type
183
        // currently we are supporting two types of deployments:
184
        // software and configuration
185
        Type DeploymentType `json:"type,omitempty" bson:"type"`
186

187
        // A field containing a configuration object.
188
        // The deployments service will use it to generate configuration
189
        // artifact for the device.
190
        // The artifact will be generated when the device will ask
191
        // for an update.
192
        Configuration deploymentConfiguration `json:"configuration,omitempty" bson:"configuration"`
193
}
194

195
type DeploymentArtifactsUpdate struct {
196
        // List of artifact id's targeted for deployments, optional
197
        Artifacts []string `bson:"artifacts"`
198
}
199

200
// NewDeployment creates new deployment object, sets create data by default.
201
func NewDeployment() (*Deployment, error) {
1,023✔
202
        now := time.Now()
1,023✔
203

1,023✔
204
        uid, _ := uuid.NewRandom()
1,023✔
205
        id := uid.String()
1,023✔
206

1,023✔
207
        return &Deployment{
1,023✔
208
                Created:               &now,
1,023✔
209
                Id:                    id,
1,023✔
210
                DeploymentConstructor: &DeploymentConstructor{},
1,023✔
211
                Stats:                 NewDeviceDeploymentStats(),
1,023✔
212
        }, nil
1,023✔
213
}
1,023✔
214

215
// NewDeploymentFromConstructor creates new Deployments object based on constructor data
216
func NewDeploymentFromConstructor(constructor *DeploymentConstructor) (*Deployment, error) {
5✔
217

5✔
218
        deployment, err := NewDeployment()
5✔
219
        if err != nil {
5✔
220
                return nil, errors.Wrap(err, "failed to create deployment from constructor")
×
221
        }
×
222

223
        deployment.DeploymentConstructor = constructor
5✔
224
        if constructor != nil {
9✔
225
                deployment.DeploymentConstructorChecksum = constructor.Checksum()
4✔
226
        }
4✔
227
        deployment.Status = DeploymentStatusPending
5✔
228

5✔
229
        deviceCount := 0
5✔
230
        deployment.DeviceCount = &deviceCount
5✔
231

5✔
232
        return deployment, nil
5✔
233
}
234

235
// Validate checks structure validation rules
236
func (d Deployment) Validate() error {
3✔
237
        return validation.ValidateStruct(&d,
3✔
238
                validation.Field(&d.DeploymentConstructor, validation.NotNil),
3✔
239
                validation.Field(&d.Created, validation.Required),
3✔
240
                validation.Field(&d.Id, validation.Required, is.UUID),
3✔
241
                validation.Field(&d.Artifacts, validation.Each(validation.Required)),
3✔
242
                validation.Field(&d.DeviceList, validation.Each(validation.Required)),
3✔
243
        )
3✔
244
}
3✔
245

246
func (r *Deployment) MarshalBSON() ([]byte, error) {
2✔
247
        type Alias Deployment
2✔
248
        r.Active = r.Status != DeploymentStatusFinished
2✔
249
        return bson.Marshal((*Alias)(r))
2✔
250
}
2✔
251

252
// To be able to hide devices field, from API output provide custom marshaler
253
func (d *Deployment) MarshalJSON() ([]byte, error) {
2✔
254

2✔
255
        //Prevents from inheriting original MarshalJSON (if would, infinite loop)
2✔
256
        type Alias Deployment
2✔
257

2✔
258
        slim := struct {
2✔
259
                *Alias
2✔
260
                Devices []string       `json:"devices,omitempty"`
2✔
261
                Type    DeploymentType `json:"type,omitempty"`
2✔
262
        }{
2✔
263
                Alias:   (*Alias)(d),
2✔
264
                Devices: nil,
2✔
265
                Type:    d.Type,
2✔
266
        }
2✔
267
        if slim.Type == "" {
3✔
268
                slim.Type = DeploymentTypeSoftware
1✔
269
        }
1✔
270
        slim.Statistics.Status = slim.Stats
2✔
271

2✔
272
        return json.Marshal(&slim)
2✔
273
}
274

275
func (d *Deployment) IsNotPending() bool {
3,022✔
276
        if d.Stats[DeviceDeploymentStatusDownloadingStr] > 0 ||
3,022✔
277
                d.Stats[DeviceDeploymentStatusInstallingStr] > 0 ||
3,022✔
278
                d.Stats[DeviceDeploymentStatusRebootingStr] > 0 ||
3,022✔
279
                d.Stats[DeviceDeploymentStatusSuccessStr] > 0 ||
3,022✔
280
                d.Stats[DeviceDeploymentStatusAlreadyInstStr] > 0 ||
3,022✔
281
                d.Stats[DeviceDeploymentStatusFailureStr] > 0 ||
3,022✔
282
                d.Stats[DeviceDeploymentStatusAbortedStr] > 0 ||
3,022✔
283
                d.Stats[DeviceDeploymentStatusNoArtifactStr] > 0 ||
3,022✔
284
                d.Stats[DeviceDeploymentStatusPauseBeforeInstallStr] > 0 ||
3,022✔
285
                d.Stats[DeviceDeploymentStatusPauseBeforeCommitStr] > 0 ||
3,022✔
286
                d.Stats[DeviceDeploymentStatusPauseBeforeRebootStr] > 0 {
6,040✔
287

3,018✔
288
                return true
3,018✔
289
        }
3,018✔
290

291
        return false
5✔
292
}
293

294
func (d *Deployment) IsFinished() bool {
3,031✔
295
        if d.Finished != nil ||
3,031✔
296
                d.MaxDevices > 0 && ((d.Stats[DeviceDeploymentStatusAlreadyInstStr]+
3,031✔
297
                        d.Stats[DeviceDeploymentStatusSuccessStr]+
3,031✔
298
                        d.Stats[DeviceDeploymentStatusFailureStr]+
3,031✔
299
                        d.Stats[DeviceDeploymentStatusNoArtifactStr]+
3,031✔
300
                        d.Stats[DeviceDeploymentStatusDecommissionedStr]+
3,031✔
301
                        d.Stats[DeviceDeploymentStatusAbortedStr]) >= d.MaxDevices) {
3,045✔
302
                return true
14✔
303
        }
14✔
304

305
        return false
3,018✔
306
}
307

308
func (d *Deployment) GetStatus() DeploymentStatus {
3,016✔
309
        if d.IsFinished() {
3,024✔
310
                return DeploymentStatusFinished
8✔
311
        } else if d.IsNotPending() {
6,024✔
312
                return DeploymentStatusInProgress
3,007✔
313
        } else {
3,010✔
314
                return DeploymentStatusPending
3✔
315
        }
3✔
316
}
317

318
type StatusQuery int
319

320
const (
321
        StatusQueryAny StatusQuery = iota
322
        StatusQueryPending
323
        StatusQueryInProgress
324
        StatusQueryFinished
325
        StatusQueryAborted
326

327
        SortDirectionAscending  = "asc"
328
        SortDirectionDescending = "desc"
329
)
330

331
// Deployment lookup query
332
type Query struct {
333
        // list of IDs
334
        IDs []string
335

336
        // match deployments by text by looking at deployment name and artifact name
337
        SearchText string
338

339
        // deployment type
340
        Type DeploymentType
341

342
        // deployment status
343
        Status StatusQuery
344
        Limit  int
345
        Skip   int
346
        // only return deployments between timestamp range
347
        CreatedAfter  *time.Time
348
        CreatedBefore *time.Time
349

350
        // sort values by creation date
351
        Sort string
352

353
        // disable the counting
354
        DisableCount bool
355
}
356

357
type DeploymentIDs struct {
358
        IDs []string `json:"deployment_ids"`
359
}
360

361
func (d DeploymentIDs) Validate() error {
×
362
        return validation.Validate(d.IDs,
×
363
                validation.Required,
×
364
                validation.Length(1, 100),
×
365
                validation.Each(is.UUID),
×
366
        )
×
367
}
×
368

369
type DeploymentStats struct {
370
        ID    string `json:"id" bson:"_id"`
371
        Stats Stats  `json:"stats" bson:"stats"`
372
}
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