Skip to content

Quick Tips

Output variables in PowerShell and GitHub Actions

When running a PowerShell script within a GitHub Actions workflow, you may wish to output variables from one step and access them in another.

Below is a simple example of outputting a variable in one step and accessing it in another:

name: Pass value from one step to another
on: 
  workflow_dispatch:
  push:
    branches:
      - main

jobs:
  PassTheParcel:
    runs-on: ubuntu-latest
    steps:
    - name: Determine some values
      id: calculate_values
      shell: pwsh
      run: |
        $ErrorActionPreference = "Stop" # not required but I always include it so errors don't get missed

        # Normally these would be calculated rather than just hard-coded strings
        $someValue = "Hello, World!"
        $anotherValue = "Goodbye, World!"

        # Outputting for diagnostic purposes - don't do this with sensitive data!
        Write-Host "Some value: $someValue"
        Write-Host "Another value: $anotherValue"

        "SOME_VALUE=$someValue" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
        "ANOTHER_VALUE=$anotherValue" | Out-File -FilePath $env:GITHUB_OUTPUT -Append

    - name: Use the values
      shell: pwsh
      env: 
        VALUE_TO_USE_1: ${{ steps.calculate_values.outputs.SOME_VALUE }}
        VALUE_TO_USE_2: ${{ steps.calculate_values.outputs.ANOTHER_VALUE }}
      run: |
        $ErrorActionPreference = "Stop" # not required but I always include it so errors don't get missed

        Write-Host "Values received were `"$($env:VALUE_TO_USE_1)`" and `"$($env:VALUE_TO_USE_2)`""

Whilst both examples use PowerShell, there is no requirement that both steps use the same shell.

Adding an additional route to a VPN connection in Windows 11

If you're running a split VPN in Windows 11 (i.e. one which sends only certain traffic over the VPN and does not use the VPN as the default gateway), you may wish to add specific IP addresses to also be routed over the VPN. An example of this might be a service which is only accessible via a certain IP address.

Assuming you've already set up your VPN connection in Windows, you can add a route using the following PowerShell command:

Add-VpnConnectionRoute -Name '<Split VPN Name>' -DestinationPrefix <123.45.67.89>/32

Where:

  • <Split VPN Name> is the name of the VPN connection you wish to add the route to
  • <123.45.67.89> is the IP address you wish to route over the VPN rather than via your default gateway/normal internet connection

For example, if you have a VPN connection named My VPN and you wish to route traffic to 216.58.204.68 over the VPN, you would use the following command:

Add-VpnConnectionRoute -Name 'My VPN' -DestinationPrefix 216.58.204.68/32

Delete a Kubernetes resource that is stuck "Terminating"

General Resource

Run the following command to force a resource to delete. It works by removing all finalizers. Only use this if a resource has become “stuck”.

kubectl patch resourcetype resource-name -n resource-namespace -p '{"metadata":{"finalizers":[]}}' --type=merge

Replace resourcetype with the type of resource (e.g. pod, helmrelease, etc…), replace resource-name with the name of your resource and resource-namespace with the name of the resource which the resource belongs to.

If the resource has become orphaned (i.e. the namespace a resource belongs to has been deleted), recreate the namespace and then run the above command for each resource.

Namespace

If a namespace is stuck and the above method doesn’t work, run the following command, replacing my-namespace with the name of your namespace to delete.

kubectl get ns my-namespace -o json | jq '.spec.finalizers = []' | kubectl replace --raw "/api/v1/namespaces/my-namespace/finalize" -f -

Solution found on Stack Overflow.

Setting up an Arrange, Act, Assert comment template in Visual Studio 2022

To add a simple code snippet that allows a set of arrange, act and assert comments to be inserted using the short code of aaa, firstly, create a simple XML file with the following contents and save it as ArrangeActAssert.snippet:

<?xml version="1.0" encoding="utf-8"?>
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <CodeSnippet Format="1.0.0">
        <Header>
            <Title>Arrange Act Assert</Title>
            <Author>Neil Docherty</Author>
            <Description>Adds an arrange, act, assert to a test</Description>
            <HelpUrl />
            <Keywords />
            <SnippetTypes />
            <Shortcut>aaa</Shortcut>
        </Header>
        <Snippet>
            <Declarations />
            <References />
            <Code Kind="any" Language="CSharp"><![CDATA[// Arrange
$end$

// Act


// Assert]]></Code>
        </Snippet>
    </CodeSnippet>
</CodeSnippets>

Then, to import it into Visual Studio, perform the following steps:

  1. Go to the Tools menu
  2. Select Code Snippets Manager
  3. Choose the Import button and locate and select your ArrangeActAssert.snippet file and click Open.

Now, when editing some C# code, typing aaa and hitting tab twice will create a block of text similar to:

// Arrange


// Act


// Assert

HTTP Client Factory (IHttpClientFactory) mock using NSubstitute

Following the controversy around Moq, I decided to look into using NSubstitute as an alternative mocking library.

The example below is an NSubstitute version of the class created as part of my C# TDD blog article. The only change in functionality is the optional passing in of an HTTP status code.

public static IHttpClientFactory GetStringClient(string returnValue, HttpStatusCode returnStatusCode = HttpStatusCode.OK)
{
    var httpMessageHandler = Substitute.For<HttpMessageHandler>();
    var httpClientFactory = Substitute.For<IHttpClientFactory>();

    httpMessageHandler
        .GetType()
        .GetMethod("SendAsync", BindingFlags.NonPublic | BindingFlags.Instance)
        .Invoke(httpMessageHandler, new object[] { Arg.Any<HttpRequestMessage>(), Arg.Any<CancellationToken>() })
        .Returns(Task.FromResult(new HttpResponseMessage(returnStatusCode) { Content = new StringContent(returnValue) }));

    HttpClient httpClient = new(httpMessageHandler);
    httpClientFactory.CreateClient(Arg.Any<string>()).Returns(httpClient);

    return httpClientFactory;
}

Changing branch used by Flux deployment

If the need arises to change the branch used by a Flux installation, this can be done without bootstrapping the cluster again. Not that this is recommended only on similar setups (e.g. you’re trying out a new change on a dev cluster and want to point to your dev branch which is based on the default branch).

Components

This article assumes the following components are in play:

  • Kubernetes cluster called cluster01
  • Flux mono repo with the cluster definition in the locations clusters/development (this folder contains the flux-system folder)
  • A default branch called main
  • A working branch called 12345-something-to-test that is based on main with a new change in it

Process

  1. Switch to your working branch
  2. Run the following command at a shell prompt (Bash, PowerShell, etc…) when the current context is you the target cluster: flux suspend source git flux-system
  3. Update clusters/development/flux-system/gotk-sync.yaml to set the value of branch to 12345-something-to-test and commit and push it
  4. Within your Kubernetes cluster, update the resource of type GitRepository named flux-system in the flux-system namespace so the branch field is also 12345-something-to-test
  5. Run the following command at a shell prompt (Bash, PowerShell, etc…) when the current context is you the target cluster: flux resume source git flux-system

Once testing is complete, repeat the above process but setting the branch back to main.

.NET Container Running as Non-Root in Kubernetes

This is a quick guide on how to get a standard .NET container running as a non-root, non privileged user in Kubernetes. It is not a complete security guide but rather just enough if you require your pods to not run under root.

Update Dockerfile

The first step is to update the Dockerfile. Two changes are required here; one to change the port and one to specify the user to use.

Exported Port

At the start of the Dockerfile, replace any EXPORT statements with the following:

ENV ASPNETCORE_URLS http://+:8000
EXPOSE 8000

This will expose your application to the cluster on port 8000 rather than port 80.

User

Next, just before the ENTRYPOINT instruction, add the following line:

USER $APP_UID

Now build and push the container to your container registry of choice either manually or via a CI/CD pipeline.

Kubernetes Manifests

Deployment Manifest

Add the following snippet to the deployment manifest under the container entry that is to be locked down:

securityContext:
  allowPrivilegeEscalation: false
  runAsNonRoot: true
  runAsUser: 1654

Service Manifest

As the exported container port has now changed, update any service you may have defined so it looks similar to the following service manifest:

apiVersion: v1
kind: Service
metadata:
  name: your-service
  namespace: service-namespace
spec:
  selector:
     app: your-app
  ports:
    - name: http
      port: 80
      targetPort: 8000
  type: ClusterIP

The pod will still be accessible via its service on port 80 so things like ingress or gateway definitions or references from other apps do not need to be updated.

Install Metrics Server into Kubernetes running in Docker Desktop using Helm

If using the Helm chart to install Metrics Server in a Docker Desktop instance of Kubernetes, it may not start due to insecure certificates. To resolve this, run the following command when doing the install (it can also be applied to an existing installation):

helm upgrade --install --set args[0]='--kubelet-insecure-tls' metrics-server metrics-server/metrics-server

If the repo hasn’t been added already, run the following first:

helm repo add metrics-server https://kubernetes-sigs.github.io/metrics-server/

.NET 5/6, Docker and custom NuGet server

When using a custom NuGet server and you've added a nuget.config file to the solution, you'll need to add the following line to the default Dockerfile build by Visual Studio to allow the container to be built.

COPY ["nuget.config", "/src/"]

This should be placed before the RUN dotnet restore … line.

The filename is case sensitive within the container so using all lowercase is recommended for the file name. If you need to change the case, you may need to do two commits of the file (e.g. rename it NuGet.config –commit–> nuget1.config –commit–> nuget.config).

If running in a CI/CD pipeline and you have fixed custom NuGet servers, you can inject a nuget.config file into the CI pipeline however the file will still need referencing in the Dockerfile as above to be correctly used by the container build process.

Setup local Redis container

To set up a local Redis container that automatically starts up on reboot, run the following command:

docker run --name redis -d --restart always -p 6379:6379 redis:latest

To include a UI, run the following command:

docker run --name redis-ui -d --restart always -p 8001:8001 redislabs/redisinsight