Aktualizr
C++ SOTA Client
dockerapp_bundles.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  * Docker Apps are very analogous to docker-compose. In fact, this module
13  * currently "renders" the docker-app file into a docker-compose file. Each
14  * Docker App appears as a custom metadata for a Target in the TUF targets list.
15  *
16  * "targets": {
17  * "raspberrypi3-64-lmp-144" : {
18  * "custom" : {
19  * "docker_apps" : {
20  * "httpd" : {
21  * "uri" :
22  * "hub.foundries.io/andy-corp/shellhttpd@sha256:611052af5d819ea979bd555b508808c8a84f9159130d455ed52a084ab5b6b398"
23  * }
24  * },
25  * "hardwareIds" : ["raspberrypi3-64"],
26  * "name" : "raspberrypi3-64-lmp",
27  * "targetFormat" : "OSTREE",
28  * "version" : "144"
29  * },
30  * "hashes" : {"sha256" : "20ac4f7cd50cda6bfed0caa1f8231cc9a7e40bec60026c66df5f7e143af96942"},
31  * "length" : 0
32  * }
33  * }
34  */
35 
36 struct AppBundle {
37  AppBundle(std::string app_name, const DockerAppManagerConfig &config)
38  : name(std::move(app_name)),
39  app_root(config.docker_apps_root / name),
40  app_params(config.docker_app_params),
41  compose_bin(boost::filesystem::canonical(config.docker_compose_bin).string()) {}
42 
43  // Utils::shell isn't interactive. The docker app commands can take a few
44  // seconds to run, so we use std::system to stream it to stdout/sterr
45  static bool cmd_streaming(const std::string &cmd) { return std::system(cmd.c_str()) == 0; }
46 
47  bool fetch(const std::string &app_uri) { return cmd_streaming("docker app pull " + app_uri); }
48 
49  bool start(const std::string &app_uri) {
50  boost::filesystem::create_directories(app_root);
51  std::string cmd("cd " + app_root.string() + " && docker app image render -o docker-compose.yml " + app_uri);
52  if (!app_params.empty()) {
53  cmd += " --parameters-file " + app_params.string();
54  }
55  if (!cmd_streaming(cmd)) {
56  return false;
57  }
58 
59  return cmd_streaming("cd " + app_root.string() + " && " + compose_bin + " up --remove-orphans -d");
60  }
61 
62  void remove() {
63  if (cmd_streaming("cd " + app_root.string() + " && " + compose_bin + " down")) {
64  boost::filesystem::remove_all(app_root);
65  } else {
66  LOG_ERROR << "docker-compose was unable to bring down: " << app_root;
67  }
68  }
69 
70  std::string name;
71  boost::filesystem::path app_root;
72  boost::filesystem::path app_params;
73  std::string compose_bin;
74 };
75 
76 std::vector<std::pair<std::string, std::string>> DockerAppBundles::iterate_apps(const Uptane::Target &target) const {
77  auto apps = target.custom_data()["docker_apps"];
79  // checkMetaOffline pulls in data from INvStorage to properly initialize
80  // the targets member of the instance so that we can use the LazyTargetList
81  try {
82  repo.checkMetaOffline(*storage_);
83  } catch (const std::exception &e) {
84  // ignore errors here
85  }
86 
87  if (!apps) {
88  LOG_DEBUG << "Detected an update target from Director with no docker-apps data";
89  for (const auto &t : Uptane::LazyTargetsList(repo, storage_, fake_fetcher_)) {
90  if (t.MatchTarget(target)) {
91  LOG_DEBUG << "Found the match " << t;
92  apps = t.custom_data()["docker_apps"];
93  break;
94  }
95  }
96  }
97 
98  std::vector<std::pair<std::string, std::string>> bundles;
99  for (Json::ValueIterator i = apps.begin(); i != apps.end(); ++i) {
100  if ((*i).isObject() && (*i).isMember("uri")) {
101  for (const auto &app : dappcfg_.docker_apps) {
102  if (i.key().asString() == app) {
103  bundles.emplace_back(i.key().asString(), (*i)["uri"].asString());
104  break;
105  }
106  }
107  } else if ((*i).isObject() && (*i).isMember("filename")) {
108  LOG_TRACE << "Skipping old docker app format for " << i.key().asString() << " -> " << *i;
109  } else {
110  LOG_ERROR << "Invalid custom data for docker-app: " << i.key().asString() << " -> " << *i;
111  }
112  }
113  return bundles;
114 }
115 
116 bool DockerAppBundles::fetchTarget(const Uptane::Target &target, Uptane::Fetcher &fetcher, const KeyManager &keys,
117  const FetcherProgressCb &progress_cb, const api::FlowControlToken *token) {
118  if (!OstreeManager::fetchTarget(target, fetcher, keys, progress_cb, token)) {
119  return false;
120  }
121 
122  LOG_INFO << "Looking for DockerApps to fetch";
123  bool passed = true;
124  for (const auto &pair : iterate_apps(target)) {
125  LOG_INFO << "Fetching " << pair.first << " -> " << pair.second;
126  if (!AppBundle(pair.first, dappcfg_).fetch(pair.second)) {
127  passed = false;
128  }
129  }
130  return passed;
131 }
132 
133 data::InstallationResult DockerAppBundles::install(const Uptane::Target &target) const {
135  Uptane::Target current = OstreeManager::getCurrent();
136  if (current.sha256Hash() != target.sha256Hash()) {
137  res = OstreeManager::install(target);
138  if (res.result_code.num_code == data::ResultCode::Numeric::kInstallFailed) {
139  LOG_ERROR << "Failed to install OSTree target, skipping Docker Apps";
140  return res;
141  }
142  } else {
143  LOG_INFO << "Target " << target.sha256Hash() << " is same as current";
144  res = data::InstallationResult(data::ResultCode::Numeric::kOk, "OSTree hash already installed, same as current");
145  }
146 
147  handleRemovedApps(target);
148  for (const auto &pair : iterate_apps(target)) {
149  LOG_INFO << "Installing " << pair.first << " -> " << pair.second;
150  if (!AppBundle(pair.first, dappcfg_).start(pair.second)) {
151  res = data::InstallationResult(data::ResultCode::Numeric::kInstallFailed, "Could not install docker app");
152  }
153  };
154 
155  if (dappcfg_.docker_prune) {
156  LOG_INFO << "Pruning unused docker images";
157  // Utils::shell which isn't interactive, we'll use std::system so that
158  // stdout/stderr is streamed while docker sets things up.
159  if (std::system("docker image prune -a -f --filter=\"label!=aktualizr-no-prune\"") != 0) {
160  LOG_WARNING << "Unable to prune unused docker images";
161  }
162  }
163 
164  return res;
165 }
166 
167 // Handle the case like:
168 // 1) sota.toml is configured with 2 docker apps: "app1, app2"
169 // 2) update is applied, so we are now running both app1 and app2
170 // 3) sota.toml is updated with 1 docker app: "app1"
171 // At this point we should stop app2 and remove it.
172 void DockerAppBundles::handleRemovedApps(const Uptane::Target &target) const {
173  if (!boost::filesystem::is_directory(dappcfg_.docker_apps_root)) {
174  LOG_DEBUG << "dappcfg_.docker_apps_root does not exist";
175  return;
176  }
177 
178  std::vector<std::string> target_apps = target.custom_data()["docker_apps"].getMemberNames();
179 
180  for (auto &entry : boost::make_iterator_range(boost::filesystem::directory_iterator(dappcfg_.docker_apps_root), {})) {
181  if (boost::filesystem::is_directory(entry)) {
182  std::string name = entry.path().filename().native();
183  if (std::find(dappcfg_.docker_apps.begin(), dappcfg_.docker_apps.end(), name) == dappcfg_.docker_apps.end()) {
184  LOG_WARNING << "Docker App(" << name
185  << ") installed, but is now removed from configuration. Removing from system";
186  AppBundle(name, dappcfg_).remove();
187  }
188  if (std::find(target_apps.begin(), target_apps.end(), name) == target_apps.end()) {
189  LOG_WARNING << "Docker App(" << name
190  << ") configured, but not defined in installation target. Removing from system";
191  AppBundle(name, dappcfg_).remove();
192  }
193  }
194  }
195 }
Uptane::Fetcher
Definition: fetcher.h:33
DockerAppManagerConfig
Definition: dockerappmanager.h:9
KeyManager
Definition: keymanager.h:13
data::InstallationResult
Definition: types.h:277
Uptane::ImageRepository
Definition: imagerepository.h:13
AppBundle
This package manager compliments the OSTreePackageManager by also including optional Docker Apps.
Definition: dockerapp_bundles.cc:36
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.