Plane.so, Open-Source Pjm: Part 4: Open-source, helm charts, and Github Codespace

Published: Jan 11, 2024 by Isaac Johnson

In Part 3 we built out a containerized app we could run in Docker or Kubernetes, but it was all still private and not really a sharable setup.

In this post, we’ll create a Public GPL’ed Github Repo and populate it with a working version. We’ll apply some tweaks and move to creating a proper Helm Chart. Once created, we’ll (try) and publish with Github Pages and Harbor CR. We’ll finish out the Github actions to push on tags and have a working sharable solution.

We are going to explore the full update path and show off using Github Codespaces to leverage ephemeral cloud compute and containers to build and test updates and associate with Github Issues.

Open Source it

What is the point of being an Open-Source junkie if I don’t do it myself?

I created a blank new repo with GPL v3 license set at https://github.com/idjohnson/pyplanereport

/content/images/2023/12/planeso3-09.png

I’ll bring it down

builder@DESKTOP-QADGF36:~/Workspaces$ git clone https://github.com/idjohnson/pyplanereport.git
Cloning into 'pyplanereport'...
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (5/5), done.
remote: Total 5 (delta 0), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (5/5), 14.00 KiB | 494.00 KiB/s, done.
builder@DESKTOP-QADGF36:~/Workspaces$ cd pyplanereport/
builder@DESKTOP-QADGF36:~/Workspaces/pyplanereport$ git checkout -b initialsync
Switched to a new branch 'initialsync'
builder@DESKTOP-QADGF36:~/Workspaces/pyplanereport$ code .

There is a lot in my initial PR. We’ll just cover some of it:

here you can explore the helm chart with the values that need to be set by users

image:
  repository: pyplanereport
  pullPolicy: IfNotPresent
  # Overrides the image tag whose default is the chart appVersion.
  tag: "0.0.1"

imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""

planeso:
  projectid: "" # e.g. 9ca799e6-52c4-4a9e-8b40-461eef4f57e9
  workspacename: "" # e.g. tpk
  apikey: "" #e.g. plane_api_xxxxxxxxxx
  tabletitle: "Project Issue Report"
  ignorestate: "Cancelled"

I also made a first pass at a Github Workflow to build and push to ghcr.io

name: Create and publish a Docker image
on:
  push:
     branches: ['main']
env:
  REGISTRY: ghcr.io
  IMAGE_NAME: $

jobs:
  build-and-push-image:
    runs-on: ubuntu-latest

    permissions:
      contents: read
      packages: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
      - name: Log in to the Container registry
        uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
        with:
          registry: $
          username: $
          password: $

      - name: Extract metadata (tags, labels) for Docker
        id: meta
        uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
        with:
          images: $/$

      - name: Build and push Docker image
        uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
        with:
          context: src
          push: true
          tags: $
          labels: $

If I read the build output properly, it did end up pushing to ghcr.io/idjohnson/pyplanereport:main

/content/images/2023/12/planeso3-10.png

Next, let’s add some steps to push out the Helm chart too

...
      - name: Configure Git
        run: |
          git config user.name "$GITHUB_ACTOR"
          git config user.email "$GITHUB_ACTOR@users.noreply.github.com"

      - name: Install Helm
        uses: azure/setup-helm@v3

      - name: Run chart-releaser
        uses: helm/chart-releaser-action@v1.6.0
        env:
          CR_TOKEN: "$"

We can see the new yaml in PR 2

which completed after merging

/content/images/2023/12/planeso3-11.png

I fought this for a bit. I could not seem to get it to work. Most guides out there said it should just show up in the gh-pages for this Repo but try as I might, i got stuck.

Instead, I pivoted to using my Harbor which will do just fine.

First, I package the chart

builder@DESKTOP-QADGF36:~/Workspaces/pyplanereport$ helm package pyplanereport
Successfully packaged chart and saved it to: /home/builder/Workspaces/pyplanereport/pyplanereport-0.1.0.tgz

Then pushed up to Harbor where I created a public project to hold the charts

$ helm push pyplanereport-0.1.0.tgz oci://harbor.freshbrewed.science/chartrepo
Pushed: harbor.freshbrewed.science/chartrepo/pyplanereport:0.1.0
Digest: sha256:ba365f7d34043e8a5b0e6fb8b80b44724a008ec965f6613a89d6cff04ff174b7

I can now use helm pull to fetch it

builder@DESKTOP-QADGF36:/tmp/testchart$ helm pull oci://harbor.freshbrewed.science/chartrepo/pyplanereport --version 0.1.0
Pulled: harbor.freshbrewed.science/chartrepo/pyplanereport:0.1.0
Digest: sha256:ba365f7d34043e8a5b0e6fb8b80b44724a008ec965f6613a89d6cff04ff174b7
builder@DESKTOP-QADGF36:/tmp/testchart$ ls -ltra
total 52
-rw-r--r--   1 builder builder  4106 Dec 23 11:20 pyplanereport-0.1.0.tgz
drwxrwxrwt 104 root    root    36864 Dec 23 11:20 ..
drwxr-xr-x   2 builder builder  4096 Dec 23 11:20 .

Now to test using the remote repo:

$ helm install --set image.repository=ghcr.io/idjohnson/pyplanereport --set image.tag=main --set planeso.projectid=9ca799e6-52c4-4a9e-8b40-461eef4f57e9 --set planeso.workspacename=tpk --set planeso.apikey=plane_api_aabbccddeeffaabbccddeeffaabb myplanefromhelm oci://harbor.freshbrewed.science/chartrepo/pyplanereport
Pulled: harbor.freshbrewed.science/chartrepo/pyplanereport:0.1.0
Digest: sha256:ba365f7d34043e8a5b0e6fb8b80b44724a008ec965f6613a89d6cff04ff174b7
NAME: myplanefromhelm
LAST DEPLOYED: Sat Dec 23 11:25:54 2023
NAMESPACE: default
STATUS: deployed
REVISION: 1
NOTES:
1. Get the application URL by running these commands:
  export POD_NAME=$(kubectl get pods --namespace default -l "app.kubernetes.io/name=pyplanereport,app.kubernetes.io/instance=myplanefromhelm" -o jsonpath="{.items[0].metadata.name}")
  export CONTAINER_PORT=$(kubectl get pod --namespace default $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
  echo "Visit http://127.0.0.1:8080 to use your application"
  kubectl --namespace default port-forward $POD_NAME 8080:$CONTAINER_PORT

Which with a kubectl port-forward worked fine:

$ kubectl port-forward svc/myplanefromhelm-pyplanereport 8989:80
Forwarding from 127.0.0.1:8989 -> 80
Forwarding from [::1]:8989 -> 80
Handling connection for 8989
Handling connection for 8989

/content/images/2023/12/planeso3-12.png

To test ingress, I need to create an A Record first

builder@DESKTOP-QADGF36:~/Workspaces/pyplanereport$ cat r53-planereport.json
{
    "Comment": "CREATE planereport fb.s A record ",
    "Changes": [
      {
        "Action": "CREATE",
        "ResourceRecordSet": {
          "Name": "planereport.freshbrewed.science",
          "Type": "A",
          "TTL": 300,
          "ResourceRecords": [
            {
              "Value": "75.73.224.240"
            }
          ]
        }
      }
    ]
  }
builder@DESKTOP-QADGF36:~/Workspaces/pyplanereport$ aws route53 change-resource-record-sets --hosted-zone-id Z39E8QFU0F9PZP --change-batch file://r53-planereport.json
{
    "ChangeInfo": {
        "Id": "/change/C07149561EOVHKWBG2TBA",
        "Status": "PENDING",
        "SubmittedAt": "2023-12-23T17:32:19.130Z",
        "Comment": "CREATE planereport fb.s A record "
    }
}

I can now create a values file with the Nginx settings. I know it can also be done with some more complicated “set” commands, but the moment I start dealing with annotations and hosts settings, I move to just doing it in a values.yaml file

$ cat helmvalues.yaml
autoscaling:
  enabled: false
  maxReplicas: 100
  minReplicas: 1
  targetCPUUtilizationPercentage: 80
fullnameOverride: ""
image:
  pullPolicy: IfNotPresent
  repository: ghcr.io/idjohnson/pyplanereport
  tag: main
imagePullSecrets: []
ingress:
  annotations:
    kubernetes.io/ingress.class: nginx
    ingress.kubernetes.io/ssl-redirect: "true"
    cert-manager.io/cluster-issuer: letsencrypt-prod
  className: ""
  enabled: true
  hosts:
  - host: planereport.freshbrewed.science
    paths:
    - path: /
      pathType: ImplementationSpecific
  tls:
  - hosts:
    - planereport.freshbrewed.science
    secretName: planereport-tls
planeso:
  apikey: plane_api_aabbccddeeffaabbccddeeffaabb
  ignorestate: Cancelled
  projectid: 9ca799e6-52c4-4a9e-8b40-461eef4f57e9
  tabletitle: Project Issue Report
  workspacename: tpk
podAnnotations: {}
podSecurityContext: {}
replicaCount: 1
resources: {}
securityContext: {}
service:
  port: 80
  type: ClusterIP
serviceAccount:
  annotations: {}
  create: true
  name: ""
tolerations: []

$ helm upgrade myplanefromhelm -f helmvalues.yaml oci://harbor.freshbrewed.science/chartrepo/pyplanereport
Pulled: harbor.freshbrewed.science/chartrepo/pyplanereport:0.1.0
Digest: sha256:ba365f7d34043e8a5b0e6fb8b80b44724a008ec965f6613a89d6cff04ff174b7
Release "myplanefromhelm" has been upgraded. Happy Helming!
NAME: myplanefromhelm
LAST DEPLOYED: Sat Dec 23 11:38:15 2023
NAMESPACE: default
STATUS: deployed
REVISION: 2
NOTES:
1. Get the application URL by running these commands:
  https://planereport.freshbrewed.science/

I can see the ingress came up

$ kubectl get ingress | grep plane
myplanefromhelm-pyplanereport   <none>   planereport.freshbrewed.science         192.168.1.215,192.168.1.36,192.168.1.57,192.168.1.78   80, 443   74s

Once the cert was issued, I could see the report as served through my local k8s with TLS

/content/images/2023/12/planeso3-13.png

Let’s see how this is really live:

I now list some other ways to reach me on the feedback form, but I didn’t always do that. So I decided I wanted one more feature.

Full flow

Let’s use this to demonstrate the full flow.

I would really like to hide multiple states. For instance, I have some requests there now with private invites to Slack which were meant just for me - again, from when I didn’t have other ways to reach me shown.

The other, lower priority, is that the table title is rather boring

/content/images/2023/12/planeso3-15.png

Seems that should really be a parameter.

Since this is about the report app, I would expect these issues in the Github repo itself

/content/images/2023/12/planeso3-16.png

and

/content/images/2023/12/planeso3-17.png

Also, this is the right time to make a release before I mess the whole thing up.

We now have 0.1.0 listed with notes

/content/images/2023/12/planeso3-18a.png

I’ll try using codespace for the first issue

/content/images/2023/12/planeso3-19a.png

This fires up a shell (noting it’s paid for by idjohnson - so we’ll see how that plays out).

/content/images/2023/12/planeso3-20a.png

I’ll add a quick list check def and add the compare. I can do a docker build here in codespace

/content/images/2023/12/planeso3-21a.png

I did a quick run locally and saw my logic is inverted

/content/images/2023/12/planeso3-22a.png

That looks fantastic

/content/images/2023/12/planeso3-23a.png

I’ll create a PR and indicate it fixes that issue

/content/images/2023/12/planeso3-24a.png

The PR Diff looks good so I’ll merge it

We can see from resulting page, on the right, it is associated and completes that issue

/content/images/2023/12/planeso3-25a.png

Seen here:

/content/images/2023/12/planeso3-26a.png

Testing

Since I merged it, we edit the deployment to force it pull always

$ kubectl edit deployments myplanefromhelm-pyplanereport
deployment.apps/myplanefromhelm-pyplanereport edited
$ kubectl get deployments myplanefromhelm-pyplanereport -o yaml | grep -i imagepullpolicy
        imagePullPolicy: Always
$ kubectl get pods | grep plane
myplanefromhelm-pyplanereport-f59b9c584-h4tvc            1/1     Running   0                 6h32m
myplanefromhelm-pyplanereport-bc77f898-lqt7k             1/1     Running   0                 10s
$ kubectl get pods | grep plane
myplanefromhelm-pyplanereport-bc77f898-lqt7k             1/1     Running       0                 16s
myplanefromhelm-pyplanereport-f59b9c584-h4tvc            1/1     Terminating   0                 6h32m

Now to use it, I need to edit my values:

builder@LuiGi17:~/Workspaces/pyTest$ helm get values myplanefromhelm -o yaml > values.yaml
builder@LuiGi17:~/Workspaces/pyTest$ vi values.yaml
builder@LuiGi17:~/Workspaces/pyTest$ cat values.yaml | grep -i ignore
  ignorestate: Cancelled,Hidden

And redeploy

$ helm upgrade myplanefromhelm -f values.yaml oci://harbor.freshbrewed.science/chartrepo/pyplanereport
Pulled: harbor.freshbrewed.science/chartrepo/pyplanereport:0.1.0
Digest: sha256:ba365f7d34043e8a5b0e6fb8b80b44724a008ec965f6613a89d6cff04ff174b7
Release "myplanefromhelm" has been upgraded. Happy Helming!
NAME: myplanefromhelm
LAST DEPLOYED: Sat Dec 23 18:06:15 2023
NAMESPACE: default
STATUS: deployed
REVISION: 3
NOTES:
1. Get the application URL by running these commands:
  https://planereport.freshbrewed.science/

If there is any doubt on the values, I can just check the CM

$ kubectl get cm myplanefromhelm-pyplanereport-cfg -o yaml | grep -i ignore
  IGNORESTATE: Cancelled,Hidden

Now to use it, I need to add that new State to the Project

/content/images/2023/12/planeso3-18.png

Now I can change the ignore values and set some on issues

$ kubectl get cm myplanefromhelm-pyplanereport-cfg -o yaml | grep -i ignore
  IGNORESTATE: Cancelled,Hidden

$ helm get values myplanefromhelm -o yaml > values.yaml
$ vi values.yaml
$ helm upgrade myplanefromhelm -f values.yaml oci://harbor.freshbrewed.science/chartrepo/pyplanereport
Pulled: harbor.freshbrewed.science/chartrepo/pyplanereport:0.1.0
Digest: sha256:ba365f7d34043e8a5b0e6fb8b80b44724a008ec965f6613a89d6cff04ff174b7
Release "myplanefromhelm" has been upgraded. Happy Helming!
NAME: myplanefromhelm
LAST DEPLOYED: Sat Dec 23 18:20:23 2023
NAMESPACE: default
STATUS: deployed
REVISION: 4
NOTES:
1. Get the application URL by running these commands:
  https://planereport.freshbrewed.science/

$ kubectl get cm myplanefromhelm-pyplanereport-cfg -o yaml | grep -i ignore
  IGNORESTATE: Cancelled,Hidden,Done

I then rotated the pod

$ kubectl delete pod myplanefromhelm-pyplanereport-f59b9c584-gbdcm
pod "myplanefromhelm-pyplanereport-f59b9c584-gbdcm" deleted

$ kubectl get pods | grep plane
myplanefromhelm-pyplanereport-f59b9c584-qcqhd            1/1     Running   0                 62s

and tested

/content/images/2023/12/planeso3-19.png

Once pushed, I created a tag, 0.2.1, which ended up building ghcr.io/idjohnson/pyplanereport:0.2.1

/content/images/2023/12/planeso3-20.png

And our report page is still live at planereport.freshbrewed.science

Summary

I really hope you enjoyed following this 4-part series as much as I did writing it. We started in Part 1 by trying Plane.so self-host and hosted. After exploring the SaaS option in detail, we moved on to Part 2 were we got into integrations and the REST API. In that post we attempted to Sync with Github and did succeed with JIRA. In Part 3 we built something new - a containerized Issue Report App using Python and Jinja2.

Lastly, here in Part 4 we Open-sourced it using Github. We made a proper Helm Chart, Github workflow and used Github itself to host the containers (ghcr.io). We tried to get gh-pages to host the Helm charts, but failing that, shoved them into a public repo location in my Harbor CR. We tackled tags and releases and lastly a full Issue update inside of Github using Github Codespace.

I am not done, of course. I expect to do more updates such as supporting self-hosted instances. I’m debating going back to build out my own self-hosted, however, I’m quite happy so far with their SaaS solution.

Agile OpenSource Kubernetes Docker Plane

Have something to add? Feedback? You can use the feedback form

Isaac Johnson

Isaac Johnson

Cloud Solutions Architect

Isaac is a CSA and DevOps engineer who focuses on cloud migrations and devops processes. He also is a dad to three wonderful daughters (hence the references to Princess King sprinkled throughout the blog).

Theme built by C.S. Rhymes