-
Notifications
You must be signed in to change notification settings - Fork 18
Expand file tree
/
Copy pathappengine.cc
More file actions
293 lines (270 loc) · 11 KB
/
appengine.cc
File metadata and controls
293 lines (270 loc) · 11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
#include "appengine.h"
#include <boost/format.hpp>
#include "aktualizr-lite/storage/stat.h"
#include "exec.h"
namespace composeapp {
enum class ExitCode { ExitCodeInsufficientSpace = 100 };
static bool checkAppStatus(const AppEngine::App& app, const Json::Value& status);
static bool checkAppInstallationStatus(const AppEngine::App& app, const Json::Value& status);
static bool isNullOrEmptyOrUnset(const Json::Value& val, const std::string& field);
static Json::Value parseJSON(const std::string& json_str);
AppEngine::Result AppEngine::fetch(const App& app) {
Result res{false};
bool was_proxy_set{false};
try {
// If a given app was fetched before, then don't consider it as a fetched app if a caller tries to fetch it again
// for one reason or another - hence remove it from the set of fetched apps.
fetched_apps_.erase(app.uri);
if (local_source_path_.empty()) {
if (proxy_) {
// If the proxy provider is set, then obtain the proxy URL and CA from it,
// and set the corresponding environment variables for `composectl`.
const auto proxy{proxy_()};
if (!proxy.first.empty()) {
::setenv("COMPOSE_APPS_PROXY", proxy.first.c_str(), 1);
::setenv("COMPOSE_APPS_PROXY_CA", proxy.second.c_str(), 1);
was_proxy_set = true;
}
}
exec(boost::format{"%s --store %s pull -p %s --storage-usage-watermark %d"} % composectl_cmd_ % storeRoot() %
app.uri % storage_watermark_,
"failed to pull compose app", "", nullptr, "4h", true);
} else {
exec(boost::format{"%s --store %s pull -p %s -l %s --storage-usage-watermark %d"} % composectl_cmd_ %
storeRoot() % app.uri % local_source_path_ % storage_watermark_,
"failed to pull compose app", "", nullptr, "4h", true);
}
res = true;
fetched_apps_.insert(app.uri);
} catch (const ExecError& exc) {
if (exc.ExitCode == static_cast<int>(ExitCode::ExitCodeInsufficientSpace)) {
const auto usage_stat{Utils::parseJSON(exc.StdErr)};
auto usage_info{storageSpaceFunc()(usage_stat["path"].asString())};
res = {Result::ID::InsufficientSpace, exc.what(), usage_info.withRequired(usage_stat["required"].asUInt64())};
} else {
res = {false, exc.what()};
}
} catch (const std::exception& exc) {
res = {false, exc.what()};
}
if (was_proxy_set) {
::unsetenv("COMPOSE_APPS_PROXY");
::unsetenv("COMPOSE_APPS_PROXY_CA");
}
return res;
}
void AppEngine::remove(const App& app) {
try {
fetched_apps_.erase(app.uri);
// "App removal" in this context refers to deleting app images from the Docker store
// and removing the app compose project (app uninstall).
// Unused app blobs will be removed from the blob store via the prune() method,
// provided they are not utilized by any other app(s).
// Note: Ensure the app is stopped before attempting to uninstall it.
exec(boost::format{"%s --store %s --compose %s stop %s"} % composectl_cmd_ % storeRoot() % installRoot() % app.name,
"failed to stop app");
// Uninstall app, it only removes the app compose/project directory, docker store pruning is in the `prune` call
exec(boost::format{"%s --store %s --compose %s uninstall --ignore-non-installed %s"} % composectl_cmd_ %
storeRoot() % installRoot() % app.name,
"failed to uninstall app");
} catch (const std::exception& exc) {
LOG_WARNING << "App: " << app.name << ", failed to remove: " << exc.what();
}
}
bool AppEngine::isRunning(const App& app) const {
bool res{false};
try {
std::string output;
exec(boost::format{"%s --store %s --compose %s ps %s --format json"} % composectl_cmd_ % storeRoot() %
installRoot() % app.uri,
"", "", &output);
const auto app_status{parseJSON(output)};
// Make sure app images and bundle are properly installed
res = checkAppInstallationStatus(app, app_status);
if (res) {
// Make sure app is running
res = checkAppStatus(app, app_status);
}
} catch (const std::exception& exc) {
LOG_ERROR << "failed to verify whether app is running; app: " << app.name << ", err: " << exc.what();
}
return res;
}
Json::Value AppEngine::getRunningAppsInfo() const {
Json::Value app_statuses;
try {
std::string output;
exec(boost::format{"%s --store %s ps --format json"} % composectl_cmd_ % storeRoot(), "", "", &output);
app_statuses = parseJSON(output);
} catch (const std::exception& exc) {
LOG_WARNING << "Failed to get an info about running containers: " << exc.what();
}
return app_statuses;
}
void AppEngine::prune(const Apps& app_shortlist) {
try {
// Remove apps that are not in the shortlist
std::string output;
exec(boost::format{"%s --store %s ls --format json"} % composectl_cmd_ % storeRoot(), "failed to list apps", "",
&output);
const auto app_list{parseJSON(output)};
Apps apps_to_prune;
for (const auto& store_app_json : app_list) {
if (!(store_app_json.isMember("name") && store_app_json.isMember("uri"))) {
continue;
}
bool is_in_shortlist{false};
App store_app{store_app_json["name"].asString(), store_app_json["uri"].asString()};
for (const auto& shortlisted_app : app_shortlist) {
if (store_app == shortlisted_app) {
is_in_shortlist = true;
break;
}
}
if (!is_in_shortlist) {
apps_to_prune.push_back(store_app);
}
}
for (const auto& app : apps_to_prune) {
fetched_apps_.erase(app.uri);
exec(boost::format{"%s --store %s rm %s --prune=false --quiet"} % composectl_cmd_ % storeRoot() % app.uri,
"failed to remove app");
}
} catch (const std::exception& exc) {
LOG_WARNING << "Failed to remove unused apps: " << exc.what();
}
try {
// Pruning unused store blobs
std::string output;
exec(boost::format{"%s --store %s prune --format=json"} % composectl_cmd_ % storeRoot(),
"failed to prune app blobs", "", &output);
const auto pruned_blobs{Utils::parseJSON(output)};
// If at least one blob was pruned then the docker store needs to be pruned too to remove corresponding blobs
// from the docker store
if (!pruned_blobs.isNull() && !pruned_blobs.empty()) {
LOG_INFO << "Pruning unused docker containers";
dockerClient()->pruneContainers();
LOG_INFO << "Pruning unused docker images";
dockerClient()->pruneImages();
}
} catch (const std::exception& exc) {
LOG_WARNING << "Failed to remove unused apps: " << exc.what();
}
}
bool AppEngine::isAppFetched(const App& app) const {
bool res{false};
if (fetched_apps_.count(app.uri) > 0) {
return true;
}
try {
std::string output;
exec(boost::format{"%s --store %s check %s --local --format json"} % composectl_cmd_ % storeRoot() % app.uri, "",
"", &output);
const auto app_fetch_status{parseJSON(output)};
if (app_fetch_status.isMember("fetch_check") && app_fetch_status["fetch_check"].isMember("missing_blobs") &&
app_fetch_status["fetch_check"]["missing_blobs"].empty()) {
res = true;
fetched_apps_.insert(app.uri);
}
} catch (const ExecError& exc) {
LOG_DEBUG << "app is not fully fetched; app: " << app.name << ", status: " << exc.what();
} catch (const std::exception& exc) {
LOG_ERROR << "failed to verify whether app is fetched; app: " << app.name << ", err: " << exc.what();
throw;
}
return res;
}
bool AppEngine::isAppInstalled(const App& app) const {
bool res{false};
try {
std::string output;
exec(boost::format{"%s --store %s check %s --local --install --format json"} % composectl_cmd_ % storeRoot() %
app.uri,
"", "", &output);
const auto app_fetch_status{parseJSON(output)};
if (app_fetch_status.isMember("install_check") && app_fetch_status["install_check"].isMember(app.uri) &&
app_fetch_status["install_check"][app.uri].isMember("missing_images") &&
(app_fetch_status["install_check"][app.uri]["missing_images"].isNull() ||
app_fetch_status["install_check"][app.uri]["missing_images"].empty())) {
res = true;
}
} catch (const ExecError& exc) {
LOG_DEBUG << "app is not fully fetched or installed; app: " << app.name << ", status: " << exc.what();
} catch (const std::exception& exc) {
LOG_ERROR << "failed to verify whether app is installed; app: " << app.name << ", err: " << exc.what();
throw;
}
return res;
}
void AppEngine::installAppAndImages(const App& app) {
exec(boost::format{"%s --store %s --compose %s --host %s install %s"} % composectl_cmd_ % storeRoot() %
installRoot() % dockerHost() % app.uri,
"failed to install compose app", "", nullptr, "4h", true);
}
static bool checkAppStatus(const AppEngine::App& app, const Json::Value& status) {
if (!status.isMember(app.uri)) {
LOG_ERROR << "could not get app status; uri: " << app.uri;
return false;
}
if (!status[app.uri].isMember("services") || status[app.uri]["services"].isNull()) {
LOG_INFO << app.name << " is not running; uri: " << app.uri;
return false;
}
bool is_running{true};
const std::set<std::string> broken_states{"created", "missing", "unknown"};
for (const auto& s : status[app.uri]["services"]) {
if (broken_states.count(s["state"].asString()) > 0) {
is_running = false;
break;
}
}
if (!is_running) {
LOG_INFO << app.name << " is not running; uri: " << app.uri;
LOG_INFO << status[app.uri];
}
return is_running;
}
static bool checkAppInstallationStatus(const AppEngine::App& app, const Json::Value& status) {
const auto& app_status{status.get(app.uri, Json::Value())};
if (!app_status.isObject()) {
LOG_ERROR << "could not get app status; uri: " << app.uri;
return false;
}
if (isNullOrEmptyOrUnset(app_status, "in_store")) {
LOG_ERROR << "could not check if app is in store; uri: " << app.uri;
return false;
}
if (!app_status["in_store"].asBool()) {
LOG_INFO << app.name << " is not found in the local store";
return false;
}
if (!isNullOrEmptyOrUnset(app_status, "missing_images")) {
LOG_INFO << app.name << " is not fully installed; missing images:\n" << app_status["missing_images"];
return false;
}
if (!isNullOrEmptyOrUnset(app_status, "bundle_errors")) {
LOG_INFO << app.name << " is not fully installed; invalid bundle installation:\n" << app_status["bundle_errors"];
return false;
}
return true;
}
static bool isNullOrEmptyOrUnset(const Json::Value& val, const std::string& field) {
bool res{false};
if (val.isMember(field)) {
res = val[field].isNull() || val[field].empty();
} else {
res = true;
}
return res;
}
static Json::Value parseJSON(const std::string& json_str) {
std::istringstream strs(json_str);
Json::Value json_value;
std::string parse_error;
parseFromStream(Json::CharReaderBuilder(), strs, &json_value, &parse_error);
if (json_value.isNull() && !parse_error.empty()) {
throw std::runtime_error("failed to parse json; err: " + parse_error + ", json: " + json_str);
}
return json_value;
}
} // namespace composeapp