blog

Portfall

A desktop k8s port-forwarding portal for easy access to all your cluster UIs

Written by Reuben Thomas-Davis
Posted on

When developing and debugging web applications on kubernetes I often find myself writing this command:

kubectl port-forward svc/my-app 8000:8000 -n my-ns

This is fine when you are developing a single application, however when working on multiple, applications at once you may end up having a lot of terminal tabs lying around exposing various ports. You also need to remember all the namespaces and service names.

Introducing Portfall

Portfall GID

Portfall (Port-Forward + Portal) is a cross platform desktop application I wrote to solve this problem. It looks through pods in the selected namespaces and port-forwards them to available ports on your machine. It decides if a service is a website or not by making an http request to that port and checking if the response is valid and an icon of can be found. You can read how to install it here.

How its built

Language

I wanted to use golang to write Portfall since its Kubernetes client library is more fully featured and nicer to use than any of the other languages I use. Having decided on the language I had a look at the Desktop application ecosystem for go.

The framework

The desktop framework I decided upon is called Wails. The killer feature of Wails for me was that I could write the frontend portion using web technologies, without the heavyness of something like Electron which includes an entire browser. Wails uses native rendering engines to achieve this and the resulting binary end up being a mere 10mb.

Wails uses websockets to allow the js side to talk to the go side or if this is not available via bindings in the webview library. This is bidirectional however for my use case it made sense to mostly use the bindings (js calling go) as opposed to the events (go calling js).

Invocations from the js side end up looking like this:

window.backend.Client.GetCurrentConfigPath().then(result => doSomething());

On the go side this will call a function

// define our struct
type Client struct {
    configPath string
}
// and our method - always returning a string
func (c *Client) GetCurrentConfigPath() string {
    return c.configPath
}

app := wails.CreateApp(&wails.AppConfig{
    Width:     1024,
    Height:    768,
    Title:     "Portfall",
    JS:        js,
    CSS:       css,
	Colour:    "#fff",
	Resizable: true,
})      
// bind the struct for its methods to be callable by the 
c := &Client{}
app.Bind(c)
app.Run()

And it’s as simple as that!

This sort of pattern will feel very familiar to developers who mostly work on web applications and as such making a desktop application with Wails was a very smooth experience for me. They even bundle hello world applications for Vue, React and Angular!

Packaging.

Wails is set up to spit out a binary on Linux, and on Windows a .exe and a .app on OSX. For windows a .exe worked fine, but for OSX I wanted a .dmg as the installation process feels a bit smoother (just drag and drop to /Applications). For linux I also wanted to have a go at packaging up for the snap store.

Github actions

Github actions provide an awesome way to package cross platform applications since they are:

  1. Free for open source projects
  2. Have Windows, Linux and OSX build machines
  3. Have a great selection of plugins (called actions)
  4. Sit with the rest of your code in version control and can interact with other Github facets like Releases.

Here’s how I set mine up to package up Portfall

First I wanted to build only when a tag was pushed so my action starts by specifying that.

on:
  push:
    tags:
    - 'v*' # any tag starting in v e.g. v1.0, v.0.1.2, etc 

Now we want it to build with go version 1.14.x on ubuntu, osx and windows.

jobs:
  package:
    strategy:
      matrix:
        go-version: [1.14.x]
        platform: [ubuntu-latest, macos-latest, windows-latest]
    runs-on: ${{ matrix.platform }}

The matrix strategy will go through all combinations of platform and version and run the steps following for each.

The first part of the build is to get go, checkout the code, and get Wails:

    steps:
    - name: Install Go
      uses: actions/setup-go@v1
      with:
        go-version: ${{ matrix.go-version }}
    - name: Checkout code
      uses: actions/checkout@v2
    - name: Get wails
      run: go get -u github.com/wailsapp/wails/cmd/wails

Next we do the build for each platform

    - name: Build package osx
      run: |
        export PATH=${PATH}:`go env GOPATH`/bin
        echo "building on ${{ matrix.platform }}"
        mkdir -p ~/.wails
        cp wails.json ~/.wails/
        wails build -p
        ls
        echo "turning the .app into a .dmg"
        npm install -g appdmg
        appdmg dmg-spec.json Portfall.dmg
      if: matrix.platform == 'macos-latest'

The first line ensures that wails is in the PATH. We also need to ensure that wails.json which contains the name and email of the application author is in its folder. Finally after building the application I used a nice little node module called appdmg to package up the .app as a .dmg.

This is much the same for linux and windows except that on windows we make sure mingw is installed, and on ubuntu we install some dependencies.

    - name: Build package linux
      run: |
        sudo apt update && sudo apt install -y libgtk-3-dev libwebkit2gtk-4.0-dev
        export PATH=${PATH}:`go env GOPATH`/bin
        echo "building on ${{ matrix.platform }}"
        mkdir -p ~/.wails
        cp wails.json ~/.wails/
        wails build
      if: matrix.platform == 'ubuntu-latest'
    - name: Build package windows
      run: |
        $GP = (go env GOPATH)
        $env:path = "$env:path;$GP\bin"
        echo "building on ${{ matrix.platform }}"
        New-Item -ItemType directory -Path "$HOME\.wails" -Force
        Copy-Item -Path "$PWD\wails.json" -Destination "$HOME\.wails\wails.json"
        choco install mingw
        wails build -p
      if: matrix.platform == 'windows-latest'

There are a few files needed for supporting the build too

  • Windows
    • Portfall.rc # specifies where to find the manifest and icon
    • Portfall.exe.manifest # sets some reasonable defaults with xml
    • Portfall.ico # the icon
  • OSX
    • appicon.png # the icon
    • dmg-spec.json # some config for appdmg to use

After this the artifacts were uploaded to be used by the release job.

    - name: upload artifact osx
      uses: actions/upload-artifact@v1
      with:
        name: portfall-osx
        path: Portfall.dmg
      if: matrix.platform == 'macos-latest'
    # ... similar for other platforms

Finally the release job creates a github release for the tag, downloads the artifacts from the package job and uploads these to the release

  release:
    runs-on: ubuntu-latest
    needs: package
    steps:
    - name: Create Release
      id: create_release
      uses: actions/create-release@v1
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      with:
        tag_name: ${{ github.ref }}
        release_name: Release ${{ github.ref }}
        draft: false
        prerelease: false
    - name: Download osx package
      uses: actions/download-artifact@v1
      with:
        name: portfall-osx
    - name: Upload OSX package to release
      id: upload-osx-release-asset
      uses: actions/upload-release-asset@v1
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      with:
        upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above
        asset_path: ./portfall-osx/Portfall.dmg
        asset_name: Portfall.dmg
        asset_content_type: application/octet-stream
    # ... similar for other platforms
And we end up with a nice release page looking like this
And we end up with a nice release page looking like this

You can find the full action here.

Snapcraft

Adding Portfall to the Snap Store was a bit more tricky. Snapcraft sandboxes applications by default, requiring special permissions to access particular files, networking, etc. Portfall is set up to access Kubernetes cluster configuration files which could be in any location on disk and use authentication files, and certificates which could also be anywhere. Given this I realized it would be wiser to use the classic confinement which allows non-sandboxed access to the host system. I am currently waiting for my classic confinement request to be approved. The snapcraft.yaml file can be found here.

Conclusion

Portfall was a fun little project to get to solve a problem I and hopefully other developers had and also get familiar with the Wails framework. I didn’t find much info on packaging so I thought I would share the solution I came up with here.