Aktualizr
C++ SOTA Client
dockerapp_standalone.cc
1 #include <sstream>
2 
3 #include "dockerappmanager.h"
4 #include "libaktualizr/packagemanagerfactory.h"
5 
6 /**
7  * @brief This package manager compliments the OSTreePackageManager by also including optional Docker Apps.
8  *
9  * A full description of the Docker App project can be found here:
10  * https://github.com/docker/app/
11  *
12  * NOTE: This implementation is based on the <= 0.8 versions of Docker App that
13  * included a "standalone" command mode.
14  *
15  * Docker Apps are very analogous to docker-compose. In fact, this module
16  * currently "renders" the docker-app file into a docker-compose file. Each
17  * Docker App appears as a Target in the TUF targets list. Each OSTree target
18  * can then reference these docker apps in its custom data section. eg:
19  *
20  * "targets": {
21  * httpd.dockerapp-1 : {
22  * "custom" : {"hardwareIds" : ["all"], "name" : "httpd.dockerapp", "version" : "1"},
23  * "hashes" : {"sha256" : "f0ad4e3ce6a5e9cb70c9d747e977fddfacd08419deec0714622029b12dde8338"},
24  * "length" : 889
25  * },
26  * "raspberrypi3-64-lmp-144" : {
27  * "custom" : {
28  * "docker_apps" : {
29  * "httpd" : {
30  * "filename" : "httpd.dockerapp-1"
31  * }
32  * },
33  * "hardwareIds" : ["raspberrypi3-64"],
34  * "name" : "raspberrypi3-64-lmp",
35  * "targetFormat" : "OSTREE",
36  * "version" : "144"
37  * },
38  * "hashes" : {"sha256" : "20ac4f7cd50cda6bfed0caa1f8231cc9a7e40bec60026c66df5f7e143af96942"},
39  * "length" : 0
40  * }
41  * }
42  */
43 
44 struct DockerApp {
45  DockerApp(std::string app_name, const DockerAppManagerConfig &config)
46  : name(std::move(app_name)),
47  app_root(config.docker_apps_root / name),
48  app_params(config.docker_app_params),
49  app_bin(config.docker_app_bin),
50  compose_bin(config.docker_compose_bin) {}
51 
52  bool render(const std::string &app_content, bool persist) {
53  auto bin = boost::filesystem::canonical(app_bin).string();
54  Utils::writeFile(app_root / (name + ".dockerapp"), app_content);
55  std::string cmd("cd " + app_root.string() + " && " + bin + " render " + name);
56  if (!app_params.empty()) {
57  cmd += " --parameters-file " + app_params.string();
58  }
59  if (persist) {
60  cmd += " -o " + (app_root / "docker-compose.yml").string();
61  }
62  std::string output;
63  if (Utils::shell(cmd, &output, true) != 0) {
64  LOG_ERROR << "Unable to run " << cmd << " output:\n" << output;
65  return false;
66  }
67  return true;
68  }
69 
70  bool fetch() {
71  auto bin = boost::filesystem::canonical(compose_bin).string();
72  std::string cmd("cd " + app_root.string() + " && " + bin + " pull");
73  return std::system(cmd.c_str()) == 0;
74  }
75 
76  bool start() {
77  // Depending on the number and size of the containers in the docker-app,
78  // this command can take a bit of time to complete. Rather than using,
79  // Utils::shell which isn't interactive, we'll use std::system so that
80  // stdout/stderr is streamed while docker sets things up.
81  auto bin = boost::filesystem::canonical(compose_bin).string();
82  std::string cmd("cd " + app_root.string() + " && " + bin + " up --remove-orphans -d");
83  return std::system(cmd.c_str()) == 0;
84  }
85 
86  void remove() {
87  auto bin = boost::filesystem::canonical(compose_bin).string();
88  std::string cmd("cd " + app_root.string() + " && " + bin + " down");
89  if (std::system(cmd.c_str()) == 0) {
90  boost::filesystem::remove_all(app_root);
91  } else {
92  LOG_ERROR << "docker-compose was unable to bring down: " << app_root;
93  }
94  }
95 
96  std::string name;
97  boost::filesystem::path app_root;
98  boost::filesystem::path app_params;
99  boost::filesystem::path app_bin;
100  boost::filesystem::path compose_bin;
101 };
102 
103 bool DockerAppStandalone::iterate_apps(const Uptane::Target &target, const DockerAppCb &cb) const {
104  auto apps = target.custom_data()["docker_apps"];
105  bool res = true;
107  // checkMetaOffline pulls in data from INvStorage to properly initialize
108  // the targets member of the instance so that we can use the LazyTargetList
109  try {
110  repo.checkMetaOffline(*storage_);
111  } catch (const std::exception &e) {
112  // ignore errors here
113  }
114 
115  if (!apps) {
116  LOG_DEBUG << "Detected an update target from Director with no docker-apps data";
117  for (const auto &t : Uptane::LazyTargetsList(repo, storage_, fake_fetcher_)) {
118  if (t.MatchTarget(target)) {
119  LOG_DEBUG << "Found the match " << t;
120  apps = t.custom_data()["docker_apps"];
121  break;
122  }
123  }
124  }
125 
126  for (const auto &t : Uptane::LazyTargetsList(repo, storage_, fake_fetcher_)) {
127  for (Json::ValueIterator i = apps.begin(); i != apps.end(); ++i) {
128  if ((*i).isObject() && (*i).isMember("filename")) {
129  for (const auto &app : dappcfg_.docker_apps) {
130  if (i.key().asString() == app && (*i)["filename"].asString() == t.filename()) {
131  if (!cb(app, t)) {
132  res = false;
133  }
134  }
135  }
136  } else if ((*i).isObject() && (*i).isMember("uri")) {
137  LOG_TRACE << "Skipping docker app bundle format for " << i.key().asString() << " -> " << *i;
138  } else {
139  LOG_ERROR << "Invalid custom data for docker-app: " << i.key().asString() << " -> " << *i;
140  }
141  }
142  }
143  return res;
144 }
145 
146 bool DockerAppStandalone::fetchTarget(const Uptane::Target &target, Uptane::Fetcher &fetcher, const KeyManager &keys,
147  const FetcherProgressCb &progress_cb, const api::FlowControlToken *token) {
148  if (!OstreeManager::fetchTarget(target, fetcher, keys, progress_cb, token)) {
149  return false;
150  }
151 
152  LOG_INFO << "Looking for DockerApps to fetch";
153  auto cb = [this, &fetcher, &keys, progress_cb, token](const std::string &app, const Uptane::Target &app_target) {
154  LOG_INFO << "Fetching " << app << " -> " << app_target;
155  // NOLINTNEXTLINE(bugprone-parent-virtual-call)
156  if (!PackageManagerInterface::fetchTarget(app_target, fetcher, keys, progress_cb, token)) {
157  return false;
158  }
159  std::stringstream ss;
160  ss << openTargetFile(app_target).rdbuf();
161  DockerApp dapp(app, config);
162  return dapp.render(ss.str(), true) && dapp.fetch();
163  };
164  return iterate_apps(target, cb);
165 }
166 
167 data::InstallationResult DockerAppStandalone::install(const Uptane::Target &target) const {
169  Uptane::Target current = OstreeManager::getCurrent();
170  if (current.sha256Hash() != target.sha256Hash()) {
171  res = OstreeManager::install(target);
172  if (res.result_code.num_code == data::ResultCode::Numeric::kInstallFailed) {
173  LOG_ERROR << "Failed to install OSTree target, skipping Docker Apps";
174  return res;
175  }
176  } else {
177  LOG_INFO << "Target " << target.sha256Hash() << " is same as current";
178  res = data::InstallationResult(data::ResultCode::Numeric::kOk, "OSTree hash already installed, same as current");
179  }
180 
181  handleRemovedApps(target);
182  auto cb = [this](const std::string &app, const Uptane::Target &app_target) {
183  LOG_INFO << "Installing " << app << " -> " << app_target;
184  return DockerApp(app, config).start();
185  };
186  if (!iterate_apps(target, cb)) {
187  res = data::InstallationResult(data::ResultCode::Numeric::kInstallFailed, "Could not render docker app");
188  }
189 
190  if (dappcfg_.docker_prune) {
191  LOG_INFO << "Pruning unused docker images";
192  // Utils::shell which isn't interactive, we'll use std::system so that
193  // stdout/stderr is streamed while docker sets things up.
194  if (std::system("docker image prune -a -f --filter=\"label!=aktualizr-no-prune\"") != 0) {
195  LOG_WARNING << "Unable to prune unused docker images";
196  }
197  }
198  return res;
199 }
200 
201 // Handle the case like:
202 // 1) sota.toml is configured with 2 docker apps: "app1, app2"
203 // 2) update is applied, so we are now running both app1 and app2
204 // 3) sota.toml is updated with 1 docker app: "app1"
205 // At this point we should stop app2 and remove it.
206 void DockerAppStandalone::handleRemovedApps(const Uptane::Target &target) const {
207  if (!boost::filesystem::is_directory(dappcfg_.docker_apps_root)) {
208  LOG_DEBUG << "dappcfg_.docker_apps_root does not exist";
209  return;
210  }
211 
212  std::vector<std::string> target_apps = target.custom_data()["docker_apps"].getMemberNames();
213 
214  for (auto &entry : boost::make_iterator_range(boost::filesystem::directory_iterator(dappcfg_.docker_apps_root), {})) {
215  if (boost::filesystem::is_directory(entry)) {
216  std::string name = entry.path().filename().native();
217  if (std::find(dappcfg_.docker_apps.begin(), dappcfg_.docker_apps.end(), name) == dappcfg_.docker_apps.end()) {
218  LOG_WARNING << "Docker App(" << name
219  << ") installed, but is now removed from configuration. Removing from system";
220  DockerApp(name, dappcfg_).remove();
221  }
222  if (std::find(target_apps.begin(), target_apps.end(), name) == target_apps.end()) {
223  LOG_WARNING << "Docker App(" << name
224  << ") configured, but not defined in installation target. Removing from system";
225  DockerApp(name, dappcfg_).remove();
226  }
227  }
228  }
229 }
Uptane::Fetcher
Definition: fetcher.h:33
DockerAppManagerConfig
Definition: dockerappmanager.h:9
KeyManager
Definition: keymanager.h:13
DockerApp
This package manager compliments the OSTreePackageManager by also including optional Docker Apps.
Definition: dockerapp_standalone.cc:44
data::InstallationResult
Definition: types.h:277
Uptane::ImageRepository
Definition: imagerepository.h:13
Uptane::LazyTargetsList
Definition: iterator.h:12
api::FlowControlToken
Provides a thread-safe way to pause and terminate task execution.
Definition: apiqueue.h:19
Uptane::Target
Definition: types.h:379
data::ResultCode::Numeric::kInstallFailed
@ kInstallFailed
Package installation failed.