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

mendersoftware / mender-server / 1927764361

15 Jul 2025 11:39AM UTC coverage: 65.436% (-0.02%) from 65.454%
1927764361

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

139 of 232 new or added lines in 7 files covered. (59.91%)

77 existing lines in 3 files now uncovered.

32212 of 49227 relevant lines covered (65.44%)

1.39 hits per line

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

94.62
/backend/services/deployments/api/http/routing.go
1
// Copyright 2024 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 http
16

17
import (
18
        "context"
19
        "net/http"
20
        "net/url"
21
        "os"
22
        "strings"
23

24
        "github.com/gin-gonic/gin"
25

26
        "github.com/mendersoftware/mender-server/pkg/accesslog"
27
        "github.com/mendersoftware/mender-server/pkg/contenttype"
28
        "github.com/mendersoftware/mender-server/pkg/identity"
29
        "github.com/mendersoftware/mender-server/pkg/log"
30
        "github.com/mendersoftware/mender-server/pkg/routing"
31

32
        "github.com/mendersoftware/mender-server/services/deployments/app"
33
        "github.com/mendersoftware/mender-server/services/deployments/store"
34
        "github.com/mendersoftware/mender-server/services/deployments/utils/restutil"
35
        "github.com/mendersoftware/mender-server/services/deployments/utils/restutil/view"
36
)
37

38
const (
39
        ApiUrlInternal   = "/api/internal/v1/deployments"
40
        ApiUrlManagement = "/api/management/v1/deployments"
41
        ApiUrlDevices    = "/api/devices/v1/deployments"
42

43
        ApiUrlManagementArtifacts               = "/artifacts"
44
        ApiUrlManagementArtifactsList           = "/artifacts/list"
45
        ApiUrlManagementArtifactsGenerate       = "/artifacts/generate"
46
        ApiUrlManagementArtifactsDirectUpload   = "/artifacts/directupload"
47
        ApiUrlManagementArtifactsCompleteUpload = ApiUrlManagementArtifactsDirectUpload +
48
                "/:id/complete"
49
        ApiUrlManagementArtifactsId         = "/artifacts/:id"
50
        ApiUrlManagementArtifactsIdDownload = "/artifacts/:id/download"
51

52
        ApiUrlManagementDeployments                   = "/deployments"
53
        ApiUrlManagementMultipleDeploymentsStatistics = "/deployments/statistics/list"
54
        ApiUrlManagementDeploymentsGroup              = "/deployments/group/:name"
55
        ApiUrlManagementDeploymentsId                 = "/deployments/:id"
56
        ApiUrlManagementDeploymentsStatistics         = "/deployments/:id/statistics"
57
        ApiUrlManagementDeploymentsStatus             = "/deployments/:id/status"
58
        ApiUrlManagementDeploymentsDevices            = "/deployments/:id/devices"
59
        ApiUrlManagementDeploymentsDevicesList        = "/deployments/:id/devices/list"
60
        ApiUrlManagementDeploymentsLog                = "/deployments/:id/devices/:devid/log"
61
        ApiUrlManagementDeploymentsDeviceId           = "/deployments/devices/:id"
62
        ApiUrlManagementDeploymentsDeviceHistory      = "/deployments/devices/:id/history"
63
        ApiUrlManagementDeploymentsDeviceList         = "/deployments/:id/device_list"
64

65
        ApiUrlManagementReleases     = "/deployments/releases"
66
        ApiUrlManagementReleasesList = "/deployments/releases/list"
67

68
        ApiUrlManagementLimitsName = "/limits/:name"
69

70
        ApiUrlManagementV2                      = "/api/management/v2/deployments"
71
        ApiUrlManagementV2Releases              = "/deployments/releases"
72
        ApiUrlManagementV2ReleasesName          = ApiUrlManagementV2Releases + "/:name"
73
        ApiUrlManagementV2ReleaseTags           = ApiUrlManagementV2Releases + "/:name/tags"
74
        ApiUrlManagementV2ReleaseAllTags        = "/releases/all/tags"
75
        ApiUrlManagementV2ReleaseAllUpdateTypes = "/releases/all/types"
76
        ApiUrlManagementV2Deployments           = "/deployments"
77

78
        ApiUrlDevicesDeploymentsNext  = "/device/deployments/next"
79
        ApiUrlDevicesDeploymentStatus = "/device/deployments/:id/status"
80
        ApiUrlDevicesDeploymentsLog   = "/device/deployments/:id/log"
81
        ApiUrlDevicesDownloadConfig   = "/download/configuration" +
82
                "/:deployment_id/:device_type/:device_id"
83

84
        ApiUrlInternalAlive                          = "/alive"
85
        ApiUrlInternalHealth                         = "/health"
86
        ApiUrlInternalTenants                        = "/tenants"
87
        ApiUrlInternalTenantDeployments              = "/tenants/:tenant/deployments"
88
        ApiUrlInternalTenantDeploymentsDevices       = "/tenants/:tenant/deployments/devices"
89
        ApiUrlInternalTenantDeploymentsDevice        = "/tenants/:tenant/deployments/devices/:id"
90
        ApiUrlInternalTenantArtifacts                = "/tenants/:tenant/artifacts"
91
        ApiUrlInternalTenantStorageSettings          = "/tenants/:tenant/storage/settings"
92
        ApiUrlInternalDeviceConfigurationDeployments = "/tenants/:tenant/configuration/deployments" +
93
                "/:deployment_id/devices/:device_id"
94
        ApiUrlInternalDeviceDeploymentLastStatusDeployments = "/tenants/:tenant/devices/deployments" +
95
                "/last"
96
)
97

98
func init() {
3✔
99
        if mode := os.Getenv(gin.EnvGinMode); mode != "" {
3✔
100
                gin.SetMode(mode)
×
101
        } else {
3✔
102
                gin.SetMode(gin.ReleaseMode)
3✔
103
        }
3✔
104
        gin.DisableConsoleColor()
3✔
105
}
106

107
// NewRouter defines all REST API routes.
108
func NewRouter(
109
        ctx context.Context,
110
        app app.App,
111
        ds store.DataStore,
112
        cfg *Config,
113
) http.Handler {
3✔
114
        router := routing.NewGinRouter()
3✔
115
        // Create and configure API handlers
3✔
116
        //
3✔
117
        // Encode base64 secret in either std or URL encoding ignoring padding.
3✔
118
        deploymentsHandlers := NewDeploymentsApiHandlers(
3✔
119
                ds, new(view.RESTView), app, cfg,
3✔
120
        )
3✔
121

3✔
122
        // Routing
3✔
123
        internalAPIs := router.Group(ApiUrlInternal)
3✔
124

3✔
125
        publicAPIs := router.Group(".")
3✔
126

3✔
127
        withAuth := publicAPIs.Group(".")
3✔
128
        withAuth.Use(identity.Middleware())
3✔
129

3✔
130
        NewImagesResourceRoutes(withAuth, deploymentsHandlers, cfg)
3✔
131
        NewDeploymentsResourceRoutes(publicAPIs, deploymentsHandlers)
3✔
132
        NewLimitsResourceRoutes(withAuth, deploymentsHandlers)
3✔
133
        InternalRoutes(internalAPIs, deploymentsHandlers)
3✔
134
        ReleasesRoutes(withAuth, deploymentsHandlers)
3✔
135

3✔
136
        restutil.AutogenOptionsRoutes(
3✔
137
                restutil.NewOptionsHandler,
3✔
138
                router)
3✔
139

3✔
140
        return router
3✔
141
}
3✔
142

143
func NewImagesResourceRoutes(router *gin.RouterGroup,
144
        controller *DeploymentsApiHandlers, cfg *Config) {
3✔
145

3✔
146
        if controller == nil {
3✔
147
                return
×
148
        }
×
149
        mgmtV1 := router.Group(ApiUrlManagement)
3✔
150
        mgmtV2 := router.Group(ApiUrlManagementV2)
3✔
151

3✔
152
        artifcatType := contenttype.Middleware("multipart/form-data", "multipart/mixed")
3✔
153

3✔
154
        mgmtV1.GET(ApiUrlManagementArtifacts, controller.GetImages)
3✔
155
        mgmtV2.GET(ApiUrlManagementArtifacts, controller.ListImagesV2)
3✔
156
        mgmtV1.GET(ApiUrlManagementArtifactsList, controller.ListImages)
3✔
157
        mgmtV1.GET(ApiUrlManagementArtifactsId, controller.GetImage)
3✔
158
        mgmtV1.GET(ApiUrlManagementArtifactsIdDownload, controller.DownloadLink)
3✔
159
        if !controller.config.DisableNewReleasesFeature {
6✔
160
                mgmtV1.DELETE(ApiUrlManagementArtifactsId, controller.DeleteImage)
3✔
161
                mgmtV1.Group(".").Use(artifcatType).
3✔
162
                        POST(ApiUrlManagementArtifacts, controller.NewImage).
3✔
163
                        POST(ApiUrlManagementArtifactsGenerate, controller.GenerateImage)
3✔
164
                mgmtV1.Group(".").Use(contenttype.CheckJSON()).
3✔
165
                        PUT(ApiUrlManagementArtifactsId, controller.EditImage)
3✔
166

3✔
167
        } else {
4✔
168
                mgmtV1.DELETE(ApiUrlManagementArtifactsId, ServiceUnavailable)
1✔
169

1✔
170
                mgmtV1.Group(".").Use(artifcatType).
1✔
171
                        POST(ApiUrlManagementArtifacts, ServiceUnavailable).
1✔
172
                        POST(ApiUrlManagementArtifactsGenerate, ServiceUnavailable)
1✔
173
                mgmtV1.PUT(ApiUrlManagementArtifactsId, ServiceUnavailable)
1✔
174

1✔
175
        }
1✔
176
        if !controller.config.DisableNewReleasesFeature && cfg.EnableDirectUpload {
5✔
177
                log.NewEmpty().Infof(
2✔
178
                        "direct upload enabled: POST %s",
2✔
179
                        ApiUrlManagementArtifactsDirectUpload,
2✔
180
                )
2✔
181
                if cfg.EnableDirectUploadSkipVerify {
3✔
182
                        log.NewEmpty().Info(
1✔
183
                                "direct upload enabled SkipVerify",
1✔
184
                        )
1✔
185
                }
1✔
186
                mgmtV1.Group(".").Use(contenttype.CheckJSON()).
2✔
187
                        POST(ApiUrlManagementArtifactsDirectUpload,
2✔
188
                                controller.UploadLink).
2✔
189
                        POST(ApiUrlManagementArtifactsCompleteUpload,
2✔
190
                                controller.CompleteUpload)
2✔
191
        }
192
}
193

194
func NewDeploymentsResourceRoutes(router *gin.RouterGroup, controller *DeploymentsApiHandlers) {
3✔
195

3✔
196
        if controller == nil {
3✔
UNCOV
197
                return
×
UNCOV
198
        }
×
199
        mgmtV1 := router.Group(ApiUrlManagement)
3✔
200
        mgmtV1.Use(identity.Middleware())
3✔
201
        mgmtV2 := router.Group(ApiUrlManagementV2)
3✔
202
        mgmtV2.Use(identity.Middleware())
3✔
203

3✔
204
        mgmtV1.GET(ApiUrlManagementDeployments, controller.LookupDeployment)
3✔
205
        mgmtV2.GET(ApiUrlManagementV2Deployments, controller.LookupDeploymentV2)
3✔
206
        mgmtV1.GET(ApiUrlManagementDeploymentsId, controller.GetDeployment)
3✔
207
        mgmtV1.GET(ApiUrlManagementDeploymentsStatistics, controller.GetDeploymentStats)
3✔
208
        mgmtV1.GET(ApiUrlManagementDeploymentsDevices,
3✔
209
                controller.GetDeviceStatusesForDeployment)
3✔
210
        mgmtV1.GET(ApiUrlManagementDeploymentsDevicesList,
3✔
211
                controller.GetDevicesListForDeployment)
3✔
212
        mgmtV1.GET(ApiUrlManagementDeploymentsLog,
3✔
213
                controller.GetDeploymentLogForDevice)
3✔
214
        mgmtV1.GET(ApiUrlManagementDeploymentsDeviceId,
3✔
215
                controller.ListDeviceDeployments)
3✔
216
        mgmtV1.GET(ApiUrlManagementDeploymentsDeviceList,
3✔
217
                controller.GetDeploymentDeviceList)
3✔
218

3✔
219
        mgmtV1.DELETE(ApiUrlManagementDeploymentsDeviceId,
3✔
220
                controller.AbortDeviceDeployments)
3✔
221
        mgmtV1.DELETE(ApiUrlManagementDeploymentsDeviceHistory,
3✔
222
                controller.DeleteDeviceDeploymentsHistory)
3✔
223

3✔
224
        mgmtV1.Group(".").Use(contenttype.CheckJSON()).
3✔
225
                POST(ApiUrlManagementDeployments, controller.PostDeployment).
3✔
226
                POST(ApiUrlManagementDeploymentsGroup, controller.DeployToGroup).
3✔
227
                POST(ApiUrlManagementMultipleDeploymentsStatistics,
3✔
228
                        controller.GetDeploymentsStats).
3✔
229
                PUT(ApiUrlManagementDeploymentsStatus, controller.AbortDeployment)
3✔
230

3✔
231
        // Devices
3✔
232
        devices := router.Group(ApiUrlDevices)
3✔
233

3✔
234
        devices.GET(ApiUrlDevicesDownloadConfig,
3✔
235
                controller.DownloadConfiguration)
3✔
236

3✔
237
        devices.Use(identity.Middleware())
3✔
238

3✔
239
        devices.GET(ApiUrlDevicesDeploymentsNext, controller.GetDeploymentForDevice)
3✔
240
        devices.Group(".").Use(contenttype.CheckJSON()).
3✔
241
                POST(ApiUrlDevicesDeploymentsNext,
3✔
242
                        controller.GetDeploymentForDevice).
3✔
243
                PUT(ApiUrlDevicesDeploymentStatus,
3✔
244
                        controller.PutDeploymentStatusForDevice).
3✔
245
                PUT(ApiUrlDevicesDeploymentsLog,
3✔
246
                        controller.PutDeploymentLogForDevice)
3✔
247

248
}
249

250
func NewLimitsResourceRoutes(router *gin.RouterGroup, controller *DeploymentsApiHandlers) {
3✔
251

3✔
252
        if controller == nil {
3✔
UNCOV
253
                return
×
UNCOV
254
        }
×
255
        mgmtV1 := router.Group(ApiUrlManagement)
3✔
256

3✔
257
        mgmtV1.GET(ApiUrlManagementLimitsName, controller.GetLimit)
3✔
258

259
}
260

261
func InternalRoutes(router *gin.RouterGroup, controller *DeploymentsApiHandlers) {
3✔
262
        if controller == nil {
3✔
UNCOV
263
                return
×
UNCOV
264
        }
×
265
        accesslogErrorsOnly := accesslog.AccessLogger{
3✔
266
                DisableLog: func(c *gin.Context) bool {
4✔
267
                        if c.Writer.Status()/100 == 2 {
2✔
268
                                // 2XX
1✔
269
                                return true
1✔
270
                        }
1✔
UNCOV
271
                        return false
×
272
                },
273
        }
274
        // Health Check
275
        // Skiping logging 2XX status code requests to decrease        noise
276
        router.GET(ApiUrlInternalAlive, accesslogErrorsOnly.Middleware,
3✔
277
                controller.AliveHandler)
3✔
278
        router.GET(ApiUrlInternalHealth, accesslogErrorsOnly.Middleware,
3✔
279
                controller.HealthHandler)
3✔
280

3✔
281
        router.Use(accesslog.Middleware())
3✔
282

3✔
283
        router.POST(ApiUrlInternalTenants, controller.ProvisionTenantsHandler)
3✔
284
        router.GET(ApiUrlInternalTenantDeployments, controller.DeploymentsPerTenantHandler)
3✔
285
        router.GET(ApiUrlInternalTenantDeploymentsDevices,
3✔
286
                controller.ListDeviceDeploymentsByIDsInternal)
3✔
287
        router.GET(ApiUrlInternalTenantDeploymentsDevice,
3✔
288
                controller.ListDeviceDeploymentsInternal)
3✔
289
        router.DELETE(ApiUrlInternalTenantDeploymentsDevice,
3✔
290
                controller.AbortDeviceDeploymentsInternal)
3✔
291
        // per-tenant storage settings
3✔
292
        router.GET(ApiUrlInternalTenantStorageSettings, controller.GetTenantStorageSettingsHandler)
3✔
293
        router.PUT(ApiUrlInternalTenantStorageSettings, controller.PutTenantStorageSettingsHandler)
3✔
294

3✔
295
        // Configuration deployments (internal)
3✔
296
        router.POST(ApiUrlInternalDeviceConfigurationDeployments,
3✔
297
                controller.PostDeviceConfigurationDeployment)
3✔
298

3✔
299
        // Last device deployment status deployments (internal)
3✔
300
        router.POST(ApiUrlInternalDeviceDeploymentLastStatusDeployments,
3✔
301
                controller.GetDeviceDeploymentLastStatus)
3✔
302

3✔
303
        if !controller.config.DisableNewReleasesFeature {
6✔
304
                router.POST(ApiUrlInternalTenantArtifacts, controller.NewImageForTenantHandler)
3✔
305
        } else {
4✔
306
                router.POST(ApiUrlInternalTenantArtifacts, ServiceUnavailable)
1✔
307
        }
1✔
308
}
309

310
func ReleasesRoutes(router *gin.RouterGroup, controller *DeploymentsApiHandlers) {
3✔
311
        if controller == nil {
3✔
UNCOV
312
                return
×
UNCOV
313
        }
×
314
        mgmtV1 := router.Group(ApiUrlManagement)
3✔
315
        mgmtV2 := router.Group(ApiUrlManagementV2)
3✔
316

3✔
317
        if controller.config.DisableNewReleasesFeature {
4✔
318
                mgmtV1.GET(ApiUrlManagementReleases, controller.GetReleases)
1✔
319
                mgmtV1.GET(ApiUrlManagementReleasesList, controller.ListReleases)
1✔
320

1✔
321
        } else {
4✔
322

3✔
323
                mgmtV1.GET(ApiUrlManagementReleases, controller.GetReleases)
3✔
324
                mgmtV1.GET(ApiUrlManagementReleasesList, controller.ListReleases)
3✔
325
                mgmtV2.GET(ApiUrlManagementV2Releases, controller.ListReleasesV2)
3✔
326
                mgmtV2.GET(ApiUrlManagementV2ReleasesName, controller.GetRelease)
3✔
327
                mgmtV2.GET(ApiUrlManagementV2ReleaseAllTags, controller.GetReleaseTagKeys)
3✔
328
                mgmtV2.GET(ApiUrlManagementV2ReleaseAllUpdateTypes, controller.GetReleasesUpdateTypes)
3✔
329
                mgmtV2.DELETE(ApiUrlManagementV2Releases, controller.DeleteReleases)
3✔
330
                mgmtV2.Group(".").Use(contenttype.CheckJSON()).
3✔
331
                        PUT(ApiUrlManagementV2ReleaseTags, controller.PutReleaseTags).
3✔
332
                        PATCH(ApiUrlManagementV2ReleasesName, controller.PatchRelease)
3✔
333

3✔
334
        }
3✔
335
}
336

337
func FMTConfigURL(scheme, hostname, deploymentID, deviceType, deviceID string) string {
2✔
338
        repl := strings.NewReplacer(
2✔
339
                ":"+ParamDeploymentID, url.PathEscape(deploymentID),
2✔
340
                ":"+ParamDeviceType, url.PathEscape(deviceType),
2✔
341
                ":"+ParamDeviceID, url.PathEscape(deviceID),
2✔
342
        )
2✔
343
        return scheme + "://" + hostname + ApiUrlDevices + repl.Replace(ApiUrlDevicesDownloadConfig)
2✔
344
}
2✔
345

346
func ServiceUnavailable(c *gin.Context) {
1✔
347
        c.Status(http.StatusServiceUnavailable)
1✔
348
}
1✔
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