Skip to content
This repository has been archived by the owner on May 21, 2024. It is now read-only.

RFC: Add a docker-app package manager #1189

Merged
merged 7 commits into from
May 31, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ option(PEDANTIC_WARNINGS "Compile with pedantic warnings" OFF)
option(BUILD_WITH_CODE_COVERAGE "Enable gcov code coverage" OFF)
option(BUILD_OSTREE "Set to ON to compile with ostree support" OFF)
option(BUILD_DEB "Set to ON to compile with debian packages support" OFF)
option(BUILD_DOCKERAPP "Set to ON to compile with package manager support of docker-app" OFF)
option(BUILD_P11 "Support for key storage in a HSM via PKCS#11" OFF)
option(BUILD_SOTA_TOOLS "Set to ON to build SOTA tools" OFF)
option(BUILD_PARTIAL "Set to ON to compile with partial secondaries support" OFF)
Expand Down Expand Up @@ -122,6 +123,10 @@ else(BUILD_DEB)
unset(LIBDPKG_LIBRARIES CACHE)
endif(BUILD_DEB)

if(BUILD_DOCKERAPP)
add_definitions(-DBUILD_DOCKERAPP)
endif(BUILD_DOCKERAPP)

if(BUILD_P11)
find_package(LibP11 REQUIRED)
add_definitions(-DBUILD_P11)
Expand Down
17 changes: 16 additions & 1 deletion src/aktualizr_lite/main.cc
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ static int list_main(Config &config, const bpo::variables_map &unused) {
}
}

LOG_INFO << "Updates for available to " << hwid << ":";
LOG_INFO << "Updates available to " << hwid << ":";
for (auto &t : client->allTargets()) {
for (auto const &it : t.hardwareIds()) {
if (it == hwid) {
Expand All @@ -47,6 +47,21 @@ static int list_main(Config &config, const bpo::variables_map &unused) {
name = t.custom_version();
}
LOG_INFO << name << "\tsha256:" << t.sha256Hash();
if (config.pacman.type == PackageManager::kOstreeDockerApp) {
bool shown = false;
auto apps = t.custom_data()["docker_apps"];
for (Json::ValueIterator i = apps.begin(); i != apps.end(); ++i) {
if (!shown) {
shown = true;
LOG_INFO << "\tDocker Apps:";
}
if ((*i).isObject() && (*i).isMember("filename")) {
LOG_INFO << "\t\t" << i.key().asString() << " -> " << (*i)["filename"].asString();
} else {
LOG_ERROR << "\t\tInvalid custom data for docker-app: " << i.key().asString();
}
}
}
break;
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/libaktualizr/package_manager/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ if(BUILD_OSTREE)
add_aktualizr_test(NAME ostree SOURCES ostreemanager_test.cc PROJECT_WORKING_DIRECTORY NO_VALGRIND
ARGS ${PROJECT_BINARY_DIR}/ostree_repo)

if(BUILD_DOCKERAPP)
target_sources(package_manager PRIVATE dockerappmanager.cc)
add_aktualizr_test(NAME dockerapp SOURCES dockerappmanager_test.cc PROJECT_WORKING_DIRECTORY NO_VALGRIND
ARGS ${PROJECT_BINARY_DIR}/ostree_repo "$<TARGET_FILE:aktualizr-repo>")
endif(BUILD_DOCKERAPP)
endif(BUILD_OSTREE)

add_aktualizr_test(NAME packagemanager_factory SOURCES packagemanagerfactory_test.cc NO_VALGRIND
Expand All @@ -55,6 +60,7 @@ aktualizr_source_file_checks(ostreemanager.cc ostreereposync.cc
ostreemanager.h ostreereposync.h)

aktualizr_source_file_checks(androidmanager.cc androidmanager.h)
aktualizr_source_file_checks(dockerappmanager.cc dockerappmanager.h dockerappmanager_test.cc)

if(ANDROID)
target_sources(package_manager PRIVATE androidmanager.cc)
Expand Down
29 changes: 29 additions & 0 deletions src/libaktualizr/package_manager/docker_fake.sh
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
34 changes: 34 additions & 0 deletions src/libaktualizr/package_manager/dockerapp_test_repo.sh
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
116 changes: 116 additions & 0 deletions src/libaktualizr/package_manager/dockerappmanager.cc
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) {
Copy link
Collaborator

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 over Utils::shell?

Copy link
Collaborator Author

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.

Copy link
Collaborator

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.

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_);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why checkMetaOffline here? That should be unnecessary unless I'm overlooking something.

It is on our radar that we should recheck the metadata (offline) before downloading and installing; we currently do it redundantly during checkUpdates. It's on my todo list, but once it is fixed, it will be fixed for all package managers, which would make this call explicitly redundant (if I'm not mistaken).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

checkMetaOffline pulls in data from INvStorage to properly initialize the targets member. It could be avoided by getting the SotaUptaneClient to pass its copy of that variable, but when I tried that approach I didn't really like the way to code looked. I'm open to trying it that way again and letting you decide what you prefer.

Copy link
Collaborator

Choose a reason for hiding this comment

The 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;
}
28 changes: 28 additions & 0 deletions src/libaktualizr/package_manager/dockerappmanager.h
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_
Loading