vorpal-sdk 0.2.0

Rust SDK for building Vorpal artifacts.
Documentation
use crate::{
    api::artifact::ArtifactSystem::{Aarch64Linux, X8664Linux},
    artifact::{crane::Crane, get_env_key, rsync::Rsync, step, Artifact},
    context::ConfigContext,
};
use anyhow::Result;
use indoc::formatdoc;

pub struct OciImage<'a> {
    aliases: Vec<&'a str>,
    artifacts: Vec<&'a str>,
    crane: Option<&'a str>,
    name: &'a str,
    rootfs: &'a str,
    rsync: Option<&'a str>,
}

impl<'a> OciImage<'a> {
    pub fn new(name: &'a str, rootfs: &'a str) -> Self {
        Self {
            aliases: vec![],
            artifacts: vec![],
            crane: None,
            name,
            rootfs,
            rsync: None,
        }
    }

    pub fn with_aliases(mut self, aliases: Vec<&'a str>) -> Self {
        self.aliases = aliases;
        self
    }

    pub fn with_artifacts(mut self, artifacts: Vec<&'a str>) -> Self {
        self.artifacts = artifacts;
        self
    }

    pub fn with_crane(mut self, crane: &'a str) -> Self {
        self.crane = Some(crane);
        self
    }

    pub fn with_rsync(mut self, rsync: &'a str) -> Self {
        self.rsync = Some(rsync);
        self
    }

    pub async fn build(self, context: &mut ConfigContext) -> Result<String> {
        if self.name != self.name.to_lowercase() {
            anyhow::bail!("container image name must be lowercase: '{}'", self.name);
        }

        for c in self.name.chars() {
            if !matches!(c, 'a'..='z' | '0'..='9' | '/' | ':' | '-' | '.' | '_') {
                anyhow::bail!(
                    "container image name invalid character '{}': '{}'. \
                     Allowed: lowercase letters, digits, and / : - . _",
                    c,
                    self.name
                );
            }
        }

        let crane = match self.crane {
            Some(val) => val.to_string(),
            None => Crane::new().build(context).await?,
        };

        let rsync = match self.rsync {
            Some(val) => val.to_string(),
            None => Rsync::new().build(context).await?,
        };

        let rootfs = self.rootfs.to_string();

        let artifacts_list = self.artifacts.join(" ");

        let step_script = formatdoc! {"
            OCI_IMAGE_ARTIFACTS=\"{artifacts_list}\"
            OCI_IMAGE_CRANE=\"{crane}\"
            OCI_IMAGE_NAME=\"{name}\"
            OCI_IMAGE_ROOTFS=\"{rootfs}\"
            OCI_IMAGE_RSYNC=\"{rsync}\"
            OUTPUT_TAR=${{PWD}}/rootfs.tar
            ROOTFS_DIR=${{PWD}}/rootfs
            STORE_PREFIX=var/lib/vorpal/store/artifact/output/{namespace}

            # Detect platform based on build architecture
            case \"$(uname -m)\" in
                x86_64)  OCI_PLATFORM=\"linux/amd64\" ;;
                aarch64) OCI_PLATFORM=\"linux/arm64\" ;;
                *)       OCI_PLATFORM=\"linux/$(uname -m)\" ;;
            esac

            mkdir -p ${{ROOTFS_DIR}}

            for artifact in ${{OCI_IMAGE_ARTIFACTS}}; do
                SOURCE_DIR=/${{STORE_PREFIX}}/${{artifact}}
                TARGET_PATH=${{STORE_PREFIX}}/${{artifact}}

                mkdir -p ${{ROOTFS_DIR}}/${{TARGET_PATH}}

                echo \"Copying artifact layer ${{artifact}}...\"

                ${{OCI_IMAGE_RSYNC}}/bin/rsync -aW ${{SOURCE_DIR}}/ ${{ROOTFS_DIR}}/${{TARGET_PATH}}

                echo \"Copied artifact layer ${{artifact}}\"

                # Symlink bin files to /usr/local/bin
                if [ -d \"${{SOURCE_DIR}}/bin\" ]; then
                    mkdir -p ${{ROOTFS_DIR}}/usr/local/bin
                    for bin_file in ${{SOURCE_DIR}}/bin/*; do
                        if [ -f \"${{bin_file}}\" ]; then
                            bin_name=$(basename \"${{bin_file}}\")
                            ln -sf /${{TARGET_PATH}}/bin/${{bin_name}} ${{ROOTFS_DIR}}/usr/local/bin/${{bin_name}}
                            echo \"Symlinked ${{bin_name}} to /usr/local/bin\"
                        fi
                    done
                fi
            done

            echo \"Copying Vorpal operating system files...\"

            ${{OCI_IMAGE_RSYNC}}/bin/rsync -aW ${{OCI_IMAGE_ROOTFS}}/ ${{ROOTFS_DIR}}

            echo \"Copied Vorpal operating system files\"

            echo \"Creating output tarball...\"

            tar -cf ${{OUTPUT_TAR}} -C ${{ROOTFS_DIR}} .

            echo \"Created output tarball\"

            mkdir -p ${{VORPAL_OUTPUT}}

            echo \"Creating OCI image ${{OCI_IMAGE_NAME}}:latest\"

            ${{OCI_IMAGE_CRANE}}/bin/crane append \\
                --new_layer ${{OUTPUT_TAR}} \\
                --new_tag ${{OCI_IMAGE_NAME}}:latest \\
                --oci-empty-base \\
                --output ${{VORPAL_OUTPUT}}/image.tar \\
                --platform ${{OCI_PLATFORM}}

            echo \"Setting platform metadata in image config...\"

            # Extract tarball to modify config (crane mutate cannot work with local files)
            WORK_DIR=${{PWD}}/image-work
            mkdir -p ${{WORK_DIR}}
            tar -xf ${{VORPAL_OUTPUT}}/image.tar -C ${{WORK_DIR}}

            # Get config filename from manifest
            CONFIG_FILE=$(sed -n 's/.*\"Config\":\"\\([^\"]*\\)\".*/\\1/p' ${{WORK_DIR}}/manifest.json)

            # Detect architecture for config metadata
            case \"$(uname -m)\" in
                x86_64)  CONFIG_ARCH=\"amd64\" ;;
                aarch64) CONFIG_ARCH=\"arm64\" ;;
                *)       CONFIG_ARCH=\"$(uname -m)\" ;;
            esac

            # Modify config to set platform (crane append leaves these empty)
            sed -i \"s/\\\"architecture\\\":\\\"\\\"/\\\"architecture\\\":\\\"${{CONFIG_ARCH}}\\\"/\" ${{WORK_DIR}}/${{CONFIG_FILE}}
            sed -i \"s/\\\"os\\\":\\\"\\\"/\\\"os\\\":\\\"linux\\\"/\" ${{WORK_DIR}}/${{CONFIG_FILE}}

            # Compute new hash and rename config file
            NEW_HASH=$(sha256sum ${{WORK_DIR}}/${{CONFIG_FILE}} | awk '{{print $1}}')
            NEW_CONFIG=\"sha256:${{NEW_HASH}}\"
            mv ${{WORK_DIR}}/${{CONFIG_FILE}} ${{WORK_DIR}}/${{NEW_CONFIG}}

            # Update manifest with new config reference
            sed -i \"s|${{CONFIG_FILE}}|${{NEW_CONFIG}}|\" ${{WORK_DIR}}/manifest.json

            # Repackage tarball
            pushd ${{WORK_DIR}}
            tar -cf ${{VORPAL_OUTPUT}}/image.tar manifest.json ${{NEW_CONFIG}} *.tar.gz
            popd

            # Cleanup
            rm -rf ${{WORK_DIR}}

            echo \"Created OCI image ${{OCI_IMAGE_NAME}}:latest\"",
            artifacts_list = artifacts_list,
            crane = get_env_key(&crane),
            name = self.name,
            namespace = context.get_artifact_namespace(),
            rootfs = get_env_key(&rootfs),
            rsync = get_env_key(&rsync),
        };

        let mut step_artifacts = vec![crane, rsync, rootfs];

        for artifact in &self.artifacts {
            step_artifacts.push(artifact.to_string());
        }

        let step = step::shell(context, step_artifacts, vec![], step_script, vec![]).await?;

        let systems = vec![Aarch64Linux, X8664Linux];

        Artifact::new(self.name, vec![step], systems)
            .with_aliases(self.aliases.into_iter().map(String::from).collect())
            .build(context)
            .await
    }
}