Skip to main content

cloudpub_common/
downloads.rs

1// The client release file names, defined once for everyone who has to agree
2// on them: the client, which downloads the files, and the server, which may
3// offer an agent an upgrade only when the file that agent would fetch
4// actually exists in its channel (CLOUDPUB-1527).
5//
6// The naming used to live only in client/src/upgrade/mod.rs, cfg-gated per
7// host OS - a server could not name files for platforms it does not run on,
8// and the agent's platform report does not match the file tokens everywhere
9// anyway: an ARM agent reports "linux-armv7" but downloads linux-arm files,
10// an Android agent reports "android-aarch64" and downloads android-arm64.
11
12use crate::utils::get_platform;
13
14/// What an agent downloads: the GUI bundle, the CLI archive, or the system
15/// package the local package manager takes.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum ArtifactKind {
18    Gui,
19    Cli,
20    Deb,
21    Rpm,
22}
23
24/// The release file name for (os, arch) exactly as the client's download
25/// selection reports them (get_current_os/get_current_arch in
26/// client/src/upgrade/mod.rs), with the version and branch baked in the way
27/// every release of them is named. `None` where the platform ships no
28/// artifact of the kind - no GUI on ARM Linux, no packages off x86_64.
29pub fn release_file_name(
30    version: &str,
31    branch: &str,
32    os: &str,
33    arch: &str,
34    kind: ArtifactKind,
35) -> Option<String> {
36    // The arch token as it appears in the file name: identical to the
37    // selection's arch everywhere but Android, whose files are called
38    // arm64 while the selection reports aarch64.
39    let file_arch = match (os, arch) {
40        ("android", "aarch64") => "arm64",
41        _ => arch,
42    };
43    let platform = format!("{os}-{file_arch}");
44    let name = match (os, arch, kind) {
45        // Linux: the CLI archive for every architecture, deb and rpm
46        // (the GUI bundles) for x86_64 only
47        ("linux", _, ArtifactKind::Cli) => format!("clo-{version}-{branch}-{platform}.tar.gz"),
48        ("linux", "x86_64", ArtifactKind::Deb) => {
49            format!("cloudpub-{version}-{branch}-{platform}.deb")
50        }
51        ("linux", "x86_64", ArtifactKind::Rpm) => {
52            format!("cloudpub-{version}-{branch}-{platform}.rpm")
53        }
54        // macOS: GUI bundle and CLI archive for both architectures
55        ("macos", _, ArtifactKind::Gui) => format!("cloudpub-{version}-{branch}-{platform}.dmg"),
56        ("macos", _, ArtifactKind::Cli) => format!("clo-{version}-{branch}-{platform}.tar.gz"),
57        // Windows: x86_64 only
58        ("windows", "x86_64", ArtifactKind::Gui) => {
59            format!("cloudpub-{version}-{branch}-{platform}.msi")
60        }
61        ("windows", "x86_64", ArtifactKind::Cli) => {
62            format!("clo-{version}-{branch}-{platform}.zip")
63        }
64        // Android: the CLI archive
65        ("android", "aarch64", ArtifactKind::Cli) => {
66            format!("clo-{version}-{branch}-{platform}.tar.gz")
67        }
68        _ => return None,
69    };
70    Some(name)
71}
72
73/// (os, arch) a client's download selection would use on the machine that
74/// reported this AgentInfo.platform - the strings differ from the file
75/// tokens for ARM ("linux-armv7" selects the "arm" arch) and Android
76/// ("android-aarch64" downloads "arm64" files), which is exactly why the
77/// agent's platform alone cannot name files. `None` for platform strings
78/// no client of ours reports.
79pub fn agent_platform_parts(agent_platform: &str) -> Option<(&'static str, &'static str)> {
80    match agent_platform {
81        "linux-x86_64" => Some(("linux", "x86_64")),
82        "linux-i686" => Some(("linux", "i686")),
83        "linux-aarch64" => Some(("linux", "aarch64")),
84        // The agent reports the ARM profile, its downloads are named "arm"
85        "linux-armv7" => Some(("linux", "arm")),
86        "linux-mips" => Some(("linux", "mips")),
87        "linux-mipsel" => Some(("linux", "mipsel")),
88        "macos-x86_64" => Some(("macos", "x86_64")),
89        "macos-aarch64" => Some(("macos", "aarch64")),
90        "windows-x86_64" => Some(("windows", "x86_64")),
91        "windows-i686" => Some(("windows", "i686")),
92        "android-aarch64" => Some(("android", "aarch64")),
93        _ => None,
94    }
95}
96
97/// The platform string of the running machine, as the agent reports it -
98/// the key the other two functions meet at.
99pub fn current_platform() -> String {
100    get_platform()
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    // HYPOTHESIS: the server cannot offer upgrades by comparing version
108    //     numbers, and it cannot name the file from the agent's platform
109    //     string either - the ARM and Android tokens differ between what
110    //     the agent reports and what the file is called. Offering by any
111    //     local guess sends agents of whole platforms into 404s.
112    // SOLUTION: one naming table in common, keyed by the (os, arch) the
113    //     client's own selection uses, plus the mapping from the agent's
114    //     platform report to those pairs. The client builds its download
115    //     URLs from the table; the server builds the very same names and
116    //     offers a version only when the file is there.
117    // RESULT: an ARM agent is offered the linux-arm file, an Android agent
118    //     the android-arm64 one, and no agent is offered a file its own
119    //     download code would never pick.
120    // STATUS: FIXED - see CLOUDPUB-1527
121
122    const V: &str = "3.5.1126";
123    const B: &str = "beta";
124
125    fn name(os: &str, arch: &str, kind: ArtifactKind) -> Option<String> {
126        release_file_name(V, B, os, arch, kind)
127    }
128
129    // Every entry of the old client-side DownloadConfig, verbatim: this is
130    // the test that the move into common changed no file name. A difference
131    // here is either a client regression (its URLs move) or a server one
132    // (it starts offering files that do not exist).
133    #[test]
134    fn file_names_are_exactly_the_clients_download_scheme() {
135        // macOS
136        assert_eq!(
137            name("macos", "aarch64", ArtifactKind::Gui),
138            Some("cloudpub-3.5.1126-beta-macos-aarch64.dmg".to_string())
139        );
140        assert_eq!(
141            name("macos", "aarch64", ArtifactKind::Cli),
142            Some("clo-3.5.1126-beta-macos-aarch64.tar.gz".to_string())
143        );
144        assert_eq!(
145            name("macos", "x86_64", ArtifactKind::Gui),
146            Some("cloudpub-3.5.1126-beta-macos-x86_64.dmg".to_string())
147        );
148        assert_eq!(
149            name("macos", "x86_64", ArtifactKind::Cli),
150            Some("clo-3.5.1126-beta-macos-x86_64.tar.gz".to_string())
151        );
152        // Windows
153        assert_eq!(
154            name("windows", "x86_64", ArtifactKind::Gui),
155            Some("cloudpub-3.5.1126-beta-windows-x86_64.msi".to_string())
156        );
157        assert_eq!(
158            name("windows", "x86_64", ArtifactKind::Cli),
159            Some("clo-3.5.1126-beta-windows-x86_64.zip".to_string())
160        );
161        // Linux CLI, one archive per architecture
162        for arch in [
163            "arm", "armv5te", "aarch64", "mips", "mipsel", "i686", "x86_64",
164        ] {
165            assert_eq!(
166                name("linux", arch, ArtifactKind::Cli),
167                Some(format!("clo-3.5.1126-beta-linux-{arch}.tar.gz")),
168                "the linux {arch} CLI archive name changed"
169            );
170        }
171        // Linux packages: x86_64 only, GUI bundles both
172        assert_eq!(
173            name("linux", "x86_64", ArtifactKind::Deb),
174            Some("cloudpub-3.5.1126-beta-linux-x86_64.deb".to_string())
175        );
176        assert_eq!(
177            name("linux", "x86_64", ArtifactKind::Rpm),
178            Some("cloudpub-3.5.1126-beta-linux-x86_64.rpm".to_string())
179        );
180        // Android
181        assert_eq!(
182            name("android", "aarch64", ArtifactKind::Cli),
183            Some("clo-3.5.1126-beta-android-arm64.tar.gz".to_string())
184        );
185        // Nothing else is shipped
186        assert_eq!(name("linux", "x86_64", ArtifactKind::Gui), None);
187        for kind in [ArtifactKind::Deb, ArtifactKind::Rpm] {
188            assert_eq!(name("macos", "aarch64", kind), None);
189            assert_eq!(name("windows", "x86_64", kind), None);
190            assert_eq!(name("android", "aarch64", kind), None);
191        }
192        for kind in [ArtifactKind::Gui, ArtifactKind::Deb, ArtifactKind::Rpm] {
193            assert_eq!(name("linux", "aarch64", kind), None);
194            assert_eq!(name("linux", "arm", kind), None);
195            assert_eq!(name("linux", "i686", kind), None);
196        }
197        // And no artifacts exist for pairs nothing builds
198        assert_eq!(name("windows", "i686", ArtifactKind::Cli), None);
199        assert_eq!(name("plan9", "x86_64", ArtifactKind::Cli), None);
200    }
201
202    // The agent report -> download selection mapping, with the two known
203    // renames pinned: these are the regressions a platform-string compare
204    // would cause (an ARM and every Android agent never seeing an upgrade).
205    #[test]
206    fn agent_platforms_map_to_the_download_selection_pairs() {
207        let cases = [
208            ("linux-x86_64", ("linux", "x86_64")),
209            ("linux-i686", ("linux", "i686")),
210            ("linux-aarch64", ("linux", "aarch64")),
211            ("linux-armv7", ("linux", "arm")),
212            ("linux-mips", ("linux", "mips")),
213            ("linux-mipsel", ("linux", "mipsel")),
214            ("macos-x86_64", ("macos", "x86_64")),
215            ("macos-aarch64", ("macos", "aarch64")),
216            ("windows-x86_64", ("windows", "x86_64")),
217            ("windows-i686", ("windows", "i686")),
218            ("android-aarch64", ("android", "aarch64")),
219        ];
220        for (report, expected) in cases {
221            assert_eq!(
222                agent_platform_parts(report),
223                Some(expected),
224                "the agent report {report} must select the {expected:?} downloads"
225            );
226        }
227        assert_eq!(agent_platform_parts("linux-armv6"), None);
228        assert_eq!(agent_platform_parts(""), None);
229    }
230
231    #[test]
232    fn every_reported_platform_resolves_to_a_download_pair() {
233        // current_platform() is what the agent puts in AgentInfo; every
234        // value it can produce has to be mappable, or that machine can
235        // never be offered an upgrade.
236        for report in [
237            "linux-x86_64",
238            "linux-armv7",
239            "linux-aarch64",
240            "linux-i686",
241            "linux-mips",
242            "linux-mipsel",
243            "windows-x86_64",
244            "windows-i686",
245            "macos-x86_64",
246            "macos-aarch64",
247            "android-aarch64",
248        ] {
249            assert!(
250                agent_platform_parts(report).is_some(),
251                "no download selection for the reported platform {report}"
252            );
253        }
254    }
255}