Skip to content
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

feat: add AWS uploading image #579

Merged
merged 1 commit into from
Jun 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 63 additions & 1 deletion packages/backend/src/build-disk-image.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
***********************************************************************/

import { beforeEach, expect, test, vi } from 'vitest';
import os from 'node:os';
import {
buildExists,
createBuilderImageOptions,
Expand All @@ -29,7 +30,7 @@ import type { ContainerInfo, Configuration } from '@podman-desktop/api';
import { containerEngine } from '@podman-desktop/api';
import type { BootcBuildInfo } from '/@shared/src/models/bootc';
import * as fs from 'node:fs';
import { resolve } from 'node:path';
import path, { resolve } from 'node:path';

const configurationGetConfigurationMock = vi.fn();

Expand Down Expand Up @@ -311,3 +312,64 @@ test('create podman run command', async () => {

expect(command).toEqual(expectedCommand);
});

test('expect aws options to be included in the command for volume and paramters', async () => {
const name = 'test123-bootc-image-builder';
const build = {
image: 'test-image',
tag: 'latest',
type: ['raw'],
arch: 'amd64',
folder: '/Users/cdrage/bootc/qemutest4',
awsBucket: 'test-bucket',
awsRegion: 'us-west-2',
awsAmiName: 'test-ami',
} as BootcBuildInfo;

const options = createBuilderImageOptions(name, build);

expect(options).toBeDefined();
expect(options.HostConfig).toBeDefined();
expect(options.HostConfig?.Binds).toBeDefined();
if (options.HostConfig?.Binds) {
expect(options.HostConfig.Binds[0]).toEqual(build.folder + ':/output/');
expect(options.HostConfig.Binds[1]).toEqual('/var/lib/containers/storage:/var/lib/containers/storage');
expect(options.HostConfig.Binds[2]).toEqual(path.join(os.homedir(), '.aws') + ':/root/.aws:ro');
}

// Check that the aws options are included in the command
expect(options.Cmd).toContain('--aws-bucket');
expect(options.Cmd).toContain(build.awsBucket);
expect(options.Cmd).toContain('--aws-region');
expect(options.Cmd).toContain(build.awsRegion);
expect(options.Cmd).toContain('--aws-ami-name');
expect(options.Cmd).toContain(build.awsAmiName);
});

test('test that if aws options are not provided, they are NOT included in the command', async () => {
const name = 'test123-bootc-image-builder';
const build = {
image: 'test-image',
tag: 'latest',
type: ['raw'],
arch: 'amd64',
folder: '/Users/cdrage/bootc/qemutest4',
} as BootcBuildInfo;

const options = createBuilderImageOptions(name, build);

expect(options).toBeDefined();
expect(options.HostConfig).toBeDefined();
expect(options.HostConfig?.Binds).toBeDefined();
if (options.HostConfig?.Binds) {
// Expect the length to ONLY be two. The first bind is the output folder, the second is the storage folder
expect(options.HostConfig.Binds.length).toEqual(2);
expect(options.HostConfig.Binds[0]).toEqual(build.folder + ':/output/');
expect(options.HostConfig.Binds[1]).toEqual('/var/lib/containers/storage:/var/lib/containers/storage');
}

// Check that the aws options are NOT included in the command
expect(options.Cmd).not.toContain('--aws-bucket');
expect(options.Cmd).not.toContain('--aws-region');
expect(options.Cmd).not.toContain('--aws-ami-name');
});
25 changes: 24 additions & 1 deletion packages/backend/src/build-disk-image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
import type { ContainerCreateOptions } from '@podman-desktop/api';
import * as extensionApi from '@podman-desktop/api';
import * as fs from 'node:fs';
import { resolve } from 'node:path';
import path, { resolve } from 'node:path';
import os from 'node:os';
import * as containerUtils from './container-utils';
import { bootcImageBuilder, bootcImageBuilderCentos, bootcImageBuilderRHEL } from './constants';
import type { BootcBuildInfo, BuildType } from '/@shared/src/models/bootc';
Expand Down Expand Up @@ -75,6 +76,17 @@ export async function buildDiskImage(build: BootcBuildInfo, history: History, ov
}
}

// If one of awsAmiName, awsBucket, or awsRegion is defined, all three must be defined
if (
(build.awsAmiName && !build.awsBucket) ??
(!build.awsAmiName && build.awsBucket) ??
(!build.awsAmiName && build.awsBucket && build.awsRegion)
) {
const response = 'If you are using AWS, you must provide an AMI name, bucket, and region.';
await extensionApi.window.showErrorMessage(response);
throw new Error(response);
}

// Use build.type to check for existing files
if (
!overwrite &&
Expand Down Expand Up @@ -359,6 +371,17 @@ export function createBuilderImageOptions(
Cmd: cmd,
};

// If awsAmiName, awsBucket, and awsRegion are defined. We will add the mounted volume
// of the OS homedir & the .aws directory to the container.
if (build.awsAmiName && build.awsBucket && build.awsRegion) {
// Add the commands to the container, --aws-ami-name, --aws-bucket, --aws-region
cmd.push('--aws-ami-name', build.awsAmiName, '--aws-bucket', build.awsBucket, '--aws-region', build.awsRegion);

if (options.HostConfig?.Binds) {
options?.HostConfig?.Binds.push(path.join(os.homedir(), '.aws') + ':/root/.aws:ro');
}
}

return options;
}

Expand Down
27 changes: 27 additions & 0 deletions packages/frontend/src/Build.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -696,3 +696,30 @@ test('if a manifest is created that has the label "6.8.9-300.fc40.aarch64" in as
// expect it to be selected
expect(xfsRadio.classList.contains('bg-purple-500'));
});

test('collapse and uncollapse of advanced options', async () => {
await waitRender();
const advancedOptions = screen.getByLabelText('advanced-options');
expect(advancedOptions).toBeDefined();

// expect the input labels to be hidden on load
const amiName = screen.queryByRole('label', { name: 'AMI Name' });
expect(amiName).toBeNull();
const amiBucket = screen.queryByRole('label', { name: 'S3 Bucket' });
expect(amiBucket).toBeNull();
const amiRegion = screen.queryByRole('label', { name: 'S3 Region' });
expect(amiRegion).toBeNull();

// Click on the Advanced Options span
advancedOptions.click();

// expect the label "AMI Name" to be shown
const amiName2 = screen.queryByRole('label', { name: 'AMI Name' });
expect(amiName2).toBeDefined();
// expect the label "S3 Bucket" to be shown
const amiBucket2 = screen.queryByRole('label', { name: 'S3 Bucket' });
expect(amiBucket2).toBeDefined();
// expect the label "S3 Region" to be shown
const amiRegion2 = screen.queryByRole('label', { name: 'S3 Region' });
expect(amiRegion2).toBeDefined();
});
71 changes: 69 additions & 2 deletions packages/frontend/src/Build.svelte
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
<script lang="ts">
import './app.css';
import { faCube, faQuestionCircle, faTriangleExclamation } from '@fortawesome/free-solid-svg-icons';
import {
faCaretDown,
faCaretRight,
faCube,
faQuestionCircle,
faTriangleExclamation,
} from '@fortawesome/free-solid-svg-icons';
import { bootcClient } from './api/client';
import type { BootcBuildInfo, BuildType } from '/@shared/src/models/bootc';
import Fa from 'svelte-fa';
import { onMount } from 'svelte';
import type { ImageInfo, ManifestInspectInfo } from '@podman-desktop/api';
import { router } from 'tinro';
import DiskImageIcon from './lib/DiskImageIcon.svelte';
import { Button, Input, EmptyScreen, FormPage, Checkbox } from '@podman-desktop/ui-svelte';
import { Button, Input, EmptyScreen, FormPage, Checkbox, Link } from '@podman-desktop/ui-svelte';

export let imageName: string | undefined = undefined;
export let imageTag: string | undefined = undefined;
Expand Down Expand Up @@ -44,6 +50,17 @@ let errorFormValidation: string | undefined = undefined;
// this boolean will be set to true if the selected image is Fedora and shown as a warning to the user.
let fedoraDetected = false;

// AWS Related
let awsAmiName: string = '';
let awsBucket: string = '';
let awsRegion: string = '';

// Show/hide advanced options
let showAdvanced = false; // State to show/hide advanced options
function toggleAdvanced() {
showAdvanced = !showAdvanced;
}

function findImage(repoTag: string): ImageInfo | undefined {
return bootcAvailableImages.find(
image => image.RepoTags && image.RepoTags.length > 0 && image.RepoTags[0] === repoTag,
Expand Down Expand Up @@ -170,6 +187,9 @@ async function buildBootcImage() {
type: buildType,
arch: buildArch,
filesystem: buildFilesystem,
awsAmiName: awsAmiName,
awsBucket: awsBucket,
awsRegion: awsRegion,
};

buildInProgress = true;
Expand Down Expand Up @@ -595,6 +615,53 @@ export function goToHomePage(): void {
within the image or manifest.
</p>
</div>
<div class="mb-2">
<!-- Use a span for this until we have a "dropdown toggle" UI element implemented. -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<span
class="text-md font-semibold mb-2 block cursor-pointer"
aria-label="advanced-options"
on:click="{toggleAdvanced}"
><Fa icon="{showAdvanced ? faCaretDown : faCaretRight}" class="inline-block mr-1" /> Advanced Options
</span>
{#if showAdvanced}
<div>
<span class="text-sm font-semibold mb-2 block">Upload image to AWS</span>
</div>

<label for="amiName" class="block mb-2 text-sm font-bold text-gray-400">AMI Name</label>
<Input
bind:value="{awsAmiName}"
name="amiName"
id="amiName"
placeholder="AMI Name to be used"
class="w-full" />

<label for="awsBucket" class="block mb-2 text-sm font-bold text-gray-400">S3 Bucket</label>
<Input
bind:value="{awsBucket}"
name="awsBucket"
id="awsBucket"
placeholder="AWS S3 bucket"
class="w-full" />

<label for="awsRegion" class="block mb-2 text-sm font-bold text-gray-400">S3 Region</label>
<Input
bind:value="{awsRegion}"
name="awsRegion"
id="awsRegion"
placeholder="AWS S3 region"
class="w-full" />

<p class="text-gray-300 text-xs pt-2">
This will upload the image to a specific AWS S3 bucket. Credentials stored at ~/.aws/credentials will
be used for uploading. You must have <Link
externalRef="https://docs.aws.amazon.com/vm-import/latest/userguide/required-permissions.html"
>vmimport service role</Link> configured to upload to the bucket.
</p>
{/if}
</div>
</div>
</div>
{#if existingBuild}
Expand Down
3 changes: 3 additions & 0 deletions packages/shared/src/models/bootc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ export interface BootcBuildInfo {
status?: BootcBuildStatus;
timestamp?: string;
buildContainerId?: string; // The image ID that is used to build the image
awsAmiName?: string;
awsBucket?: string;
awsRegion?: string;
}

export type BootcBuildStatus = 'running' | 'creating' | 'success' | 'error' | 'lost' | 'deleting';