-
Notifications
You must be signed in to change notification settings - Fork 272
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Scripts: Add guestagent and trivy into wsl-distro tarball instead of installing at runtime #7145
Changes from all commits
22d36cf
c5b03d0
69c9752
fe76137
759acc1
fa44f72
0c3e88b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
import path from 'path'; | ||
|
||
import { AlpineLimaISOVersion, Dependency, DownloadContext } from 'scripts/lib/dependencies'; | ||
import { simpleSpawn } from 'scripts/simple_process'; | ||
|
||
type GoDependencyOptions = { | ||
/** | ||
* The output file name, relative to the platform-specific resources directory. | ||
* If this does not contain any directory separators ('/'), it is assumed to | ||
* be a directory name (defaults to `bin`) and the leaf name of the source | ||
* path is appended as the executable name. | ||
*/ | ||
outputPath: string; | ||
/** | ||
* Additional environment for the go compiler; e.g. for GOARCH overrides. | ||
*/ | ||
env?: NodeJS.ProcessEnv; | ||
}; | ||
|
||
/** | ||
* GoDependency represents a golang binary that is built from the local source | ||
* code. | ||
*/ | ||
export class GoDependency implements Dependency { | ||
/** | ||
* Construct a new GoDependency. | ||
* @param sourcePath The path to be compiled, relative to .../src/go | ||
* @param options Additional configuration option; if a string is given, this | ||
* is the outputPath option, defaulting to `bin`. | ||
*/ | ||
constructor(sourcePath: string, options: string | GoDependencyOptions = 'bin') { | ||
this.sourcePath = sourcePath; | ||
this.options = typeof options === 'string' ? { outputPath: options } : options; | ||
} | ||
|
||
get name(): string { | ||
if (this.options.outputPath.includes('/')) { | ||
return path.basename(this.options.outputPath); | ||
} | ||
|
||
return path.basename(this.sourcePath); | ||
} | ||
|
||
sourcePath: string; | ||
options: GoDependencyOptions; | ||
|
||
async download(context: DownloadContext): Promise<void> { | ||
// Rather than actually downloading anything, this builds the source code. | ||
const sourceDir = path.join(process.cwd(), 'src', 'go', this.sourcePath); | ||
const outFile = this.outFile(context); | ||
|
||
console.log(`Building go utility \x1B[1;33;40m${ this.name }\x1B[0m from ${ sourceDir } to ${ outFile }...`); | ||
await simpleSpawn('go', ['build', '-ldflags', '-s -w', '-o', outFile, '.'], { | ||
cwd: sourceDir, | ||
env: this.environment(context), | ||
}); | ||
} | ||
|
||
environment(context: DownloadContext): NodeJS.ProcessEnv { | ||
return { | ||
...process.env, | ||
GOOS: context.goPlatform, | ||
GOARCH: context.isM1 ? 'arm64' : 'amd64', | ||
...this.options.env ?? {}, | ||
}; | ||
} | ||
|
||
outFile(context: DownloadContext): string { | ||
const suffix = context.platform === 'win32' ? '.exe' : ''; | ||
let outputPath = `${ this.options.outputPath }${ suffix }`; | ||
|
||
if (!this.options.outputPath.includes('/')) { | ||
outputPath = `${ this.options.outputPath }/${ this.name }${ suffix }`; | ||
} | ||
|
||
return path.join(context.resourcesDir, context.platform, outputPath); | ||
} | ||
|
||
getAvailableVersions(includePrerelease?: boolean | undefined): Promise<string[]> { | ||
throw new Error('Go dependencies do not have available versions.'); | ||
} | ||
|
||
rcompareVersions(version1: string | AlpineLimaISOVersion, version2: string): 0 | 1 | -1 { | ||
throw new Error('Go dependencies do not have available versions.'); | ||
} | ||
} | ||
|
||
export class RDCtl extends GoDependency { | ||
constructor() { | ||
super('rdctl'); | ||
} | ||
|
||
dependencies(context: DownloadContext): string[] { | ||
if (context.dependencyPlatform === 'wsl') { | ||
// For the WSL copy depend on the Windows one to generate code | ||
return ['rdctl:win32']; | ||
} | ||
|
||
return []; | ||
} | ||
|
||
override async download(context: DownloadContext): Promise<void> { | ||
// For WSL, don't re-generate the code; the win32 copy did it. | ||
if (context.dependencyPlatform !== 'wsl') { | ||
await simpleSpawn('node', ['scripts/ts-wrapper.js', | ||
'scripts/generateCliCode.ts', | ||
'pkg/rancher-desktop/assets/specs/command-api.yaml', | ||
'src/go/rdctl/pkg/options/generated/options.go']); | ||
} | ||
await super.download(context); | ||
} | ||
} | ||
|
||
export class WSLHelper extends GoDependency { | ||
constructor() { | ||
super('wsl-helper', { outputPath: 'internal', env: { CGO_ENABLED: '0' } }); | ||
} | ||
|
||
dependencies(context: DownloadContext): string[] { | ||
return ['mobyOpenAPISpec:win32']; | ||
} | ||
} | ||
|
||
export class NerdctlStub extends GoDependency { | ||
constructor() { | ||
super('nerdctl-stub'); | ||
} | ||
|
||
override outFile(context: DownloadContext) { | ||
// nerdctl-stub is the actual nerdctl binary to be run on linux; | ||
// there is also a `nerdctl` wrapper in the same directory to make it | ||
// easier to handle permissions for Linux-in-WSL. | ||
const leafName = context.platform === 'win32' ? 'nerdctl.exe' : 'nerdctl-stub'; | ||
|
||
return path.join(context.resourcesDir, context.platform, 'bin', leafName); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
import crypto from 'crypto'; | ||
import fs from 'fs'; | ||
import os from 'os'; | ||
import path from 'path'; | ||
import stream from 'stream'; | ||
|
||
import tar from 'tar-stream'; | ||
|
||
import { AlpineLimaISOVersion, Dependency, DownloadContext } from 'scripts/lib/dependencies'; | ||
|
||
export class ExtensionProxyImage implements Dependency { | ||
name = 'rdx-proxy.tar'; | ||
dependencies(context: DownloadContext) { | ||
return [`extension-proxy:linux`]; | ||
} | ||
|
||
async download(context: DownloadContext): Promise<void> { | ||
// Build the extension proxy image. | ||
const workDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'rd-build-rdx-pf-')); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, because they throw exceptions (which will fall over correctly). |
||
|
||
try { | ||
const executablePath = path.join(context.resourcesDir, 'linux', 'staging', 'extension-proxy'); | ||
const layerPath = path.join(workDir, 'layer.tar'); | ||
const imagePath = path.join(context.resourcesDir, 'rdx-proxy.tar'); | ||
|
||
console.log('Building RDX proxying image...'); | ||
|
||
// Build the layer tarball | ||
// tar streams don't implement piping to multiple writers, and stream.Duplex | ||
// can't deal with it either; so we need to fully write out the file, then | ||
// calculate the hash as a separate step. | ||
const layer = tar.pack(); | ||
const layerOutput = layer.pipe(fs.createWriteStream(layerPath)); | ||
const executableStats = await fs.promises.stat(executablePath); | ||
|
||
await stream.promises.finished( | ||
fs.createReadStream(executablePath) | ||
.pipe(layer.entry({ | ||
name: path.basename(executablePath), | ||
mode: 0o755, | ||
type: 'file', | ||
mtime: new Date(0), | ||
size: executableStats.size, | ||
}))); | ||
layer.finalize(); | ||
await stream.promises.finished(layerOutput); | ||
|
||
// calculate the hash | ||
const layerReader = fs.createReadStream(layerPath); | ||
const layerHasher = layerReader.pipe(crypto.createHash('sha256')); | ||
|
||
await stream.promises.finished(layerReader); | ||
|
||
// Build the image tarball | ||
const layerHash = layerHasher.digest().toString('hex'); | ||
const image = tar.pack(); | ||
const imageStream = fs.createWriteStream(imagePath); | ||
const imageWritten = stream.promises.finished(imageStream); | ||
|
||
image.pipe(imageStream); | ||
const addEntry = (name: string, input: Buffer | stream.Readable, size?: number) => { | ||
if (Buffer.isBuffer(input)) { | ||
size = input.length; | ||
input = stream.Readable.from(input); | ||
} | ||
|
||
return stream.promises.finished((input as stream.Readable).pipe(image.entry({ | ||
name, | ||
size, | ||
type: 'file', | ||
mtime: new Date(0), | ||
}))); | ||
}; | ||
|
||
image.entry({ name: layerHash, type: 'directory' }); | ||
await addEntry(`${ layerHash }/VERSION`, Buffer.from('1.0')); | ||
await addEntry(`${ layerHash }/layer.tar`, fs.createReadStream(layerPath), layerOutput.bytesWritten); | ||
await addEntry(`${ layerHash }/json`, Buffer.from(JSON.stringify({ | ||
id: layerHash, | ||
config: { | ||
ExposedPorts: { '80/tcp': {} }, | ||
WorkingDir: '/', | ||
Entrypoint: [`/${ path.basename(executablePath) }`], | ||
}, | ||
}))); | ||
await addEntry(`${ layerHash }.json`, Buffer.from(JSON.stringify({ | ||
architecture: context.isM1 ? 'arm64' : 'amd64', | ||
config: { | ||
ExposedPorts: { '80/tcp': {} }, | ||
Entrypoint: [`/${ path.basename(executablePath) }`], | ||
WorkingDir: '/', | ||
}, | ||
history: [], | ||
os: 'linux', | ||
rootfs: { | ||
type: 'layers', | ||
diff_ids: [`sha256:${ layerHash }`], | ||
}, | ||
}))); | ||
await addEntry('manifest.json', Buffer.from(JSON.stringify([ | ||
{ | ||
Config: `${ layerHash }.json`, | ||
RepoTags: ['ghcr.io/rancher-sandbox/rancher-desktop/rdx-proxy:latest'], | ||
Layers: [`${ layerHash }/layer.tar`], | ||
}, | ||
]))); | ||
image.finalize(); | ||
await imageWritten; | ||
console.log('Built RDX port proxy image'); | ||
} finally { | ||
await fs.promises.rm(workDir, { recursive: true }); | ||
} | ||
} | ||
|
||
getAvailableVersions(includePrerelease?: boolean | undefined): Promise<string[] | AlpineLimaISOVersion[]> { | ||
throw new Error('extension-proxy does not have versions.'); | ||
} | ||
|
||
rcompareVersions(version1: string | AlpineLimaISOVersion, version2: string | AlpineLimaISOVersion): 0 | 1 | -1 { | ||
throw new Error('extension-proxy does not have versions.'); | ||
} | ||
} | ||
|
||
export class WSLDistroImage implements Dependency { | ||
name = 'WSLDistroImage'; | ||
dependencies(context: DownloadContext): string[] { | ||
return ['WSLDistro:win32', 'guestagent:linux']; | ||
} | ||
|
||
async download(context: DownloadContext): Promise<void> { | ||
const tarName = `distro-${ context.versions.WSLDistro }.tar`; | ||
const pristinePath = path.join(context.resourcesDir, context.platform, 'staging', tarName); | ||
const pristineFile = fs.createReadStream(pristinePath); | ||
const extractor = tar.extract(); | ||
const destPath = path.join(context.resourcesDir, context.platform, tarName); | ||
const destFile = fs.createWriteStream(destPath); | ||
const packer = tar.pack(); | ||
|
||
console.log('Building WSLDistro image...'); | ||
|
||
// Copy the pristine tar file to the destination. | ||
packer.pipe(destFile); | ||
extractor.on('entry', (header, stream, callback) => { | ||
stream.pipe(packer.entry(header, callback)); | ||
}); | ||
await stream.promises.finished(pristineFile.pipe(extractor)); | ||
|
||
async function addFile(fromPath: string, name: string, options: Omit<tar.Headers, 'name' | 'size'> = {}) { | ||
const { size } = await fs.promises.stat(fromPath); | ||
const inputFile = fs.createReadStream(fromPath); | ||
|
||
console.log(`WSL Distro: Adding ${ fromPath } to ${ name }...`); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit; I noticed on line 172 it is spelled |
||
await stream.promises.finished(inputFile.pipe(packer.entry({ | ||
name, | ||
size, | ||
mode: 0o755, | ||
type: 'file', | ||
mtime: new Date(0), | ||
...options, | ||
}))); | ||
} | ||
|
||
// Add extra files. | ||
await addFile(path.join(context.resourcesDir, 'linux', 'staging', 'guestagent'), | ||
'usr/local/bin/rancher-desktop-guestagent'); | ||
await addFile(path.join(context.resourcesDir, 'linux', 'staging', 'trivy'), | ||
'usr/local/bin/trivy'); | ||
|
||
// Finish the archive. | ||
packer.finalize(); | ||
await stream.promises.finished(packer); | ||
console.log('Built WSLDistro image.'); | ||
} | ||
|
||
getAvailableVersions(includePrerelease?: boolean | undefined): Promise<string[] | AlpineLimaISOVersion[]> { | ||
throw new Error('WSLDistroImage does not have versions.'); | ||
} | ||
|
||
rcompareVersions(version1: string | AlpineLimaISOVersion, version2: string | AlpineLimaISOVersion): 0 | 1 | -1 { | ||
throw new Error('WSLDistroImage does not have versions.'); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to handle any potential errors here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any errors that return a non-zero exit code would throw an exception here, so it should be fine.