Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[InstallSSHKeyV0] Added config editing #13340

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"loc.helpMarkDown": "[Learn more about this task](https://go.microsoft.com/fwlink/?linkid=875267)",
"loc.description": "Install an SSH key prior to a build or deployment",
"loc.instanceNameFormat": "Install an SSH key",
"loc.group.displayName.advanced": "Advanced",
"loc.input.label.hostName": "Known Hosts Entry",
"loc.input.help.hostName": "The entry for this SSH key for the known_hosts file. Supports several hosts.",
"loc.input.label.sshPublicKey": "SSH Public Key",
Expand All @@ -11,9 +12,22 @@
"loc.input.help.sshPassphrase": "The passphrase for the SSH key, if any.",
"loc.input.label.sshKeySecureFile": "SSH Key",
"loc.input.help.sshKeySecureFile": "Select the SSH key that was uploaded to `Secure Files` to install on the agent.",
"loc.input.label.addEntryToConfig": "Add entry to SSH config",
"loc.input.help.addEntryToConfig": "Add entry related to the key installed to the SSH config file. The key file will be available for all subsequent tasks.",
"loc.input.label.configHostAlias": "Alias",
"loc.input.help.configHostAlias": "Name of SSH config entry",
"loc.input.label.configHostname": "Host name",
"loc.input.help.configHostname": "Host name property of SSH config entry",
"loc.input.label.configUser": "User",
"loc.input.help.configUser": "Username property of SSH config entry",
"loc.input.label.configPort": "Port",
"loc.input.help.configPort": "Port of SSH config entry",
"loc.messages.SSHKeyAlreadyInstalled": "The SSH key is already installed.",
"loc.messages.SSHPublicKeyMalformed": "Could not get the base64 portion of the public SSH key.",
"loc.messages.SSHKeyInstallFailed": "Failed to install the SSH key.",
"loc.messages.CannotResetKnownHosts": "Cannot reset the known_hosts file to its original values.",
"loc.messages.GeneratingPublicKey": "Generating public key out of private one."
"loc.messages.GeneratingPublicKey": "Generating public key out of private one.",
"loc.messages.CannotResetFile": "Cannot reset the %s file to its original values.",
"loc.messages.DeletePrivateKeyFile": "Deleting private key file.",
"loc.messages.InsertingIntoConfig": "Inserting entry into config file:"
}
28 changes: 28 additions & 0 deletions Tasks/InstallSSHKeyV0/config-entry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const os = require('os');

/**
* Represents SSH configuration file entry.
*/
export class ConfigFileEntry {
public constructor(
private alias:string,
private hostName: string,
private user: string,
private identityFile: string,
private port: string) { }

public toString(): string {
let result: string = '';
result += `Host ${this.alias}${os.EOL}`;
result += `HostName ${this.hostName}${os.EOL}`;
result += `IdentityFile "${this.identityFile}"${os.EOL}`;

if (this.user) {
result += `User "${this.user}"${os.EOL}`;
}
if (this.port) {
result += `Port ${this.port}${os.EOL}`;
}
return result;
}
}
119 changes: 103 additions & 16 deletions Tasks/InstallSSHKeyV0/installsshkey-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,19 @@ import child = require('child_process');
import * as tl from 'azure-pipelines-task-lib/task';
import * as trm from 'azure-pipelines-task-lib/toolrunner';

import { SecureFileHelpers } from 'securefiles-common';
import { ConfigFileEntry } from './config-entry';

export const postKillAgentSetting: string = 'INSTALL_SSH_KEY_KILL_SSH_AGENT_PID';
export const postDeleteKeySetting: string = 'INSTALL_SSH_KEY_DELETE_KEY';
export const postKnownHostsContentsSetting: string = 'INSTALL_SSH_KEY_KNOWN_HOSTS_CONTENTS';
export const postKnownHostsLocationSetting: string = 'INSTALL_SSH_KEY_KNOWN_HOSTS_LOCATION';
export const postKnownHostsDeleteFileSetting: string = 'INSTALL_SSH_KEY_KNOWN_HOSTS_FILE_DELETE';
export const postConfigContentsSetting: string = 'INSTALL_SSH_KEY_CONFIG_CONTENTS';
export const postConfigLocationSetting: string = 'INSTALL_SSH_KEY_CONFIG_LOCATION';
export const postConfigDeleteFileSetting: string = 'INSTALL_SSH_KEY_CONFIG_FILE_DELETE';

export const preservedKeyFileIDVariableKey: string = 'INSTALL_SSH_KEY_PRESERVED_KEY_FILE_ID';

export const sshAgentPidEnvVariableKey: string = 'SSH_AGENT_PID';
export const sshAgentSockEnvVariableKey: string = 'SSH_AUTH_SOCK';
Expand Down Expand Up @@ -176,6 +184,8 @@ export class SshToolRunner {
if (!publicKey || publicKey.length === 0) {
publicKey = this.generatePublicKey(privateKeyLocation, passphrase);
}
const publicKeyFile: string = `${privateKeyLocation}.pub`;
fs.writeFileSync(publicKeyFile, publicKey);

let publicKeyComponents: string[] = publicKey.split(' ');
if (publicKeyComponents.length <= 1) {
Expand Down Expand Up @@ -218,35 +228,112 @@ export function setKnownHosts(knownHostsEntry: string) {
let knownHostsFolder: string = path.join(os.homedir(), '.ssh');
let knownHostsFile: string = path.join(knownHostsFolder, 'known_hosts');
let knownHostsContent: string = '';
let knownHostsDeleteFileOnClose: string = 'true';
let knownHostsDeleteFileOnClose: boolean = true;
if (!fs.existsSync(knownHostsFolder)) {
fs.mkdirSync(knownHostsFolder);
} else if (fs.existsSync(knownHostsFile)) {
tl.debug('Read known_hosts');
knownHostsDeleteFileOnClose = '';
knownHostsDeleteFileOnClose = false;
knownHostsContent = fs.readFileSync(knownHostsFile).toString();
}

tl.debug('Inserting entry into known_hosts');
const taskAlreadyUsed: boolean = !!tl.getVariable(postKnownHostsLocationSetting);
if (taskAlreadyUsed) {
fs.appendFileSync(knownHostsFile, `${knownHostsEntry}${os.EOL}`);
} else {
fs.writeFileSync(knownHostsFile, `${knownHostsEntry}${os.EOL}`);
}

tl.setTaskVariable(postKnownHostsContentsSetting, knownHostsContent);
tl.setTaskVariable(postKnownHostsLocationSetting, knownHostsFile);
tl.setTaskVariable(postKnownHostsDeleteFileSetting, knownHostsDeleteFileOnClose);
tl.setVariable(postKnownHostsLocationSetting, knownHostsFile);
tl.setTaskVariable(postKnownHostsDeleteFileSetting, knownHostsDeleteFileOnClose.toString());
}

tl.debug('Inserting entry into known_hosts');
fs.writeFileSync(knownHostsFile, knownHostsEntry + os.EOL);
/**
* Adds entry to SSH configuration file.
* @param {ConfigFileEntry} configEntry
*/
export function addConfigEntry(configEntry: ConfigFileEntry): void {
egor-bryzgalov marked this conversation as resolved.
Show resolved Hide resolved
const configFolder: string = path.join(os.homedir(), '.ssh');
const configFilePath: string = path.join(configFolder, 'config');
let configFileContent: string = '';
let deleteConfigFileOnClose: boolean = true;
if (!fs.existsSync(configFolder)) {
fs.mkdirSync(configFolder);
} else if (fs.existsSync(configFilePath)) {
tl.debug('Reading config file');
deleteConfigFileOnClose = false;
configFileContent = fs.readFileSync(configFilePath).toString();
}

const configEntryContent: string = configEntry.toString();
console.log(tl.loc("InsertingIntoConfig"));
console.log(configEntryContent);
const configAlreadyChanged: boolean = !!tl.getVariable(postConfigLocationSetting);
if (configAlreadyChanged) {
fs.appendFileSync(configFilePath, `${os.EOL}${configEntryContent}`);
} else {
fs.writeFileSync(configFilePath, configEntryContent);
}

tl.setTaskVariable(postConfigContentsSetting, configFileContent);
tl.setVariable(postConfigLocationSetting, configFilePath);
tl.setTaskVariable(postConfigDeleteFileSetting, deleteConfigFileOnClose.toString());
}

/**
*
* @param {string} fileName File name
* @param {string} contents File contents which should be restored.
* @param {string} location Path to file being restored
* @param {boolean} deleteOnExit File should be deleted.
*/
function tryRestore(fileName: string, contents: string, location: string, deleteOnExit: boolean): void {
if (deleteOnExit && location) {
fs.unlinkSync(location);
} else if (contents && location) {
fs.writeFileSync(location, contents);
} else if (location || contents) {
tl.warning(tl.loc('CannotResetFile', fileName));
tl.debug('(location=' + location + ' content=' + contents + ')');
}
}

/**
* Restores known_hosts file to it's initial state.
*/
export function tryRestoreKnownHosts() {
let knownHostsContents: string = tl.getTaskVariable(postKnownHostsContentsSetting);
let knownHostsLocation: string = tl.getTaskVariable(postKnownHostsLocationSetting);
let knownHostsDeleteFileOnExit: string = tl.getTaskVariable(postKnownHostsDeleteFileSetting);
const knownHostsContents: string = tl.getTaskVariable(postKnownHostsContentsSetting);
const knownHostsLocation: string = tl.getVariable(postKnownHostsLocationSetting);
const knownHostsDeleteFileOnExit: boolean = tl.getTaskVariable(postKnownHostsDeleteFileSetting) === 'true';

tl.debug('Restoring known_hosts');
if (knownHostsDeleteFileOnExit && knownHostsLocation) {
fs.unlinkSync(knownHostsLocation);
} else if (knownHostsContents && knownHostsLocation) {
fs.writeFileSync(knownHostsLocation, knownHostsContents);
} else if (knownHostsLocation || knownHostsContents) {
tl.warning(tl.loc('CannotResetKnownHosts'));
tl.debug('(location=' + knownHostsLocation + ' content=' + knownHostsContents + ')');
tryRestore('known_hosts', knownHostsContents, knownHostsLocation, knownHostsDeleteFileOnExit);
}

/**
* Restores SSH configuration file to it's initial state.
*/
export function tryRestoreConfig() {
const configContents: string = tl.getTaskVariable(postConfigContentsSetting);
const configLocation: string = tl.getVariable(postConfigLocationSetting);
const configDeleteFileOnExit: boolean = tl.getTaskVariable(postConfigDeleteFileSetting) === 'true';

tl.debug('Restoring config');
tryRestore('config', configContents, configLocation, configDeleteFileOnExit);
}

/**
* Deletes private key file with ID specified.
* @param {string} privateKeyFileID
*/
export function tryDeletePrivateKeyFile(privateKeyFileID: string) {
if (privateKeyFileID) {
egor-bryzgalov marked this conversation as resolved.
Show resolved Hide resolved
tl.debug(tl.loc("DeletePrivateKeyFile"));
const secureFileHelpers: SecureFileHelpers = new SecureFileHelpers();
secureFileHelpers.deleteSecureFile(privateKeyFileID);
} else {
tl.debug('No private key file ID was specified.');
}
}
6 changes: 6 additions & 0 deletions Tasks/InstallSSHKeyV0/postinstallsshkey.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import * as tl from 'azure-pipelines-task-lib/task';
import ps = require('process');
import path = require('path');
import util = require('./installsshkey-util');

async function run() {
tl.setResourcePath(path.join(__dirname, 'task.json'));
try {
util.tryRestoreKnownHosts();
util.tryRestoreConfig();

let agentPid: string = tl.getTaskVariable(util.postKillAgentSetting);
if (agentPid) {
Expand All @@ -20,6 +23,9 @@ async function run() {
let sshTool: util.SshToolRunner = new util.SshToolRunner();
sshTool.deleteKey(deleteKey)
}

const privateKeyFileID: string = tl.getTaskVariable(util.preservedKeyFileIDVariableKey);
util.tryDeletePrivateKeyFile(privateKeyFileID);
} catch (err) {
tl.setResult(tl.TaskResult.Failed, err);
}
Expand Down
14 changes: 13 additions & 1 deletion Tasks/InstallSSHKeyV0/preinstallsshkey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import path = require('path');
import secureFilesCommon = require('securefiles-common/securefiles-common');
import * as tl from 'azure-pipelines-task-lib/task';
import util = require('./installsshkey-util');
import { ConfigFileEntry } from "./config-entry"

async function run() {

let secureFileId: string;
let secureFileHelpers: secureFilesCommon.SecureFileHelpers;
const addConfigEntry: boolean = tl.getBoolInput('addEntryToConfig', false);

try {
let publicKey: string = tl.getInput('sshPublicKey', false);
Expand All @@ -35,11 +37,21 @@ async function run() {

await sshTool.installKey(publicKey, privateKeyLocation, passphrase);
util.setKnownHosts(knownHostsEntry);

if (addConfigEntry) {
const alias: string = tl.getInput('configHostAlias', true);
const user: string = tl.getInput('configUser', false);
const hostname: string = tl.getInput('configHostname', true);
const port: string = tl.getInput('configPort', false);
const configEntry: ConfigFileEntry = new ConfigFileEntry(alias, hostname, user, privateKeyLocation, port);
util.addConfigEntry(configEntry);
tl.setTaskVariable(util.preservedKeyFileIDVariableKey, secureFileId);
}
} catch(err) {
tl.setResult(tl.TaskResult.Failed, err);
} finally {
// delete SSH key from temp location after installing
if (secureFileId && secureFileHelpers) {
if (!addConfigEntry && secureFileId && secureFileHelpers) {
secureFileHelpers.deleteSecureFile(secureFileId);
}
}
Expand Down
63 changes: 61 additions & 2 deletions Tasks/InstallSSHKeyV0/task.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"author": "Microsoft Corporation",
"version": {
"Major": 0,
"Minor": 172,
"Minor": 174,
"Patch": 0
},
"runsOn": [
Expand All @@ -23,6 +23,13 @@
"demands": [],
"minimumAgentVersion": "2.117.0",
"instanceNameFormat": "Install an SSH key",
"groups": [
{
"name": "advanced",
"displayName": "Advanced",
"isExpanded": false
}
],
"inputs": [
{
"name": "hostName",
Expand Down Expand Up @@ -58,6 +65,55 @@
"defaultValue": "",
"required": true,
"helpMarkDown": "Select the SSH key that was uploaded to `Secure Files` to install on the agent."
},
{
"name": "addEntryToConfig",
"type": "boolean",
"label": "Add entry to SSH config",
"defaultValue": false,
"required": false,
"groupName": "advanced",
"helpMarkDown": "Add entry related to the key installed to the SSH config file. The key file will be available for all subsequent tasks."
},
{
"name": "configHostAlias",
"type": "string",
"label": "Alias",
"defaultValue": "",
"required": true,
"visibleRule": "addEntryToConfig = true",
"groupName": "advanced",
"helpMarkDown": "Name of SSH config entry"
},
{
"name": "configHostname",
"type": "string",
"label": "Host name",
"defaultValue": "",
"required": true,
"visibleRule": "addEntryToConfig = true",
"groupName": "advanced",
"helpMarkDown": "Host name property of SSH config entry"
},
{
"name": "configUser",
"type": "string",
"label": "User",
"defaultValue": "",
"required": false,
"visibleRule": "addEntryToConfig = true",
"groupName": "advanced",
"helpMarkDown": "Username property of SSH config entry"
},
{
"name": "configPort",
"type": "string",
"label": "Port",
"defaultValue": "",
"required": false,
"visibleRule": "addEntryToConfig = true",
"groupName": "advanced",
"helpMarkDown": "Port of SSH config entry"
}
],
"prejobexecution": {
Expand All @@ -77,6 +133,9 @@
"SSHPublicKeyMalformed": "Could not get the base64 portion of the public SSH key.",
"SSHKeyInstallFailed": "Failed to install the SSH key.",
"CannotResetKnownHosts": "Cannot reset the known_hosts file to its original values.",
"GeneratingPublicKey": "Generating public key out of private one."
"GeneratingPublicKey": "Generating public key out of private one.",
"CannotResetFile": "Cannot reset the %s file to its original values.",
"DeletePrivateKeyFile": "Deleting private key file.",
"InsertingIntoConfig": "Inserting entry into config file:"
}
}
Loading