Merge pull request #70 from SanjulaGanepola/fix/linux-act-setup
Add support for fixing docker permissions in Linux environment
This commit is contained in:
15
package.json
15
package.json
@@ -156,6 +156,12 @@
|
|||||||
"title": "Start",
|
"title": "Start",
|
||||||
"icon": "$(debug-start)"
|
"icon": "$(debug-start)"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"category": "GitHub Local Actions",
|
||||||
|
"command": "githubLocalActions.fixPermissions",
|
||||||
|
"title": "Fix Permissions",
|
||||||
|
"icon": "$(unlock)"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"category": "GitHub Local Actions",
|
"category": "GitHub Local Actions",
|
||||||
"command": "githubLocalActions.runAllWorkflows",
|
"command": "githubLocalActions.runAllWorkflows",
|
||||||
@@ -366,6 +372,10 @@
|
|||||||
"command": "githubLocalActions.startComponent",
|
"command": "githubLocalActions.startComponent",
|
||||||
"when": "never"
|
"when": "never"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"command": "githubLocalActions.fixPermissions",
|
||||||
|
"when": "never"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"command": "githubLocalActions.runAllWorkflows",
|
"command": "githubLocalActions.runAllWorkflows",
|
||||||
"when": "never"
|
"when": "never"
|
||||||
@@ -555,6 +565,11 @@
|
|||||||
"when": "view == components && viewItem =~ /^githubLocalActions.component_Not Running.*/",
|
"when": "view == components && viewItem =~ /^githubLocalActions.component_Not Running.*/",
|
||||||
"group": "inline@1"
|
"group": "inline@1"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"command": "githubLocalActions.fixPermissions",
|
||||||
|
"when": "view == components && viewItem =~ /^githubLocalActions.component_Invalid Permissions.*/",
|
||||||
|
"group": "inline@1"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"command": "githubLocalActions.runAllWorkflows",
|
"command": "githubLocalActions.runAllWorkflows",
|
||||||
"when": "view == workflows && viewItem =~ /^githubLocalActions.workspaceFolderWorkflows.*/ && workspaceFolderCount > 1",
|
"when": "view == workflows && viewItem =~ /^githubLocalActions.workspaceFolderWorkflows.*/ && workspaceFolderCount > 1",
|
||||||
|
|||||||
37
src/act.ts
37
src/act.ts
@@ -2,7 +2,7 @@ import * as childProcess from "child_process";
|
|||||||
import * as fs from "fs/promises";
|
import * as fs from "fs/promises";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import sanitize from "sanitize-filename";
|
import sanitize from "sanitize-filename";
|
||||||
import { CustomExecution, env, EventEmitter, ExtensionContext, Pseudoterminal, ShellExecution, TaskDefinition, TaskGroup, TaskPanelKind, TaskRevealKind, tasks, TaskScope, TerminalDimensions, Uri, window, workspace, WorkspaceFolder } from "vscode";
|
import { commands, CustomExecution, env, EventEmitter, ExtensionContext, Pseudoterminal, ShellExecution, TaskDefinition, TaskGroup, TaskPanelKind, TaskRevealKind, tasks, TaskScope, TerminalDimensions, Uri, window, workspace, WorkspaceFolder } from "vscode";
|
||||||
import { ComponentsManager } from "./componentsManager";
|
import { ComponentsManager } from "./componentsManager";
|
||||||
import { ConfigurationManager, Platform, Section } from "./configurationManager";
|
import { ConfigurationManager, Platform, Section } from "./configurationManager";
|
||||||
import { componentsTreeDataProvider, historyTreeDataProvider } from './extension';
|
import { componentsTreeDataProvider, historyTreeDataProvider } from './extension';
|
||||||
@@ -70,8 +70,8 @@ export interface CommandArgs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class Act {
|
export class Act {
|
||||||
static command: string = 'act';
|
static defaultActCommand: string = 'act';
|
||||||
static githubCliCommand: string = 'gh act';
|
static githubCliActCommand: string = 'gh act';
|
||||||
context: ExtensionContext;
|
context: ExtensionContext;
|
||||||
storageManager: StorageManager;
|
storageManager: StorageManager;
|
||||||
secretManager: SecretManager;
|
secretManager: SecretManager;
|
||||||
@@ -127,6 +127,7 @@ export class Act {
|
|||||||
this.installationCommands = {
|
this.installationCommands = {
|
||||||
'Homebrew': 'brew install act',
|
'Homebrew': 'brew install act',
|
||||||
'Nix': 'nix run nixpkgs#act',
|
'Nix': 'nix run nixpkgs#act',
|
||||||
|
'Arch': 'pacman -Syu act',
|
||||||
'AUR': 'yay -Syu act',
|
'AUR': 'yay -Syu act',
|
||||||
'COPR': 'dnf copr enable goncalossilva/act && dnf install act-cli',
|
'COPR': 'dnf copr enable goncalossilva/act && dnf install act-cli',
|
||||||
'GitHub CLI': '(gh auth status || gh auth login) && gh extension install https://github.com/nektos/gh-act'
|
'GitHub CLI': '(gh auth status || gh auth login) && gh extension install https://github.com/nektos/gh-act'
|
||||||
@@ -174,20 +175,29 @@ export class Act {
|
|||||||
tasks.onDidEndTaskProcess(async e => {
|
tasks.onDidEndTaskProcess(async e => {
|
||||||
const taskDefinition = e.execution.task.definition;
|
const taskDefinition = e.execution.task.definition;
|
||||||
if (taskDefinition.type === 'nektos/act installation' && e.exitCode === 0) {
|
if (taskDefinition.type === 'nektos/act installation' && e.exitCode === 0) {
|
||||||
// Update base act command based on installation method
|
this.updateActCommand(taskDefinition.ghCliInstall ? Act.githubCliActCommand : Act.defaultActCommand);
|
||||||
if (taskDefinition.ghCliInstall) {
|
|
||||||
await ConfigurationManager.set(Section.actCommand, Act.githubCliCommand);
|
|
||||||
} else {
|
|
||||||
await ConfigurationManager.set(Section.actCommand, Act.command);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentsTreeDataProvider.refresh();
|
componentsTreeDataProvider.refresh();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static getActCommand() {
|
static getActCommand() {
|
||||||
return ConfigurationManager.get<string>(Section.actCommand) || Act.command;
|
return ConfigurationManager.get<string>(Section.actCommand) || Act.defaultActCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateActCommand(newActCommand: string) {
|
||||||
|
const actCommand = ConfigurationManager.get(Section.actCommand);
|
||||||
|
|
||||||
|
if (newActCommand !== actCommand) {
|
||||||
|
window.showInformationMessage(`The act command is currently set to "${actCommand}". Once the installation is complete, it is recommended to update this to "${newActCommand}" for this selected installation method.`, 'Proceed', 'Manually Edit').then(async value => {
|
||||||
|
if (value === 'Proceed') {
|
||||||
|
await ConfigurationManager.set(Section.actCommand, newActCommand);
|
||||||
|
componentsTreeDataProvider.refresh();
|
||||||
|
} else if (value === 'Manually Edit') {
|
||||||
|
await commands.executeCommand('workbench.action.openSettings', ConfigurationManager.getSearchTerm(Section.actCommand));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async runAllWorkflows(workspaceFolder: WorkspaceFolder) {
|
async runAllWorkflows(workspaceFolder: WorkspaceFolder) {
|
||||||
@@ -454,7 +464,10 @@ export class Act {
|
|||||||
await tasks.executeTask({
|
await tasks.executeTask({
|
||||||
name: 'nektos/act',
|
name: 'nektos/act',
|
||||||
detail: 'Install nektos/act',
|
detail: 'Install nektos/act',
|
||||||
definition: { type: 'nektos/act installation', ghCliInstall: command.includes('gh-act') },
|
definition: {
|
||||||
|
type: 'nektos/act installation',
|
||||||
|
ghCliInstall: command.includes('gh-act')
|
||||||
|
},
|
||||||
source: 'GitHub Local Actions',
|
source: 'GitHub Local Actions',
|
||||||
scope: TaskScope.Workspace,
|
scope: TaskScope.Workspace,
|
||||||
isBackground: true,
|
isBackground: true,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export interface Component<T extends CliStatus | ExtensionStatus> {
|
|||||||
information: string,
|
information: string,
|
||||||
installation: () => Promise<void>,
|
installation: () => Promise<void>,
|
||||||
start?: () => Promise<void>,
|
start?: () => Promise<void>,
|
||||||
|
fixPermissions?: () => Promise<void>,
|
||||||
message?: string
|
message?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,7 +23,8 @@ export enum CliStatus {
|
|||||||
Installed = 'Installed',
|
Installed = 'Installed',
|
||||||
NotInstalled = 'Not Installed',
|
NotInstalled = 'Not Installed',
|
||||||
Running = 'Running',
|
Running = 'Running',
|
||||||
NotRunning = 'Not Running'
|
NotRunning = 'Not Running',
|
||||||
|
InvalidPermissions = 'Invalid Permissions'
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ExtensionStatus {
|
export enum ExtensionStatus {
|
||||||
@@ -31,10 +33,13 @@ export enum ExtensionStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ComponentsManager {
|
export class ComponentsManager {
|
||||||
|
static actVersionRegExp: RegExp = /act version (.+)/;
|
||||||
|
static dockerVersionRegExp: RegExp = /Docker Engine Version:\s(.+)/;
|
||||||
|
|
||||||
async getComponents(): Promise<Component<CliStatus | ExtensionStatus>[]> {
|
async getComponents(): Promise<Component<CliStatus | ExtensionStatus>[]> {
|
||||||
const components: Component<CliStatus | ExtensionStatus>[] = [];
|
const components: Component<CliStatus | ExtensionStatus>[] = [];
|
||||||
|
|
||||||
const actCliInfo = await this.getCliInfo(`${Act.getActCommand()} --version`, /act version (.+)/, false, false);
|
const actCliInfo = await this.getCliInfo(`${Act.getActCommand()} --version`, ComponentsManager.actVersionRegExp, false, false);
|
||||||
components.push({
|
components.push({
|
||||||
name: 'nektos/act',
|
name: 'nektos/act',
|
||||||
icon: 'terminal',
|
icon: 'terminal',
|
||||||
@@ -110,6 +115,8 @@ export class ComponentsManager {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
act.updateActCommand(Act.defaultActCommand);
|
||||||
} else if (selectedInstallationMethod.link) {
|
} else if (selectedInstallationMethod.link) {
|
||||||
await env.openExternal(Uri.parse(selectedInstallationMethod.link));
|
await env.openExternal(Uri.parse(selectedInstallationMethod.link));
|
||||||
window.showInformationMessage('Once nektos/act is successfully installed, add it to your shell\'s PATH and then refresh the components view.', 'Refresh').then(async value => {
|
window.showInformationMessage('Once nektos/act is successfully installed, add it to your shell\'s PATH and then refresh the components view.', 'Refresh').then(async value => {
|
||||||
@@ -117,6 +124,8 @@ export class ComponentsManager {
|
|||||||
componentsTreeDataProvider.refresh();
|
componentsTreeDataProvider.refresh();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
act.updateActCommand(Act.defaultActCommand);
|
||||||
} else {
|
} else {
|
||||||
await act.install(selectedInstallationMethod.label);
|
await act.install(selectedInstallationMethod.label);
|
||||||
}
|
}
|
||||||
@@ -124,7 +133,7 @@ export class ComponentsManager {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const dockerCliInfo = await this.getCliInfo('docker version', /Client:\n.+\n\sVersion:\s+(.+)/, true, true);
|
const dockerCliInfo = await this.getCliInfo(`docker version --format "Docker Engine Version: {{.Client.Version}}"`, ComponentsManager.dockerVersionRegExp, true, true);
|
||||||
const dockerDesktopPath = ConfigurationManager.get<string>(Section.dockerDesktopPath);
|
const dockerDesktopPath = ConfigurationManager.get<string>(Section.dockerDesktopPath);
|
||||||
components.push({
|
components.push({
|
||||||
name: 'Docker Engine',
|
name: 'Docker Engine',
|
||||||
@@ -146,7 +155,9 @@ export class ComponentsManager {
|
|||||||
await tasks.executeTask({
|
await tasks.executeTask({
|
||||||
name: 'Docker Engine',
|
name: 'Docker Engine',
|
||||||
detail: 'Start Docker Engine',
|
detail: 'Start Docker Engine',
|
||||||
definition: { type: 'GitHub Local Actions' },
|
definition: {
|
||||||
|
type: 'Start Docker Engine'
|
||||||
|
},
|
||||||
source: 'GitHub Local Actions',
|
source: 'GitHub Local Actions',
|
||||||
scope: TaskScope.Workspace,
|
scope: TaskScope.Workspace,
|
||||||
isBackground: true,
|
isBackground: true,
|
||||||
@@ -162,7 +173,7 @@ export class ComponentsManager {
|
|||||||
problemMatchers: [],
|
problemMatchers: [],
|
||||||
runOptions: {},
|
runOptions: {},
|
||||||
group: TaskGroup.Build,
|
group: TaskGroup.Build,
|
||||||
execution: new ShellExecution('sudo dockerd', { executable: env.shell })
|
execution: new ShellExecution('systemctl start docker', { executable: env.shell })
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
window.showErrorMessage(`Invalid environment: ${process.platform}`, 'Report an Issue').then(async value => {
|
window.showErrorMessage(`Invalid environment: ${process.platform}`, 'Report an Issue').then(async value => {
|
||||||
@@ -180,7 +191,7 @@ export class ComponentsManager {
|
|||||||
await delay(4000);
|
await delay(4000);
|
||||||
|
|
||||||
// Check again for docker status
|
// Check again for docker status
|
||||||
const newDockerCliInfo = await this.getCliInfo('docker version', /Client:\n.+\n\sVersion:\s+(.+)/, true, true);
|
const newDockerCliInfo = await this.getCliInfo(`docker version --format "Docker Engine Version: {{.Client.Version}}"`, ComponentsManager.dockerVersionRegExp, true, true);
|
||||||
if (dockerCliInfo.status !== newDockerCliInfo.status) {
|
if (dockerCliInfo.status !== newDockerCliInfo.status) {
|
||||||
componentsTreeDataProvider.refresh();
|
componentsTreeDataProvider.refresh();
|
||||||
} else {
|
} else {
|
||||||
@@ -199,6 +210,55 @@ export class ComponentsManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
fixPermissions: async () => {
|
||||||
|
if (process.platform === Platform.linux) {
|
||||||
|
window.showInformationMessage('By default, the Docker daemon binds to a Unix socket owned by the root user. To manage Docker as a non-root user, a Unix group called "docker" should be created with your user added to it.', 'Proceed', 'Learn More').then(async value => {
|
||||||
|
if (value === 'Proceed') {
|
||||||
|
await tasks.executeTask({
|
||||||
|
name: 'Docker Engine',
|
||||||
|
detail: 'Fix Docker Engine Permissions',
|
||||||
|
definition: {
|
||||||
|
type: 'Fix Docker Engine Permissions'
|
||||||
|
},
|
||||||
|
source: 'GitHub Local Actions',
|
||||||
|
scope: TaskScope.Workspace,
|
||||||
|
isBackground: true,
|
||||||
|
presentationOptions: {
|
||||||
|
reveal: TaskRevealKind.Always,
|
||||||
|
focus: false,
|
||||||
|
clear: true,
|
||||||
|
close: false,
|
||||||
|
echo: true,
|
||||||
|
panel: TaskPanelKind.Shared,
|
||||||
|
showReuseMessage: false
|
||||||
|
},
|
||||||
|
problemMatchers: [],
|
||||||
|
runOptions: {},
|
||||||
|
group: TaskGroup.Build,
|
||||||
|
execution: new ShellExecution('sudo groupadd docker; sudo usermod -aG docker $USER')
|
||||||
|
});
|
||||||
|
|
||||||
|
window.withProgress({ location: { viewId: ComponentsTreeDataProvider.VIEW_ID } }, async () => {
|
||||||
|
// Delay 4 seconds for Docker to be started
|
||||||
|
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
await delay(4000);
|
||||||
|
|
||||||
|
// Check again for docker status
|
||||||
|
const newDockerCliInfo = await this.getCliInfo(`docker version --format "Docker Engine Version: {{.Client.Version}}"`, ComponentsManager.dockerVersionRegExp, true, true);
|
||||||
|
if (dockerCliInfo.status !== newDockerCliInfo.status) {
|
||||||
|
componentsTreeDataProvider.refresh();
|
||||||
|
} else {
|
||||||
|
window.showInformationMessage('You may need to restart your PC for these changes to take affect.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (value === 'Learn More') {
|
||||||
|
await env.openExternal(Uri.parse('https://docs.docker.com/engine/install/linux-postinstall/#manage-docker-as-a-non-root-user'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
window.showErrorMessage(`Permissions cannot be automatically fixed for ${process.platform} environment.`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -235,7 +295,7 @@ export class ComponentsManager {
|
|||||||
|
|
||||||
async getUnreadyComponents(): Promise<Component<CliStatus | ExtensionStatus>[]> {
|
async getUnreadyComponents(): Promise<Component<CliStatus | ExtensionStatus>[]> {
|
||||||
const components = await this.getComponents();
|
const components = await this.getComponents();
|
||||||
return components.filter(component => component.required && [CliStatus.NotInstalled, CliStatus.NotRunning, ExtensionStatus.NotActivated].includes(component.status));
|
return components.filter(component => component.required && [CliStatus.NotInstalled, CliStatus.NotRunning, CliStatus.InvalidPermissions, ExtensionStatus.NotActivated].includes(component.status));
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCliInfo(command: string, versionRegex: RegExp, ignoreError: boolean, checksIfRunning: boolean): Promise<{ version?: string, status: CliStatus }> {
|
async getCliInfo(command: string, versionRegex: RegExp, ignoreError: boolean, checksIfRunning: boolean): Promise<{ version?: string, status: CliStatus }> {
|
||||||
@@ -247,7 +307,9 @@ export class ComponentsManager {
|
|||||||
if (ignoreError && version) {
|
if (ignoreError && version) {
|
||||||
resolve({
|
resolve({
|
||||||
version: version[1],
|
version: version[1],
|
||||||
status: CliStatus.NotRunning
|
status: (process.platform === Platform.linux && error.message.toLowerCase().includes('permission denied')) ?
|
||||||
|
CliStatus.InvalidPermissions :
|
||||||
|
CliStatus.NotRunning
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
resolve({
|
resolve({
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export namespace ConfigurationManager {
|
|||||||
|
|
||||||
let actCommand = ConfigurationManager.get<string>(Section.actCommand);
|
let actCommand = ConfigurationManager.get<string>(Section.actCommand);
|
||||||
if (!actCommand) {
|
if (!actCommand) {
|
||||||
await ConfigurationManager.set(Section.actCommand, Act.command);
|
await ConfigurationManager.set(Section.actCommand, Act.defaultActCommand);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,9 +27,15 @@ export default class ComponentsTreeDataProvider implements TreeDataProvider<Gith
|
|||||||
const start = componentTreeItem.component.start;
|
const start = componentTreeItem.component.start;
|
||||||
if (start) {
|
if (start) {
|
||||||
await start();
|
await start();
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
commands.registerCommand('githubLocalActions.fixPermissions', async (componentTreeItem: ComponentTreeItem) => {
|
||||||
|
const fixPermissions = componentTreeItem.component.fixPermissions;
|
||||||
|
if (fixPermissions) {
|
||||||
|
await fixPermissions();
|
||||||
|
this.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.refresh();
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,12 +22,12 @@ export class DecorationProvider implements FileDecorationProvider {
|
|||||||
badge: '✅',
|
badge: '✅',
|
||||||
color: new ThemeColor('GitHubLocalActions.green')
|
color: new ThemeColor('GitHubLocalActions.green')
|
||||||
};
|
};
|
||||||
} else if (!required && (status === CliStatus.NotInstalled || status === CliStatus.NotRunning || status === ExtensionStatus.NotActivated)) {
|
} else if (!required && (status === CliStatus.NotInstalled || status === CliStatus.NotRunning || status === CliStatus.InvalidPermissions || status === ExtensionStatus.NotActivated)) {
|
||||||
return {
|
return {
|
||||||
badge: '⚠️',
|
badge: '⚠️',
|
||||||
color: new ThemeColor('GitHubLocalActions.yellow')
|
color: new ThemeColor('GitHubLocalActions.yellow')
|
||||||
};
|
};
|
||||||
} else if (required && (status === CliStatus.NotInstalled || status === CliStatus.NotRunning || status === ExtensionStatus.NotActivated)) {
|
} else if (required && (status === CliStatus.NotInstalled || status === CliStatus.NotRunning || status === CliStatus.InvalidPermissions || status === ExtensionStatus.NotActivated)) {
|
||||||
return {
|
return {
|
||||||
badge: '❌',
|
badge: '❌',
|
||||||
color: new ThemeColor('GitHubLocalActions.red')
|
color: new ThemeColor('GitHubLocalActions.red')
|
||||||
|
|||||||
Reference in New Issue
Block a user