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

mendersoftware / mender / 1071875915

14 Nov 2023 11:46AM UTC coverage: 80.182% (+0.08%) from 80.107%
1071875915

push

gitlab-ci

kacf
chore: Get rid of direct comparisons with `Error::code`.

Such comparisons are dangerous, because if you don't also compare the
category, you can get false positives.

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

12 of 12 new or added lines in 3 files covered. (100.0%)

79 existing lines in 9 files now uncovered.

6967 of 8689 relevant lines covered (80.18%)

9263.44 hits per line

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

78.66
/src/mender-update/deployments/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 <api/client.hpp>
23
#include <common/common.hpp>
24
#include <common/error.hpp>
25
#include <common/events.hpp>
26
#include <common/expected.hpp>
27
#include <common/http.hpp>
28
#include <common/io.hpp>
29
#include <common/json.hpp>
30
#include <common/log.hpp>
31
#include <common/optional.hpp>
32
#include <common/path.hpp>
33
#include <mender-update/context.hpp>
34

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

39
using namespace std;
40

41
namespace api = mender::api;
42
namespace common = mender::common;
43
namespace context = mender::update::context;
44
namespace error = mender::common::error;
45
namespace events = mender::common::events;
46
namespace expected = mender::common::expected;
47
namespace io = mender::common::io;
48
namespace json = mender::common::json;
49
namespace log = mender::common::log;
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 {
2✔
59
        switch (code) {
2✔
60
        case NoError:
61
                return "Success";
×
62
        case InvalidDataError:
63
                return "Invalid data error";
×
64
        case BadResponseError:
65
                return "Bad response error";
×
66
        case DeploymentAbortedError:
67
                return "Deployment was aborted on the server";
2✔
68
        }
69
        assert(false);
70
        return "Unknown";
×
71
}
72

73
error::Error MakeError(DeploymentsErrorCode code, const string &msg) {
25✔
74
        return error::Error(error_condition(code, DeploymentsErrorCategory), msg);
30✔
75
}
76

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

80
error::Error DeploymentClient::CheckNewDeployments(
7✔
81
        context::MenderContext &ctx, api::Client &client, CheckUpdatesAPIResponseHandler api_handler) {
82
        auto ex_dev_type = ctx.GetDeviceType();
7✔
83
        if (!ex_dev_type) {
7✔
84
                return ex_dev_type.error();
1✔
85
        }
86
        string device_type = ex_dev_type.value();
6✔
87

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

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

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

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

109
        string v2_payload = ss.str();
110
        log::Debug("deployments/next v2 payload " + v2_payload);
6✔
111
        http::BodyGenerator payload_gen = [v2_payload]() {
48✔
112
                return make_shared<io::StringReader>(v2_payload);
6✔
113
        };
6✔
114

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

123
        string v1_args = "artifact_name=" + http::URLEncode(provides["artifact_name"])
18✔
124
                                         + "&device_type=" + http::URLEncode(device_type);
18✔
125
        auto v1_req = make_shared<api::APIRequest>();
6✔
126
        v1_req->SetPath(check_updates_v1_uri + "?" + v1_args);
12✔
127
        v1_req->SetMethod(http::Method::GET);
6✔
128
        v1_req->SetHeader("Accept", "application/json");
12✔
129

130
        auto received_body = make_shared<vector<uint8_t>>();
6✔
131
        auto handle_data = [received_body, api_handler](unsigned status) {
4✔
132
                if (status == http::StatusOK) {
4✔
133
                        auto ex_j = json::Load(common::StringFromByteVector(*received_body));
4✔
134
                        if (ex_j) {
2✔
135
                                CheckUpdatesAPIResponse response {optional<json::Json> {ex_j.value()}};
2✔
136
                                api_handler(response);
4✔
137
                        } else {
138
                                api_handler(expected::unexpected(ex_j.error()));
×
139
                        }
140
                } else if (status == http::StatusNoContent) {
2✔
141
                        api_handler(CheckUpdatesAPIResponse {nullopt});
4✔
142
                } else {
143
                        log::Warning(
×
144
                                "DeploymentClient::CheckNewDeployments - received unhandled http response: "
145
                                + to_string(status));
×
146
                        api_handler(expected::unexpected(MakeError(
×
147
                                DeploymentAbortedError, "received unhandled HTTP response: " + to_string(status))));
×
148
                }
149
        };
16✔
150

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

160
                        auto resp = exp_resp.value();
9✔
161
                        received_body->clear();
9✔
162
                        auto body_writer = make_shared<io::ByteWriter>(received_body);
9✔
163
                        body_writer->SetUnlimited(true);
9✔
164
                        resp->SetBodyWriter(body_writer);
18✔
165
                };
12✔
166

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

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

231
        return client.AsyncCall(v2_req, header_handler, v2_body_handler);
18✔
232
}
233

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

245
static const string deployments_uri_prefix = "/api/devices/v1/deployments/device/deployments";
246
static const string status_uri_suffix = "/status";
247

248
string DeploymentStatusString(DeploymentStatus status) {
494✔
249
        return deployment_status_strings[static_cast<int>(status)];
494✔
250
}
251

252
error::Error DeploymentClient::PushStatus(
4✔
253
        const string &deployment_id,
254
        DeploymentStatus status,
255
        const string &substate,
256
        api::Client &client,
257
        StatusAPIResponseHandler api_handler) {
258
        string payload = R"({"status":")" + DeploymentStatusString(status) + "\"";
8✔
259
        if (substate != "") {
4✔
260
                payload += R"(,"substate":")" + json::EscapeString(substate) + "\"}";
6✔
261
        } else {
262
                payload += "}";
1✔
263
        }
264
        http::BodyGenerator payload_gen = [payload]() {
32✔
265
                return make_shared<io::StringReader>(payload);
4✔
266
        };
4✔
267

268
        auto req = make_shared<api::APIRequest>();
4✔
269
        req->SetPath(http::JoinUrl(deployments_uri_prefix, deployment_id, status_uri_suffix));
8✔
270
        req->SetMethod(http::Method::PUT);
4✔
271
        req->SetHeader("Content-Type", "application/json");
8✔
272
        req->SetHeader("Content-Length", to_string(payload.size()));
8✔
273
        req->SetHeader("Accept", "application/json");
8✔
274
        req->SetBodyGenerator(payload_gen);
4✔
275

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

286
                        auto body_writer = make_shared<io::ByteWriter>(received_body);
4✔
287
                        auto resp = exp_resp.value();
4✔
288
                        auto content_length = resp->GetHeader("Content-Length");
8✔
289
                        if (!content_length) {
4✔
UNCOV
290
                                log::Debug(
×
291
                                        "Failed to get content length from the status API response headers: "
292
                                        + content_length.error().String());
×
293
                        } else {
294
                                auto ex_len = common::StringToLongLong(content_length.value());
4✔
295
                                if (!ex_len) {
4✔
UNCOV
296
                                        log::Error(
×
297
                                                "Failed to convert the content length from the status API response headers to an integer: "
298
                                                + ex_len.error().String());
×
UNCOV
299
                                        body_writer->SetUnlimited(true);
×
300
                                } else {
301
                                        received_body->resize(ex_len.value());
4✔
302
                                }
303
                        }
304
                        resp->SetBodyWriter(body_writer);
8✔
305
                },
306
                [received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
4✔
307
                        if (!exp_resp) {
4✔
UNCOV
308
                                log::Error("Request to push status data failed: " + exp_resp.error().message);
×
UNCOV
309
                                api_handler(exp_resp.error());
×
310
                                return;
×
311
                        }
312

313
                        auto resp = exp_resp.value();
4✔
314
                        auto status = resp->GetStatusCode();
4✔
315
                        if (status == http::StatusNoContent) {
4✔
316
                                api_handler(error::NoError);
4✔
317
                        } else if (status == http::StatusConflict) {
2✔
318
                                api_handler(
1✔
319
                                        MakeError(DeploymentAbortedError, "Could not send status update to server"));
2✔
320
                        } else {
321
                                auto ex_err_msg = api::ErrorMsgFromErrorResponse(*received_body);
1✔
322
                                string err_str;
323
                                if (ex_err_msg) {
1✔
324
                                        err_str = ex_err_msg.value();
1✔
325
                                } else {
UNCOV
326
                                        err_str = resp->GetStatusMessage();
×
327
                                }
328
                                api_handler(MakeError(
2✔
329
                                        BadResponseError,
330
                                        "Got unexpected response " + to_string(status)
2✔
331
                                                + " from status API: " + err_str));
2✔
332
                        }
333
                });
20✔
334
}
335

336
using mender::common::expected::ExpectedSize;
337

338
static ExpectedSize GetLogFileDataSize(const string &path) {
3✔
339
        auto ex_istr = io::OpenIfstream(path);
3✔
340
        if (!ex_istr) {
3✔
UNCOV
341
                return expected::unexpected(ex_istr.error());
×
342
        }
343
        auto istr = std::move(ex_istr.value());
6✔
344

345
        // We want the size of the actual data without a potential trailing
346
        // newline. So let's seek one byte before the end of file, check if the last
347
        // byte is a newline and return the appropriate number.
348
        istr.seekg(-1, ios_base::end);
3✔
349
        char c = istr.get();
3✔
350
        if (c == '\n') {
3✔
351
                return istr.tellg() - static_cast<ifstream::off_type>(1);
3✔
352
        } else {
UNCOV
353
                return istr.tellg();
×
354
        }
355
}
356

357
const vector<uint8_t> JsonLogMessagesReader::header_ = {
358
        '{', '"', 'm', 'e', 's', 's', 'a', 'g', 'e', 's', '"', ':', '['};
359
const vector<uint8_t> JsonLogMessagesReader::closing_ = {']', '}'};
360

361
ExpectedSize JsonLogMessagesReader::Read(
89✔
362
        vector<uint8_t>::iterator start, vector<uint8_t>::iterator end) {
363
        if (header_rem_ > 0) {
89✔
364
                io::Vsize target_size = end - start;
9✔
365
                auto copy_end = copy_n(
366
                        header_.begin() + (header_.size() - header_rem_), min(header_rem_, target_size), start);
10✔
367
                auto n_copied = copy_end - start;
368
                header_rem_ -= n_copied;
9✔
369
                return static_cast<size_t>(n_copied);
370
        } else if (rem_raw_data_size_ > 0) {
80✔
371
                if (static_cast<size_t>(end - start) > rem_raw_data_size_) {
64✔
372
                        end = start + rem_raw_data_size_;
373
                }
374
                auto ex_sz = reader_->Read(start, end);
64✔
375
                if (!ex_sz) {
64✔
376
                        return ex_sz;
377
                }
378
                auto n_read = ex_sz.value();
64✔
379
                rem_raw_data_size_ -= n_read;
64✔
380

381
                // We control how much we read from the file so we should never read
382
                // 0 bytes (meaning EOF reached). If we do, it means the file is
383
                // smaller than what we were told.
384
                assert(n_read > 0);
385
                if (n_read == 0) {
64✔
UNCOV
386
                        return expected::unexpected(
×
UNCOV
387
                                MakeError(InvalidDataError, "Unexpected EOF when reading logs file"));
×
388
                }
389

390
                // Replace all newlines with commas
391
                const auto read_end = start + n_read;
392
                for (auto it = start; it < read_end; it++) {
1,916✔
393
                        if (it[0] == '\n') {
1,852✔
394
                                it[0] = ',';
12✔
395
                        }
396
                }
397
                return n_read;
398
        } else if (closing_rem_ > 0) {
16✔
399
                io::Vsize target_size = end - start;
8✔
400
                auto copy_end = copy_n(
401
                        closing_.begin() + (closing_.size() - closing_rem_),
8✔
402
                        min(closing_rem_, target_size),
8✔
403
                        start);
8✔
404
                auto n_copied = copy_end - start;
405
                closing_rem_ -= n_copied;
8✔
406
                return static_cast<size_t>(copy_end - start);
407
        } else {
408
                return 0;
409
        }
410
};
411

412
static const string logs_uri_suffix = "/log";
413

414
error::Error DeploymentClient::PushLogs(
3✔
415
        const string &deployment_id,
416
        const string &log_file_path,
417
        api::Client &client,
418
        LogsAPIResponseHandler api_handler) {
419
        auto ex_size = GetLogFileDataSize(log_file_path);
3✔
420
        if (!ex_size) {
3✔
421
                // api_handler(ex_size.error()) ???
UNCOV
422
                return ex_size.error();
×
423
        }
424
        auto data_size = ex_size.value();
3✔
425

426
        auto file_reader = make_shared<io::FileReader>(log_file_path);
3✔
427
        auto logs_reader = make_shared<JsonLogMessagesReader>(file_reader, data_size);
3✔
428

429
        auto req = make_shared<api::APIRequest>();
3✔
430
        req->SetPath(http::JoinUrl(deployments_uri_prefix, deployment_id, logs_uri_suffix));
6✔
431
        req->SetMethod(http::Method::PUT);
3✔
432
        req->SetHeader("Content-Type", "application/json");
6✔
433
        req->SetHeader("Content-Length", to_string(JsonLogMessagesReader::TotalDataSize(data_size)));
6✔
434
        req->SetHeader("Accept", "application/json");
6✔
435
        req->SetBodyGenerator([logs_reader]() {
18✔
436
                logs_reader->Rewind();
6✔
437
                return logs_reader;
3✔
438
        });
6✔
439

440
        auto received_body = make_shared<vector<uint8_t>>();
3✔
441
        return client.AsyncCall(
442
                req,
443
                [received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
3✔
444
                        if (!exp_resp) {
3✔
UNCOV
445
                                log::Error("Request to push logs data failed: " + exp_resp.error().message);
×
UNCOV
446
                                api_handler(exp_resp.error());
×
447
                                return;
×
448
                        }
449

450
                        auto body_writer = make_shared<io::ByteWriter>(received_body);
3✔
451
                        auto resp = exp_resp.value();
3✔
452
                        auto content_length = resp->GetHeader("Content-Length");
6✔
453
                        if (!content_length) {
3✔
UNCOV
454
                                log::Debug(
×
455
                                        "Failed to get content length from the status API response headers: "
456
                                        + content_length.error().String());
×
457
                        } else {
458
                                auto ex_len = common::StringToLongLong(content_length.value());
3✔
459
                                if (!ex_len) {
3✔
UNCOV
460
                                        log::Error(
×
461
                                                "Failed to convert the content length from the status API response headers to an integer: "
462
                                                + ex_len.error().String());
×
UNCOV
463
                                        body_writer->SetUnlimited(true);
×
464
                                } else {
465
                                        received_body->resize(ex_len.value());
3✔
466
                                }
467
                        }
468
                        resp->SetBodyWriter(body_writer);
6✔
469
                },
470
                [received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
3✔
471
                        if (!exp_resp) {
3✔
UNCOV
472
                                log::Error("Request to push logs data failed: " + exp_resp.error().message);
×
UNCOV
473
                                api_handler(exp_resp.error());
×
474
                                return;
×
475
                        }
476

477
                        auto resp = exp_resp.value();
3✔
478
                        auto status = resp->GetStatusCode();
3✔
479
                        if (status == http::StatusNoContent) {
3✔
480
                                api_handler(error::NoError);
4✔
481
                        } else {
482
                                auto ex_err_msg = api::ErrorMsgFromErrorResponse(*received_body);
1✔
483
                                string err_str;
484
                                if (ex_err_msg) {
1✔
485
                                        err_str = ex_err_msg.value();
1✔
486
                                } else {
UNCOV
487
                                        err_str = resp->GetStatusMessage();
×
488
                                }
489
                                api_handler(MakeError(
2✔
490
                                        BadResponseError,
491
                                        "Got unexpected response " + to_string(status) + " from logs API: " + err_str));
2✔
492
                        }
493
                });
12✔
494
}
495

496
} // namespace deployments
497
} // namespace update
498
} // 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