Skip to content

Commit

Permalink
Add support for snaps (#582)
Browse files Browse the repository at this point in the history
This change updates the repo to support building IoT Identity Service components as snaps. One of the main differences with snaps is that they run in a tightly confined sandbox, so many hard-coded references to users and filesystem paths had to be adapted to support compile-time variables.
  • Loading branch information
damonbarry authored Mar 1, 2024
1 parent 98419ce commit 9f5c079
Show file tree
Hide file tree
Showing 45 changed files with 636 additions and 142 deletions.
35 changes: 35 additions & 0 deletions .github/workflows/packages.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,38 @@ jobs:
with:
name: "${{ steps.generate-artifact-properties.outputs.artifact-name }}"
path: 'packages'

snap:
strategy:
fail-fast: false

matrix:
runner:
- arch: amd64
pool: iot-identity-1es-hosted-linux-amd64
image: agent-aziotedge-ubuntu-22.04-msmoby
- arch: aarch64
pool: iot-identity-1es-hosted-linux-arm64
image: agent-aziotedge-ubuntu-22.04-arm64-msmoby

runs-on:
- self-hosted
- 1ES.Pool=${{ matrix.runner.pool }}
- 1ES.ImageOverride=${{ matrix.runner.image }}

steps:
- uses: 'actions/checkout@v3'
with:
submodules: 'recursive'

- name: 'Run'
uses: 'snapcore/action-build@v1'
id: 'snapcraft'
with:
build-info: true

- name: 'Upload'
uses: 'actions/upload-artifact@v3'
with:
name: 'packages_snap_${{ matrix.runner.arch }}'
path: '${{ steps.snapcraft.outputs.snap }}'
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@
/packages
/target
.vscode

/parts
/stage
/prime
*.snap
59 changes: 51 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@
BINDGEN = bindgen
CBINDGEN = cbindgen

# Default users under which the services will run. Override by specifying on the CLI for make.
USER_AZIOTID ?= aziotid
USER_AZIOTCS ?= aziotcs
USER_AZIOTKS ?= aziotks
USER_AZIOTTPM ?= aziottpm

# Default socket directory. Override by specifying on the CLI for make.
SOCKET_DIR ?= /run/aziot

# 0 => false, _ => true
V = 0

Expand All @@ -20,6 +29,15 @@ ARCH =

INSTALL_PRESET = true

# Enable special features for specific runtime platforms
# '' => none, 'snapd' => snapd features
PLATFORM_FEATURES ?=
CARGO_FEATURES =

ifeq ($(PLATFORM_FEATURES), snapd)
CARGO_FEATURES += --features snapctl
endif

ifeq ($(V), 0)
BINDGEN_VERBOSE =
CARGO_VERBOSE = --quiet
Expand All @@ -38,11 +56,11 @@ else
CARGO_PROFILE_DIRECTORY = release
endif

ifeq ($(ARCH), arm32v7)
ifneq (,$(filter $(ARCH), arm32v7 armhf))
CARGO_TARGET = armv7-unknown-linux-gnueabihf
CROSS_HOST_TRIPLE = arm-linux-gnueabihf
DPKG_ARCH_FLAGS = --host-arch armhf
else ifeq ($(ARCH), aarch64)
else ifneq (,$(filter $(ARCH), aarch64 arm64))
CARGO_TARGET = aarch64-unknown-linux-gnu
CROSS_HOST_TRIPLE = aarch64-linux-gnu
DPKG_ARCH_FLAGS = --host-arch arm64 --host-type aarch64-linux-gnu --target-type aarch64-linux-gnu
Expand All @@ -56,7 +74,12 @@ CARGO_OUTPUT_ABSPATH = $(abspath ./target/$(CARGO_TARGET)/$(CARGO_PROFILE_DIRECT
VENDOR_PREFIX = $(CARGO_OUTPUT_ABSPATH)/fakeroot
VENDOR_PKGCONFIG = $(VENDOR_PREFIX)$(AZIOT_PRIVATE_LIBRARIES)/pkgconfig

CARGO = VENDOR_PREFIX="$(VENDOR_PREFIX)" VENDOR_PKGCONFIG="$(VENDOR_PKGCONFIG)" cargo
CARGO = VENDOR_PREFIX="$(VENDOR_PREFIX)" VENDOR_PKGCONFIG="$(VENDOR_PKGCONFIG)" \
USER_AZIOTID="$(USER_AZIOTID)" \
USER_AZIOTCS="$(USER_AZIOTCS)" \
USER_AZIOTKS="$(USER_AZIOTKS)" \
USER_AZIOTTPM="$(USER_AZIOTTPM)" \
SOCKET_DIR="$(SOCKET_DIR)" cargo

# Some of the targets use bash-isms like `set -o pipefail`
SHELL = /bin/bash
Expand Down Expand Up @@ -96,7 +119,7 @@ default:
# incorrect assumption of /usr/local. There is probably a better
# way to do this...
set -euo pipefail; \
if [ -d third-party/tpm2-tss ]; then \
if [ $(VENDOR_LIBTSS) != 0 -a -d third-party/tpm2-tss ]; then \
cd third-party/tpm2-tss; \
./bootstrap; \
./configure \
Expand All @@ -120,13 +143,13 @@ default:
# See the doc header of the aziot-keys-common crate for more info.
$(CARGO) build \
-p aziot-keys \
$(CARGO_PROFILE) --target $(CARGO_TARGET) $(CARGO_VERBOSE)
$(CARGO_PROFILE) $(CARGO_FEATURES) --target $(CARGO_TARGET) $(CARGO_VERBOSE)

$(CARGO) build \
-p aziotctl \
-p aziotd \
-p aziot-key-openssl-engine-shared \
$(CARGO_PROFILE) --target $(CARGO_TARGET) $(CARGO_VERBOSE)
$(CARGO_PROFILE) $(CARGO_FEATURES) --target $(CARGO_TARGET) $(CARGO_VERBOSE)

clean:
$(CARGO) clean $(CARGO_VERBOSE)
Expand Down Expand Up @@ -309,6 +332,9 @@ deb: dist
# Copy package files
cp -R contrib/debian /tmp/aziot-identity-service-$(PACKAGE_VERSION)/
sed -i -e 's/@version@/$(PACKAGE_VERSION)/g; s/@release@/$(PACKAGE_RELEASE)/g' /tmp/aziot-identity-service-$(PACKAGE_VERSION)/debian/changelog
sed -i -e 's/@user_aziotid@/$(USER_AZIOTID)/g; s/@user_aziotks@/$(USER_AZIOTKS)/g; s/@user_aziotcs@/$(USER_AZIOTCS)/g; s/@user_aziottpm@/$(USER_AZIOTTPM)/g' /tmp/aziot-identity-service-$(PACKAGE_VERSION)/debian/postinst
sed -i -e 's/@user_aziotid@/$(USER_AZIOTID)/g; s/@user_aziotks@/$(USER_AZIOTKS)/g; s/@user_aziotcs@/$(USER_AZIOTCS)/g; s/@user_aziottpm@/$(USER_AZIOTTPM)/g; s|@socket_dir@|$(SOCKET_DIR)|g' /tmp/aziot-identity-service-$(PACKAGE_VERSION)/debian/postrm
sed -i -e 's/@user_aziotid@/$(USER_AZIOTID)/g; s/@user_aziotks@/$(USER_AZIOTKS)/g; s/@user_aziotcs@/$(USER_AZIOTCS)/g; s/@user_aziottpm@/$(USER_AZIOTTPM)/g' /tmp/aziot-identity-service-$(PACKAGE_VERSION)/debian/preinst

cd /tmp/aziot-identity-service-$(PACKAGE_VERSION) && dpkg-buildpackage -us -uc $(DPKG_ARCH_FLAGS)

Expand Down Expand Up @@ -359,6 +385,10 @@ rpm:
-e "s|@devtoolset@|$$DEVTOOLSET|g" \
-e "s|@llvm_toolset@|$$LLVM_TOOLSET|g" \
-e "s|@openssl_engine_filename@|$$OPENSSL_ENGINE_FILENAME|g" \
-e "s/@user_aziotid@/$(USER_AZIOTID)/g" \
-e "s/@user_aziotks@/$(USER_AZIOTKS)/g" \
-e "s/@user_aziotcs@/$(USER_AZIOTCS)/g" \
-e "s/@user_aziottpm@/$(USER_AZIOTTPM)/g" \
>$(RPMBUILDDIR)/SPECS/aziot-identity-service.spec

# Copy preset file to be included in the package
Expand Down Expand Up @@ -427,7 +457,7 @@ install-common:
# tpm2-tss
# See comment above regarding environment bleedover on RPM
# builds.
if [ -d third-party/tpm2-tss ]; then \
if [ $(VENDOR_LIBTSS) != 0 -a -d third-party/tpm2-tss ]; then \
cd third-party/tpm2-tss; \
$(MAKE) libdir=$(AZIOT_PRIVATE_LIBRARIES) install-exec; \
fi
Expand Down Expand Up @@ -457,15 +487,28 @@ install-common:
$(INSTALL) -d -m 0700 $(DESTDIR)$(localstatedir)/lib/aziot/tpmd

# Systemd services and sockets
$(INSTALL) -d $(DESTDIR)$(unitdir)
# NOTE: We do not use "install -D ... -t ..." since it is broken on
# RHEL 7 derivatives and will not be fixed.
# Ref: https://bugzilla.redhat.com/show_bug.cgi?format=multiple&id=1758488
for i in cert identity key tpm; do \
OUTPUT_SOCKET="$(DESTDIR)$(unitdir)/aziot-$${i}d.socket"; \
<"$$i/aziot-$${i}d/aziot-$${i}d.socket.in" sed \
-e 's|@user_aziotid@|$(USER_AZIOTID)|' \
-e 's|@user_aziotks@|$(USER_AZIOTKS)|' \
-e 's|@user_aziotcs@|$(USER_AZIOTCS)|' \
-e 's|@user_aziottpm@|$(USER_AZIOTTPM)|' \
-e 's|@socket_dir@|$(SOCKET_DIR)|' \
>"$$OUTPUT_SOCKET"; \
chmod 0644 "$$OUTPUT_SOCKET"; \
OUTPUT_SERVICE="$(DESTDIR)$(unitdir)/aziot-$${i}d.service"; \
$(INSTALL_DATA) -D "$$i/aziot-$${i}d/aziot-$${i}d.socket" "$(DESTDIR)$(unitdir)/aziot-$${i}d.socket"; \
<"$$i/aziot-$${i}d/aziot-$${i}d.service.in" sed \
-e 's|@private-libs@|$(AZIOT_PRIVATE_LIBRARIES)|' \
-e 's|@libexecdir@|$(libexecdir)|' \
-e 's|@user_aziotid@|$(USER_AZIOTID)|' \
-e 's|@user_aziotks@|$(USER_AZIOTKS)|' \
-e 's|@user_aziotcs@|$(USER_AZIOTCS)|' \
-e 's|@user_aziottpm@|$(USER_AZIOTTPM)|' \
>"$$OUTPUT_SERVICE"; \
chmod 0644 "$$OUTPUT_SERVICE"; \
done
Expand Down
6 changes: 6 additions & 0 deletions aziotctl/.cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[env]
USER_AZIOTID = "aziotid"
USER_AZIOTKS = "aziotks"
USER_AZIOTCS = "aziotcs"
USER_AZIOTTPM = "aziottpm"
SOCKET_DIR = "/run/aziot"
3 changes: 3 additions & 0 deletions aziotctl/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,6 @@ openssl-sys2 = { path = "../openssl-sys2" }

[dev-dependencies]
bytes = "1"

[features]
snapctl = ["aziotctl-common/snapctl"]
3 changes: 3 additions & 0 deletions aziotctl/aziotctl-common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,6 @@ http-common = { path = "../../http-common" }
[dev-dependencies]
bytes = "1"
toml = "0.7"

[features]
snapctl = []
14 changes: 12 additions & 2 deletions aziotctl/aziotctl-common/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,22 @@ pub fn write_file(
let path = path.as_ref();
let path_displayable = path.display();

// We're about to truncate the file anyway, but it is a little safer to
// fully remove it, as a truncating write does not actually reset ownership
// or user permissions (which can lead to needing CAP_FOWNER, as then we
// encounter a set_permissions call on a file we may or may not own). We
// can just ignore any errors returned here as they either mean a) the file
// doesn't exist, in which case great, or b) there is some sort of permission
// or path error which will just crop up in the fs::write call anyway

let _file = fs::remove_file(path);

let () =
fs::write(path, content).with_context(|| format!("could not create {path_displayable}"))?;
let () = unistd::chown(path, Some(user.uid), Some(user.gid))
.with_context(|| format!("could not set ownership on {path_displayable}"))?;
let () = fs::set_permissions(path, fs::Permissions::from_mode(mode))
.with_context(|| format!("could not set permissions on {path_displayable}"))?;
let () = unistd::chown(path, Some(user.uid), Some(user.gid))
.with_context(|| format!("could not set ownership on {path_displayable}"))?;

Ok(())
}
Expand Down
10 changes: 6 additions & 4 deletions aziotctl/aziotctl-common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,10 +139,12 @@ pub fn is_rfc_1035_valid(hostname: &str) -> bool {
}

fn program_name() -> String {
std::env::args_os()
.next()
.and_then(|arg| arg.into_string().ok())
.unwrap_or_else(|| "<current program>".to_owned())
std::env::current_exe()
.expect("Cannot get the exec path")
.file_name()
.and_then(std::ffi::OsStr::to_str)
.unwrap_or("<current program>")
.to_owned()
}

#[cfg(test)]
Expand Down
37 changes: 37 additions & 0 deletions aziotctl/aziotctl-common/src/system/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub struct ServiceDefinition {
}

// Note, the ordering is important, since the first service is considered the root and will be started by the restart command.
#[cfg(not(feature = "snapctl"))]
pub const SERVICE_DEFINITIONS: &[&ServiceDefinition] = &[
&ServiceDefinition {
service: "aziot-identityd.service",
Expand All @@ -37,6 +38,28 @@ pub const SERVICE_DEFINITIONS: &[&ServiceDefinition] = &[
},
];

// Ordering is not important here, we defer it to snapd
#[cfg(feature = "snapctl")]
pub const SERVICE_DEFINITIONS: &[&ServiceDefinition] = &[
&ServiceDefinition {
service: "snap.azure-iot-identity.identityd.service",
sockets: &["aziot-identityd.socket"],
},
&ServiceDefinition {
service: "snap.azure-iot-identity.keyd.service",
sockets: &["aziot-keyd.socket"],
},
&ServiceDefinition {
service: "snap.azure-iot-identity.certd.service",
sockets: &["aziot-certd.socket"],
},
&ServiceDefinition {
service: "snap.azure-iot-identity.tpmd.service",
sockets: &["aziot-tpmd.socket"],
},
];

#[cfg(not(feature = "snapctl"))]
fn print_command_error(result: &std::process::Output) {
use std::io::{self, Write};

Expand All @@ -49,3 +72,17 @@ fn print_command_error(result: &std::process::Output) {
io::stdout().write_all(&result.stderr).unwrap();
eprintln!();
}

#[cfg(feature = "snapctl")]
fn print_command_error(result: &std::process::Output) {
use std::io::{self, Write};

eprintln!("snapctl exited with non-zero status code.");
eprintln!("stdout:");
eprintln!("=======");
io::stdout().write_all(&result.stdout).unwrap();
eprintln!("stderr:");
eprintln!("=======");
io::stdout().write_all(&result.stderr).unwrap();
eprintln!();
}
43 changes: 43 additions & 0 deletions aziotctl/aziotctl-common/src/system/restart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use anyhow::{Context, Result};

use super::{print_command_error, stop, ServiceDefinition};

#[cfg(not(feature = "snapctl"))]
pub fn restart(services: &[&ServiceDefinition]) -> Result<()> {
// stop all services
stop(services)?;
Expand All @@ -20,6 +21,7 @@ pub fn restart(services: &[&ServiceDefinition]) -> Result<()> {
start(services[0].service)
}

#[cfg(not(feature = "snapctl"))]
fn start(name: &str) -> Result<()> {
print!("Starting {name}...");
let result = Command::new("systemctl")
Expand All @@ -35,3 +37,44 @@ fn start(name: &str) -> Result<()> {

Ok(())
}

#[cfg(feature = "snapctl")]
pub fn restart(services: &[&ServiceDefinition]) -> Result<()> {
// stop all services
stop(services)?;

// start all services
start(services)
}

#[cfg(feature = "snapctl")]
pub fn start(services: &[&ServiceDefinition]) -> Result<()> {
let snap_instance_name = match std::env::var("SNAP_INSTANCE_NAME") {
Ok(snap_instance_name) => snap_instance_name,
Err(_) => {
std::env::var("SNAP_NAME").expect("snapctl must be used within the context of a snap")
}
};

print!("Starting {} services...", snap_instance_name);

let service_names = services.iter().map(|s| {
s.service
.trim_start_matches("snap.")
.trim_end_matches(".service")
});

let result = Command::new("snapctl")
.arg("start")
.args(service_names)
.output()
.context("Failed to call snapctl start")?;

if result.status.success() {
println!("Started!");
} else {
print_command_error(&result);
}

Ok(())
}
Loading

0 comments on commit 9f5c079

Please sign in to comment.