cloudpub-common 3.6.1173

Common code for the client, server, and GUI
Documentation
// The client release file names, defined once for everyone who has to agree
// on them: the client, which downloads the files, and the server, which may
// offer an agent an upgrade only when the file that agent would fetch
// actually exists in its channel (CLOUDPUB-1527).
//
// The naming used to live only in client/src/upgrade/mod.rs, cfg-gated per
// host OS - a server could not name files for platforms it does not run on,
// and the agent's platform report does not match the file tokens everywhere
// anyway: an ARM agent reports "linux-armv7" but downloads linux-arm files,
// an Android agent reports "android-aarch64" and downloads android-arm64.

use crate::utils::get_platform;

/// What an agent downloads: the GUI bundle, the CLI archive, or the system
/// package the local package manager takes.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArtifactKind {
    Gui,
    Cli,
    Deb,
    Rpm,
}

/// The release file name for (os, arch) exactly as the client's download
/// selection reports them (get_current_os/get_current_arch in
/// client/src/upgrade/mod.rs), with the version and branch baked in the way
/// every release of them is named. `None` where the platform ships no
/// artifact of the kind - no GUI on ARM Linux, no packages off x86_64.
pub fn release_file_name(
    version: &str,
    branch: &str,
    os: &str,
    arch: &str,
    kind: ArtifactKind,
) -> Option<String> {
    // The arch token as it appears in the file name: identical to the
    // selection's arch everywhere but Android, whose files are called
    // arm64 while the selection reports aarch64.
    let file_arch = match (os, arch) {
        ("android", "aarch64") => "arm64",
        _ => arch,
    };
    let platform = format!("{os}-{file_arch}");
    let name = match (os, arch, kind) {
        // Linux: the CLI archive for every architecture, deb and rpm
        // (the GUI bundles) for x86_64 only
        ("linux", _, ArtifactKind::Cli) => format!("clo-{version}-{branch}-{platform}.tar.gz"),
        ("linux", "x86_64", ArtifactKind::Deb) => {
            format!("cloudpub-{version}-{branch}-{platform}.deb")
        }
        ("linux", "x86_64", ArtifactKind::Rpm) => {
            format!("cloudpub-{version}-{branch}-{platform}.rpm")
        }
        // macOS: GUI bundle and CLI archive for both architectures
        ("macos", _, ArtifactKind::Gui) => format!("cloudpub-{version}-{branch}-{platform}.dmg"),
        ("macos", _, ArtifactKind::Cli) => format!("clo-{version}-{branch}-{platform}.tar.gz"),
        // Windows: x86_64 only
        ("windows", "x86_64", ArtifactKind::Gui) => {
            format!("cloudpub-{version}-{branch}-{platform}.msi")
        }
        ("windows", "x86_64", ArtifactKind::Cli) => {
            format!("clo-{version}-{branch}-{platform}.zip")
        }
        // Android: the CLI archive
        ("android", "aarch64", ArtifactKind::Cli) => {
            format!("clo-{version}-{branch}-{platform}.tar.gz")
        }
        _ => return None,
    };
    Some(name)
}

/// (os, arch) a client's download selection would use on the machine that
/// reported this AgentInfo.platform - the strings differ from the file
/// tokens for ARM ("linux-armv7" selects the "arm" arch) and Android
/// ("android-aarch64" downloads "arm64" files), which is exactly why the
/// agent's platform alone cannot name files. `None` for platform strings
/// no client of ours reports.
pub fn agent_platform_parts(agent_platform: &str) -> Option<(&'static str, &'static str)> {
    match agent_platform {
        "linux-x86_64" => Some(("linux", "x86_64")),
        "linux-i686" => Some(("linux", "i686")),
        "linux-aarch64" => Some(("linux", "aarch64")),
        // The agent reports the ARM profile, its downloads are named "arm"
        "linux-armv7" => Some(("linux", "arm")),
        "linux-mips" => Some(("linux", "mips")),
        "linux-mipsel" => Some(("linux", "mipsel")),
        "macos-x86_64" => Some(("macos", "x86_64")),
        "macos-aarch64" => Some(("macos", "aarch64")),
        "windows-x86_64" => Some(("windows", "x86_64")),
        "windows-i686" => Some(("windows", "i686")),
        "android-aarch64" => Some(("android", "aarch64")),
        _ => None,
    }
}

/// The platform string of the running machine, as the agent reports it -
/// the key the other two functions meet at.
pub fn current_platform() -> String {
    get_platform()
}

#[cfg(test)]
mod tests {
    use super::*;

    // HYPOTHESIS: the server cannot offer upgrades by comparing version
    //     numbers, and it cannot name the file from the agent's platform
    //     string either - the ARM and Android tokens differ between what
    //     the agent reports and what the file is called. Offering by any
    //     local guess sends agents of whole platforms into 404s.
    // SOLUTION: one naming table in common, keyed by the (os, arch) the
    //     client's own selection uses, plus the mapping from the agent's
    //     platform report to those pairs. The client builds its download
    //     URLs from the table; the server builds the very same names and
    //     offers a version only when the file is there.
    // RESULT: an ARM agent is offered the linux-arm file, an Android agent
    //     the android-arm64 one, and no agent is offered a file its own
    //     download code would never pick.
    // STATUS: FIXED - see CLOUDPUB-1527

    const V: &str = "3.5.1126";
    const B: &str = "beta";

    fn name(os: &str, arch: &str, kind: ArtifactKind) -> Option<String> {
        release_file_name(V, B, os, arch, kind)
    }

    // Every entry of the old client-side DownloadConfig, verbatim: this is
    // the test that the move into common changed no file name. A difference
    // here is either a client regression (its URLs move) or a server one
    // (it starts offering files that do not exist).
    #[test]
    fn file_names_are_exactly_the_clients_download_scheme() {
        // macOS
        assert_eq!(
            name("macos", "aarch64", ArtifactKind::Gui),
            Some("cloudpub-3.5.1126-beta-macos-aarch64.dmg".to_string())
        );
        assert_eq!(
            name("macos", "aarch64", ArtifactKind::Cli),
            Some("clo-3.5.1126-beta-macos-aarch64.tar.gz".to_string())
        );
        assert_eq!(
            name("macos", "x86_64", ArtifactKind::Gui),
            Some("cloudpub-3.5.1126-beta-macos-x86_64.dmg".to_string())
        );
        assert_eq!(
            name("macos", "x86_64", ArtifactKind::Cli),
            Some("clo-3.5.1126-beta-macos-x86_64.tar.gz".to_string())
        );
        // Windows
        assert_eq!(
            name("windows", "x86_64", ArtifactKind::Gui),
            Some("cloudpub-3.5.1126-beta-windows-x86_64.msi".to_string())
        );
        assert_eq!(
            name("windows", "x86_64", ArtifactKind::Cli),
            Some("clo-3.5.1126-beta-windows-x86_64.zip".to_string())
        );
        // Linux CLI, one archive per architecture
        for arch in [
            "arm", "armv5te", "aarch64", "mips", "mipsel", "i686", "x86_64",
        ] {
            assert_eq!(
                name("linux", arch, ArtifactKind::Cli),
                Some(format!("clo-3.5.1126-beta-linux-{arch}.tar.gz")),
                "the linux {arch} CLI archive name changed"
            );
        }
        // Linux packages: x86_64 only, GUI bundles both
        assert_eq!(
            name("linux", "x86_64", ArtifactKind::Deb),
            Some("cloudpub-3.5.1126-beta-linux-x86_64.deb".to_string())
        );
        assert_eq!(
            name("linux", "x86_64", ArtifactKind::Rpm),
            Some("cloudpub-3.5.1126-beta-linux-x86_64.rpm".to_string())
        );
        // Android
        assert_eq!(
            name("android", "aarch64", ArtifactKind::Cli),
            Some("clo-3.5.1126-beta-android-arm64.tar.gz".to_string())
        );
        // Nothing else is shipped
        assert_eq!(name("linux", "x86_64", ArtifactKind::Gui), None);
        for kind in [ArtifactKind::Deb, ArtifactKind::Rpm] {
            assert_eq!(name("macos", "aarch64", kind), None);
            assert_eq!(name("windows", "x86_64", kind), None);
            assert_eq!(name("android", "aarch64", kind), None);
        }
        for kind in [ArtifactKind::Gui, ArtifactKind::Deb, ArtifactKind::Rpm] {
            assert_eq!(name("linux", "aarch64", kind), None);
            assert_eq!(name("linux", "arm", kind), None);
            assert_eq!(name("linux", "i686", kind), None);
        }
        // And no artifacts exist for pairs nothing builds
        assert_eq!(name("windows", "i686", ArtifactKind::Cli), None);
        assert_eq!(name("plan9", "x86_64", ArtifactKind::Cli), None);
    }

    // The agent report -> download selection mapping, with the two known
    // renames pinned: these are the regressions a platform-string compare
    // would cause (an ARM and every Android agent never seeing an upgrade).
    #[test]
    fn agent_platforms_map_to_the_download_selection_pairs() {
        let cases = [
            ("linux-x86_64", ("linux", "x86_64")),
            ("linux-i686", ("linux", "i686")),
            ("linux-aarch64", ("linux", "aarch64")),
            ("linux-armv7", ("linux", "arm")),
            ("linux-mips", ("linux", "mips")),
            ("linux-mipsel", ("linux", "mipsel")),
            ("macos-x86_64", ("macos", "x86_64")),
            ("macos-aarch64", ("macos", "aarch64")),
            ("windows-x86_64", ("windows", "x86_64")),
            ("windows-i686", ("windows", "i686")),
            ("android-aarch64", ("android", "aarch64")),
        ];
        for (report, expected) in cases {
            assert_eq!(
                agent_platform_parts(report),
                Some(expected),
                "the agent report {report} must select the {expected:?} downloads"
            );
        }
        assert_eq!(agent_platform_parts("linux-armv6"), None);
        assert_eq!(agent_platform_parts(""), None);
    }

    #[test]
    fn every_reported_platform_resolves_to_a_download_pair() {
        // current_platform() is what the agent puts in AgentInfo; every
        // value it can produce has to be mappable, or that machine can
        // never be offered an upgrade.
        for report in [
            "linux-x86_64",
            "linux-armv7",
            "linux-aarch64",
            "linux-i686",
            "linux-mips",
            "linux-mipsel",
            "windows-x86_64",
            "windows-i686",
            "macos-x86_64",
            "macos-aarch64",
            "android-aarch64",
        ] {
            assert!(
                agent_platform_parts(report).is_some(),
                "no download selection for the reported platform {report}"
            );
        }
    }
}