Skip to content
Draft
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
58 changes: 54 additions & 4 deletions src/devfile/applyCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ import * as fs from 'fs/promises';
import * as yaml from 'js-yaml';
import * as path from 'path';
import { window } from 'vscode';
import { CommandText } from '../base/command';
import { DownloadUtil } from '../downloadUtil/download';
import { Oc } from '../oc/ocWrapper';
import { Apply, DeployedResource } from '../odo/componentTypeDescription';
import { ComponentWorkspaceFolder } from '../odo/workspace';
import { ContainerRuntimeDetector } from '../util/containerRuntime';
import { OpenShiftTerminalManager } from '../webview/openshift-terminal/openShiftTerminal';
import { VariableResolver } from './variableResolver';

export class ApplyCommandExecutor {
Expand Down Expand Up @@ -51,10 +54,11 @@ export class ApplyCommandExecutor {
);
}
if ((component as any).image) {
// Image build component - skip for now (requires podman/docker/buildah)
// TODO: Implement image building in future PR
// Silently skip image builds - they require container runtime integration
return []; // No resources deployed for image builds
return await this.buildImageComponent(
(component as any).image,
path.dirname(devfilePath),
componentFolder,
);
}
throw new Error(
`Component '${resolvedApply.component}' is not a kubernetes, openshift, or image component`,
Expand Down Expand Up @@ -139,6 +143,52 @@ export class ApplyCommandExecutor {
}
}

private static async buildImageComponent(
imageComponent: any,
devfileDir: string,
componentFolder: ComponentWorkspaceFolder,
): Promise<DeployedResource[]> {
const runtime = await ContainerRuntimeDetector.detectBuildRuntime();
if (!runtime) {
throw new Error(
'No container runtime found. Install podman, docker, or buildah to build images.',
);
}

const imageName = imageComponent.imageName;
const dockerfilePath = imageComponent.dockerfile?.uri || 'Dockerfile';
const buildContext = imageComponent.dockerfile?.buildContext || '.';

// Resolve paths relative to devfile directory
const resolvedDockerfile = path.isAbsolute(dockerfilePath)
? dockerfilePath
: path.join(devfileDir, dockerfilePath);
const resolvedContext = path.isAbsolute(buildContext)
? buildContext
: devfileDir;

const buildArgs = ContainerRuntimeDetector.getBuildCommand(
runtime, imageName, resolvedDockerfile, resolvedContext,
);

// Run build in terminal - allows interactive auth for registry push
const command = new CommandText(runtime, buildArgs.slice(runtime.length + 1));

return new Promise<DeployedResource[]>((resolve, reject) => {
void OpenShiftTerminalManager.getInstance().createTerminal(
command,
`Build: ${imageName}`,
componentFolder.contextPath,
process.env,
{
onExit() {
resolve([]);
},
},
).catch(reject);
});
}

private static parseDeployedResources(manifestContent: string): DeployedResource[] {
const resources: DeployedResource[] = [];
const timestamp = new Date().toISOString();
Expand Down
128 changes: 128 additions & 0 deletions src/util/containerRuntime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*-----------------------------------------------------------------------------------------------
* Copyright (c) Red Hat, Inc. All rights reserved.
* Licensed under the MIT License. See LICENSE file in the project root for license information.
*-----------------------------------------------------------------------------------------------*/

import { platform } from 'os';
import { which } from 'shelljs';
import { ChildProcessUtil, CliExitData } from './childProcessUtil';

export type ContainerRuntime = 'podman' | 'docker' | 'buildah';

/**
* Utility for detecting and validating container runtimes (podman, docker, buildah).
*/
export class ContainerRuntimeDetector {

/**
* Detect available container runtime with preference order: podman > docker > buildah
*
* @returns The detected runtime or null if none available
*/
public static async detectBuildRuntime(): Promise<ContainerRuntime | null> {
// Try podman first (preferred for rootless builds)
if (await this.isPodmanAvailable()) {
return 'podman';
}

// Try docker second (widely available)
if (await this.isDockerAvailable()) {
return 'docker';
}

// Try buildah third (OCI-compliant, rootless)
if (await this.isBuildahAvailable()) {
return 'buildah';
}

return null;
}

/**
* Check if podman is available and properly configured.
*/
public static async isPodmanAvailable(): Promise<boolean> {
const podmanPath = which('podman');
if (!podmanPath) {
return false;
}

// On Linux, podman works natively
if (platform() === 'linux') {
return true;
}

// On macOS/Windows, check if podman machine is running
try {
const result: CliExitData = await ChildProcessUtil.Instance.execute(
`"${podmanPath}" machine list --format json`
);
const machines: { Running: boolean }[] = JSON.parse(result.stdout);
return machines.length > 0 && machines.some(m => m.Running);
} catch {
return false;
}
}

/**
* Check if docker is available.
*/
public static async isDockerAvailable(): Promise<boolean> {
const dockerPath = which('docker');
if (!dockerPath) {
return false;
}

// Verify docker daemon is accessible
try {
await ChildProcessUtil.Instance.execute(`"${dockerPath}" info`);
return true;
} catch {
return false;
}
}

/**
* Check if buildah is available.
*/
public static async isBuildahAvailable(): Promise<boolean> {
const buildahPath = which('buildah');
if (!buildahPath) {
return false;
}

// Verify buildah works
try {
await ChildProcessUtil.Instance.execute(`"${buildahPath}" version`);
return true;
} catch {
return false;
}
}

/**
* Get the build command for the specified runtime.
*
* @param runtime The container runtime to use
* @param imageName The image name/tag
* @param dockerfilePath Path to Dockerfile (relative to build context)
* @param buildContext Build context path
* @returns The build command string
*/
public static getBuildCommand(
runtime: ContainerRuntime,
imageName: string,
dockerfilePath: string,
buildContext: string
): string {
switch (runtime) {
case 'podman':
case 'docker':
return `${runtime} build -t ${imageName} -f ${dockerfilePath} ${buildContext}`;
case 'buildah':
return `buildah bud -t ${imageName} -f ${dockerfilePath} ${buildContext}`;
default:
throw new Error(`Unsupported container runtime: ${runtime as string}`);
}
}
}
Loading
Loading