Skip to content

Commit

Permalink
feat: add AWS uploading image
Browse files Browse the repository at this point in the history
### What does this PR do?

* Adds the ability to upload to AWS through build
* Toggled advanced features section

### Screenshot / video of UI

<!-- If this PR is changing UI, please include
screenshots or screencasts showing the difference -->

### What issues does this PR fix or reference?

<!-- Include any related issues from Podman Desktop
repository (or from another issue tracker). -->

Closes #487

### How to test this PR?

<!-- Please explain steps to reproduce -->

1. Add your credentials to ~/.aws/credentials
2. Build your image + provide your bucket, aminame, etc.

Signed-off-by: Charlie Drage <[email protected]>
  • Loading branch information
cdrage committed Jun 26, 2024
1 parent 2525c27 commit 1ce09e7
Show file tree
Hide file tree
Showing 5 changed files with 187 additions and 3 deletions.
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: 70 additions & 1 deletion packages/frontend/src/Build.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
<script lang="ts">
import './app.css';
import { faCheck, faCube, faQuestionCircle, faTriangleExclamation } from '@fortawesome/free-solid-svg-icons';
import {
faCaretDown,
faCaretRight,
faCheck,
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';
Expand All @@ -9,6 +16,7 @@ import type { ImageInfo, ManifestInspectInfo } from '@podman-desktop/api';
import { router } from 'tinro';
import DiskImageIcon from './lib/DiskImageIcon.svelte';
import { Button, Input, EmptyScreen, FormPage } from '@podman-desktop/ui-svelte';
import Link from './lib/Link.svelte';
export let imageName: string | undefined = undefined;
export let imageTag: string | undefined = undefined;
Expand Down Expand Up @@ -44,6 +52,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 +189,9 @@ async function buildBootcImage() {
type: buildType,
arch: buildArch,
filesystem: buildFilesystem,
awsAmiName: awsAmiName,
awsBucket: awsBucket,
awsRegion: awsRegion,
};
buildInProgress = true;
Expand Down Expand Up @@ -648,6 +670,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';

0 comments on commit 1ce09e7

Please sign in to comment.