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

mendersoftware / mender / 946015310

pending completion
946015310

push

gitlab-ci

kacf
chore: Expose deployment status strings to the outside.

Signed-off-by: Kristian Amlie <kristian.amlie@northern.tech>

3 of 3 new or added lines in 1 file covered. (100.0%)

4268 of 5996 relevant lines covered (71.18%)

148.55 hits per line

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

78.85
/mender-update/deployments.cpp
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
#include <mender-update/deployments.hpp>
16

17
#include <algorithm>
18
#include <sstream>
19
#include <string>
20

21
#include <api/api.hpp>
22
#include <common/common.hpp>
23
#include <common/error.hpp>
24
#include <common/events.hpp>
25
#include <common/expected.hpp>
26
#include <common/http.hpp>
27
#include <common/io.hpp>
28
#include <common/json.hpp>
29
#include <common/log.hpp>
30
#include <common/optional.hpp>
31
#include <common/path.hpp>
32
#include <mender-update/context.hpp>
33

34
namespace mender {
35
namespace update {
36
namespace deployments {
37

38
using namespace std;
39

40
namespace api = mender::api;
41
namespace common = mender::common;
42
namespace context = mender::update::context;
43
namespace error = mender::common::error;
44
namespace events = mender::common::events;
45
namespace expected = mender::common::expected;
46
namespace io = mender::common::io;
47
namespace json = mender::common::json;
48
namespace log = mender::common::log;
49
namespace optional = mender::common::optional;
50
namespace path = mender::common::path;
51

52
const DeploymentsErrorCategoryClass DeploymentsErrorCategory;
53

54
const char *DeploymentsErrorCategoryClass::name() const noexcept {
×
55
        return "DeploymentsErrorCategory";
×
56
}
57

58
string DeploymentsErrorCategoryClass::message(int code) const {
×
59
        switch (code) {
×
60
        case NoError:
×
61
                return "Success";
×
62
        case InvalidDataError:
×
63
                return "Invalid data error";
×
64
        case BadResponseError:
×
65
                return "Bad response error";
×
66
        }
67
        assert(false);
×
68
        return "Unknown";
69
}
70

71
error::Error MakeError(DeploymentsErrorCode code, const string &msg) {
4✔
72
        return error::Error(error_condition(code, DeploymentsErrorCategory), msg);
8✔
73
}
74

75
static const string check_updates_v1_uri = "/api/devices/v1/deployments/device/deployments/next";
76
static const string check_updates_v2_uri = "/api/devices/v2/deployments/device/deployments/next";
77

78
error::Error DeploymentClient::CheckNewDeployments(
6✔
79
        context::MenderContext &ctx,
80
        const string &server_url,
81
        http::Client &client,
82
        CheckUpdatesAPIResponseHandler api_handler) {
83
        auto ex_dev_type = ctx.GetDeviceType();
12✔
84
        if (!ex_dev_type) {
6✔
85
                return ex_dev_type.error();
×
86
        }
87
        string device_type = ex_dev_type.value();
12✔
88

89
        auto ex_provides = ctx.LoadProvides();
12✔
90
        if (!ex_provides) {
6✔
91
                return ex_provides.error();
×
92
        }
93
        auto provides = ex_provides.value();
12✔
94
        if (provides.find("artifact_name") == provides.end()) {
6✔
95
                return MakeError(InvalidDataError, "Missing artifact name data");
×
96
        }
97

98
        stringstream ss;
12✔
99
        ss << R"({"update_control_map":false,"device_provides":{)";
6✔
100
        ss << R"("device_type":")";
6✔
101
        ss << json::EscapeString(device_type);
6✔
102

103
        for (const auto &kv : provides) {
14✔
104
                ss << "\",\"" + json::EscapeString(kv.first) + "\":\"";
8✔
105
                ss << json::EscapeString(kv.second);
8✔
106
        }
107

108
        ss << R"("}})";
6✔
109

110
        string v2_payload = ss.str();
12✔
111
        http::BodyGenerator payload_gen = [v2_payload]() {
6✔
112
                return make_shared<io::StringReader>(v2_payload);
6✔
113
        };
12✔
114

115
        // TODO: APIRequest
116
        auto v2_req = make_shared<http::OutgoingRequest>();
12✔
117
        v2_req->SetAddress(http::JoinUrl(server_url, check_updates_v2_uri));
6✔
118
        v2_req->SetMethod(http::Method::POST);
6✔
119
        v2_req->SetHeader("Content-Type", "application/json");
6✔
120
        v2_req->SetHeader("Content-Length", to_string(v2_payload.size()));
6✔
121
        v2_req->SetHeader("Accept", "application/json");
6✔
122
        v2_req->SetBodyGenerator(payload_gen);
6✔
123

124
        string v1_args = "artifact_name=" + http::URLEncode(provides["artifact_name"])
12✔
125
                                         + "&device_type=" + http::URLEncode(device_type);
24✔
126
        auto v1_req = make_shared<http::OutgoingRequest>();
12✔
127
        v1_req->SetAddress(http::JoinUrl(server_url, check_updates_v1_uri) + "?" + v1_args);
6✔
128
        v1_req->SetMethod(http::Method::GET);
6✔
129
        v1_req->SetHeader("Accept", "application/json");
6✔
130

131
        auto received_body = make_shared<vector<uint8_t>>();
12✔
132
        auto handle_data = [received_body, api_handler](unsigned status) {
4✔
133
                if (status == http::StatusOK) {
4✔
134
                        auto ex_j = json::Load(common::StringFromByteVector(*received_body));
4✔
135
                        if (ex_j) {
2✔
136
                                CheckUpdatesAPIResponse response {optional::optional<json::Json> {ex_j.value()}};
2✔
137
                                api_handler(response);
2✔
138
                        } else {
139
                                api_handler(expected::unexpected(ex_j.error()));
×
140
                        }
141
                } else if (status == http::StatusNoContent) {
2✔
142
                        api_handler(CheckUpdatesAPIResponse {optional::nullopt});
2✔
143
                }
144
        };
16✔
145

146
        http::ResponseHandler header_handler =
147
                [received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
9✔
148
                        if (!exp_resp) {
9✔
149
                                log::Error("Request to check new deployments failed: " + exp_resp.error().message);
×
150
                                CheckUpdatesAPIResponse response = expected::unexpected(exp_resp.error());
×
151
                                api_handler(response);
×
152
                                return;
×
153
                        }
154

155
                        auto resp = exp_resp.value();
18✔
156
                        received_body->clear();
9✔
157
                        auto body_writer = make_shared<io::ByteWriter>(received_body);
9✔
158
                        body_writer->SetUnlimited(true);
9✔
159
                        resp->SetBodyWriter(body_writer);
9✔
160
                };
12✔
161

162
        http::ResponseHandler v1_body_handler =
163
                [received_body, api_handler, handle_data](http::ExpectedIncomingResponsePtr exp_resp) {
3✔
164
                        if (!exp_resp) {
3✔
165
                                log::Error("Request to check new deployments failed: " + exp_resp.error().message);
×
166
                                CheckUpdatesAPIResponse response = expected::unexpected(exp_resp.error());
×
167
                                api_handler(response);
×
168
                                return;
×
169
                        }
170
                        auto resp = exp_resp.value();
6✔
171
                        auto status = resp->GetStatusCode();
3✔
172
                        if ((status == http::StatusOK) || (status == http::StatusNoContent)) {
3✔
173
                                handle_data(status);
2✔
174
                        } else {
175
                                auto ex_err_msg = api::ErrorMsgFromErrorResponse(*received_body);
2✔
176
                                string err_str;
1✔
177
                                if (ex_err_msg) {
1✔
178
                                        err_str = ex_err_msg.value();
×
179
                                } else {
180
                                        err_str = resp->GetStatusMessage();
1✔
181
                                }
182
                                api_handler(expected::unexpected(MakeError(
2✔
183
                                        BadResponseError,
184
                                        "Got unexpected response " + to_string(status) + ": " + err_str)));
3✔
185
                        }
186
                };
12✔
187

188
        http::ResponseHandler v2_body_handler = [received_body,
6✔
189
                                                                                         v1_req,
190
                                                                                         header_handler,
191
                                                                                         v1_body_handler,
192
                                                                                         api_handler,
193
                                                                                         handle_data,
194
                                                                                         &client](http::ExpectedIncomingResponsePtr exp_resp) {
3✔
195
                if (!exp_resp) {
6✔
196
                        log::Error("Request to check new deployments failed: " + exp_resp.error().message);
×
197
                        CheckUpdatesAPIResponse response = expected::unexpected(exp_resp.error());
×
198
                        api_handler(response);
×
199
                        return;
×
200
                }
201
                auto resp = exp_resp.value();
12✔
202
                auto status = resp->GetStatusCode();
6✔
203
                if ((status == http::StatusOK) || (status == http::StatusNoContent)) {
6✔
204
                        handle_data(status);
2✔
205
                } else if (status == http::StatusNotFound) {
4✔
206
                        log::Info(
3✔
207
                                "POST request to v2 version of the deployments API failed, falling back to v1 version and GET");
6✔
208
                        auto err = client.AsyncCall(v1_req, header_handler, v1_body_handler);
9✔
209
                        if (err != error::NoError) {
3✔
210
                                api_handler(expected::unexpected(err.WithContext("While calling v1 endpoint")));
×
211
                        }
212
                } else {
213
                        auto ex_err_msg = api::ErrorMsgFromErrorResponse(*received_body);
2✔
214
                        string err_str;
1✔
215
                        if (ex_err_msg) {
1✔
216
                                err_str = ex_err_msg.value();
1✔
217
                        } else {
218
                                err_str = resp->GetStatusMessage();
×
219
                        }
220
                        api_handler(expected::unexpected(MakeError(
2✔
221
                                BadResponseError,
222
                                "Got unexpected response " + to_string(status) + ": " + err_str)));
3✔
223
                }
224
        };
6✔
225

226
        return client.AsyncCall(v2_req, header_handler, v2_body_handler);
6✔
227
}
228

229
static const string deployment_status_strings[static_cast<int>(DeploymentStatus::End_) + 1] = {
230
        "installing",
231
        "pause_before_installing",
232
        "downloading",
233
        "pause_before_rebooting",
234
        "rebooting",
235
        "pause_before_committing",
236
        "success",
237
        "failure",
238
        "already-installed"};
239

240
static const string deployments_uri_prefix = "/api/devices/v1/deployments/device/deployments";
241
static const string status_uri_suffix = "/status";
242

243
string DeploymentStatusString(DeploymentStatus status) {
3✔
244
        return deployment_status_strings[static_cast<int>(status)];
3✔
245
}
246

247
error::Error DeploymentClient::PushStatus(
3✔
248
        const string &deployment_id,
249
        DeploymentStatus status,
250
        const string &substate,
251
        const string &server_url,
252
        http::Client &client,
253
        StatusAPIResponseHandler api_handler) {
254
        string payload = R"({"status":")" + DeploymentStatusString(status) + "\"";
9✔
255
        if (substate != "") {
3✔
256
                payload += R"(,"substate":")" + json::EscapeString(substate) + "\"}";
2✔
257
        } else {
258
                payload += "}";
1✔
259
        }
260
        http::BodyGenerator payload_gen = [payload]() {
3✔
261
                return make_shared<io::StringReader>(payload);
3✔
262
        };
6✔
263

264
        auto req = make_shared<http::OutgoingRequest>();
6✔
265
        req->SetAddress(
3✔
266
                http::JoinUrl(server_url, deployments_uri_prefix, deployment_id, status_uri_suffix));
6✔
267
        req->SetMethod(http::Method::PUT);
3✔
268
        req->SetHeader("Content-Type", "application/json");
3✔
269
        req->SetHeader("Content-Length", to_string(payload.size()));
3✔
270
        req->SetHeader("Accept", "application/json");
3✔
271
        req->SetBodyGenerator(payload_gen);
3✔
272

273
        auto received_body = make_shared<vector<uint8_t>>();
3✔
274
        return client.AsyncCall(
275
                req,
276
                [received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
3✔
277
                        if (!exp_resp) {
3✔
278
                                log::Error("Request to push status data failed: " + exp_resp.error().message);
×
279
                                api_handler(exp_resp.error());
×
280
                                return;
×
281
                        }
282

283
                        auto body_writer = make_shared<io::ByteWriter>(received_body);
6✔
284
                        auto resp = exp_resp.value();
6✔
285
                        auto content_length = resp->GetHeader("Content-Length");
9✔
286
                        auto ex_len = common::StringToLongLong(content_length.value());
3✔
287
                        if (!ex_len) {
3✔
288
                                log::Error("Failed to get content length from the status API response headers");
×
289
                                body_writer->SetUnlimited(true);
×
290
                        } else {
291
                                received_body->resize(ex_len.value());
3✔
292
                        }
293
                        resp->SetBodyWriter(body_writer);
3✔
294
                },
295
                [received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
3✔
296
                        if (!exp_resp) {
3✔
297
                                log::Error("Request to push status data failed: " + exp_resp.error().message);
×
298
                                api_handler(exp_resp.error());
×
299
                                return;
×
300
                        }
301

302
                        auto resp = exp_resp.value();
6✔
303
                        auto status = resp->GetStatusCode();
3✔
304
                        if (status == http::StatusNoContent) {
3✔
305
                                api_handler(error::NoError);
2✔
306
                        } else {
307
                                auto ex_err_msg = api::ErrorMsgFromErrorResponse(*received_body);
2✔
308
                                string err_str;
1✔
309
                                if (ex_err_msg) {
1✔
310
                                        err_str = ex_err_msg.value();
1✔
311
                                } else {
312
                                        err_str = resp->GetStatusMessage();
×
313
                                }
314
                                api_handler(MakeError(
1✔
315
                                        BadResponseError,
316
                                        "Got unexpected response " + to_string(status)
2✔
317
                                                + " from status API: " + err_str));
2✔
318
                        }
319
                });
6✔
320
}
321

322
using mender::common::expected::ExpectedSize;
323

324
static ExpectedSize GetLogFileDataSize(const string &path) {
3✔
325
        auto ex_istr = io::OpenIfstream(path);
6✔
326
        if (!ex_istr) {
3✔
327
                return expected::unexpected(ex_istr.error());
×
328
        }
329
        auto istr = std::move(ex_istr.value());
6✔
330

331
        // We want the size of the actual data without a potential trailing
332
        // newline. So let's seek one byte before the end of file, check if the last
333
        // byte is a newline and return the appropriate number.
334
        istr.seekg(-1, ios_base::end);
3✔
335
        char c = istr.get();
3✔
336
        if (c == '\n') {
3✔
337
                return istr.tellg() - static_cast<ifstream::off_type>(1);
6✔
338
        } else {
339
                return istr.tellg();
×
340
        }
341
}
342

343
const vector<uint8_t> JsonLogMessagesReader::header_ = {
344
        '{', '"', 'm', 'e', 's', 's', 'a', 'g', 'e', 's', '"', ':', '['};
345
const vector<uint8_t> JsonLogMessagesReader::closing_ = {']', '}'};
346

347
ExpectedSize JsonLogMessagesReader::Read(
89✔
348
        vector<uint8_t>::iterator start, vector<uint8_t>::iterator end) {
349
        if (header_rem_ > 0) {
89✔
350
                io::Vsize target_size = end - start;
9✔
351
                auto copy_end = copy_n(
352
                        header_.begin() + (header_.size() - header_rem_), min(header_rem_, target_size), start);
9✔
353
                auto n_copied = copy_end - start;
9✔
354
                header_rem_ -= n_copied;
9✔
355
                return static_cast<size_t>(n_copied);
18✔
356
        } else if (rem_raw_data_size_ > 0) {
80✔
357
                if (static_cast<size_t>(end - start) > rem_raw_data_size_) {
64✔
358
                        end = start + rem_raw_data_size_;
8✔
359
                }
360
                auto ex_sz = reader_->Read(start, end);
128✔
361
                if (!ex_sz) {
64✔
362
                        return ex_sz;
×
363
                }
364
                auto n_read = ex_sz.value();
64✔
365
                rem_raw_data_size_ -= n_read;
64✔
366

367
                // We control how much we read from the file so we should never read
368
                // 0 bytes (meaning EOF reached). If we do, it means the file is
369
                // smaller than what we were told.
370
                assert(n_read > 0);
64✔
371
                if (n_read == 0) {
64✔
372
                        return expected::unexpected(
×
373
                                MakeError(InvalidDataError, "Unexpected EOF when reading logs file"));
×
374
                }
375

376
                // Replace all newlines with commas
377
                const auto read_end = start + n_read;
64✔
378
                for (auto it = start; it < read_end; it++) {
1,916✔
379
                        if (it[0] == '\n') {
1,852✔
380
                                it[0] = ',';
12✔
381
                        }
382
                }
383
                return n_read;
64✔
384
        } else if (closing_rem_ > 0) {
16✔
385
                io::Vsize target_size = end - start;
8✔
386
                auto copy_end = copy_n(
387
                        closing_.begin() + (closing_.size() - closing_rem_),
×
388
                        min(closing_rem_, target_size),
8✔
389
                        start);
8✔
390
                auto n_copied = copy_end - start;
8✔
391
                closing_rem_ -= n_copied;
8✔
392
                return static_cast<size_t>(copy_end - start);
16✔
393
        } else {
394
                return 0;
8✔
395
        }
396
};
397

398
static const string logs_uri_suffix = "/log";
399

400
error::Error DeploymentClient::PushLogs(
3✔
401
        const string &deployment_id,
402
        const string &log_file_path,
403
        const string &server_url,
404
        http::Client &client,
405
        LogsAPIResponseHandler api_handler) {
406
        auto ex_size = GetLogFileDataSize(log_file_path);
6✔
407
        if (!ex_size) {
3✔
408
                // api_handler(ex_size.error()) ???
409
                return ex_size.error();
×
410
        }
411
        auto data_size = ex_size.value();
3✔
412

413
        auto file_reader = make_shared<io::FileReader>(log_file_path);
6✔
414
        auto logs_reader = make_shared<JsonLogMessagesReader>(file_reader, data_size);
6✔
415

416
        auto req = make_shared<http::OutgoingRequest>();
6✔
417
        req->SetAddress(
3✔
418
                http::JoinUrl(server_url, deployments_uri_prefix, deployment_id, logs_uri_suffix));
6✔
419
        req->SetMethod(http::Method::PUT);
3✔
420
        req->SetHeader("Content-Type", "application/json");
3✔
421
        req->SetHeader("Content-Length", to_string(JsonLogMessagesReader::TotalDataSize(data_size)));
3✔
422
        req->SetHeader("Accept", "application/json");
3✔
423
        req->SetBodyGenerator([logs_reader]() {
6✔
424
                logs_reader->Rewind();
3✔
425
                return logs_reader;
3✔
426
        });
6✔
427

428
        auto received_body = make_shared<vector<uint8_t>>();
3✔
429
        return client.AsyncCall(
430
                req,
431
                [received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
3✔
432
                        if (!exp_resp) {
3✔
433
                                log::Error("Request to push logs data failed: " + exp_resp.error().message);
×
434
                                api_handler(exp_resp.error());
×
435
                                return;
×
436
                        }
437

438
                        auto body_writer = make_shared<io::ByteWriter>(received_body);
6✔
439
                        auto resp = exp_resp.value();
6✔
440
                        auto content_length = resp->GetHeader("Content-Length");
9✔
441
                        auto ex_len = common::StringToLongLong(content_length.value());
3✔
442
                        if (!ex_len) {
3✔
443
                                log::Error("Failed to get content length from the logs API response headers");
×
444
                                body_writer->SetUnlimited(true);
×
445
                        } else {
446
                                received_body->resize(ex_len.value());
3✔
447
                        }
448
                        resp->SetBodyWriter(body_writer);
3✔
449
                },
450
                [received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
3✔
451
                        if (!exp_resp) {
3✔
452
                                log::Error("Request to push logs data failed: " + exp_resp.error().message);
×
453
                                api_handler(exp_resp.error());
×
454
                                return;
×
455
                        }
456

457
                        auto resp = exp_resp.value();
6✔
458
                        auto status = resp->GetStatusCode();
3✔
459
                        if (status == http::StatusNoContent) {
3✔
460
                                api_handler(error::NoError);
2✔
461
                        } else {
462
                                auto ex_err_msg = api::ErrorMsgFromErrorResponse(*received_body);
2✔
463
                                string err_str;
1✔
464
                                if (ex_err_msg) {
1✔
465
                                        err_str = ex_err_msg.value();
1✔
466
                                } else {
467
                                        err_str = resp->GetStatusMessage();
×
468
                                }
469
                                api_handler(MakeError(
1✔
470
                                        BadResponseError,
471
                                        "Got unexpected response " + to_string(status) + " from logs API: " + err_str));
2✔
472
                        }
473
                });
3✔
474
}
475

476
} // namespace deployments
477
} // namespace update
478
} // namespace mender
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