Sunday, May 28, 2017

Access Windows Environment Variables from within Bash in WSL

I have been using my MacBook a lot now that it is my main computer at work. So much so that I found it necessary to invert my scroll wheel on my mouse on my windows desktop to behave like my MacBook. I've also been using bash a lot more since I can have similar experience between the 2 machines.

While in the process of writing the scripts to configure my bash environment on my Windows machine, I found the need to be able to access environment variables that are set in Windows. With WSL, the only environment variables that really come over to bash is PATH.

I googled around for a bit, but didn't find any way to actually do this. Then I remembered that WSL has interop between Windows and WSL. This means that I can execute a Windows executable and redirect the output back to bash. Which means I should be able to execute powershell.exe to get the information I need.

I first started with a test of just doing:

$ echo $(powershell.exe -Command "gci ENV:")

And that gave me what I wanted back. Now there are some differences in the paths between WSL and Windows, so I knew I would also have to adjust for that.

What I did was put a file called ~/.env.ps1 in my home path.

#!~/bin/powershell
# Will return all the environment variables in KEY=VALUE format
function Get-EnvironmentVariables {
	return (Get-ChildItem ENV: | foreach { "WIN_$(Get-LinuxSafeValue -Value ($_.Name -replace '\(|\)','').ToUpper())=$(Convert-ToWSLPath -Path $_.Value)" })
}

# converts the C:\foo\bar path to the WSL counter part of /mnt/c/foo/bar
function Convert-ToWSLPath {
	param (
		[Parameter(Mandatory=$true)]
		$Path
	)
	(Get-LinuxSafeValue -Value (($Path -split ';' | foreach {
		if ($_ -ne $null -and $_ -ne '' -and $_.Length -gt 0) {
			(( (Fix-Path -Path $_) -replace '(^[A-Za-z])\:(.*)', '/mnt/$1$2') -replace '\\','/')
		}
	} ) -join ':'));
}

function Fix-Path {
	param (
		[Parameter(Mandatory=$true)]
		$Path
	)
	if ( $Path -match '^[A-Z]\:' ) {
		return $Path.Substring(0,1).ToLower()+$Path.Substring(1);
	} else {
		return $Path
	}
}

# Ouputs a string of exports that can be evaluated
function Import-EnvironmentVariables {
	return (Get-EnvironmentVariables | foreach { "export $_;" }) | Out-String
}

# Just escapes special characters
function Get-LinuxSafeValue {
	param (
		[Parameter(Mandatory=$true)]
		$Value
	)
	process {
		return $Value -replace "(\s|'|`"|\$|\#|&|!|~|``|\*|\?|\(|\)|\|)",'\$1';
	}
}

Now in my `.bashrc` I have the following:

#!/usr/bin/env bash

source ~/.wsl_helper.bash
eval $(winenv)

If I run env now, I get output like the following:

WIN_ONEDRIVE=/mnt/d/users/rconr/onedrive
PATH=~/bin:/foo:/usr/bin
WIN_PATH=/mnt/c/windows:/mnt/c/windows/system32

Notice the environment variables that are prefixed with WIN_? These are environment variables directly from Windows. I can now add additional steps to my .bashrc using these variables.

ln -s "$WIN_ONEDRIVE" ~/OneDrive

Additionally, I added a script to my ~/bin folder that is in my path called powershell. This will allow me to make "native" style calls to powershell from within bash scripts.
#!/usr/bin/env bash

# rename to `powershell`
# chmod +x powershell

. ~/.wsl_helper.bash

PS_WORKING_DIR=$(lxssdir)
if [ -f "$1" ] && "$1" ~= ".ps1$"; then
	powershell.exe  -NoLogo -ExecutionPolicy ByPass -Command "Set-Location '${PS_WORKING_DIR}'; Invoke-Command -ScriptBlock ([ScriptBlock]::Create((Get-Content $1))) ${*:2}"
elif [ -f "$1" ] && "$1" ~!= "\.ps1$"; then
	powershell.exe -NoLogo -ExecutionPolicy ByPass -Command "Set-Location '${PS_WORKING_DIR}'; Invoke-Command -ScriptBlock ([ScriptBlock]::Create((Get-Content $1))) ${*:2}"
else
	powershell.exe -NoLogo -ExecutionPolicy ByPass ${*:1}
fi
unset PS_WORKING_DIR

In the powershell file, you will see a call to source a file called .wsl_helper.bash. This script has some helper functions that will do things like transform a path from a Windows style path to a linux WSL path, and do the opposite as well.

#!/usr/bin/env bash
# This is the translated path to where the LXSS root directory is
export LXSS_ROOT=/mnt/c/Users/$USER/AppData/Local/lxss

# translate to linux path from windows path
function windir() {
	echo "$1" | sed -e 's|^\([a-z]\):\(.*\)|/mnt/\L\1\E\2|' -e 's|\\|/|g'
}

# translate the path back to windows path
function wsldir() {
	echo "$1" | sed -e 's|^/mnt/\([a-z]\)/\(.*\)|\U\1\:\\\E\2|' -e 's|/|\\|g'
}

# gets the lxss path from windows
function lxssdir() {
	if [ $# -eq 0 ]; then
		if echo "$PWD" | grep "^/mnt/[a-zA-Z]/" > /dev/null 2>&1; then
			echo "$PWD";
		else
			echo "$LXSS_ROOT$PWD";
		fi
	else
		echo "$LXSS_ROOT$1";
	fi
}

function printwinenv() {
	_winenv --get
}

# this will load the output exports of the windows envrionment variables
function winenv() {
	_winenv --import
}

function _winenv() {
	if [ $# -eq 0 ]; then
		CMD_VERB="Get"
	else
		while test $# -gt 0; do
		  case "$1" in
				-g|--get)
				CMD_VERB="Get"
				shift
				;;
				-i|--import)
				CMD_VERB="Import"
				shift
				;;
				*)
				CMD_VERB="Get"
				break
				;;
			esac
		done
	fi
	CMD_DIR=$(wsldir "$LXSS_ROOT$HOME/\.env.ps1")
	echo $(powershell.exe -Command "Import-Module -Name $CMD_DIR; $CMD_VERB-EnvironmentVariables") | sed -e 's|\r|\n|g' -e 's|^[\s\t]*||g';
}

Wednesday, May 17, 2017

Jenkins + NPM Install + Git

I have been working on setting up Jenkins Pipelines for some projects and had an issue that I think others have had, but I could not find a clear answer on the way to handle it.

We have some NPM Packages that are pulled from a private git repo, and all of the accounts have MFA enabled, including the CI user account. This means that SSH authentication is mandatory for CI user.

If there is only one host that you need to ssh auth with jenkins, or you use the exact same ssh key for all hosts, then you can just put the private key on your Jenkins server at ~/.ssh/id_rsa. If you need to specify a key dependant upon the host, which is the situation I was in, it was not working to pull the package.

The solution for this that I found was to use the ~/.ssh/config. In there you specify the hosts, the user, and what identity file to use. It can look something like this:

Host github.com
 User git
 IdentityFile ~/.ssh/github.key

Host bitbucket.org
 User git
 IdentityFile ~/.ssh/bitbucket.key

Host tfs.myonprem-domain.com
 User my-ci-user
 IdentityFile ~/.ssh/onprem-tfs.key

So now, when running npm install, ssh will know what identity file to use.

Bonus tip: Not everyone uses ssh, so in the package.json, it may not be configured to use ssh. You can put options in the global .gitconfig on the Jenkins server that will redirect the https protocol requests to ssh:

[url "ssh://git@github.com/"]
 insteadOf = "https://github.com/"
[url "ssh://git@bitbucket.org/"]
 insteadOf = "https://bitbucket.org/"
[url "ssh://tfs.myonprem-domain.com:22/"]
 instadOf = "https://tfs.myonprem-domain.com/

 

So with that, when git detects an https request, it will switch to use ssh.

Access Windows Environment Variables from within Bash in WSL

I have been using my MacBook a lot now that it is my main computer at work. So much so that I found it necessary to invert my scroll wheel on my mouse on my windows desktop to behave like my MacBook. I've also been using bash a lot more since I can have similar experience between the 2 machines.

While in the process of writing the scripts to configure my bash environment on my Windows machine, I found the need to be able to access environment variables that are set in Windows. With WSL, the only environment variables that really come over to bash is PATH.

I googled around for a bit, but didn't find any way to actually do this. Then I remembered that WSL has interop between Windows and WSL. This means that I can execute a Windows executable and redirect the output back to bash. Which means I should be able to execute powershell.exe to get the information I need.

I first started with a test of just doing:

$ echo $(powershell.exe -Command "gci ENV:")

And that gave me what I wanted back. Now there are some differences in the paths between WSL and Windows, so I knew I would also have to adjust for that.

What I did was put a file called ~/.env.ps1 in my home path.

#!~/bin/powershell
# Will return all the environment variables in KEY=VALUE format
function Get-EnvironmentVariables {
	return (Get-ChildItem ENV: | foreach { "WIN_$(Get-LinuxSafeValue -Value ($_.Name -replace '\(|\)','').ToUpper())=$(Convert-ToWSLPath -Path $_.Value)" })
}

# converts the C:\foo\bar path to the WSL counter part of /mnt/c/foo/bar
function Convert-ToWSLPath {
	param (
		[Parameter(Mandatory=$true)]
		$Path
	)
	(Get-LinuxSafeValue -Value (($Path -split ';' | foreach {
		if ($_ -ne $null -and $_ -ne '' -and $_.Length -gt 0) {
			(( (Fix-Path -Path $_) -replace '(^[A-Za-z])\:(.*)', '/mnt/$1$2') -replace '\\','/')
		}
	} ) -join ':'));
}

function Fix-Path {
	param (
		[Parameter(Mandatory=$true)]
		$Path
	)
	if ( $Path -match '^[A-Z]\:' ) {
		return $Path.Substring(0,1).ToLower()+$Path.Substring(1);
	} else {
		return $Path
	}
}

# Ouputs a string of exports that can be evaluated
function Import-EnvironmentVariables {
	return (Get-EnvironmentVariables | foreach { "export $_;" }) | Out-String
}

# Just escapes special characters
function Get-LinuxSafeValue {
	param (
		[Parameter(Mandatory=$true)]
		$Value
	)
	process {
		return $Value -replace "(\s|'|`"|\$|\#|&|!|~|``|\*|\?|\(|\)|\|)",'\$1';
	}
}

Now in my `.bashrc` I have the following:

#!/usr/bin/env bash

source ~/.wsl_helper.bash
eval $(winenv)

If I run env now, I get output like the following:

WIN_ONEDRIVE=/mnt/d/users/rconr/onedrive
PATH=~/bin:/foo:/usr/bin
WIN_PATH=/mnt/c/windows:/mnt/c/windows/system32

Notice the environment variables that are prefixed with WIN_? These are environment variables directly from Windows. I can now add additional steps to my .bashrc using these variables.

ln -s "$WIN_ONEDRIVE" ~/OneDrive

Additionally, I added a script to my ~/bin folder that is in my path called powershell. This will allow me to make "native" style calls to powershell from within bash scripts.
#!/usr/bin/env bash

# rename to `powershell`
# chmod +x powershell

. ~/.wsl_helper.bash

PS_WORKING_DIR=$(lxssdir)
if [ -f "$1" ] && "$1" ~= ".ps1$"; then
	powershell.exe  -NoLogo -ExecutionPolicy ByPass -Command "Set-Location '${PS_WORKING_DIR}'; Invoke-Command -ScriptBlock ([ScriptBlock]::Create((Get-Content $1))) ${*:2}"
elif [ -f "$1" ] && "$1" ~!= "\.ps1$"; then
	powershell.exe -NoLogo -ExecutionPolicy ByPass -Command "Set-Location '${PS_WORKING_DIR}'; Invoke-Command -ScriptBlock ([ScriptBlock]::Create((Get-Content $1))) ${*:2}"
else
	powershell.exe -NoLogo -ExecutionPolicy ByPass ${*:1}
fi
unset PS_WORKING_DIR

In the powershell file, you will see a call to source a file called .wsl_helper.bash. This script has some helper functions that will do things like transform a path from a Windows style path to a linux WSL path, and do the opposite as well.

#!/usr/bin/env bash
# This is the translated path to where the LXSS root directory is
export LXSS_ROOT=/mnt/c/Users/$USER/AppData/Local/lxss

# translate to linux path from windows path
function windir() {
	echo "$1" | sed -e 's|^\([a-z]\):\(.*\)|/mnt/\L\1\E\2|' -e 's|\\|/|g'
}

# translate the path back to windows path
function wsldir() {
	echo "$1" | sed -e 's|^/mnt/\([a-z]\)/\(.*\)|\U\1\:\\\E\2|' -e 's|/|\\|g'
}

# gets the lxss path from windows
function lxssdir() {
	if [ $# -eq 0 ]; then
		if echo "$PWD" | grep "^/mnt/[a-zA-Z]/" > /dev/null 2>&1; then
			echo "$PWD";
		else
			echo "$LXSS_ROOT$PWD";
		fi
	else
		echo "$LXSS_ROOT$1";
	fi
}

function printwinenv() {
	_winenv --get
}

# this will load the output exports of the windows envrionment variables
function winenv() {
	_winenv --import
}

function _winenv() {
	if [ $# -eq 0 ]; then
		CMD_VERB="Get"
	else
		while test $# -gt 0; do
		  case "$1" in
				-g|--get)
				CMD_VERB="Get"
				shift
				;;
				-i|--import)
				CMD_VERB="Import"
				shift
				;;
				*)
				CMD_VERB="Get"
				break
				;;
			esac
		done
	fi
	CMD_DIR=$(wsldir "$LXSS_ROOT$HOME/\.env.ps1")
	echo $(powershell.exe -Command "Import-Module -Name $CMD_DIR; $CMD_VERB-EnvironmentVariables") | sed -e 's|\r|\n|g' -e 's|^[\s\t]*||g';
}

Jenkins + NPM Install + Git

I have been working on setting up Jenkins Pipelines for some projects and had an issue that I think others have had, but I could not find a clear answer on the way to handle it.

We have some NPM Packages that are pulled from a private git repo, and all of the accounts have MFA enabled, including the CI user account. This means that SSH authentication is mandatory for CI user.

If there is only one host that you need to ssh auth with jenkins, or you use the exact same ssh key for all hosts, then you can just put the private key on your Jenkins server at ~/.ssh/id_rsa. If you need to specify a key dependant upon the host, which is the situation I was in, it was not working to pull the package.

The solution for this that I found was to use the ~/.ssh/config. In there you specify the hosts, the user, and what identity file to use. It can look something like this:

Host github.com
 User git
 IdentityFile ~/.ssh/github.key

Host bitbucket.org
 User git
 IdentityFile ~/.ssh/bitbucket.key

Host tfs.myonprem-domain.com
 User my-ci-user
 IdentityFile ~/.ssh/onprem-tfs.key

So now, when running npm install, ssh will know what identity file to use.

Bonus tip: Not everyone uses ssh, so in the package.json, it may not be configured to use ssh. You can put options in the global .gitconfig on the Jenkins server that will redirect the https protocol requests to ssh:

[url "ssh://git@github.com/"]
 insteadOf = "https://github.com/"
[url "ssh://git@bitbucket.org/"]
 insteadOf = "https://bitbucket.org/"
[url "ssh://tfs.myonprem-domain.com:22/"]
 instadOf = "https://tfs.myonprem-domain.com/

 

So with that, when git detects an https request, it will switch to use ssh.