') {
51 | k.targetNamespace = '';
52 | }
53 |
54 | return k;
55 | }
56 |
57 |
58 | export function unwrapModel() {
59 | const model: ParamsDictionary = {};
60 |
61 | if(createSource()) {
62 | model.source = unwrapSource();
63 | }
64 |
65 | if(createWorkload()) {
66 | model.kustomization = unwrapKustomization();
67 | }
68 |
69 | model.clusterInfo = unwrap(params).clusterInfo;
70 |
71 | return model;
72 | }
73 |
--------------------------------------------------------------------------------
/webview-ui/configureGitOps/src/lib/utils/debug.ts:
--------------------------------------------------------------------------------
1 | import { setParams } from '../params';
2 |
3 | export function debug(str: string) {
4 | const e = document.getElementById('debug');
5 | if(e) {
6 | e.innerHTML = `${e.innerHTML}\n${str}`;
7 | // e.style.display = 'block';
8 | e.scrollTop = e.scrollHeight;
9 | }
10 | }
11 |
12 | export function debugStandalone() {
13 | const debugParams = {
14 | 'clusterInfo': {
15 | 'contextName': 'kind-context',
16 | 'clusterName': 'kind-cluster',
17 | 'clusterProvider': 'Generic',
18 | 'isClusterProviderUserOverride': false,
19 | 'isAzure': false,
20 | },
21 | 'gitInfo': {
22 | 'name': 'debug-standalone',
23 | 'url': 'ssh://git@github.com/juozasg/pooodinfo.git',
24 | 'branch': 'master',
25 | },
26 | 'namespaces': [ 'default', 'flux-system', 'foobar'],
27 | 'sources': [
28 | {'kind': 'GitRepository', metadata: {'name': 'podinfo', 'namespace': 'default'}},
29 | {'kind': 'OCIRepository', metadata: {'name': 'podinfo', 'namespace': 'default'}},
30 | {'kind': 'OCIRepository', metadata: {'name': 'podinfo', 'namespace': 'flux-system'}},
31 | {'kind': 'GitRepository', metadata: {'name': 'podinfo2', 'namespace': 'default'}},
32 | {'kind': 'OCIRepository', metadata: {'name': 'podinfo11', 'namespace': 'default'}}],
33 | 'selectSourceTab': false,
34 | 'selectedSource': 'GitRepository/podinfo2.default',
35 | // 'selectedSource': '',
36 | 'set': {
37 | 'kustomization': {
38 | 'path': './test-set-path',
39 | },
40 | 'createWorkload': true,
41 | },
42 | };
43 |
44 | setTimeout(() => {
45 | for(const [param, value] of Object.entries(debugParams)) {
46 | setParams(param, value);
47 | }
48 | }, 300);
49 | }
50 |
--------------------------------------------------------------------------------
/webview-ui/createFromTemplate/src/components/Main.tsx:
--------------------------------------------------------------------------------
1 | import { postModel } from 'App';
2 | import { gitOpsTemplate, TemplateParam, values } from 'lib/model';
3 | import { For, Show } from 'solid-js';
4 | import { ParamSelect } from './Common/ParamSelect';
5 | import ParamInput from './Common/ParamTextInput';
6 |
7 |
8 | const isSelectOption = (param: TemplateParam) => param.options && param.options.length > 0;
9 |
10 | const requiredParams = () => gitOpsTemplate().params.filter(p => p.required).map(p => p.name);
11 | export const missingParams = () => requiredParams().filter(name => !values.get(name));
12 |
13 | function ValidationMessage() {
14 | return (
15 | Missing required parameters
16 | );
17 | }
18 |
19 | export default function Main() {
20 | return(
21 |
22 |
Configure {gitOpsTemplate().name}
23 |
{gitOpsTemplate().description}
24 |
{(param, i) =>
25 | }>
26 |
27 |
28 | }
29 |
30 |
31 |
32 |
33 |
34 |
35 | postModel('show-yaml')} class="big">
36 | Save
37 |
38 |
39 |
40 |
41 |
42 |
Save YAML to: {gitOpsTemplate().folder}
43 |
44 |
45 |
46 |
47 | );
48 | }
49 |
50 |
--------------------------------------------------------------------------------
/src/commands/trace.ts:
--------------------------------------------------------------------------------
1 | import { window } from 'vscode';
2 | import { telemetry } from '../extension';
3 | import { fluxTools } from '../flux/fluxTools';
4 | import { kubernetesTools } from '../kubernetes/kubernetesTools';
5 | import { AnyResourceNode } from '../views/nodes/anyResourceNode';
6 | import { WorkloadNode } from '../views/nodes/workloadNode';
7 |
8 | /**
9 | * Run flux trace for the Workloads tree view node.
10 | */
11 | export async function trace(node: AnyResourceNode | WorkloadNode) {
12 | const resourceName = node.resource.metadata?.name || '';
13 | const resourceNamespace = node.resource.metadata?.namespace || 'flux-system';
14 | const resourceKind = node.resource.kind || '';
15 | let resourceApiVersion = node.resource.apiVersion || '';
16 |
17 | if (!resourceName) {
18 | window.showErrorMessage('"name" is required to run `flux trace`.');
19 | telemetry.sendError('"name" is required to run `flux trace`.');
20 | return;
21 | }
22 | if (!resourceKind) {
23 | window.showErrorMessage('"kind" is required to run `flux trace`');
24 | telemetry.sendError('"kind" is required to run `flux trace`');
25 | return;
26 | }
27 |
28 | // flux tree fetched items don't have the "apiVersion" property
29 | if (!resourceApiVersion) {
30 | const resource = await kubernetesTools.getResource(resourceName, resourceNamespace, resourceKind);
31 | const apiVersion = resource?.apiVersion;
32 | if (!apiVersion && !apiVersion) {
33 | window.showErrorMessage('"apiVersion" is required to run `flux trace`');
34 | telemetry.sendError('"apiVersion" is required to run `flux trace`');
35 | return;
36 | }
37 | resourceApiVersion = apiVersion;
38 | }
39 |
40 | await fluxTools.trace(resourceName, resourceKind, resourceApiVersion, resourceNamespace);
41 | }
42 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-var-requires */
2 | //@ts-check
3 |
4 | 'use strict';
5 |
6 | const path = require('path');
7 |
8 | /**@type {import('webpack').Configuration}*/
9 | const config = {
10 | target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/
11 | mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production')
12 |
13 | entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/
14 | output: {
15 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/
16 | path: path.resolve(__dirname, 'dist'),
17 | filename: 'extension.js',
18 | libraryTarget: 'commonjs2',
19 | },
20 | devtool: 'nosources-source-map',
21 | externals: {
22 | vscode: 'commonjs vscode', // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/
23 | 'applicationinsights-native-metrics': 'commonjs applicationinsights-native-metrics', // https://github.com/microsoft/vscode-extension-telemetry/issues/41
24 | // modules added here also need to be added in the .vsceignore file
25 | },
26 | resolve: {
27 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader
28 | extensions: ['.ts', '.js'],
29 | },
30 | module: {
31 | rules: [
32 | {
33 | test: /\.ts$/,
34 | exclude: /node_modules/,
35 | use: [
36 | {
37 | loader: 'ts-loader',
38 | },
39 | ],
40 | },
41 | ],
42 | },
43 | infrastructureLogging: {
44 | level: 'log', // enables logging required for problem matchers
45 | },
46 | };
47 | module.exports = config;
48 |
--------------------------------------------------------------------------------
/src/terminal.ts:
--------------------------------------------------------------------------------
1 | import { Disposable, ExtensionContext, Terminal, window } from 'vscode';
2 | import { getExtensionContext } from './extensionContext';
3 |
4 | const terminalName = 'gitops';
5 |
6 | let _terminal: Terminal | undefined;
7 | let _currentDirectory: string | undefined;
8 | let _disposable: Disposable | undefined;
9 |
10 | /**
11 | * Gets gitops treminal instance.
12 | * @param context VScode extension context.
13 | * @param workingDirectory Optional working directory path to cd to.
14 | * @returns
15 | */
16 | function getTerminal(context: ExtensionContext, workingDirectory?: string): Terminal {
17 | if (_terminal === undefined) {
18 | _terminal = window.createTerminal(terminalName);
19 | _disposable = window.onDidCloseTerminal((e: Terminal) => {
20 | if (e.name === terminalName) {
21 | _terminal = undefined;
22 | _disposable?.dispose();
23 | _disposable = undefined;
24 | }
25 | });
26 |
27 | context.subscriptions.push(_disposable);
28 | _currentDirectory = undefined;
29 | }
30 |
31 | if (_currentDirectory !== workingDirectory &&
32 | workingDirectory && workingDirectory.length > 0) {
33 | _terminal.sendText(`cd "${workingDirectory}"`, true); // add new line
34 | _currentDirectory = workingDirectory;
35 | }
36 |
37 | return _terminal;
38 | }
39 |
40 | /**
41 | * Runs terminal command.
42 | * @param command Command name.
43 | * @param cwd Optional working directory path to cd to.
44 | * @param focusTerminal Optional whether or not to shift the focus to the terminal.
45 | */
46 | export function runTerminalCommand(
47 | command: string,
48 | {
49 | cwd,
50 | focusTerminal,
51 | }: {
52 | cwd?: string;
53 | focusTerminal?: boolean;
54 | } = {}): void {
55 |
56 | const terminal = getTerminal(getExtensionContext(), cwd);
57 | terminal.show(!focusTerminal);
58 | terminal.sendText(command, true);
59 | }
60 |
--------------------------------------------------------------------------------
/webview-ui/configureGitOps/src/toolkit.d.ts:
--------------------------------------------------------------------------------
1 | import "solid-js";
2 |
3 | // An important part of getting the Webview UI Toolkit to work with
4 | // Solid + TypeScript + JSX is to extend the solid-js JSX.IntrinsicElements
5 | // type interface to include type annotations for each of the toolkit's components.
6 | //
7 | // Without this, type errors will occur when you try to use any toolkit component
8 | // in your Solid + TypeScript + JSX component code. (Note that this file shouldn't be
9 | // necessary if you're not using TypeScript or are using tagged template literals
10 | // instead of JSX for your Solid component code).
11 | //
12 | // Important: This file should be updated whenever a new component is added to the
13 | // toolkit. You can find a list of currently available toolkit components here:
14 | //
15 | // https://github.com/microsoft/vscode-webview-ui-toolkit/blob/main/docs/components.md
16 | //
17 | declare module "solid-js" {
18 | namespace JSX {
19 | interface IntrinsicElements {
20 | "auto-complete": any;
21 | "vscode-badge": any;
22 | "vscode-button": any;
23 | "vscode-checkbox": any;
24 | "vscode-data-grid": any;
25 | "vscode-divider": any;
26 | "vscode-dropdown": any;
27 | "vscode-link": any;
28 | "vscode-option": any;
29 | "vscode-panels": any;
30 | "vscode-panel-tab": any;
31 | "vscode-panel-view": any;
32 | "vscode-progress-ring": any;
33 | "vscode-radio": any;
34 | "vscode-radio-group": any;
35 | "vscode-tag": any;
36 | "vscode-text-area": any;
37 | "vscode-text-field": any;
38 | }
39 |
40 | interface Directives {
41 | bindInputModelSource: any,
42 | bindInputModelKustomization: any,
43 | bindInputSignal: any,
44 | bindInputStore: any,
45 | bindDropdownStore: any,
46 | bindChangeValueFunc: any,
47 | bindChangeValueSignal: any,
48 | }
49 | }
50 | }
--------------------------------------------------------------------------------
/webview-ui/createFromTemplate/src/toolkit.d.ts:
--------------------------------------------------------------------------------
1 | import "solid-js";
2 |
3 | // An important part of getting the Webview UI Toolkit to work with
4 | // Solid + TypeScript + JSX is to extend the solid-js JSX.IntrinsicElements
5 | // type interface to include type annotations for each of the toolkit's components.
6 | //
7 | // Without this, type errors will occur when you try to use any toolkit component
8 | // in your Solid + TypeScript + JSX component code. (Note that this file shouldn't be
9 | // necessary if you're not using TypeScript or are using tagged template literals
10 | // instead of JSX for your Solid component code).
11 | //
12 | // Important: This file should be updated whenever a new component is added to the
13 | // toolkit. You can find a list of currently available toolkit components here:
14 | //
15 | // https://github.com/microsoft/vscode-webview-ui-toolkit/blob/main/docs/components.md
16 | //
17 | declare module "solid-js" {
18 | namespace JSX {
19 | interface IntrinsicElements {
20 | "auto-complete": any;
21 | "vscode-badge": any;
22 | "vscode-button": any;
23 | "vscode-checkbox": any;
24 | "vscode-data-grid": any;
25 | "vscode-divider": any;
26 | "vscode-dropdown": any;
27 | "vscode-link": any;
28 | "vscode-option": any;
29 | "vscode-panels": any;
30 | "vscode-panel-tab": any;
31 | "vscode-panel-view": any;
32 | "vscode-progress-ring": any;
33 | "vscode-radio": any;
34 | "vscode-radio-group": any;
35 | "vscode-tag": any;
36 | "vscode-text-area": any;
37 | "vscode-text-field": any;
38 | }
39 |
40 | interface Directives {
41 | bindInputModelSource: any,
42 | bindInputModelKustomization: any,
43 | bindInputSignal: any,
44 | bindInputStore: any,
45 | bindDropdownStore: any,
46 | bindChangeValueFunc: any,
47 | bindChangeValueSignal: any,
48 | }
49 | }
50 | }
--------------------------------------------------------------------------------
/webview-ui/configureGitOps/src/components/Source.tsx:
--------------------------------------------------------------------------------
1 | import { Tabs } from '@microsoft/fast-foundation';
2 | import { createEffect, onMount, Show } from 'solid-js';
3 | import { setCreateSource } from 'lib/model';
4 | import { params } from 'lib/params';
5 | import { bindChangeTabsFunc } from 'lib/bindDirectives'; bindChangeTabsFunc; // TS will elide 'unused' imports
6 | import NewSource from 'components/Source/NewSource';
7 | import SelectSource from 'components/Source/SelectSource';
8 |
9 | let tabs: Tabs;
10 |
11 | function Source() {
12 | const selectSourceEnabled = () => (params.sources?.length > 0);
13 |
14 | onMount(() => {
15 | tabs.addEventListener('change', (e: Event) => setCreateSource(tabs.activeid === 'new-source-tab'));
16 | });
17 |
18 | createEffect(() => {
19 | const activeid = (params.selectSourceTab && params.sources?.length > 0) ? 'select-source-tab' : 'new-source-tab';
20 | tabs.activeid = activeid;
21 | setCreateSource(tabs.activeid === 'new-source-tab');
22 | });
23 |
24 |
25 | return(
26 |
27 |
Source Repository
28 | {/* */}
29 |
30 | New Source...
31 |
32 |
33 | Select Source
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | );
47 | }
48 |
49 | export default Source;
50 |
--------------------------------------------------------------------------------
/webview-ui/configureGitOps/src/components/Source/NewSource/Settings/GitRepository/GitConnection.tsx:
--------------------------------------------------------------------------------
1 | import Checkbox from 'components/Common/Checkbox';
2 | import { ToolkitHelpLink } from 'components/Common/HelpLink';
3 | import TextInput from 'components/Common/TextInput';
4 | import { gitRepository, source } from 'lib/model';
5 | import { Show } from 'solid-js';
6 |
7 | function SecretRefInput() {
8 | return (
9 |
10 |
11 |
12 |
13 | )
14 | ;
15 | }
16 |
17 | const isSSH = () => gitRepository.url.toLowerCase().indexOf('ssh') === 0;
18 |
19 | function SSHPrivateKeyFile() {
20 | return (
21 |
22 |
23 |
24 |
25 | );
26 | }
27 |
28 |
29 | function GitConnection() {
30 | return (
31 |
32 |
Create new Secret with credentials
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | );
55 | }
56 |
57 | export default GitConnection;
58 |
--------------------------------------------------------------------------------
/webview-ui/configureGitOps/src/components/Source/NewSource/OCIRepository.tsx:
--------------------------------------------------------------------------------
1 | import ListSelect from 'components/Common/ListSelect';
2 | import SettingsPanel from './Settings/OCIRepository/Panel';
3 | import Name from './Common/Name';
4 | import Namespace from './Common/Namespace';
5 | import TextInput from 'components/Common/TextInput';
6 | import { ToolkitHelpLink } from 'components/Common/HelpLink';
7 |
8 |
9 | function OCIRepository() {
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | ['tag', 'tagSemver', 'digest']}/>
25 |
26 |
27 |
28 |
29 |
30 |
Authentication settings for private repositories
31 |
32 |
33 |
34 | ['generic', 'aws', 'azure', 'gcp']}/>
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | );
54 | }
55 |
56 | export default OCIRepository;
57 |
--------------------------------------------------------------------------------
/webview-ui/configureGitOps/src/components/Source/NewSource/Settings/HelmRepository/HelmConnection.tsx:
--------------------------------------------------------------------------------
1 | import { Show } from 'solid-js';
2 |
3 | import TextInput from 'components/Common/TextInput';
4 | import { isOCIHelm } from '../../HelmRepository';
5 | import Checkbox from 'components/Common/Checkbox';
6 | import { source } from 'lib/model';
7 | import { ToolkitHelpLink } from 'components/Common/HelpLink';
8 |
9 |
10 |
11 | function SecretRefInput() {
12 | return (
13 |
14 |
15 |
16 |
17 |
18 | );
19 | }
20 |
21 |
22 | function HelmConnection() {
23 | return (
24 |
25 |
26 |
Create new Secret with credentials
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | );
62 | }
63 |
64 | export default HelmConnection;
65 |
--------------------------------------------------------------------------------
/webview-ui/configureGitOps/src/components/Source/NewSource.tsx:
--------------------------------------------------------------------------------
1 | import { Tabs } from '@microsoft/fast-foundation';
2 | import { ToolkitHelpLink } from 'components/Common/HelpLink';
3 | import { params } from 'lib/params';
4 | import { onMount, Show } from 'solid-js';
5 | import { setSource, source } from '../../lib/model';
6 |
7 | import Bucket from './NewSource/Bucket';
8 | import GitRepository from './NewSource/GitRepository';
9 | import HelmRepository from './NewSource/HelmRepository';
10 | import OCIRepository from './NewSource/OCIRepository';
11 |
12 | let tabs: Tabs;
13 |
14 |
15 | function NewSource() {
16 | onMount(() => {
17 | tabs.addEventListener('change', (e: Event) => {
18 | const kind = tabs.activeid.slice(0, -4);
19 | setSource('kind', kind);
20 | });
21 | });
22 |
23 |
24 | return (
25 |
26 |
27 |
28 |
29 | GitRepository
30 |
31 |
32 |
33 |
34 | HelmRepository
35 |
36 |
37 |
38 | OCIRepository
39 |
40 |
41 |
42 | Bucket
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | );
65 | }
66 |
67 | export default NewSource;
68 |
--------------------------------------------------------------------------------
/src/commands/createGitRepositoryForPath.ts:
--------------------------------------------------------------------------------
1 | import gitUrlParse from 'git-url-parse';
2 | import { Uri, window, workspace } from 'vscode';
3 | import { failed } from '../errorable';
4 | import { getFolderGitInfo } from '../git/gitInfo';
5 | import { checkGitVersion } from '../install';
6 | import { getCurrentClusterInfo } from '../views/treeViews';
7 | import { openConfigureGitOpsWebview } from '../webview-backend/configureGitOps/openWebview';
8 |
9 | /**
10 | * Add git repository source whether from an opened folder
11 | * or make user pick a folder to infer the url & branch
12 | * of the new git repository source.
13 | * @param fileExplorerUri uri of the file in the file explorer
14 | */
15 | export async function createGitRepositoryForPath(fileExplorerUri?: Uri) {
16 |
17 | const gitInstalled = await checkGitVersion();
18 | if (!gitInstalled) {
19 | return;
20 | }
21 |
22 | const currentClusterInfo = await getCurrentClusterInfo();
23 | if (failed(currentClusterInfo)) {
24 | return;
25 | }
26 |
27 | let gitInfo;
28 |
29 | if (fileExplorerUri) {
30 | gitInfo = await getFolderGitInfo(fileExplorerUri.fsPath);
31 | } else {
32 | let gitFolderFsPath = '';
33 | // executed from Command Palette
34 | if (!workspace.workspaceFolders || workspace.workspaceFolders?.length === 0) {
35 | // no opened folders => show OS dialog
36 | const pickedFolder = await window.showOpenDialog({
37 | canSelectFiles: false,
38 | canSelectFolders: true,
39 | canSelectMany: false,
40 | });
41 | if (!pickedFolder) {
42 | return;
43 | }
44 | gitFolderFsPath = pickedFolder[0].fsPath;
45 | } else if (workspace.workspaceFolders.length > 1) {
46 | // multiple folders opened (multi-root) => make user pick one
47 | const pickedFolder = await window.showQuickPick(workspace.workspaceFolders.map(folder => folder.uri.fsPath));
48 | if (!pickedFolder) {
49 | return;
50 | }
51 | gitFolderFsPath = pickedFolder;
52 | } else {
53 | // just one folder opened => use it
54 | gitFolderFsPath = workspace.workspaceFolders[0].uri.fsPath;
55 | }
56 |
57 | gitInfo = await getFolderGitInfo(gitFolderFsPath);
58 | }
59 |
60 | openConfigureGitOpsWebview(false, '', {createWorkload: false}, gitInfo);
61 | }
62 |
--------------------------------------------------------------------------------
/src/commands/showInstalledVersions.ts:
--------------------------------------------------------------------------------
1 | import os from 'os';
2 | import { env, extensions, version, window } from 'vscode';
3 | import { failed } from '../errorable';
4 | import { GitOpsExtensionConstants } from '../extension';
5 | import { getAzureVersion, getFluxVersion, getGitVersion, getKubectlVersion } from '../install';
6 |
7 | /**
8 | * Show all installed cli versions.
9 | */
10 | export async function showInstalledVersions() {
11 | const [kubectlVersion, fluxVersion, gitVersion, azureVersion] = await Promise.all([
12 | getKubectlVersion(),
13 | getFluxVersion(),
14 | getGitVersion(),
15 | getAzureVersion(),
16 | ]);
17 |
18 | const kubectlVersionString = failed(kubectlVersion) ? 'kubectl: not found' : `kubectl client ${kubectlVersion.result.clientVersion.gitVersion}\nkubectl server ${kubectlVersion.result.serverVersion.gitVersion}`;
19 |
20 | let azureVersionsString = '';
21 | if (failed(azureVersion)) {
22 | azureVersionsString = 'Azure: not found';
23 | } else {
24 | azureVersionsString = `
25 | Azure: ${azureVersion.result['azure-cli']}
26 | Azure extension "k8s-configuration": ${azureVersion.result.extensions['k8s-configuration'] || 'not installed'}
27 | Azure extension "k8s-extension": ${azureVersion.result.extensions['k8s-extension'] || 'not installed'}
28 | `.trim();
29 | }
30 |
31 | const versions = `
32 | ${kubectlVersionString}
33 | Flux: ${failed(fluxVersion) ? 'not found' : fluxVersion.result}
34 | Git: ${failed(gitVersion) ? 'not found' : gitVersion.result}
35 | ${azureVersionsString}
36 | VSCode: ${version}
37 | Extension: ${getExtensionVersion() || 'unknown'}
38 | OS: ${getOSVersion() || 'unknown'}
39 | `.trim();
40 |
41 | const copyButton = 'Copy';
42 | const pressedButton = await window.showInformationMessage(versions, { modal: true }, copyButton);
43 | if (pressedButton === copyButton) {
44 | env.clipboard.writeText(versions);
45 | }
46 | }
47 |
48 | /**
49 | * Get installed version of GitOps extension (from `package.json`).
50 | */
51 | export function getExtensionVersion() {
52 | return extensions.getExtension(GitOpsExtensionConstants.ExtensionId)?.packageJSON.version || 'unknown version';
53 | }
54 | /**
55 | * Get Operating System its verison.
56 | */
57 | function getOSVersion() {
58 | return `${os.type} ${os.arch} ${os.release}`;
59 | }
60 |
--------------------------------------------------------------------------------
/resources/icons/dark/cloud.svg:
--------------------------------------------------------------------------------
1 |
2 |
46 |
--------------------------------------------------------------------------------
/resources/icons/light/cloud.svg:
--------------------------------------------------------------------------------
1 |
2 |
46 |
--------------------------------------------------------------------------------
/src/globalState.ts:
--------------------------------------------------------------------------------
1 | import { ExtensionContext, window, workspace } from 'vscode';
2 | import { KnownClusterProviders } from './kubernetes/types/kubernetesTypes';
3 |
4 | export interface ClusterMetadata {
5 | azureResourceGroup?: string;
6 | azureSubscription?: string;
7 | azureClusterName?: string;
8 | clusterProvider?: KnownClusterProviders;
9 | }
10 |
11 | const enum GlobalStatePrefixes {
12 | ClusterMetadata = 'clusterMetadata',
13 | }
14 |
15 | export const enum GlobalStateKey {
16 | FluxPath = 'fluxPath',
17 | FirstEverActivationStorageKey = 'firstEverActivation',
18 | }
19 |
20 | interface GlobalStateKeyMapping {
21 | [GlobalStateKey.FluxPath]: string;
22 | [GlobalStateKey.FirstEverActivationStorageKey]: boolean;
23 | }
24 |
25 | export class GlobalState {
26 |
27 | constructor(private context: ExtensionContext) {}
28 |
29 | private prefix(prefixValue: GlobalStatePrefixes, str: string): string {
30 | return `${prefixValue}:${str}`;
31 | }
32 |
33 | get(stateKey: E): T[E] | undefined {
34 | return this.context.globalState.get(stateKey as string);
35 | }
36 |
37 | set(stateKey: E, newValue: T[E]): void {
38 | this.context.globalState.update(stateKey as string, newValue);
39 | }
40 |
41 | getClusterMetadata(clusterName: string): ClusterMetadata | undefined {
42 | return this.context.globalState.get(this.prefix(GlobalStatePrefixes.ClusterMetadata, clusterName));
43 | }
44 |
45 | setClusterMetadata(clusterName: string, metadata: ClusterMetadata): void {
46 | this.context.globalState.update(this.prefix(GlobalStatePrefixes.ClusterMetadata, clusterName), metadata);
47 | }
48 |
49 | /**
50 | * Run while developing to see the entire global storage contents.
51 | */
52 | async showGlobalStateValue() {
53 | const document = await workspace.openTextDocument({
54 | language: 'jsonc',
55 | // @ts-ignore
56 | content: JSON.stringify(this.context.globalState._value, null, ' '),
57 | });
58 | window.showTextDocument(document);
59 | }
60 |
61 | /**
62 | * Dev function (clear all global state properties).
63 | */
64 | clearGlobalState() {
65 | for (const key of this.context.globalState.keys()) {
66 | this.context.globalState.update(key, undefined);
67 | }
68 | }
69 | }
70 |
71 |
--------------------------------------------------------------------------------
/webview-ui/configureGitOps/resources/icons/dark/cloud.svg:
--------------------------------------------------------------------------------
1 |
2 |
46 |
--------------------------------------------------------------------------------
/webview-ui/createFromTemplate/resources/icons/dark/cloud.svg:
--------------------------------------------------------------------------------
1 |
2 |
46 |
--------------------------------------------------------------------------------
/webview-ui/configureGitOps/resources/icons/light/cloud.svg:
--------------------------------------------------------------------------------
1 |
2 |
46 |
--------------------------------------------------------------------------------
/webview-ui/createFromTemplate/resources/icons/light/cloud.svg:
--------------------------------------------------------------------------------
1 |
2 |
46 |
--------------------------------------------------------------------------------
/src/utils/stringUtils.ts:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | * Shortens revision string for display in GitOps tree views.
4 | * @param revision Revision string to shorten.
5 | * @returns Short revision string with max 8 characters for hash, all characters for semantic versions like 0.1.1-staging
6 | */
7 | export function shortenRevision(revision = ''): string {
8 | revision = revision.replace(/^(sha1|sha256|sha384|sha512|blake3):/, '');
9 | revision = revision.replace(/@(sha1|sha256|sha384|sha512|blake3):/, '/');
10 | if (revision.includes('/')) {
11 | // git revision includes branch name
12 | const [gitBranch, gitRevision] = revision.split('/');
13 | return [gitBranch, ':', gitRevision.slice(0, 7)].join('');
14 | } else {
15 | return shortenRevisionHash(revision);
16 | }
17 | }
18 |
19 | function shortenRevisionHash(revision: string) {
20 | // return full semver like 0.1.1-staging
21 | if(revision.match(/^\d+\.\d+\.\d+/)) {
22 | return revision;
23 | }
24 |
25 | // trim hash to 8
26 | return revision.slice(0, 7);
27 | }
28 |
29 | /**
30 | * Remove not allowed symbols, cast letters to lowercase
31 | * and truncate the string to match the RFC 1123 subdomain:
32 | *
33 | * - contain no more than 253 characters
34 | * - contain only lowercase alphanumeric characters, '-' or '.'
35 | * - start with an alphanumeric character
36 | * - end with an alphanumeric character
37 | * @param str string to sanitize
38 | */
39 | export function sanitizeRFC1123(str: string): string {
40 | const notAllowedSymbolsRegex = /[^a-z0-9.-]/g;
41 | const notAllowedSymbolsAtTheStartRegex = /^[^a-z0-9]+/;
42 | const notAllowedSymbolsAtTheEndRegex = /[^a-z0-9]+$/;
43 |
44 | const lowercaseString = str.toLocaleLowerCase();
45 |
46 | const sanitizedString = lowercaseString
47 | .replace(notAllowedSymbolsRegex, '')
48 | .replace(notAllowedSymbolsAtTheStartRegex, '')
49 | .replace(notAllowedSymbolsAtTheEndRegex, '');
50 |
51 | return truncateString(sanitizedString, 253);
52 | }
53 |
54 | /**
55 | * Reduce the string length if it's longer than the allowed number of characters.
56 | * @param str string to truncate
57 | * @param maxChars maximum length of the string
58 | */
59 | export function truncateString(str: string, maxChars: number): string {
60 | const chars = [...str];
61 | return chars.length > maxChars ? chars.slice(0, maxChars).join('') : str;
62 | }
63 |
--------------------------------------------------------------------------------
/src/commands/createKustomizationForPath.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import { Uri, window, workspace } from 'vscode';
3 | import { failed } from '../errorable';
4 | import { getFolderGitInfo, getGitRepositoryforGitInfo } from '../git/gitInfo';
5 | import { namespacedObject } from '../kubernetes/types/flux/object';
6 | import { getCurrentClusterInfo } from '../views/treeViews';
7 | import { openConfigureGitOpsWebview } from '../webview-backend/configureGitOps/openWebview';
8 |
9 | /**
10 | * Create kustomization from File Explorer context menu
11 | * or `+` button or Command Palette.
12 | *
13 | * @param fileExplorerUri uri of the file in the file explorer
14 | */
15 | export async function createKustomizationForPath(fileExplorerUri?: Uri): Promise {
16 |
17 | const currentClusterInfo = await getCurrentClusterInfo();
18 | if (failed(currentClusterInfo)) {
19 | return;
20 | }
21 |
22 | let kustomizationFsPath = '';
23 | let relativeKustomizationPath = '';
24 |
25 | if (fileExplorerUri) {
26 | // executed from the VSCode File Explorer
27 | kustomizationFsPath = fileExplorerUri.fsPath;
28 | } else {
29 | // executed from Command Palette
30 | const pickedFolder = await window.showOpenDialog({
31 | title: 'Select a folder (used for "path" property on the new Kustomization object)',
32 | canSelectFiles: false,
33 | canSelectFolders: true,
34 | canSelectMany: false,
35 | defaultUri: workspace.workspaceFolders?.[0].uri,
36 | });
37 | if (!pickedFolder) {
38 | return;
39 | }
40 | kustomizationFsPath = pickedFolder[0].fsPath;
41 | }
42 |
43 | // get relative path for the kustomization
44 | for (const folder of workspace.workspaceFolders || []) {
45 | const relativePath = path.relative(folder.uri.fsPath, kustomizationFsPath);
46 | if (relativePath && !relativePath.startsWith('..') && !path.isAbsolute(relativePath)) {
47 | relativeKustomizationPath = relativePath;
48 | break;
49 | }
50 | }
51 |
52 | const gitInfo = await getFolderGitInfo(kustomizationFsPath);
53 | const gr = await getGitRepositoryforGitInfo(gitInfo);
54 |
55 | const selectSource = !!gr;
56 | let sourceName = namespacedObject(gr) || '';
57 |
58 | openConfigureGitOpsWebview(selectSource, sourceName, {
59 | kustomization: {
60 | path: relativeKustomizationPath,
61 | },
62 | createWorkload: true,
63 | }, gitInfo);
64 |
65 | }
66 |
67 |
--------------------------------------------------------------------------------
/media/vscode.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --container-padding: 20px;
3 | --input-padding-vertical: 6px;
4 | --input-padding-horizontal: 4px;
5 | --input-margin-vertical: 4px;
6 | --input-margin-horizontal: 0;
7 | }
8 |
9 | body {
10 | padding: 0 var(--container-padding);
11 | color: var(--vscode-foreground);
12 | font-size: var(--vscode-font-size);
13 | font-weight: var(--vscode-font-weight);
14 | font-family: var(--vscode-font-family);
15 | background-color: var(--vscode-editor-background);
16 | }
17 |
18 | ol,
19 | ul {
20 | padding-left: var(--container-padding);
21 | }
22 |
23 | body > *,
24 | form > * {
25 | margin-block-start: var(--input-margin-vertical);
26 | margin-block-end: var(--input-margin-vertical);
27 | }
28 |
29 | *:focus {
30 | outline-color: var(--vscode-focusBorder) !important;
31 | }
32 |
33 | a {
34 | color: var(--vscode-textLink-foreground);
35 | }
36 |
37 | a:hover,
38 | a:active {
39 | color: var(--vscode-textLink-activeForeground);
40 | }
41 |
42 | code {
43 | font-size: var(--vscode-editor-font-size);
44 | font-family: var(--vscode-editor-font-family);
45 | }
46 |
47 | button {
48 | border: none;
49 | padding: var(--input-padding-vertical) var(--input-padding-horizontal);
50 | width: 100%;
51 | text-align: center;
52 | outline: 1px solid transparent;
53 | outline-offset: 2px !important;
54 | color: var(--vscode-button-foreground);
55 | background: var(--vscode-button-background);
56 | }
57 |
58 | button:hover {
59 | cursor: pointer;
60 | background: var(--vscode-button-hoverBackground);
61 | }
62 |
63 | button:focus {
64 | outline-color: var(--vscode-focusBorder);
65 | }
66 |
67 | button.secondary {
68 | color: var(--vscode-button-secondaryForeground);
69 | background: var(--vscode-button-secondaryBackground);
70 | }
71 |
72 | button.secondary:hover {
73 | background: var(--vscode-button-secondaryHoverBackground);
74 | }
75 |
76 | input:not([type='checkbox']):not([type='radio']),
77 | textarea {
78 | display: block;
79 | width: 100%;
80 | border: none;
81 | font-family: var(--vscode-font-family);
82 | padding: var(--input-padding-vertical) var(--input-padding-horizontal);
83 | color: var(--vscode-input-foreground);
84 | outline-color: var(--vscode-input-border);
85 | background-color: var(--vscode-input-background);
86 | }
87 |
88 | input::placeholder,
89 | textarea::placeholder {
90 | color: var(--vscode-input-placeholderForeground);
91 | }
--------------------------------------------------------------------------------
/src/commands/resume.ts:
--------------------------------------------------------------------------------
1 | import { window } from 'vscode';
2 | import { AzureClusterProvider, azureTools, isAzureProvider } from '../azure/azureTools';
3 | import { failed } from '../errorable';
4 | import { fluxTools } from '../flux/fluxTools';
5 | import { FluxSource, FluxWorkload } from '../flux/fluxTypes';
6 | import { GitRepositoryNode } from '../views/nodes/gitRepositoryNode';
7 | import { HelmReleaseNode } from '../views/nodes/helmReleaseNode';
8 | import { HelmRepositoryNode } from '../views/nodes/helmRepositoryNode';
9 | import { KustomizationNode } from '../views/nodes/kustomizationNode';
10 | import { OCIRepositoryNode } from '../views/nodes/ociRepositoryNode';
11 | import { getCurrentClusterInfo, refreshSourcesTreeView, refreshWorkloadsTreeView } from '../views/treeViews';
12 |
13 | /**
14 | * Resume source or workload reconciliation and refresh its Tree View.
15 | *
16 | * @param node sources tree view node
17 | */
18 | export async function resume(node: GitRepositoryNode | HelmReleaseNode | HelmRepositoryNode | KustomizationNode) {
19 |
20 | const currentClusterInfo = await getCurrentClusterInfo();
21 | if (failed(currentClusterInfo)) {
22 | return;
23 | }
24 |
25 | const fluxResourceType: FluxSource | FluxWorkload | 'unknown' = node instanceof GitRepositoryNode ?
26 | 'source git' : node instanceof HelmRepositoryNode ?
27 | 'source helm' : node instanceof OCIRepositoryNode ?
28 | 'source oci' : node instanceof HelmReleaseNode ?
29 | 'helmrelease' : node instanceof KustomizationNode ?
30 | 'kustomization' : 'unknown';
31 | if (fluxResourceType === 'unknown') {
32 | window.showErrorMessage(`Unknown object kind ${fluxResourceType}`);
33 | return;
34 | }
35 |
36 | if (currentClusterInfo.result.isAzure) {
37 | // TODO: implement
38 | if (fluxResourceType === 'helmrelease' || fluxResourceType === 'kustomization') {
39 | window.showInformationMessage('Not implemented on AKS/ARC', { modal: true });
40 | return;
41 | }
42 | await azureTools.resume(node.resource.metadata.name || '', currentClusterInfo.result.contextName, currentClusterInfo.result.clusterProvider as AzureClusterProvider);
43 | } else {
44 | await fluxTools.resume(fluxResourceType, node.resource.metadata.name || '', node.resource.metadata.namespace || '');
45 | }
46 |
47 | if (node instanceof GitRepositoryNode || node instanceof OCIRepositoryNode || node instanceof HelmRepositoryNode) {
48 | refreshSourcesTreeView();
49 | if (currentClusterInfo.result.isAzure) {
50 | refreshWorkloadsTreeView();
51 | }
52 | } else {
53 | refreshWorkloadsTreeView();
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/webview-ui/configureGitOps/src/components/Kustomization.tsx:
--------------------------------------------------------------------------------
1 | import { Show } from 'solid-js';
2 |
3 | import Checkbox from 'components/Common/Checkbox';
4 | import ListSelect from 'components/Common/ListSelect';
5 | import { ToolkitHelpLink } from './Common/HelpLink';
6 | import TextInput from './Common/TextInput';
7 |
8 | import { createSource, createWorkload, kustomization, setCreateWorkload, source } from 'lib/model';
9 | import { params } from 'lib/params';
10 |
11 | function Kustomization() {
12 | const isAzure = () => params.clusterInfo?.isAzure && (!createSource() || (source.kind === 'GitRepository' && source.createFluxConfig));
13 |
14 | const targetNamespaces = () => [...(params.namespaces?.values() || []), ''];
15 |
16 | return(
17 |
18 |
Create Kustomization
19 |
20 |
21 | Create a Kustomization
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | params.namespaces}
36 | class='medium'/>
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
53 | Namespace for objects reconciled by the Kustomization
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | Prune (remove stale resources)
65 |
66 |
67 |
68 |
69 |
70 | );
71 | }
72 |
73 | export default Kustomization;
74 |
--------------------------------------------------------------------------------
/src/commands/showNewUserGuide.ts:
--------------------------------------------------------------------------------
1 | import { readFileSync } from 'fs';
2 | import * as vscode from 'vscode';
3 | import { tim } from 'tinytim';
4 | import { asAbsolutePath } from '../extensionContext';
5 |
6 |
7 | export function showNewUserGuide() {
8 | const panel = vscode.window.createWebviewPanel(
9 | 'gitopsNewUserGuide', // Identifies the type of the webview. Used internally
10 | 'Welcome to GitOps - New User Guide',
11 | vscode.ViewColumn.One, // Editor column to show the new webview panel in.
12 | {
13 | enableScripts: false,
14 | },
15 |
16 | );
17 |
18 | panel.iconPath = asAbsolutePath('resources/icons/gitops-logo.png');
19 | panel.webview.html = getWebviewContent(panel.webview);
20 | }
21 |
22 | function getWebviewContent(webview: vscode.Webview) {
23 |
24 | const styleResetPath = webview.asWebviewUri(asAbsolutePath('media/reset.css'));
25 | const styleVSCodePath = webview.asWebviewUri(asAbsolutePath('media/vscode.css'));
26 | const styleNewUserGuide = webview.asWebviewUri(asAbsolutePath('media/newUserGuide.css'));
27 |
28 | const images = [
29 | webview.asWebviewUri(asAbsolutePath('resources/images/newUserGuide/01-enable-gitops.gif')),
30 | webview.asWebviewUri(asAbsolutePath('resources/images/newUserGuide/02-create-source.gif')),
31 | webview.asWebviewUri(asAbsolutePath('resources/images/newUserGuide/03-describe-source.gif')),
32 | webview.asWebviewUri(asAbsolutePath('resources/images/newUserGuide/04-create-kustomization.gif')),
33 | webview.asWebviewUri(asAbsolutePath('resources/images/newUserGuide/05-workloads.gif')),
34 | webview.asWebviewUri(asAbsolutePath('resources/images/newUserGuide/06-reconcile.gif')),
35 | webview.asWebviewUri(asAbsolutePath('resources/images/newUserGuide/07-logs.png')),
36 | webview.asWebviewUri(asAbsolutePath('resources/images/newUserGuide/08-trace.png')),
37 | webview.asWebviewUri(asAbsolutePath('resources/images/newUserGuide/09-docs.png')),
38 | ];
39 |
40 |
41 | const htmlTemplate = readFileSync(asAbsolutePath('media/newUserGuide.html').fsPath).toString();
42 | const htmlBody = tim(htmlTemplate, {images: images});
43 |
44 | return `
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | Welcome to GitOps - New User Guide
53 |
54 |
55 | ${htmlBody}
56 |
57 |
58 | `;
59 | }
60 |
61 |
--------------------------------------------------------------------------------
/src/commands/suspend.ts:
--------------------------------------------------------------------------------
1 | import { window } from 'vscode';
2 | import { AzureClusterProvider, azureTools, isAzureProvider } from '../azure/azureTools';
3 | import { failed } from '../errorable';
4 | import { fluxTools } from '../flux/fluxTools';
5 | import { FluxSource, FluxWorkload } from '../flux/fluxTypes';
6 | import { GitRepositoryNode } from '../views/nodes/gitRepositoryNode';
7 | import { HelmReleaseNode } from '../views/nodes/helmReleaseNode';
8 | import { HelmRepositoryNode } from '../views/nodes/helmRepositoryNode';
9 | import { KustomizationNode } from '../views/nodes/kustomizationNode';
10 | import { OCIRepositoryNode } from '../views/nodes/ociRepositoryNode';
11 | import { getCurrentClusterInfo, refreshSourcesTreeView, refreshWorkloadsTreeView } from '../views/treeViews';
12 |
13 | /**
14 | * Suspend source or workload reconciliation and refresh its Tree View.
15 | *
16 | * @param node sources tree view node
17 | */
18 | export async function suspend(node: GitRepositoryNode | HelmReleaseNode | KustomizationNode | HelmRepositoryNode) {
19 |
20 | const currentClusterInfo = await getCurrentClusterInfo();
21 | if (failed(currentClusterInfo)) {
22 | return;
23 | }
24 |
25 | const fluxResourceType: FluxSource | FluxWorkload | 'unknown' = node instanceof GitRepositoryNode ?
26 | 'source git' : node instanceof HelmRepositoryNode ?
27 | 'source helm' : node instanceof OCIRepositoryNode ?
28 | 'source oci' : node instanceof HelmReleaseNode ?
29 | 'helmrelease' : node instanceof KustomizationNode ?
30 | 'kustomization' : 'unknown';
31 |
32 | if (fluxResourceType === 'unknown') {
33 | window.showErrorMessage(`Unknown object kind ${fluxResourceType}`);
34 | return;
35 | }
36 |
37 | if (currentClusterInfo.result.isAzure) {
38 | // TODO: implement
39 | if (fluxResourceType === 'helmrelease' || fluxResourceType === 'kustomization') {
40 | window.showInformationMessage('Not implemented on AKS/ARC', { modal: true });
41 | return;
42 | }
43 |
44 | await azureTools.suspend(node.resource.metadata.name || '', currentClusterInfo.result.contextName, currentClusterInfo.result.clusterProvider as AzureClusterProvider);
45 | } else {
46 | await fluxTools.suspend(fluxResourceType, node.resource.metadata.name || '', node.resource.metadata.namespace || '');
47 | }
48 |
49 | if (node instanceof GitRepositoryNode || node instanceof OCIRepositoryNode || node instanceof HelmRepositoryNode) {
50 | refreshSourcesTreeView();
51 | if (currentClusterInfo.result.isAzure) {
52 | refreshWorkloadsTreeView();
53 | }
54 | } else {
55 | refreshWorkloadsTreeView();
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/kuberesources.ts:
--------------------------------------------------------------------------------
1 | import { QuickPickItem } from 'vscode';
2 |
3 | /**
4 | * A class from Kubernetes resources.
5 | * Needed to pass arguments to that extension.
6 | */
7 | export class ResourceKind implements QuickPickItem {
8 | constructor(readonly displayName: string, readonly pluralDisplayName: string, readonly manifestKind: string, readonly abbreviation: string, readonly apiName?: string) {
9 | }
10 |
11 | get label() {
12 | return this.displayName;
13 | }
14 | get description() {
15 | return '';
16 | }
17 | }
18 |
19 | /**
20 | * Kubernetes resource kinds implementing {@link QuickPickItem}
21 | */
22 | export const allKinds = {
23 | // endpoint: new ResourceKind('Endpoint', 'Endpoints', 'Endpoint', 'endpoints', 'endpoints'),
24 | // namespace: new ResourceKind('Namespace', 'Namespaces', 'Namespace', 'namespace' , 'namespaces'),
25 | // node: new ResourceKind('Node', 'Nodes', 'Node', 'node', 'nodes'),
26 | deployment: new ResourceKind('Deployment', 'Deployments', 'Deployment', 'deployment', 'deployments'),
27 | // daemonSet: new ResourceKind('DaemonSet', 'DaemonSets', 'DaemonSet', 'daemonset', 'daemonsets'),
28 | // replicaSet: new ResourceKind('ReplicaSet', 'ReplicaSets', 'ReplicaSet', 'rs', 'replicasets'),
29 | // replicationController: new ResourceKind('Replication Controller', 'Replication Controllers', 'ReplicationController', 'rc', 'replicationcontrollers'),
30 | // job: new ResourceKind('Job', 'Jobs', 'Job', 'job', 'jobs'),
31 | // cronjob: new ResourceKind('CronJob', 'CronJobs', 'CronJob', 'cronjob', 'cronjobs'),
32 | pod: new ResourceKind('Pod', 'Pods', 'Pod', 'pod', 'pods'),
33 | // crd: new ResourceKind('Custom Resource', 'Custom Resources', 'CustomResourceDefinition', 'crd', 'customresources'),
34 | // service: new ResourceKind('Service', 'Services', 'Service', 'service', 'services'),
35 | // configMap: new ResourceKind('ConfigMap', 'Config Maps', 'ConfigMap', 'configmap', 'configmaps'),
36 | // secret: new ResourceKind('Secret', 'Secrets', 'Secret', 'secret', 'secrets'),
37 | // ingress: new ResourceKind('Ingress', 'Ingress', 'Ingress', 'ingress', 'ingress'),
38 | // persistentVolume: new ResourceKind('Persistent Volume', 'Persistent Volumes', 'PersistentVolume', 'pv', 'persistentvolumes'),
39 | // persistentVolumeClaim: new ResourceKind('Persistent Volume Claim', 'Persistent Volume Claims', 'PersistentVolumeClaim', 'pvc', 'persistentvolumeclaims'),
40 | // storageClass: new ResourceKind('Storage Class', 'Storage Classes', 'StorageClass', 'sc', 'storageclasses'),
41 | // statefulSet: new ResourceKind('StatefulSet', 'StatefulSets', 'StatefulSet', 'statefulset', 'statefulsets'),
42 | };
43 |
--------------------------------------------------------------------------------
/src/output.ts:
--------------------------------------------------------------------------------
1 | import { OutputChannel, window } from 'vscode';
2 |
3 | type OutputChannelName = 'GitOps' | 'GitOps: kubectl';
4 |
5 | class Output {
6 | /** Main GitOps output channel. */
7 | private channel: OutputChannel;
8 | /** Channel for kubectl commands output (json) */
9 | private kubectlChannel: OutputChannel;
10 |
11 | constructor() {
12 | this.channel = window.createOutputChannel('GitOps' as OutputChannelName);
13 | this.kubectlChannel = window.createOutputChannel('GitOps: kubectl' as OutputChannelName);
14 | }
15 |
16 | /**
17 | * Send a message to one of the Output Channels of this extension.
18 | */
19 | send(
20 | message: string,
21 | {
22 | newline = 'double',
23 | revealOutputView = true,
24 | logLevel = 'info',
25 | channelName = 'GitOps',
26 | }: {
27 | newline?: 'none' | 'single' | 'double';
28 | revealOutputView?: boolean;
29 | logLevel?: 'info' | 'warn' | 'error';
30 | channelName?: OutputChannelName;
31 | } = {},
32 | ): void {
33 |
34 | let channel = this.getChannelByName(channelName);
35 |
36 | if (!channel) {
37 | channel = window.createOutputChannel(channelName);
38 | if (channelName === 'GitOps') {
39 | this.channel = channel;
40 | } else if (channelName === 'GitOps: kubectl') {
41 | this.kubectlChannel = channel;
42 | }
43 | }
44 |
45 | if (revealOutputView) {
46 | channel.show(true);
47 | }
48 |
49 | if (logLevel === 'warn') {
50 | message = `WARN ${message}`;
51 | } else if (logLevel === 'error') {
52 | message = `ERROR ${message}`;
53 | }
54 |
55 | // enforce newlines at the end, but don't append to the existing ones
56 | if (newline === 'single') {
57 | message = `${message.replace(/\n$/, '')}\n`;
58 | } else if (newline === 'double') {
59 | message = `${message.replace(/\n?\n$/, '')}\n\n`;
60 | }
61 |
62 | channel.append(message);
63 | }
64 |
65 | /**
66 | * Show and focus main output channel.
67 | */
68 | show(): void {
69 | this.channel.show();
70 | }
71 |
72 | /**
73 | * Return Output channel from its name.
74 | *
75 | * @param channelName Target Output Channel name
76 | */
77 | private getChannelByName(channelName: OutputChannelName): OutputChannel | undefined {
78 | if (channelName === 'GitOps') {
79 | return this.channel;
80 | } else if (channelName === 'GitOps: kubectl') {
81 | return this.kubectlChannel;
82 | }
83 | }
84 | }
85 |
86 | /**
87 | * Output view of this extension.
88 | */
89 | export const output = new Output();
90 |
91 | /**
92 | * @see {@link output.show}
93 | */
94 | export function showOutputChannel() {
95 | output.show();
96 | }
97 |
--------------------------------------------------------------------------------
/src/commands/deleteSource.ts:
--------------------------------------------------------------------------------
1 | import { window } from 'vscode';
2 | import { AzureClusterProvider, azureTools, isAzureProvider } from '../azure/azureTools';
3 | import { failed } from '../errorable';
4 | import { telemetry } from '../extension';
5 | import { fluxTools } from '../flux/fluxTools';
6 | import { FluxSource } from '../flux/fluxTypes';
7 | import { KubernetesObjectKinds } from '../kubernetes/types/kubernetesTypes';
8 | import { TelemetryEventNames } from '../telemetry';
9 | import { BucketNode } from '../views/nodes/bucketNode';
10 | import { GitRepositoryNode } from '../views/nodes/gitRepositoryNode';
11 | import { OCIRepositoryNode } from '../views/nodes/ociRepositoryNode';
12 | import { HelmRepositoryNode } from '../views/nodes/helmRepositoryNode';
13 | import { getCurrentClusterInfo, refreshSourcesTreeView, refreshWorkloadsTreeView } from '../views/treeViews';
14 |
15 | /**
16 | * Delete a source
17 | *
18 | * @param sourceNode Sources tree view node
19 | */
20 | export async function deleteSource(sourceNode: GitRepositoryNode | OCIRepositoryNode | HelmRepositoryNode | BucketNode) {
21 |
22 | const sourceName = sourceNode.resource.metadata.name || '';
23 | const sourceNamespace = sourceNode.resource.metadata.namespace || '';
24 | const confirmButton = 'Delete';
25 |
26 | const sourceType: FluxSource | 'unknown' = sourceNode.resource.kind === KubernetesObjectKinds.GitRepository ? 'source git' :
27 | sourceNode.resource.kind === KubernetesObjectKinds.HelmRepository ? 'source helm' :
28 | sourceNode.resource.kind === KubernetesObjectKinds.OCIRepository ? 'source oci' :
29 | sourceNode.resource.kind === KubernetesObjectKinds.Bucket ? 'source bucket' : 'unknown';
30 |
31 | if (sourceType === 'unknown') {
32 | window.showErrorMessage(`Unknown Source resource kind ${sourceNode.resource.kind}`);
33 | return;
34 | }
35 |
36 | const pressedButton = await window.showWarningMessage(`Do you want to delete ${sourceNode.resource.kind} "${sourceName}"?`, {
37 | modal: true,
38 | }, confirmButton);
39 | if (!pressedButton) {
40 | return;
41 | }
42 |
43 | telemetry.send(TelemetryEventNames.DeleteSource, {
44 | kind: sourceNode.resource.kind,
45 | });
46 |
47 | const currentClusterInfo = await getCurrentClusterInfo();
48 | if (failed(currentClusterInfo)) {
49 | return;
50 | }
51 |
52 | if (currentClusterInfo.result.isAzure) {
53 | await azureTools.deleteSource(sourceName, currentClusterInfo.result.contextName, currentClusterInfo.result.clusterProvider as AzureClusterProvider);
54 | refreshWorkloadsTreeView();
55 | } else {
56 | await fluxTools.delete(sourceType, sourceName, sourceNamespace);
57 | }
58 |
59 | refreshSourcesTreeView();
60 | }
61 |
--------------------------------------------------------------------------------
/src/commands/deleteWorkload.ts:
--------------------------------------------------------------------------------
1 | import { window } from 'vscode';
2 | import { AzureClusterProvider, azureTools, isAzureProvider } from '../azure/azureTools';
3 | import { failed } from '../errorable';
4 | import { telemetry } from '../extension';
5 | import { fluxTools } from '../flux/fluxTools';
6 | import { FluxWorkload } from '../flux/fluxTypes';
7 | import { KubernetesObjectKinds } from '../kubernetes/types/kubernetesTypes';
8 | import { TelemetryEventNames } from '../telemetry';
9 | import { KustomizationNode } from '../views/nodes/kustomizationNode';
10 | import { HelmReleaseNode } from '../views/nodes/helmReleaseNode';
11 | import { getCurrentClusterInfo, refreshWorkloadsTreeView } from '../views/treeViews';
12 |
13 |
14 | /**
15 | * Delete a workload
16 | *
17 | * @param workloadNode Workloads tree view node
18 | */
19 | export async function deleteWorkload(workloadNode: KustomizationNode | HelmReleaseNode) {
20 |
21 | const workloadName = workloadNode.resource.metadata.name || '';
22 | const workloadNamespace = workloadNode.resource.metadata.namespace || '';
23 | const confirmButton = 'Delete';
24 |
25 | let workloadType: FluxWorkload;
26 | switch(workloadNode.resource.kind) {
27 | case KubernetesObjectKinds.Kustomization: {
28 | workloadType = 'kustomization';
29 | break;
30 | }
31 |
32 | case KubernetesObjectKinds.HelmRelease: {
33 | workloadType = 'helmrelease';
34 | break;
35 | }
36 |
37 | default: {
38 | return;
39 | }
40 | }
41 |
42 | const currentClusterInfo = await getCurrentClusterInfo();
43 | if (failed(currentClusterInfo)) {
44 | return;
45 | }
46 |
47 | if (currentClusterInfo.result.isAzure && workloadType !== 'kustomization') {
48 | window.showWarningMessage('Delete HelmRelease not supported on Azure cluster.');
49 | return;
50 | }
51 |
52 |
53 |
54 | const pressedButton = await window.showWarningMessage(`Do you want to delete ${workloadNode.resource.kind} "${workloadName}"?`, {
55 | modal: true,
56 | }, confirmButton);
57 | if (!pressedButton) {
58 | return;
59 | }
60 |
61 | telemetry.send(TelemetryEventNames.DeleteWorkload, {
62 | kind: workloadNode.resource.kind,
63 | });
64 |
65 |
66 | if (currentClusterInfo.result.isAzure && workloadType === 'kustomization') {
67 | const fluxConfigName = (workloadNode.resource.spec as any).sourceRef?.name;
68 | const azResourceName = azureTools.getAzName(fluxConfigName, workloadName);
69 | await azureTools.deleteKustomization(fluxConfigName, azResourceName, currentClusterInfo.result.contextName, currentClusterInfo.result.clusterProvider as AzureClusterProvider);
70 | } else {
71 | await fluxTools.delete(workloadType, workloadName, workloadNamespace);
72 | }
73 |
74 | refreshWorkloadsTreeView();
75 | }
76 |
--------------------------------------------------------------------------------
/src/views/dataProviders/sourceDataProvider.ts:
--------------------------------------------------------------------------------
1 | import { ContextTypes, setVSCodeContext } from '../../vscodeContext';
2 | import { kubernetesTools } from '../../kubernetes/kubernetesTools';
3 | import { statusBar } from '../../statusBar';
4 | import { BucketNode } from '../nodes/bucketNode';
5 | import { GitRepositoryNode } from '../nodes/gitRepositoryNode';
6 | import { OCIRepositoryNode } from '../nodes/ociRepositoryNode';
7 | import { HelmRepositoryNode } from '../nodes/helmRepositoryNode';
8 | import { SourceNode } from '../nodes/sourceNode';
9 | import { DataProvider } from './dataProvider';
10 | import { sortByMetadataName } from '../../kubernetes/kubernetesUtils';
11 | import { NamespaceNode } from '../nodes/namespaceNode';
12 |
13 | /**
14 | * Defines Sources data provider for loading Git/Helm repositories
15 | * and Buckets in GitOps Sources tree view.
16 | */
17 | export class SourceDataProvider extends DataProvider {
18 |
19 | /**
20 | * Creates Source tree view items for the currently selected kubernetes cluster.
21 | * @returns Source tree view items to display.
22 | */
23 | async buildTree(): Promise {
24 | statusBar.startLoadingTree();
25 |
26 | const treeItems: SourceNode[] = [];
27 |
28 | setVSCodeContext(ContextTypes.LoadingSources, true);
29 |
30 | // Fetch all sources asynchronously and at once
31 | const [gitRepositories, ociRepositories, helmRepositories, buckets, namespaces] = await Promise.all([
32 | kubernetesTools.getGitRepositories(),
33 | kubernetesTools.getOciRepositories(),
34 | kubernetesTools.getHelmRepositories(),
35 | kubernetesTools.getBuckets(),
36 | kubernetesTools.getNamespaces(),
37 | ]);
38 |
39 | // add git repositories to the tree
40 | if (gitRepositories) {
41 | for (const gitRepository of sortByMetadataName(gitRepositories.items)) {
42 | treeItems.push(new GitRepositoryNode(gitRepository));
43 | }
44 | }
45 |
46 | // add oci repositories to the tree
47 | if (ociRepositories) {
48 | for (const ociRepository of sortByMetadataName(ociRepositories.items)) {
49 | treeItems.push(new OCIRepositoryNode(ociRepository));
50 | }
51 | }
52 |
53 | // add helm repositores to the tree
54 | if (helmRepositories) {
55 | for (const helmRepository of sortByMetadataName(helmRepositories.items)) {
56 | treeItems.push(new HelmRepositoryNode(helmRepository));
57 | }
58 | }
59 |
60 | // add buckets to the tree
61 | if (buckets) {
62 | for (const bucket of sortByMetadataName(buckets.items)) {
63 | treeItems.push(new BucketNode(bucket));
64 | }
65 | }
66 |
67 | setVSCodeContext(ContextTypes.LoadingSources, false);
68 | setVSCodeContext(ContextTypes.NoSources, treeItems.length === 0);
69 | statusBar.stopLoadingTree();
70 |
71 | return this.groupByNamespace(namespaces?.items || [], treeItems);
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/webview-ui/configureGitOps/src/lib/utils/vscode.ts:
--------------------------------------------------------------------------------
1 | import type { WebviewApi } from 'vscode-webview';
2 |
3 | /**
4 | * A utility wrapper around the acquireVsCodeApi() function, which enables
5 | * message passing and state management between the webview and extension
6 | * contexts.
7 | *
8 | * This utility also enables webview code to be run in a web browser-based
9 | * dev server by using native web browser features that mock the functionality
10 | * enabled by acquireVsCodeApi.
11 | */
12 | class VSCodeAPIWrapper {
13 | public readonly vsCodeApi: WebviewApi | undefined;
14 |
15 | constructor() {
16 | // Check if the acquireVsCodeApi function exists in the current development
17 | // context (i.e. VS Code development window or web browser)
18 | if (typeof acquireVsCodeApi === 'function') {
19 | this.vsCodeApi = acquireVsCodeApi();
20 | }
21 | }
22 |
23 | /**
24 | * Post a message (i.e. send arbitrary data) to the owner of the webview.
25 | *
26 | * @remarks When running webview code inside a web browser, postMessage will instead
27 | * log the given message to the console.
28 | *
29 | * @param message Abitrary data (must be JSON serializable) to send to the extension context.
30 | */
31 | public postMessage(message: unknown) {
32 | if (this.vsCodeApi) {
33 | this.vsCodeApi.postMessage(message);
34 | } else {
35 | console.log(message);
36 | }
37 | }
38 |
39 | /**
40 | * Get the persistent state stored for this webview.
41 | *
42 | * @remarks When running webview source code inside a web browser, getState will retrieve state
43 | * from local storage (https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage).
44 | *
45 | * @return The current state or `undefined` if no state has been set.
46 | */
47 | public getState(): unknown | undefined {
48 | if (this.vsCodeApi) {
49 | return this.vsCodeApi.getState();
50 | } else {
51 | const state = localStorage.getItem('vscodeState');
52 | return state ? JSON.parse(state) : undefined;
53 | }
54 | }
55 |
56 | /**
57 | * Set the persistent state stored for this webview.
58 | *
59 | * @remarks When running webview source code inside a web browser, setState will set the given
60 | * state using local storage (https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage).
61 | *
62 | * @param newState New persisted state. This must be a JSON serializable object. Can be retrieved
63 | * using {@link getState}.
64 | *
65 | * @return The new state.
66 | */
67 | public setState(newState: T): T {
68 | if (this.vsCodeApi) {
69 | return this.vsCodeApi.setState(newState);
70 | } else {
71 | localStorage.setItem('vscodeState', JSON.stringify(newState));
72 | return newState;
73 | }
74 | }
75 | }
76 |
77 | // Exports class singleton to prevent multiple invocations of acquireVsCodeApi.
78 | export const vscode = new VSCodeAPIWrapper();
79 |
--------------------------------------------------------------------------------
/webview-ui/createFromTemplate/src/lib/utils/vscode.ts:
--------------------------------------------------------------------------------
1 | import type { WebviewApi } from 'vscode-webview';
2 |
3 | /**
4 | * A utility wrapper around the acquireVsCodeApi() function, which enables
5 | * message passing and state management between the webview and extension
6 | * contexts.
7 | *
8 | * This utility also enables webview code to be run in a web browser-based
9 | * dev server by using native web browser features that mock the functionality
10 | * enabled by acquireVsCodeApi.
11 | */
12 | class VSCodeAPIWrapper {
13 | public readonly vsCodeApi: WebviewApi | undefined;
14 |
15 | constructor() {
16 | // Check if the acquireVsCodeApi function exists in the current development
17 | // context (i.e. VS Code development window or web browser)
18 | if (typeof acquireVsCodeApi === 'function') {
19 | this.vsCodeApi = acquireVsCodeApi();
20 | }
21 | }
22 |
23 | /**
24 | * Post a message (i.e. send arbitrary data) to the owner of the webview.
25 | *
26 | * @remarks When running webview code inside a web browser, postMessage will instead
27 | * log the given message to the console.
28 | *
29 | * @param message Abitrary data (must be JSON serializable) to send to the extension context.
30 | */
31 | public postMessage(message: unknown) {
32 | if (this.vsCodeApi) {
33 | this.vsCodeApi.postMessage(message);
34 | } else {
35 | console.log(message);
36 | }
37 | }
38 |
39 | /**
40 | * Get the persistent state stored for this webview.
41 | *
42 | * @remarks When running webview source code inside a web browser, getState will retrieve state
43 | * from local storage (https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage).
44 | *
45 | * @return The current state or `undefined` if no state has been set.
46 | */
47 | public getState(): unknown | undefined {
48 | if (this.vsCodeApi) {
49 | return this.vsCodeApi.getState();
50 | } else {
51 | const state = localStorage.getItem('vscodeState');
52 | return state ? JSON.parse(state) : undefined;
53 | }
54 | }
55 |
56 | /**
57 | * Set the persistent state stored for this webview.
58 | *
59 | * @remarks When running webview source code inside a web browser, setState will set the given
60 | * state using local storage (https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage).
61 | *
62 | * @param newState New persisted state. This must be a JSON serializable object. Can be retrieved
63 | * using {@link getState}.
64 | *
65 | * @return The new state.
66 | */
67 | public setState(newState: T): T {
68 | if (this.vsCodeApi) {
69 | return this.vsCodeApi.setState(newState);
70 | } else {
71 | localStorage.setItem('vscodeState', JSON.stringify(newState));
72 | return newState;
73 | }
74 | }
75 | }
76 |
77 | // Exports class singleton to prevent multiple invocations of acquireVsCodeApi.
78 | export const vscode = new VSCodeAPIWrapper();
79 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "@typescript-eslint/parser",
4 | "parserOptions": {
5 | "ecmaVersion": 2019,
6 | "sourceType": "module"
7 | },
8 | "plugins": [
9 | "@typescript-eslint"
10 | ],
11 | "extends": [
12 | "plugin:@typescript-eslint/recommended"
13 | ],
14 | "rules": {
15 | "@typescript-eslint/indent": [
16 | "warn",
17 | "tab",
18 | {
19 | "SwitchCase": 1
20 | }
21 | ],
22 | "@typescript-eslint/semi": "warn",
23 | "@typescript-eslint/naming-convention": [
24 | "warn",
25 | {
26 | "selector": "enumMember",
27 | "format": [
28 | "PascalCase",
29 | "UPPER_CASE"
30 | ]
31 | }
32 | ],
33 | "@typescript-eslint/comma-dangle": [
34 | "warn",
35 | {
36 | "arrays": "always-multiline",
37 | "objects": "always-multiline",
38 | "imports": "never",
39 | "exports": "never",
40 | "functions": "always-multiline",
41 | "enums": "always-multiline"
42 | }
43 | ],
44 | "@typescript-eslint/member-delimiter-style": [
45 | "warn",
46 | {
47 | "multiline": {
48 | "delimiter": "semi",
49 | "requireLast": true
50 | },
51 | "singleline": {
52 | "delimiter": "semi",
53 | "requireLast": true
54 | }
55 | }
56 | ],
57 | "@typescript-eslint/array-type": [
58 | "warn",
59 | {
60 | "default": "array"
61 | }
62 | ],
63 | "@typescript-eslint/type-annotation-spacing": [
64 | "warn",
65 | {
66 | "before": false,
67 | "after": true
68 | }
69 | ],
70 | "@typescript-eslint/no-shadow": "warn",
71 | "@typescript-eslint/no-non-null-assertion": "off",
72 | "@typescript-eslint/func-call-spacing": "warn",
73 | "@typescript-eslint/prefer-for-of": "warn",
74 | "@typescript-eslint/no-inferrable-types": "warn",
75 | "@typescript-eslint/explicit-module-boundary-types": "off",
76 | "@typescript-eslint/no-empty-function": "off",
77 | "@typescript-eslint/no-unused-vars": "off",
78 | "@typescript-eslint/no-explicit-any": "off",
79 | "@typescript-eslint/ban-ts-comment": "off",
80 | "eol-last": [
81 | "warn",
82 | "always"
83 | ],
84 | "quotes": [
85 | "warn",
86 | "single"
87 | ],
88 | "curly": "warn",
89 | "space-infix-ops": "warn",
90 | "prefer-template": "warn",
91 | "arrow-spacing": [
92 | "warn",
93 | {
94 | "before": true,
95 | "after": true
96 | }
97 | ],
98 | "arrow-parens": [
99 | "warn",
100 | "as-needed"
101 | ],
102 | "arrow-body-style": [
103 | "warn",
104 | "as-needed"
105 | ],
106 | "eqeqeq": "warn",
107 | "brace-style": "warn",
108 | "no-throw-literal": "warn",
109 | "prefer-const": "off",
110 | "semi": "off",
111 | "indent": "off",
112 | "comma-dangle": "off",
113 | "no-shadow": "off"
114 | },
115 | "ignorePatterns": [
116 | "out",
117 | "dist",
118 | "**/*.d.ts"
119 | ]
120 | }
121 |
--------------------------------------------------------------------------------
/src/views/dataProviders/dataProvider.ts:
--------------------------------------------------------------------------------
1 | import { Event, EventEmitter, TreeDataProvider, TreeItem } from 'vscode';
2 | import { Namespace } from '../../kubernetes/types/kubernetesTypes';
3 | import { NamespaceNode } from '../nodes/namespaceNode';
4 | import { TreeNode } from '../nodes/treeNode';
5 |
6 | /**
7 | * Defines tree view data provider base class for all GitOps tree views.
8 | */
9 | export class DataProvider implements TreeDataProvider {
10 | private treeItems: TreeItem[] | null = null;
11 | private _onDidChangeTreeData: EventEmitter = new EventEmitter();
12 | readonly onDidChangeTreeData: Event = this._onDidChangeTreeData.event;
13 |
14 | /**
15 | * Reloads tree view item and its children.
16 | * @param treeItem Tree item to refresh.
17 | */
18 | public refresh(treeItem?: TreeItem) {
19 | if (!treeItem) {
20 | // Only clear all root nodes when no node was passed
21 | this.treeItems = null;
22 | }
23 | this._onDidChangeTreeData.fire(treeItem);
24 | }
25 |
26 | /**
27 | * Gets tree view item for the specified tree element.
28 | * @param element Tree element.
29 | * @returns Tree view item.
30 | */
31 | public getTreeItem(element: TreeItem): TreeItem {
32 | return element;
33 | }
34 |
35 | /**
36 | * Gets tree element parent.
37 | * @param element Tree item to get parent for.
38 | * @returns Parent tree item or null for the top level nodes.
39 | */
40 | public getParent(element: TreeItem): TreeItem | null {
41 | if (element instanceof TreeNode && element.parent) {
42 | return element.parent;
43 | }
44 | return null;
45 | }
46 |
47 | /**
48 | * Gets children for the specified tree element.
49 | * Creates new tree view items for the root node.
50 | * @param element The tree element to get children for.
51 | * @returns Tree element children or empty array.
52 | */
53 | public async getChildren(element?: TreeItem): Promise {
54 | if (!this.treeItems) {
55 | this.treeItems = await this.buildTree();
56 | }
57 |
58 | if (element instanceof TreeNode) {
59 | return element.children;
60 | }
61 |
62 | if (!element && this.treeItems) {
63 | return this.treeItems;
64 | }
65 |
66 | return [];
67 | }
68 |
69 | /**
70 | * Creates initial tree view items collection.
71 | * @returns
72 | */
73 | buildTree(): Promise {
74 | return Promise.resolve([]);
75 | }
76 |
77 | groupByNamespace(namespaces: Namespace[], nodes: TreeNode[]): NamespaceNode[] {
78 | const namespaceNodes: NamespaceNode[] = [];
79 |
80 | namespaces.forEach(ns => {
81 | const name = ns.metadata.name;
82 |
83 | const nsChildNodes = nodes.filter(node => node.resource?.metadata?.namespace === name);
84 | if(nsChildNodes.length > 0) {
85 | const nsNode = new NamespaceNode(ns);
86 | nsChildNodes.forEach(childNode => nsNode.addChild(childNode));
87 | namespaceNodes.push(nsNode);
88 | }
89 | });
90 |
91 | return namespaceNodes;
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/commands/createSource.ts:
--------------------------------------------------------------------------------
1 | import gitUrlParse from 'git-url-parse';
2 | import { commands, env, Uri, window } from 'vscode';
3 | import { azureTools } from '../azure/azureTools';
4 | import { CommandId } from '../commands';
5 |
6 |
7 | /**
8 | * Show notifications reminding users to add a public key
9 | * to the git repository (when the url uses SSH protocol).
10 | */
11 | export function showDeployKeyNotificationIfNeeded(url: string, deployKey?: string) {
12 | if (!deployKey) {
13 | return;
14 | }
15 |
16 | const parsedGitUrl = gitUrlParse(url);
17 | const isSSH = parsedGitUrl.protocol === 'ssh';
18 | const isGitHub = isUrlSourceGitHub(parsedGitUrl.source);
19 | const isAzureDevops = isUrlSourceAzureDevops(parsedGitUrl.source);
20 |
21 | if (isSSH && deployKey) {
22 | if (isGitHub) {
23 | showDeployKeysPageNotification(Uri.parse(deployKeysGitHubPage(url)));
24 | } else if (isAzureDevops) {
25 | showDeployKeysPageNotification(Uri.parse(deployKeysAzureDevopsPage(url)));
26 | }
27 | showDeployKeyNotification(deployKey);
28 | }
29 | }
30 |
31 | /**
32 | * Transform an url from `git@github.com:usernamehw/sample-k8s.git` to
33 | * `ssh://git@github.com/usernamehw/sample-k8s`
34 | * @param gitUrl target git url
35 | */
36 | export function makeSSHUrlFromGitUrl(gitUrl: string): string {
37 | if (gitUrl.startsWith('ssh')) {
38 | return gitUrl;
39 | }
40 |
41 | const parsedGitUrl = gitUrlParse(gitUrl);
42 |
43 | return `ssh://${parsedGitUrl.user}@${parsedGitUrl.resource}${parsedGitUrl.pathname}`;
44 | }
45 | /**
46 | * Make a link to the "Deploy keys" page for
47 | * the provided GitHub repository url.
48 | * @param GitHub repository url
49 | */
50 | export function deployKeysGitHubPage(repoUrl: string) {
51 | const parsedGitUrl = gitUrlParse(repoUrl);
52 | return `https://github.com/${parsedGitUrl.owner}/${parsedGitUrl.name}/settings/keys`;
53 | }
54 | /**
55 | * Make a link to the "SSH Public Keys" page for
56 | * the provided Azure Devops repository url.
57 | * @param GitHub repository url
58 | */
59 | export function deployKeysAzureDevopsPage(repoUrl: string) {
60 | const parsedGitUrl = gitUrlParse(repoUrl);
61 | return `https://dev.azure.com/${parsedGitUrl.name}/_usersSettings/keys`;
62 | }
63 |
64 | export async function showDeployKeyNotification(deployKey: string) {
65 | const copyButton = 'Copy';
66 | const confirm = await window.showInformationMessage(`Add deploy key to the repository: ${deployKey}`, copyButton);
67 | if (confirm === copyButton) {
68 | env.clipboard.writeText(deployKey);
69 | }
70 | }
71 |
72 | export async function showDeployKeysPageNotification(uri: Uri) {
73 | const deployKeysButton = 'Open';
74 | const confirm = await window.showInformationMessage('Open repository "Deploy keys" page', deployKeysButton);
75 | if (confirm === deployKeysButton) {
76 | commands.executeCommand(CommandId.VSCodeOpen, uri);
77 | }
78 | }
79 |
80 | export function isUrlSourceAzureDevops(urlSource: string) {
81 | return urlSource === 'dev.azure.com';
82 | }
83 | export function isUrlSourceGitHub(urlSource: string) {
84 | return urlSource === 'github.com';
85 | }
86 |
--------------------------------------------------------------------------------
/src/azure/azurePrereqs.ts:
--------------------------------------------------------------------------------
1 | import { window } from 'vscode';
2 | import { ClusterProvider } from '../kubernetes/types/kubernetesTypes';
3 | import { shell } from '../shell';
4 | import { AzureClusterProvider } from './azureTools';
5 |
6 | /**
7 | * Return true if all prerequisites for installing Azure cluster extension 'microsoft.flux' are ready
8 | */
9 | export async function checkAzurePrerequisites(clusterProvider: AzureClusterProvider): Promise {
10 | if(clusterProvider === ClusterProvider.AKS) {
11 | const results = await Promise.all([
12 | checkPrerequistesProviders(),
13 | checkPrerequistesCliExtensions(),
14 | checkPrerequistesFeatures(),
15 | ]);
16 |
17 | return (results[0] && results[1] && results[2]);
18 | } else {
19 | const results = await Promise.all([
20 | checkPrerequistesProviders(),
21 | checkPrerequistesCliExtensions(),
22 | ]);
23 |
24 | return (results[0] && results[1]);
25 | }
26 | }
27 |
28 | async function checkPrerequistesFeatures(): Promise {
29 | const result = await shell.execWithOutput('az feature show --namespace Microsoft.ContainerService -n AKS-ExtensionManager');
30 | const success = result.stdout.includes('"state": "Registered"');
31 |
32 | if(!success) {
33 | window.showWarningMessage('Missing Azure Prerequisite: Feature \'Microsoft.ContainerService\AKS-ExtensionManager\'', 'OK');
34 | }
35 |
36 | return success;
37 | }
38 |
39 | async function checkPrerequistesProviders(): Promise {
40 | const result = await shell.execWithOutput('az provider list -o table');
41 | const lines = result.stdout.replace(/\r\n/g,'\n').split('\n');
42 |
43 | let registeredCompontents = 0;
44 | for(let line of lines) {
45 | if(/^Microsoft.Kubernetes\b.*\bRegistered\b/.test(line)) {
46 | registeredCompontents++;
47 | } else {
48 | window.showWarningMessage('Missing Azure Prerequisite: Provider \'Microsoft.Kubernetes\'', 'OK');
49 | }
50 | if(/^Microsoft.KubernetesConfiguration\b.*\bRegistered\b/.test(line)) {
51 | registeredCompontents++;
52 | } else {
53 | window.showWarningMessage('Missing Azure Prerequisite: Provider \'Microsoft.KubernetesConfiguration\'', 'OK');
54 | }
55 | if(/^Microsoft.ContainerService\b.*\bRegistered\b/.test(line)) {
56 | registeredCompontents++;
57 | } else {
58 | window.showWarningMessage('Missing Azure Prerequisite: Provider \'Microsoft.ContainerService\'', 'OK');
59 | }
60 | }
61 |
62 | return registeredCompontents === 3;
63 | }
64 |
65 | async function checkPrerequistesCliExtensions(): Promise {
66 | const result = await shell.execWithOutput('az extension list -o table');
67 |
68 | const configurationSuccess = result.stdout.includes('k8s-configuration');
69 | if(!configurationSuccess) {
70 | window.showWarningMessage('Missing Azure Prerequisite: az CLI extension \'k8s-configuration\'', 'OK');
71 | }
72 |
73 | const extensionSuccess = result.stdout.includes('k8s-extension');
74 | if(!extensionSuccess) {
75 | window.showWarningMessage('Missing Azure Prerequisite: az CLI extension \'k8s-extension\'', 'OK');
76 | }
77 |
78 | return configurationSuccess && extensionSuccess;
79 | }
80 |
--------------------------------------------------------------------------------
/src/webview-backend/configureGitOps/openWebview.ts:
--------------------------------------------------------------------------------
1 | import { Uri, window, workspace } from 'vscode';
2 | import { failed } from '../../errorable';
3 | import { telemetry } from '../../extension';
4 | import { getExtensionContext } from '../../extensionContext';
5 | import { getFolderGitInfo, GitInfo } from '../../git/gitInfo';
6 | import { kubernetesTools } from '../../kubernetes/kubernetesTools';
7 | import { FluxSourceObject, namespacedObject } from '../../kubernetes/types/flux/object';
8 | import { ClusterProvider, KubernetesObject } from '../../kubernetes/types/kubernetesTypes';
9 | import { TelemetryEventNames } from '../../telemetry';
10 | import { getCurrentClusterInfo } from '../../views/treeViews';
11 | import { WebviewBackend } from '../WebviewBackend';
12 |
13 | import { ConfigureGitOpsWebviewParams } from '../types';
14 | import { receiveMessage } from './receiveMessage';
15 |
16 | let webview: WebviewBackend | undefined;
17 |
18 | /**
19 | * Open the webview editor with a form to enter all the flags
20 | * needed to create a source (and possibly Kustomization)
21 | */
22 | export async function openConfigureGitOpsWebview(selectSource: boolean, selectedSource?: FluxSourceObject | string, set?: any, gitInfo?: GitInfo) {
23 | telemetry.send(TelemetryEventNames.CreateSourceOpenWebview);
24 |
25 | const clusterInfo = await getCurrentClusterInfo();
26 | if (failed(clusterInfo)) {
27 | return;
28 | }
29 | if (clusterInfo.result.clusterProvider === ClusterProvider.Unknown) {
30 | window.showErrorMessage('Cluster provider is not detected ');
31 | return;
32 | }
33 | if (clusterInfo.result.clusterProvider === ClusterProvider.DetectionFailed) {
34 | window.showErrorMessage('Cluster provider detection failed.');
35 | return;
36 | }
37 |
38 | if (!gitInfo && workspace.workspaceFolders && workspace.workspaceFolders.length > 0) {
39 | // use the first open folder
40 | gitInfo = await getFolderGitInfo(workspace.workspaceFolders[0].uri.fsPath);
41 | }
42 |
43 | const [nsResults, gitResults, ociResults, bucketResults] = await Promise.all([kubernetesTools.getNamespaces(),
44 | kubernetesTools.getGitRepositories(),
45 | kubernetesTools.getOciRepositories(),
46 | kubernetesTools.getBuckets(),
47 | ]);
48 |
49 | const namespaces = nsResults?.items.map(i => i.metadata.name) as string[];
50 |
51 | const sources: KubernetesObject[] = [...gitResults?.items || [],
52 | ...ociResults?.items || [],
53 | ...bucketResults?.items || []];
54 |
55 | const selectedSourceName = typeof selectedSource === 'string' ? selectedSource : (namespacedObject(selectedSource) || '');
56 |
57 | const webviewParams: ConfigureGitOpsWebviewParams = {
58 | clusterInfo: clusterInfo.result,
59 | gitInfo,
60 | namespaces: namespaces,
61 | sources: sources,
62 | selectSourceTab: selectSource,
63 | selectedSource: selectedSourceName,
64 | set,
65 | };
66 |
67 |
68 | if(!webview || webview.disposed) {
69 | const extensionUri = getExtensionContext().extensionUri;
70 | const uri = Uri.joinPath(extensionUri, 'webview-ui', 'configureGitOps');
71 | webview = new WebviewBackend('Configure GitOps', uri, webviewParams, receiveMessage);
72 | } else {
73 | webview.update(webviewParams);
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/webview-backend/configureGitOps/lib/createAzure.ts:
--------------------------------------------------------------------------------
1 | import { AzureClusterProvider, azureTools, CreateSourceBucketAzureArgs, CreateSourceGitAzureArgs } from '../../../azure/azureTools';
2 | import { showDeployKeyNotificationIfNeeded } from '../../../commands/createSource';
3 | import { telemetry } from '../../../extension';
4 | import { ClusterInfo, KubernetesObjectKinds } from '../../../kubernetes/types/kubernetesTypes';
5 | import { TelemetryEventNames } from '../../../telemetry';
6 | import { ParamsDictionary } from '../../../utils/typeUtils';
7 | import { refreshSourcesTreeView, refreshWorkloadsTreeView } from '../../../views/treeViews';
8 |
9 | export async function createConfigurationAzure(data: ParamsDictionary) {
10 | const clusterInfo = data.clusterInfo as ClusterInfo;
11 | const source = data.source;
12 | const kustomization = data.kustomization;
13 |
14 | if(source) {
15 | if(source.kind === 'GitRepository') {
16 | createGitSourceAzure(source, kustomization, clusterInfo);
17 | } else if(source.kind === 'Bucket') {
18 | createBucketSourceAzure(source, kustomization, clusterInfo);
19 | }
20 |
21 | } else if(kustomization) {
22 | azureTools.createKustomization(kustomization.name, kustomization.source, kustomization.path,
23 | clusterInfo.contextName, clusterInfo.clusterProvider as AzureClusterProvider, kustomization.dependsOn, kustomization.prune);
24 | }
25 | }
26 |
27 | async function createGitSourceAzure(source: ParamsDictionary, kustomization: ParamsDictionary, clusterInfo: ClusterInfo) {
28 | const args = {
29 | sourceName: source.name,
30 | url: source.url,
31 | ...source,
32 | ...clusterInfo,
33 | kustomizationName: kustomization?.name,
34 | kustomizationPath: kustomization?.path,
35 | kustomizationDependsOn: kustomization?.dependsOn,
36 | kustomizationPrune: kustomization?.prune,
37 | } as CreateSourceGitAzureArgs;
38 |
39 |
40 | telemetry.send(TelemetryEventNames.CreateSource, {
41 | kind: KubernetesObjectKinds.GitRepository,
42 | });
43 |
44 | const deployKey = await azureTools.createSourceGit(args);
45 |
46 | setTimeout(() => {
47 | // Wait a bit for the repository to have a failed state in case of SSH url
48 | refreshSourcesTreeView();
49 | refreshWorkloadsTreeView();
50 | }, 1000);
51 |
52 | showDeployKeyNotificationIfNeeded(args.url, deployKey);
53 | }
54 |
55 |
56 | async function createBucketSourceAzure(source: ParamsDictionary, kustomization: ParamsDictionary, clusterInfo: ClusterInfo) {
57 | telemetry.send(TelemetryEventNames.CreateSource, {
58 | kind: KubernetesObjectKinds.Bucket,
59 | });
60 |
61 | const args: any = {
62 | sourceName: source.name,
63 | url: source.endpoint,
64 | configurationName: source.name,
65 | bucketName: source.bucketName,
66 | sourceScope: source.azureScope,
67 | sourceNamespace: source.namespace,
68 | ...source,
69 | ...clusterInfo,
70 | kustomizationName: kustomization?.name,
71 | kustomizationPath: kustomization?.name,
72 | kustomizationDependsOn: kustomization?.name,
73 | kustomizationPrune: kustomization?.prune,
74 | } as CreateSourceGitAzureArgs;
75 |
76 | await azureTools.createSourceBucket(args);
77 |
78 | setTimeout(() => {
79 | refreshSourcesTreeView();
80 | refreshWorkloadsTreeView();
81 | }, 1000);
82 | }
83 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/src/kubernetes/types/flux/bucket.ts:
--------------------------------------------------------------------------------
1 | import { Artifact, DeploymentCondition, KubernetesObject, KubernetesObjectKinds, ObjectMeta, ResultMetadata } from '../kubernetesTypes';
2 |
3 | /**
4 | * Buckets result from running
5 | * `kubectl get Bucket -A` command.
6 | */
7 | export interface BucketResult {
8 | readonly apiVersion: string;
9 | readonly kind: KubernetesObjectKinds.List;
10 | readonly items: Bucket[];
11 | readonly metadata: ResultMetadata;
12 | }
13 |
14 | /**
15 | * Bucket info object.
16 | */
17 | export interface Bucket extends KubernetesObject {
18 |
19 | // standard kubernetes object fields
20 | readonly apiVersion: string;
21 | readonly kind: KubernetesObjectKinds.Bucket;
22 | readonly metadata: ObjectMeta;
23 |
24 | /**
25 | * Bucket spec details.
26 | *
27 | * @see https://github.com/fluxcd/source-controller/blob/main/docs/api/source.md#bucketspec
28 | */
29 | readonly spec: {
30 |
31 | /**
32 | * The S3 compatible storage provider name, default ('generic')
33 | */
34 | readonly provider?: string;
35 |
36 | /**
37 | * The bucket name
38 | */
39 | readonly bucketName: string;
40 |
41 | /**
42 | * The bucket endpoint address
43 | */
44 | readonly endpoint: string;
45 |
46 | /**
47 | * Insecure allows connecting to a non-TLS S3 HTTP endpoint
48 | */
49 | readonly insecure?: boolean;
50 |
51 | /**
52 | * The bucket region
53 | */
54 | readonly region?: string;
55 |
56 | /**
57 | * The name of the secret containing authentication credentials for the Bucket
58 | */
59 | readonly secretRef?: { name?: string; };
60 |
61 | /**
62 | * The interval at which to check for bucket updates
63 | */
64 | readonly interval: string;
65 |
66 | /**
67 | * The timeout for download operations, defaults to 20s
68 | */
69 | readonly timeout?: string;
70 |
71 | /**
72 | * Ignore overrides the set of excluded patterns in the .sourceignore format
73 | * (which is the same as .gitignore). If not provided, a default will be used,
74 | * consult the documentation for your version to find out what those are.
75 | */
76 | readonly ignore?: string;
77 |
78 | /**
79 | * This flag tells the controller to suspend the reconciliation of this source
80 | */
81 | readonly suspend?: boolean;
82 | };
83 |
84 | /**
85 | * Bucket source status info.
86 | *
87 | * @see https://github.com/fluxcd/source-controller/blob/main/docs/api/source.md#bucketstatus
88 | */
89 | readonly status: {
90 |
91 | /**
92 | * ObservedGeneration is the last observed generation
93 | */
94 | readonly observedGeneration?: number;
95 |
96 | /**
97 | * Conditions holds the conditions for the Bucket
98 | */
99 | readonly conditions?: DeploymentCondition[];
100 |
101 | /**
102 | * URL is the download link for the artifact output of the last Bucket sync
103 | */
104 | readonly url?: string;
105 |
106 | /**
107 | * Artifact represents the output of the last successful Bucket sync
108 | */
109 | readonly artifact?: Artifact;
110 |
111 | /**
112 | * LastHandledReconcileAt is the last manual reconciliation request
113 | * (by annotating the Bucket) handled by the reconciler.
114 | */
115 | readonly lastHandledReconcileAt?: string;
116 | };
117 | }
118 |
--------------------------------------------------------------------------------
/src/kubernetes/types/flux/helmRepository.ts:
--------------------------------------------------------------------------------
1 | import { Artifact, DeploymentCondition, KubernetesObject, KubernetesObjectKinds, LocalObjectReference, ObjectMeta, ResultMetadata } from '../kubernetesTypes';
2 |
3 | /**
4 | * Helm repositories result from running
5 | * `kubectl get HelmRepository -A` command.
6 | */
7 | export interface HelmRepositoryResult {
8 | readonly apiVersion: string;
9 | readonly kind: KubernetesObjectKinds.List;
10 | readonly items: HelmRepository[];
11 | readonly metadata: ResultMetadata;
12 | }
13 |
14 | /**
15 | * Helm repository info object.
16 | */
17 | export interface HelmRepository extends KubernetesObject {
18 |
19 | // standard kubernetes object fields
20 | readonly apiVersion: string;
21 | readonly kind: KubernetesObjectKinds.HelmRepository;
22 | readonly metadata: ObjectMeta;
23 |
24 | /**
25 | * Helm repository spec details.
26 | *
27 | * @see https://github.com/fluxcd/source-controller/blob/main/docs/api/source.md#helmrepositoryspec
28 | */
29 | readonly spec: {
30 |
31 | /**
32 | * The Helm repository URL, a valid URL contains at least a protocol and host
33 | */
34 | readonly url: string;
35 |
36 | /**
37 | * The name of the secret containing authentication credentials for the Helm repository.
38 | * For HTTP/S basic auth the secret must contain username and password fields.
39 | * For TLS the secret must contain a certFile and keyFile, and/or caCert fields.
40 | */
41 | readonly secretRef?: LocalObjectReference;
42 |
43 | /**
44 | * PassCredentials allows the credentials from the SecretRef
45 | * to be passed on to a host that does not match the host as defined in URL.
46 | * This may be required if the host of the advertised chart URLs in the index
47 | * differ from the defined URL. Enabling this should be done with caution,
48 | * as it can potentially result in credentials getting stolen in a MITM-attack.
49 | */
50 | readonly passCredentials?: boolean;
51 |
52 | /**
53 | * The interval at which to check the upstream for updates
54 | */
55 | readonly interval: string;
56 |
57 | /**
58 | * The timeout of index downloading, defaults to 60s
59 | */
60 | readonly timeout?: string;
61 |
62 | /**
63 | * The type of HelmRepository, can be set to `oci` or `default`
64 | * (May be missing from older Flux installs)
65 | */
66 | readonly type?: string;
67 |
68 | /**
69 | * This flag tells the controller to suspend the reconciliation of this source
70 | */
71 | readonly suspend?: boolean;
72 | };
73 |
74 | /**
75 | * Helm repository status info.
76 | *
77 | * @see https://github.com/fluxcd/source-controller/blob/main/docs/api/source.md#helmrepositorystatus
78 | */
79 | readonly status: {
80 |
81 | /**
82 | * ObservedGeneration is the last observed generation
83 | */
84 | readonly observedGeneration?: number;
85 |
86 | /**
87 | * Conditions holds the conditions for the HelmRepository
88 | */
89 | readonly conditions?: DeploymentCondition[];
90 |
91 | /**
92 | * URL is the download link for the last index fetched
93 | */
94 | readonly url?: string;
95 |
96 | /**
97 | * Artifact represents the output of the last successful repository sync
98 | */
99 | readonly artifact?: Artifact;
100 | };
101 | }
102 |
--------------------------------------------------------------------------------