-
Notifications
You must be signed in to change notification settings - Fork 61
RFC: Add a docker-app package manager #1189
Changes from all commits
c2aca2d
f812908
d746539
46e4e2f
d472131
8687ab1
3879216
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
#! /bin/bash | ||
set -eEuo pipefail | ||
|
||
if [ "$1" = "app" ] ; then | ||
echo "DOCKER-APP RENDER OUTPUT" | ||
if [ ! -f app1.dockerapp ] ; then | ||
echo "Missing docker app file!" | ||
exit 1 | ||
fi | ||
cat app1.dockerapp | ||
exit 0 | ||
fi | ||
if [ "$1" = "up" ] ; then | ||
echo "DOCKER-COMPOSE UP" | ||
if [ ! -f docker-compose.yml ] ; then | ||
echo "Missing docker-compose file!" | ||
exit 1 | ||
fi | ||
# the content of dockerapp includes the sha of the target, so this should | ||
# be present in the docker-compose.yml it creates. | ||
if ! grep primary docker-compose.yml ; then | ||
echo "Could not find expected content in docker-compose.yml" | ||
cat docker-compose.yml | ||
exit 1 | ||
fi | ||
exit 0 | ||
fi | ||
echo "Unknown command: $*" | ||
exit 1 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
#! /bin/bash | ||
set -eEuo pipefail | ||
|
||
if [ "$#" -lt 3 ]; then | ||
echo "Usage: $0 <aktualizr-repo> <output directory> <port>" | ||
exit 1 | ||
fi | ||
|
||
AKTUALIZR_REPO="$1" | ||
DEST_DIR="$2" | ||
PORT="$3" | ||
|
||
akrepo() { | ||
"$AKTUALIZR_REPO" --path "$DEST_DIR" "$@" | ||
} | ||
|
||
mkdir -p "$DEST_DIR" | ||
trap 'rm -rf "$DEST_DIR"' ERR | ||
|
||
IMAGES=$(mktemp -d) | ||
trap 'rm -rf "$IMAGES"' exit | ||
DOCKER_APP="$IMAGES/foo.dockerapp" | ||
echo "fake contents of a docker app" > "$DOCKER_APP" | ||
|
||
akrepo --command generate --expires 2021-07-04T16:33:27Z | ||
akrepo --command image --filename "$DOCKER_APP" --targetname foo.dockerapp | ||
akrepo --command addtarget --hwid primary_hw --serial CA:FE:A6:D2:84:9D --targetname foo.dockerapp | ||
akrepo --command signtargets | ||
|
||
cd $DEST_DIR | ||
echo "Target.json is: " | ||
cat repo/image/targets.json | ||
echo "Running repo server port on $PORT" | ||
exec python3 -m http.server $PORT |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
#include "dockerappmanager.h" | ||
|
||
#include <sstream> | ||
|
||
struct DockerApp { | ||
DockerApp(const std::string app_name, const PackageConfig &config) | ||
: name(std::move(app_name)), | ||
app_root(std::move(config.docker_apps_root / app_name)), | ||
app_params(std::move(config.docker_app_params)), | ||
app_bin(std::move(config.docker_app_bin)), | ||
compose_bin(std::move(config.docker_compose_bin)) {} | ||
|
||
bool render(const std::string &app_content) { | ||
auto bin = boost::filesystem::canonical(app_bin).string(); | ||
Utils::writeFile(app_root / (name + ".dockerapp"), app_content); | ||
std::string cmd("cd " + app_root.string() + " && " + bin + " app render " + name); | ||
if (!app_params.empty()) { | ||
cmd += " -f " + app_params.string(); | ||
} | ||
std::string yaml; | ||
if (Utils::shell(cmd, &yaml, true) != 0) { | ||
LOG_ERROR << "Unable to run " << cmd << " output:\n" << yaml; | ||
return false; | ||
} | ||
Utils::writeFile(app_root / "docker-compose.yml", yaml); | ||
return true; | ||
} | ||
|
||
bool start() { | ||
// Depending on the number and size of the containers in the docker-app, | ||
// this command can take a bit of time to complete. Rather than using, | ||
// Utils::shell which isn't interactive, we'll use std::system so that | ||
// stdout/stderr is streamed while docker sets things up. | ||
auto bin = boost::filesystem::canonical(compose_bin).string(); | ||
std::string cmd("cd " + app_root.string() + " && " + bin + " up --remove-orphans -d"); | ||
if (std::system(cmd.c_str()) != 0) { | ||
return false; | ||
} | ||
return true; | ||
} | ||
|
||
std::string name; | ||
boost::filesystem::path app_root; | ||
boost::filesystem::path app_params; | ||
boost::filesystem::path app_bin; | ||
boost::filesystem::path compose_bin; | ||
}; | ||
|
||
bool DockerAppManager::iterate_apps(const Uptane::Target &target, DockerAppCb cb) const { | ||
auto apps = target.custom_data()["docker_apps"]; | ||
bool res = true; | ||
Uptane::ImagesRepository repo; | ||
// checkMetaOffline pulls in data from INvStorage to properly initialize | ||
// the targets member of the instance so that we can use the LazyTargetList | ||
repo.checkMetaOffline(*storage_); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why It is on our radar that we should recheck the metadata (offline) before downloading and installing; we currently do it redundantly during There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, that would require changing all of the package managers to support it. For now it's probably fine; it just seems like an indirect way to get what you are looking for. Again, a comment to explain why the check is useful might be helpful. |
||
|
||
if (!apps) { | ||
LOG_DEBUG << "Detected an update target from Director with no docker-apps data"; | ||
for (const auto t : Uptane::LazyTargetsList(repo, storage_, fake_fetcher_)) { | ||
if (t == target) { | ||
LOG_DEBUG << "Found the match " << t; | ||
apps = t.custom_data()["docker_apps"]; | ||
break; | ||
} | ||
} | ||
} | ||
|
||
for (const auto t : Uptane::LazyTargetsList(repo, storage_, fake_fetcher_)) { | ||
for (Json::ValueIterator i = apps.begin(); i != apps.end(); ++i) { | ||
if ((*i).isObject() && (*i).isMember("filename")) { | ||
for (auto app : config.docker_apps) { | ||
if (i.key().asString() == app && (*i)["filename"].asString() == t.filename()) { | ||
if (!cb(app, t)) { | ||
res = false; | ||
} | ||
} | ||
} | ||
} else { | ||
LOG_ERROR << "Invalid custom data for docker-app: " << i.key().asString() << " -> " << *i; | ||
} | ||
} | ||
} | ||
return res; | ||
} | ||
|
||
bool DockerAppManager::fetchTarget(const Uptane::Target &target, Uptane::Fetcher &fetcher, const KeyManager &keys, | ||
FetcherProgressCb progress_cb, const api::FlowControlToken *token) { | ||
if (!OstreeManager::fetchTarget(target, fetcher, keys, progress_cb, token)) { | ||
return false; | ||
} | ||
|
||
LOG_INFO << "Looking for DockerApps to fetch"; | ||
auto cb = [this, &fetcher, &keys, progress_cb, token](const std::string &app, const Uptane::Target &app_target) { | ||
LOG_INFO << "Fetching " << app << " -> " << app_target; | ||
return PackageManagerInterface::fetchTarget(app_target, fetcher, keys, progress_cb, token); | ||
}; | ||
return iterate_apps(target, cb); | ||
} | ||
|
||
data::InstallationResult DockerAppManager::install(const Uptane::Target &target) const { | ||
auto res = OstreeManager::install(target); | ||
auto cb = [this](const std::string &app, const Uptane::Target &app_target) { | ||
LOG_INFO << "Installing " << app << " -> " << app_target; | ||
std::stringstream ss; | ||
ss << *storage_->openTargetFile(app_target); | ||
DockerApp dapp(app, config); | ||
if (!dapp.render(ss.str()) || !dapp.start()) { | ||
return false; | ||
} | ||
return true; | ||
}; | ||
if (!iterate_apps(target, cb)) { | ||
return data::InstallationResult(data::ResultCode::Numeric::kInstallFailed, "Could not render docker app"); | ||
} | ||
return res; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
#ifndef DOCKERAPPMGR_H_ | ||
#define DOCKERAPPMGR_H_ | ||
|
||
#include "ostreemanager.h" | ||
#include "uptane/iterator.h" | ||
|
||
using DockerAppCb = std::function<bool(const std::string &app, const Uptane::Target &app_target)>; | ||
|
||
class DockerAppManager : public OstreeManager { | ||
public: | ||
DockerAppManager(PackageConfig pconfig, std::shared_ptr<INvStorage> storage, std::shared_ptr<Bootloader> bootloader, | ||
std::shared_ptr<HttpInterface> http) | ||
: OstreeManager(pconfig, storage, bootloader, http) { | ||
fake_fetcher_ = std::make_shared<Uptane::Fetcher>(Config(), http_); | ||
} | ||
bool fetchTarget(const Uptane::Target &target, Uptane::Fetcher &fetcher, const KeyManager &keys, | ||
FetcherProgressCb progress_cb, const api::FlowControlToken *token = nullptr) override; | ||
data::InstallationResult install(const Uptane::Target &target) const override; | ||
std::string name() const override { return "ostree+docker-app"; } | ||
|
||
private: | ||
bool iterate_apps(const Uptane::Target &target, DockerAppCb cb) const; | ||
|
||
// iterate_apps needs an Uptane::Fetcher. However, its an unused parameter | ||
// and we just need to construct a dummy one to make the compiler happy. | ||
std::shared_ptr<Uptane::Fetcher> fake_fetcher_; | ||
}; | ||
#endif // DOCKERAPPMGR_H_ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's no problem with using
std::system
directly, but any reason to prefer it overUtils::shell
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question. The "start" command can take a while to start so std::system will print out progress as its made. So basically its a little nicer this way when watching things run. Not sure that's enough to justify doing things differently than everywhere else in the project though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there's a good reason, it's no problem. It is also interesting to consider a version of
Utils::shell
that piped output directly to the logger. For now, it's probably fine, but I'd suggest leaving a comment to explain what you just explained here so that it doesn't get changed by an overzealous refactorer in the future.