feat: add airgap binaries embedding the Podman images (#1120)

- on mac, it embeds the qemu FCOS binaries
- on Windows, it embeds the WSL binary

When initializing a new podman machine using airgap binaries it'll use the embedded files and not grab it from internet

There is also an optional way to provide a custom image in the create menu

Change-Id: I0b250ae2e018ff56eda1d2170fd91ab8cfc52eae
Signed-off-by: Florent Benoit <fbenoit@redhat.com>

Signed-off-by: Florent Benoit <fbenoit@redhat.com>
This commit is contained in:
Florent BENOIT 2023-01-10 21:56:02 +01:00 committed by GitHub
parent 3c4fbb3fb6
commit f6245907a5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 223 additions and 7 deletions

View file

@ -26,6 +26,15 @@ if (process.env.VITE_APP_VERSION === undefined) {
}`;
}
let macosArches = ['x64', 'arm64', 'universal'];
let artifactNameSuffix = '';
if (process.env.AIRGAP_DOWNLOAD) {
artifactNameSuffix = '-airgap';
// Create only one universal build for airgap mode
macosArches = ['universal'];
}
/**
* @type {import('electron-builder').Configuration}
* @see https://www.electron.build/configuration/configuration
@ -65,10 +74,10 @@ const config = {
},
files: ['packages/**/dist/**', 'extensions/**/builtin/*.cdix/**'],
portable: {
artifactName: 'podman-desktop-${version}.${ext}',
artifactName: `podman-desktop${artifactNameSuffix}-\${version}.\${ext}`,
},
nsis: {
artifactName: 'podman-desktop-${version}-setup.${ext}',
artifactName: `podman-desktop${artifactNameSuffix}-\${version}-setup.\${ext}`,
},
win: {
target: ['portable', 'nsis'],
@ -110,12 +119,12 @@ const config = {
},
afterSign: 'electron-builder-notarize',
mac: {
artifactName: 'podman-desktop-${version}-${arch}.${ext}',
artifactName: `podman-desktop${artifactNameSuffix}-\${version}-\${arch}.\${ext}`,
hardenedRuntime: true,
entitlements: './node_modules/electron-builder-notarize/entitlements.mac.inherit.plist',
target: {
target: 'default',
arch: ['x64', 'arm64', 'universal'],
arch: macosArches,
},
},
dmg: {
@ -142,6 +151,14 @@ const config = {
},*/
};
// do not publish auto-update files for airgap mode
if (process.env.AIRGAP_DOWNLOAD) {
config.publish = {
publishAutoUpdate: false,
provider: 'github'
};
}
const azureCodeSign = filePath => {
if (!process.env.AZURE_KEY_VAULT_URL) {
console.log('Skipping code signing, no environment variables set for that.');

View file

@ -119,12 +119,19 @@ jobs:
GITHUB_TOKEN: ${{ secrets.PODMAN_DESKTOP_BOT_TOKEN }}
build:
name: Build / ${{ matrix.os }}
name: Build / ${{ matrix.os }} ${{ matrix.airgap == 'true' && '(Air Gap)' || '' }}
needs: tag
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [windows-2022, ubuntu-20.04, macos-11]
include:
- os: "windows-2022"
- os: "windows-2022"
airgap: "true"
- os: "macos-11"
- os: "macos-11"
airgap: "true"
- os: "ubuntu-20.04"
timeout-minutes: 60
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@ -206,6 +213,8 @@ jobs:
- name: Run Build
timeout-minutes: 40
env:
AIRGAP_DOWNLOAD: ${{ matrix.airgap == 'true' && '1' || '' }}
run: yarn compile:next
release:

View file

@ -75,6 +75,12 @@
"default": 100,
"scope": "ContainerProviderConnectionFactory",
"description": "Disk size (GB)"
},
"podman.factory.machine.image-path": {
"type": "string",
"format": "file",
"scope": "ContainerProviderConnectionFactory",
"description": "Image Path (Optional)"
}
}
}

View file

@ -19,6 +19,7 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as https from 'node:https';
import { Octokit } from 'octokit';
import type { OctokitOptions } from '@octokit/core/dist-types/types';
import * as hasha from 'hasha';
@ -29,6 +30,8 @@ const platform = process.platform;
const MAX_DOWNLOAD_ATTEMPT = 3;
let downloadAttempt = 0;
let downloadAttemptFcos = 0;
const downloadAttemptFedora = 0;
const octokitOptions: OctokitOptions = {};
if (process.env.GITHUB_TOKEN) {
@ -130,6 +133,155 @@ async function downloadAndCheckSha(tagVersion: string, fileName: string, artifac
}
}
// grab the JSON from the given URL
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async function httpsJsonRequest(url): Promise<any> {
return new Promise((resolve, reject) => {
https.get(url, res => {
let body = '';
res.on('data', chunk => {
body += chunk;
});
res.on('end', () => {
try {
const json = JSON.parse(body);
resolve(json);
} catch (error) {
reject(error.message);
}
});
res.on('error', error => {
reject(error.message);
});
});
});
}
// download the file from the given URL and store the content in destFile
async function httpsDownloadFile(url, destFile): Promise<void> {
return new Promise((resolve, reject) => {
const request = https.get(url, async res => {
// handle url redirect
if (res.statusCode === 302) {
await httpsDownloadFile(res.headers.location, destFile);
resolve();
}
res.on('data', data => {
fs.appendFileSync(destFile, data);
});
res.on('end', () => {
try {
resolve();
} catch (error) {
reject(error.message);
}
});
res.on('error', error => {
reject(error.message);
});
});
request.end();
});
}
// For Windows binaries, grab the latest release from GitHub repository
async function downloadFedoraImage(repo: string): Promise<void> {
if (downloadAttemptFedora >= MAX_DOWNLOAD_ATTEMPT) {
console.error('Max download attempt reached, exiting...');
process.exit(1);
}
// download the file and check the sha256
const destDir = path.resolve(__dirname, '..', 'assets');
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir);
}
// now, grab the files
const release = await octokit.request('GET /repos/{owner}/{repo}/releases/latest', {
owner: 'containers',
repo,
});
const artifactRelease = release.data.assets.find(asset => asset.name === 'rootfs.tar.xz');
if (!artifactRelease) {
throw new Error(`Can't find assets to download and verify for podman image from repository ${repo}`);
}
const arch = 'x64';
const filename = `podman-image-${arch}.tar.xz`;
const destFile = path.resolve(destDir, filename);
if (!fs.existsSync(destFile)) {
// download the file from diskLocation
console.log(`Downloading Podman package from ${artifactRelease.browser_download_url}`);
await httpsDownloadFile(artifactRelease.browser_download_url, destFile);
console.log(`Downloaded to ${destFile}`);
} else {
console.log(`Podman image ${filename} already downloaded.`);
}
}
// For macOS, grab the qemu image from Fedora CoreOS
async function downloadFCosImage(arch: string): Promise<void> {
if (downloadAttemptFcos >= MAX_DOWNLOAD_ATTEMPT) {
console.error('Max download attempt reached, exiting...');
process.exit(1);
}
// download the JSON of testing Fedora CoreOS images at https://builds.coreos.fedoraproject.org/streams/testing.json
const data = await httpsJsonRequest('https://builds.coreos.fedoraproject.org/streams/testing.json');
// get the file to download
const qemuArch = arch === 'x64' ? 'x86_64' : 'aarch64';
const qemuData = data['architectures'][qemuArch]['artifacts']['qemu'];
// get disk object
const disk = qemuData.formats['qcow2.xz'].disk;
// get the disk location
const diskLocation = disk.location;
// get the sha2556
const sha256 = disk.sha256;
// download the file and check the sha256
const destDir = path.resolve(__dirname, '..', 'assets');
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir);
}
const filename = `podman-image-${arch}.qcow2.xz`;
const destFile = path.resolve(destDir, filename);
if (!fs.existsSync(destFile)) {
// download the file from diskLocation
console.log(`Downloading Podman package from ${diskLocation}`);
await httpsDownloadFile(diskLocation, destFile);
console.log(`Downloaded to ${destFile}`);
} else {
console.log(`Podman image ${filename} already downloaded.`);
}
if (!(await checkFileSha(destFile, sha256))) {
console.warn(`Checksum for downloaded ${destFile} is not matching, downloading again...`);
fs.rmSync(destFile);
downloadAttemptFcos++;
// call the loop again
downloadFCosImage(arch);
} else {
console.log(`Checksum for ${filename} matched.`);
}
}
let tagVersion: string, dlName: string;
if (platform === 'win32') {
@ -138,6 +290,11 @@ if (platform === 'win32') {
// eslint-disable-next-line prefer-const
dlName = tools.platform.win32.fileName;
downloadAndCheckSha(tagVersion, dlName, `podman-${tools.version}-setup.exe`);
// download the fedora image
if (process.env.AIRGAP_DOWNLOAD) {
downloadFedoraImage('podman-wsl-fedora');
}
} else if (platform === 'darwin') {
tagVersion = tools.platform.darwin.version;
dlName = tools.platform.darwin.arch.x64.fileName;
@ -145,4 +302,11 @@ if (platform === 'win32') {
dlName = tools.platform.darwin.arch.arm64.fileName;
downloadAndCheckSha(tagVersion, dlName, 'podman-installer-macos-aarch64.pkg');
// download the current testing image if AIRGAP_DOWNLOAD env variable is set
if (process.env.AIRGAP_DOWNLOAD) {
// download the fedora core os images for both arches
downloadFCosImage('x64');
downloadFCosImage('arm64');
}
}

View file

@ -23,7 +23,7 @@ import * as fs from 'node:fs';
import { spawn } from 'node:child_process';
import { RegistrySetup } from './registry-setup';
import { isLinux, isMac, isWindows } from './util';
import { getAssetsFolder, isLinux, isMac, isWindows } from './util';
import { PodmanInstall } from './podman-install';
import type { InstalledPodman } from './podman-cli';
import { execPromise, getPodmanCli, getPodmanInstallation } from './podman-cli';
@ -425,6 +425,26 @@ export async function activate(extensionContext: extensionApi.ExtensionContext):
parameters.push(params['podman.factory.machine.diskSize']);
}
// disk size
if (params['podman.factory.machine.image-path']) {
parameters.push('--image-path');
parameters.push(params['podman.factory.machine.image-path']);
} else if (isMac || isWindows) {
// check if we have an embedded asset for the image path for macOS or Windows
let suffix = '';
if (isWindows) {
suffix = `-${process.arch}.tar.xz`;
} else if (isMac) {
suffix = `-${process.arch}.qcow2.xz`;
}
const assetImagePath = path.resolve(getAssetsFolder(), `podman-image${suffix}`);
// check if the file exists and if it does, use it
if (fs.existsSync(assetImagePath)) {
parameters.push('--image-path');
parameters.push(assetImagePath);
}
}
// name at the end
if (params['podman.factory.machine.name']) {
parameters.push(params['podman.factory.machine.name']);