diff --git a/package.json b/package.json index 6b17f8cd9..b5f59cde1 100644 --- a/package.json +++ b/package.json @@ -324,6 +324,10 @@ "command": "aks.aksKaito", "title": "Install KAITO" }, + { + "command": "aks.aksDraftValidate", + "title": "Run Draft Validate for YAML" + }, { "command": "aks.aksKaitoTest", "title": "Test KAITO models" @@ -356,6 +360,14 @@ { "command": "aks.draftDeployment", "when": "explorerResourceIsFolder" + }, + { + "command": "aks.aksDraftValidate", + "when": "resourceExtname == .yaml || resourceExtname == .yml" + }, + { + "command": "aks.aksDraftValidate", + "when": "explorerResourceIsFolder" } ], "view/item/context": [ diff --git a/src/commands/draft/draftCommands.ts b/src/commands/draft/draftCommands.ts index 4a29d49ab..c56d56e68 100644 --- a/src/commands/draft/draftCommands.ts +++ b/src/commands/draft/draftCommands.ts @@ -10,6 +10,7 @@ import { FileType, } from "vscode"; import { DraftDockerfileDataProvider, DraftDockerfilePanel } from "../../panels/draft/DraftDockerfilePanel"; +import { DraftValidateDataProvider, DraftValidatePanel } from "../../panels/draft/DraftValidatePanel"; import { getExtension } from "../utils/host"; import { Errorable, failed, getErrorMessage, succeeded } from "../utils/errorable"; import { getDraftBinaryPath } from "../utils/helper/draftBinaryDownload"; @@ -169,6 +170,19 @@ export async function draftWorkflow(_context: IActionContext, target: unknown): panel.show(dataProvider); } +export async function draftValidate(_context: IActionContext, target: unknown): Promise { + const params = getDraftDockerfileParams(target); + const commonDependencies = await getCommonDraftDependencies(params?.workspaceFolder); + if (commonDependencies === null) { + return; + } + + const { workspaceFolder, extension, draftBinaryPath } = commonDependencies; + const panel = new DraftValidatePanel(extension.extensionUri); + const dataProvider = new DraftValidateDataProvider(workspaceFolder, draftBinaryPath, params?.initialLocation || ""); + panel.show(dataProvider); +} + async function getCommonDraftDependencies( suppliedWorkspaceFolder?: WorkspaceFolder, ): Promise { diff --git a/src/commands/draft/types.ts b/src/commands/draft/types.ts index 4c1e54787..10ec25f5b 100644 --- a/src/commands/draft/types.ts +++ b/src/commands/draft/types.ts @@ -16,6 +16,10 @@ export type DraftCommandParamsTypes = { workspaceFolder?: WorkspaceFolder; initialSelection?: WorkflowInitialSelection; }; + "aks.draftValidate": { + workspaceFolder?: WorkspaceFolder; + initialLocation?: string; + }; }; export type DraftCommandName = keyof DraftCommandParamsTypes; diff --git a/src/extension.ts b/src/extension.ts index 4350b6b59..20dee3048 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -40,7 +40,7 @@ import { aksNodeHealth, aksStorageDiagnostics, } from "./commands/detectors/detectors"; -import { draftDeployment, draftDockerfile, draftWorkflow } from "./commands/draft/draftCommands"; +import { draftValidate, draftDeployment, draftDockerfile, draftWorkflow } from "./commands/draft/draftCommands"; import periscope from "./commands/periscope/periscope"; import refreshSubscription from "./commands/refreshSubscriptions"; import { getKubeconfigYaml, getManagedCluster } from "./commands/utils/clusters"; @@ -72,7 +72,6 @@ export async function activate(context: vscode.ExtensionContext) { outputChannel: createAzExtOutputChannel("Azure Identity", ""), prefix: "", }; - context.subscriptions.push(uiExtensionVariables.outputChannel); registerUIExtensionVariables(uiExtensionVariables); @@ -118,6 +117,7 @@ export async function activate(context: vscode.ExtensionContext) { registerCommandWithTelemetry("aks.aksDeployManifest", aksDeployManifest); registerCommandWithTelemetry("aks.aksOpenKubectlPanel", aksOpenKubectlPanel); registerCommandWithTelemetry("aks.getAzureKubernetesServicePlugins", getPlugins); + registerCommandWithTelemetry("aks.aksDraftValidate", draftValidate); await registerAzureServiceNodes(context); diff --git a/src/panels/draft/DraftValidatePanel.ts b/src/panels/draft/DraftValidatePanel.ts new file mode 100644 index 000000000..c5bcb0fa6 --- /dev/null +++ b/src/panels/draft/DraftValidatePanel.ts @@ -0,0 +1,73 @@ +import { Uri, WorkspaceFolder, window } from "vscode"; +import path from "path"; +import { BasePanel, PanelDataProvider } from "../BasePanel"; +import { + InitialState, + ToVsCodeMsgDef, + ToWebViewMsgDef, +} from "../../webview-contract/webviewDefinitions/draft/draftValidate"; +import { TelemetryDefinition } from "../../webview-contract/webviewTypes"; +import { MessageHandler, MessageSink } from "../../webview-contract/messaging"; +import { ShellOptions, exec } from "../../commands/utils/shell"; +import { failed } from "../../commands/utils/errorable"; + +export class DraftValidatePanel extends BasePanel<"draftValidate"> { + constructor(extensionUri: Uri) { + super(extensionUri, "draftValidate", { + validationResult: undefined, + }); + } +} + +export class DraftValidateDataProvider implements PanelDataProvider<"draftValidate"> { + readonly draftDirectory: string; + constructor( + readonly workspaceFolder: WorkspaceFolder, + readonly draftBinaryPath: string, + readonly initialLocation: string, + ) { + this.draftDirectory = path.dirname(draftBinaryPath); + } + + getTitle(): string { + return `Draft Validate in ${this.workspaceFolder.name}`; + } + + getInitialState(): InitialState { + return { + validationResults: "Initializing validation...", + }; + } + + getTelemetryDefinition(): TelemetryDefinition<"draftValidate"> { + return { + createDraftValidateRequest: true, + }; + } + + //Messages from Webview to Vscode + getMessageHandler(webview: MessageSink): MessageHandler { + return { + createDraftValidateRequest: () => this.handleDraftValidateRequest(webview), + }; + } + + private async handleDraftValidateRequest(webview: MessageSink) { + const command = `draft validate --manifest .${path.sep}${this.initialLocation}`; + + const execOptions: ShellOptions = { + workingDir: this.workspaceFolder.uri.fsPath, + envPaths: [this.draftDirectory], + }; + + const draftResult = await exec(command, execOptions); + if (failed(draftResult)) { + window.showErrorMessage(draftResult.error); + return; + } + + const validationResults = draftResult.result.stdout; + + webview.postValidationResult({ result: validationResults }); + } +} diff --git a/src/webview-contract/webviewDefinitions/draft/draftValidate.ts b/src/webview-contract/webviewDefinitions/draft/draftValidate.ts new file mode 100644 index 000000000..86e8225c5 --- /dev/null +++ b/src/webview-contract/webviewDefinitions/draft/draftValidate.ts @@ -0,0 +1,17 @@ +import { WebviewDefinition } from "../../webviewTypes"; + +export interface InitialState { + validationResults: string; +} + +export type ExistingFiles = string[]; + +export type ToVsCodeMsgDef = { + createDraftValidateRequest: string; +}; + +export type ToWebViewMsgDef = { + validationResult: { result: string }; +}; + +export type DraftValidateDefinition = WebviewDefinition; diff --git a/src/webview-contract/webviewTypes.ts b/src/webview-contract/webviewTypes.ts index a515af8ac..86e789f2c 100644 --- a/src/webview-contract/webviewTypes.ts +++ b/src/webview-contract/webviewTypes.ts @@ -7,6 +7,7 @@ import { DetectorDefinition } from "./webviewDefinitions/detector"; import { DraftDeploymentDefinition } from "./webviewDefinitions/draft/draftDeployment"; import { DraftDockerfileDefinition } from "./webviewDefinitions/draft/draftDockerfile"; import { DraftWorkflowDefinition } from "./webviewDefinitions/draft/draftWorkflow"; +import { DraftValidateDefinition } from "./webviewDefinitions/draft/draftValidate"; import { InspektorGadgetDefinition } from "./webviewDefinitions/inspektorGadget"; import { KaitoDefinition } from "./webviewDefinitions/kaito"; import { KaitoModelsDefinition } from "./webviewDefinitions/kaitoModels"; @@ -45,6 +46,7 @@ type AllWebviewDefinitions = { draftDeployment: DraftDeploymentDefinition; draftDockerfile: DraftDockerfileDefinition; draftWorkflow: DraftWorkflowDefinition; + draftValidate: DraftValidateDefinition; gadget: InspektorGadgetDefinition; kubectl: KubectlDefinition; aso: ASODefinition; diff --git a/webview-ui/src/Draft/DraftValidate/DraftValidate.tsx b/webview-ui/src/Draft/DraftValidate/DraftValidate.tsx new file mode 100644 index 000000000..5c8f8f391 --- /dev/null +++ b/webview-ui/src/Draft/DraftValidate/DraftValidate.tsx @@ -0,0 +1,20 @@ +import { InitialState } from "../../../../src/webview-contract/webviewDefinitions/draft/draftValidate"; +import { useStateManagement } from "../../utilities/state"; +import { stateUpdater, vscode } from "./state"; +import { useEffect } from "react"; + +export function DraftValidate(initialState: InitialState) { + const { state } = useStateManagement(stateUpdater, initialState, vscode); + + //Request the validation results from the backend once when the component is mounted. + useEffect(() => { + vscode.postCreateDraftValidateRequest(""); + }, []); + + return ( + <> +

Draft Validate

+
{state.validationResults}
+ + ); +} diff --git a/webview-ui/src/Draft/DraftValidate/state.ts b/webview-ui/src/Draft/DraftValidate/state.ts new file mode 100644 index 000000000..368fd88d6 --- /dev/null +++ b/webview-ui/src/Draft/DraftValidate/state.ts @@ -0,0 +1,33 @@ +import { WebviewStateUpdater } from "../../utilities/state"; +import { getWebviewMessageContext } from "../../utilities/vscode"; + +export type EventDef = { + //Defines the events that can originate from the webview and be sent to the backend (ToVsCodeMsgDef). + draftValidateRequest: string; +}; + +export type DraftValidateState = { + validationResults: string; +}; + +export const stateUpdater: WebviewStateUpdater<"draftValidate", EventDef, DraftValidateState> = { + createState: (initialState) => ({ + validationResults: initialState.validationResults, + }), + vscodeMessageHandler: { + // This handler updates the state when a message from the extension + validationResult: (state, response) => ({ + ...state, + validationResults: response.result, + }), + }, + eventHandler: { + draftValidateRequest: (state) => ({ + ...state, + }), + }, +}; + +export const vscode = getWebviewMessageContext<"draftValidate">({ + createDraftValidateRequest: null, +}); diff --git a/webview-ui/src/Draft/index.ts b/webview-ui/src/Draft/index.ts index fe22093a4..a45c3e02a 100644 --- a/webview-ui/src/Draft/index.ts +++ b/webview-ui/src/Draft/index.ts @@ -1,5 +1,6 @@ import { DraftDockerfile } from "./DraftDockerfile/DraftDockerfile"; import { DraftDeployment } from "./DraftDeployment/DraftDeployment"; import { DraftWorkflow } from "./DraftWorkflow/DraftWorkflow"; +import { DraftValidate } from "./DraftValidate/DraftValidate"; -export { DraftDockerfile, DraftDeployment, DraftWorkflow }; +export { DraftDockerfile, DraftDeployment, DraftWorkflow, DraftValidate }; diff --git a/webview-ui/src/main.tsx b/webview-ui/src/main.tsx index 1a4e8df17..677a53d3f 100644 --- a/webview-ui/src/main.tsx +++ b/webview-ui/src/main.tsx @@ -8,7 +8,7 @@ import { AzureServiceOperator } from "./AzureServiceOperator/AzureServiceOperato import { ClusterProperties } from "./ClusterProperties/ClusterProperties"; import { CreateCluster } from "./CreateCluster/CreateCluster"; import { Detector } from "./Detector/Detector"; -import { DraftDeployment, DraftDockerfile, DraftWorkflow } from "./Draft"; +import { DraftDeployment, DraftDockerfile, DraftWorkflow, DraftValidate } from "./Draft"; import { InspektorGadget } from "./InspektorGadget/InspektorGadget"; import { Kaito } from "./Kaito/Kaito"; import { KaitoModels } from "./KaitoModels/KaitoModels"; @@ -58,6 +58,7 @@ function getVsCodeContent(): JSX.Element { draftDeployment: () => , draftDockerfile: () => , draftWorkflow: () => , + draftValidate: () => , gadget: () => , kubectl: () => , aso: () => , diff --git a/webview-ui/src/manualTest/main.tsx b/webview-ui/src/manualTest/main.tsx index 1712ccbe8..91f4648cb 100644 --- a/webview-ui/src/manualTest/main.tsx +++ b/webview-ui/src/manualTest/main.tsx @@ -46,6 +46,7 @@ const contentTestScenarios: Record = { draftDeployment: getDraftDeploymentScenarios(), draftDockerfile: getDraftDockerfileScenarios(), draftWorkflow: getDraftWorkflowScenarios(), + draftValidate: [], gadget: getInspektorGadgetScenarios(), kubectl: getKubectlScenarios(), aso: getASOScenarios(),