A New Project: Part 5: Store, Restore

Published: Jan 6, 2024 by Isaac Johnson

When we left off with Part 4 we had mostly automated the CI/CD flow for building and storing the container and charts. However, we punted on finishing the functionality of the app.

Today, let’s tackle changing how we store the YAML ingress/disabled. I also want to build out the templates repo and setup examples for Traefik, Nginx and Istio. Lastly, if I have time, I want to figure out a non-CR based helm repository endpoint for storing and sharing charts.

Update to store ingress files

As of last time, we launched the app and a basic Nginx “disabled” page:

builder@DESKTOP-QADGF36:~/Workspaces/pyK8sService$ helm list -n disabledtest
NAME    NAMESPACE       REVISION        UPDATED                                 STATUS          CHART               APP VERSION
mytest  disabledtest    5               2023-12-25 23:20:43.932018982 -0600 CST deployed        pyk8sservice-0.1.0  1.16.0
builder@DESKTOP-QADGF36:~/Workspaces/pyK8sService$ kubectl get pods -n disabledtest
NAME                                       READY   STATUS    RESTARTS   AGE
mytest-pyk8sservice-7d8798768f-9vgv8       1/1     Running   0          12h
mytest-pyk8sservice-app-74cc77666b-w8fpn   1/1     Running   0          11h
builder@DESKTOP-QADGF36:~/Workspaces/pyK8sService$ kubectl get svc -n disabledtest
NAME                      TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
mytest-pyk8sservice       ClusterIP   10.43.179.105   <none>        80/TCP     12h
mytest-pyk8sservice-app   ClusterIP   10.43.206.218   <none>        5000/TCP   12h

From the app deployment chart we can see that we mount the pvc to /config.

The first change I want to make is to store the existing Ingress into that mount. We originally wrote this up as Feature #5

/content/images/2024/01/newapp5-01.png

I’ll now create a branch for it (feature-5-store-pvc) and make some initial changes

/content/images/2024/01/newapp5-02.png

I’ll test by building locally

builder@DESKTOP-QADGF36:~/Workspaces/pyK8sService$ docker build -t harbor.freshbrewed.science/freshbrewedprivate/pyk8singresssvc:test0501 .
[+] Building 13.9s (10/10) FINISHED                                                                        
 => [internal] load build definition from Dockerfile                                                  0.0s
 => => transferring dockerfile: 193B                                                                  0.0s
 => [internal] load .dockerignore                                                                     0.0s
 => => transferring context: 2B                                                                       0.0s
 => [internal] load metadata for docker.io/library/python:3.8-slim                                    2.0s
 => [1/5] FROM docker.io/library/python:3.8-slim@sha256:d1cba0f8754d097bd333b8f3d4c655f37c2ede9042d1  1.5s
 => => resolve docker.io/library/python:3.8-slim@sha256:d1cba0f8754d097bd333b8f3d4c655f37c2ede9042d1  0.0s
 => => sha256:8ce3f2b601ccac03ff1858022363c325355bafba224123a4563dade58bc8e70f 3.51MB / 3.51MB        0.2s
 => => sha256:7f4f85c41831fbd0f3274211b4400f992d8e7a77938d86a5d24bc7af2e8df503 13.75MB / 13.75MB      0.5s
 => => sha256:61e3cec0af2bf2f5a7e75b87fb4daeeef94d7054b09c461a9da8604f805a1806 244B / 244B            0.2s
 => => sha256:d1cba0f8754d097bd333b8f3d4c655f37c2ede9042d1e7db69561d9eae2eebfa 1.86kB / 1.86kB        0.0s
 => => sha256:514ad38605babc4749a1dfb47a800a9ad30e97795ea3c52390a8cdd8a0d5952c 1.37kB / 1.37kB        0.0s
 => => sha256:067655fb1c0910227ec88b6bc6bc18d6697f82e80ab04a2ce00f36879df9b36e 6.97kB / 6.97kB        0.0s
 => => sha256:8244879b209cbcd120ac0c4a2bd01d70ee60cc770924a99c1948b6698ea4d2af 3.13MB / 3.13MB        0.4s
 => => extracting sha256:8ce3f2b601ccac03ff1858022363c325355bafba224123a4563dade58bc8e70f             0.3s
 => => extracting sha256:7f4f85c41831fbd0f3274211b4400f992d8e7a77938d86a5d24bc7af2e8df503             0.5s
 => => extracting sha256:61e3cec0af2bf2f5a7e75b87fb4daeeef94d7054b09c461a9da8604f805a1806             0.0s
 => => extracting sha256:8244879b209cbcd120ac0c4a2bd01d70ee60cc770924a99c1948b6698ea4d2af             0.2s
 => [internal] load build context                                                                     0.2s
 => => transferring context: 153.31kB                                                                 0.1s
 => [2/5] WORKDIR /app                                                                                0.1s
 => [3/5] COPY requirements.txt .                                                                     0.0s
 => [4/5] RUN pip install --no-cache-dir -r requirements.txt                                          9.0s
 => [5/5] COPY . .                                                                                    0.1s
 => exporting to image                                                                                0.4s 
 => => exporting layers                                                                               0.4s 
 => => writing image sha256:ad61fb8404c926e0748514b81aaf985f96cf8513d39fde4799e81f8224e45117          0.0s 
 => => naming to harbor.freshbrewed.science/freshbrewedprivate/pyk8singresssvc:test0501               0.0s 

Then push

builder@DESKTOP-QADGF36:~/Workspaces/pyK8sService$ docker push harbor.freshbrewed.science/freshbrewedprivate/pyk8singresssvc:test0501
The push refers to repository [harbor.freshbrewed.science/freshbrewedprivate/pyk8singresssvc]
577ac6d2efff: Pushed 
63699480bf04: Pushed 
095f4ccf99ba: Pushed 
f96fd631fd42: Pushed 
148dc688e60f: Layer already exists 
009aa92f6140: Layer already exists 
ad7efa606e4e: Layer already exists 
384858ccd7ef: Layer already exists 
7292cf786aa8: Layer already exists 
test0501: digest: sha256:1c77454910f2b4ebc3ab201b9c19b2e17d6983b184ec444e3bb2f31540f3a4b0 size: 2203

I then changed the deployment manually to set the image: and added an imagePullSecrets: section to the deployment to test

    spec:
      containers:
      - image: harbor.freshbrewed.science/freshbrewedprivate/pyk8singresssvc:test0501
        imagePullPolicy: Always
        livenessProbe:
          failureThreshold: 3
          httpGet:
            path: /
            port: http
            scheme: HTTP
          periodSeconds: 10
          successThreshold: 1
          timeoutSeconds: 1
        name: pyk8sservice
        ports:
        - containerPort: 5000
          name: http
          protocol: TCP
        readinessProbe:
          failureThreshold: 3
          httpGet:
            path: /
            port: http
            scheme: HTTP
          periodSeconds: 10
          successThreshold: 1
          timeoutSeconds: 1
        resources: {}
        securityContext: {}
        terminationMessagePath: /dev/termination-log
        terminationMessagePolicy: File
        volumeMounts:
        - mountPath: /config
          name: config
      dnsPolicy: ClusterFirst
      imagePullSecrets:
      - name: myharborreg

Actually, I came around and opted to try to use this chart to create an ingress I could use. I ended up messing up my local deploy, so i removed/deleted then re-installed:

$ cat myvalues.yaml
appservice:
  port: 5000
persistence:
  storageClass: local-path
rbac:
  create: true

appimage:
  repository: harbor.freshbrewed.science/freshbrewedprivate/pyk8singresssvc
  pullPolicy: Always
  tag: "test0501"

appimagePullSecrets:
  - name: myharborreg

ingress:
  enabled: true
  className: "traefik"
  annotations:
    kubernetes.io/ingress.class: traefik
    # kubernetes.io/tls-acme: "true"
  hosts:
    - host: pytestapp.local
      paths:
        - path: /
          pathType: ImplementationSpecific
  tls:
   - secretName: chart-example-tls
     hosts:
       - chart-example.local



$ helm install mytest -n disabledtest -f myvalues.yaml ./pyK8sService/
NAME: mytest
LAST DEPLOYED: Wed Dec 27 12:02:35 2023
NAMESPACE: disabledtest
STATUS: deployed
REVISION: 1
NOTES:
1. Get the application URL by running these commands:
  https://pytestapp.local/

I can now see the pods running

$ kubectl get pods -n disabledtest
NAME                                       READY   STATUS    RESTARTS   AGE
mytest-pyk8sservice-app-5f67b78b94-rdtmg   1/1     Running   0          49s
mytest-pyk8sservice-7d8798768f-9pwcb       1/1     Running   0          49s

with an Ingress I can use for testing

$ kubectl get ingress -n disabledtest
NAME                  CLASS     HOSTS             ADDRESS                                    PORTS     AGE
mytest-pyk8sservice   traefik   pytestapp.local   192.168.1.13,192.168.1.159,192.168.1.206   80, 443   5m40s

We can now test

$ kubectl port-forward svc/mytest-pyk8sservice-app -n disabledtest 5000:5000
Forwarding from 127.0.0.1:5000 -> 5000
Forwarding from [::1]:5000 -> 5000
Handling connection for 5000
Handling connection for 5000

/content/images/2024/01/newapp5-03.png

I then typed in a URL i hoped would work: http://localhost:5000/store?thisIngress=mytest-pyk8sservice

/content/images/2024/01/newapp5-04.png

And boom, what one gets when they forget to cast to string

/content/images/2024/01/newapp5-05.png

It took a bit of tweaking, but I landed on the following app.py code:

from flask import Flask, render_template, request
from kubernetes import client, config
import sys
import os
import yaml

app = Flask(__name__)

@app.route('/')
def index():
    ingresses, ingress_count = get_kubernetes_resources()
    print(f"Number of Ingress resources found: {ingress_count}", file=sys.stderr)
    return render_template('index.html', resources=ingresses, count=ingress_count)

@app.route('/disable', methods=['GET'])
def disable_ingress():
    ingress_name = request.args.get('thisIngress')
    print(f"Disable Ingress: {ingress_name}", file=sys.stderr)
    if ingress_name:
        update_ingress_service(ingress_name, 'disabledservice')
        return f"Ingress '{ingress_name}' updated to use service 'disabledservice'"
    else:
        return "Invalid request. Please provide 'thisIngress' as a GET parameter."

@app.route('/store', methods=['GET'])
def store_ingress():
    ingress_name = request.args.get('thisIngress')
    print(f"Store Ingress: {ingress_name}", file=sys.stderr)
    if ingress_name:
        store_ingress_service(ingress_name)
        return f"Ingress '{ingress_name}' stored to /config"
    else:
        return "Invalid request. Please provide 'thisIngress' as a GET parameter."


def get_kubernetes_resources():
    config.load_incluster_config()
    v1 = client.NetworkingV1Api()

    namespace = open("/var/run/secrets/kubernetes.io/serviceaccount/namespace").read()

    ingresses = v1.list_namespaced_ingress(namespace)
    ingress_list = [ingress.metadata.name for ingress in ingresses.items]
    ingress_count = len(ingress_list)

    return ingress_list, ingress_count

def store_ingress_service(ingress_name):
    config.load_incluster_config()
    v1 = client.NetworkingV1Api()

    namespace = open("/var/run/secrets/kubernetes.io/serviceaccount/namespace").read()

    print(f"in store_ingress", file=sys.stderr)
    try:
        print(f"0 in store_ingress", file=sys.stderr)
        ingress = v1.read_namespaced_ingress(name=ingress_name, namespace=namespace)
        # Update the Ingress backend service to 'new_service_name'
        #print(f"1 in update_ingress_service", file=sys.stderr)
        #print(f"Ingress first: {ingress}", file=sys.stderr)
        #file_path = os.path.join("/config", ingress_name, ".yaml")
        file_path = f"/config/{ingress_name}.yaml"

        ingress_dict = ingress.to_dict()
        ingress_yaml = yaml.dump(ingress_dict) 

        with open(file_path, 'w') as file:
            file.write(ingress_yaml)

        print(f"1 store_ingress: {ingress} to {file_path}", file=sys.stderr)
    except client.rest.ApiException as e:
        print(f"error in store_ingress: {e}", file=sys.stderr)
        return f"Error storing Ingress '{ingress_name}': {e}"

    return f"Ingress '{ingress_name}' stored in /config '{ingress_name}'"

def update_ingress_service(ingress_name, new_service_name):
    config.load_incluster_config()
    v1 = client.NetworkingV1Api()

    namespace = open("/var/run/secrets/kubernetes.io/serviceaccount/namespace").read()

    print(f"in update_ingress_service", file=sys.stderr)
    try:
        print(f"0 in update_ingress_service", file=sys.stderr)
        ingress = v1.read_namespaced_ingress(name=ingress_name, namespace=namespace)
        # Update the Ingress backend service to 'new_service_name'
        #print(f"1 in update_ingress_service", file=sys.stderr)
        #print(f"Ingress first: {ingress}", file=sys.stderr)
        ingress.spec.rules[0].http.paths[0].backend.service.name = new_service_name
        #print(f"2 in update_ingress_service", file=sys.stderr)
        api_response = v1.patch_namespaced_ingress(
            name='rundeckingress',
            namespace='default',  # replace with your namespace
            body=ingress
        )
        #print(f"3 in update_ingress_service", file=sys.stderr)
        #print(f"Ingress second: {ingress}", file=sys.stderr)
    except client.rest.ApiException as e:
        print(f"error in update_ingress_service: {e}", file=sys.stderr)
        return f"Error updating Ingress '{ingress_name}': {e}"

    return f"Ingress '{ingress_name}' updated to use service '{new_service_name}'"


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True)

Which when tested

$ kubectl port-forward svc/mytest-pyk8sservice-app -n disabledtest 5000:5000
Forwarding from 127.0.0.1:5000 -> 5000
Forwarding from [::1]:5000 -> 5000
Handling connection for 5000
Handling connection for 5000

Said it saved the file:

/content/images/2024/01/newapp5-06.png

We can actually verify that by cat’ing out the file

$ kubectl exec mytest-pyk8sservice-app-6cdd6d756f-bfnfr -n disabledtest cat '/config/mytest-pyk8sservice.yaml'
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.
api_version: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: traefik
    meta.helm.sh/release-name: mytest
    meta.helm.sh/release-namespace: disabledtest
  creation_timestamp: 2023-12-27 18:02:36+00:00
  deletion_grace_period_seconds: null
  deletion_timestamp: null
  finalizers: null
  generate_name: null
  generation: 1
  labels:
    app.kubernetes.io/instance: mytest
    app.kubernetes.io/managed-by: Helm
    app.kubernetes.io/name: pyk8sservice
    app.kubernetes.io/version: 1.16.0
    helm.sh/chart: pyk8sservice-0.2.0
  managed_fields:
  - api_version: networking.k8s.io/v1
    fields_type: FieldsV1
    fields_v1:
      f:metadata:
        f:annotations:
          .: {}
          f:kubernetes.io/ingress.class: {}
          f:meta.helm.sh/release-name: {}
          f:meta.helm.sh/release-namespace: {}
        f:labels:
          .: {}
          f:app.kubernetes.io/instance: {}
          f:app.kubernetes.io/managed-by: {}
          f:app.kubernetes.io/name: {}
          f:app.kubernetes.io/version: {}
          f:helm.sh/chart: {}
      f:spec:
        f:ingressClassName: {}
        f:rules: {}
        f:tls: {}
    manager: helm
    operation: Update
    subresource: null
    time: 2023-12-27 18:02:36+00:00
  - api_version: networking.k8s.io/v1
    fields_type: FieldsV1
    fields_v1:
      f:status:
        f:loadBalancer:
          f:ingress: {}
    manager: traefik
    operation: Update
    subresource: status
    time: 2023-12-27 18:02:36+00:00
  name: mytest-pyk8sservice
  namespace: disabledtest
  owner_references: null
  resource_version: '8438650'
  self_link: null
  uid: 56eed306-e43c-409b-9d17-b07f6a270c0d
spec:
  default_backend: null
  ingress_class_name: traefik
  rules:
  - host: pytestapp.local
    http:
      paths:
      - backend:
          resource: null
          service:
            name: mytest-pyk8sservice
            port:
              name: null
              number: 80
        path: /
        path_type: ImplementationSpecific
  tls:
  - hosts:
    - chart-example.local
    secret_name: chart-example-tls
status:
  load_balancer:
    ingress:
    - hostname: null
      ip: 192.168.1.13
      ports: null
    - hostname: null
      ip: 192.168.1.159
      ports: null
    - hostname: null
      ip: 192.168.1.206
      ports: null

And prove it’s in the PVC by deleting the pod and doing it again:

$ kubectl exec mytest-pyk8sservice-app-6cdd6d756f-nwf6q -n disabledtest -- bash -c 'ls -l /
config/'
total 8
-rw-r--r-- 1 root root 2342 Dec 27 18:31 mytest-pyk8sservice
-rw-r--r-- 1 root root 2342 Dec 27 18:36 mytest-pyk8sservice.yaml

note: the file without a .yaml was from an earlier test

I’m happy enough with this incremental progress to push the changes back to GIT

I then created a PR #5

/content/images/2024/01/newapp5-07.png

Now that we have Ingress objects set aside, how hard would it be to restore from them?

Let’s write a Ticket for that:

/content/images/2024/01/newapp5-08.png

I created a new branch and then wrote routines to list and restore from file

@app.route('/saved', methods=['GET'])
def showsaved_ingress():
    filenames = os.listdir('/config')
    ingress_count = len(filenames)
    print(f"1 show saved ingresses in /config: {ingress_count} total", file=sys.stderr)
    return render_template('restore.html', resources=filenames, count=ingress_count)


@app.route('/restore', methods=['GET'])
def restore_ingress():
    ingress_name = request.args.get('thisIngress')
    print(f"Restore Ingress: {ingress_name}", file=sys.stderr)
    if ingress_name:
        restore_ingress_service(ingress_name)
        return f"Ingress '{ingress_name}' restored from /config"
    else:
        return "Invalid request. Please provide 'thisIngress' as a GET parameter."
    
#==================================================================

def restore_ingress_service(ingress_name):
    config.load_incluster_config()
    v1 = client.NetworkingV1Api()

    namespace = open("/var/run/secrets/kubernetes.io/serviceaccount/namespace").read()
    print(f"in store_ingress", file=sys.stderr)

    v1 = client.NetworkingV1Api()

    print(f"0 in restore ingress", file=sys.stderr)
    # Remove IFF exists
    try:
        v1.delete_namespaced_ingress(name=ingress_name, namespace=namespace)
    except client.exceptions.ApiException as e:
        if e.status != 404:  # Ignore error if the ingress does not exist
            raise

    # Now try and restore it
    print(f"1 in restore ingress", file=sys.stderr)
    try:
        file_path = f"/config/{ingress_name}.yaml"
        with open(file_path, 'r') as file:
            ingress_dict = yaml.safe_load(file)
        
        # Convert Dict back to an Ingress Object
        ingress = client.V1beta1Ingress(**ingress_dict)
        # ingress_name = ingress.metadata.name

        v1.create_namespaced_ingress(namespace=namespace, body=ingress)
    except client.rest.ApiException as e:
        print(f"error in restore: {e}", file=sys.stderr)
        return f"Error restoring Ingress '{ingress_name}': {e}"

    return f"Ingress '{ingress_name}' restored from /config '{ingress_name}'"

I did the usual local build and push:

builder@DESKTOP-QADGF36:~/Workspaces/pyK8sService$ docker push harbor.freshbrewed.science/freshbrewedprivate/pyk8singresssvc:test0601 .
"docker push" requires exactly 1 argument.
See 'docker push --help'.

Usage:  docker push [OPTIONS] NAME[:TAG]

Push an image or a repository to a registry
builder@DESKTOP-QADGF36:~/Workspaces/pyK8sService$ docker push harbor.freshbrewed.science/freshbrewedprivate/pyk8singresssvc:test0601
The push refers to repository [harbor.freshbrewed.science/freshbrewedprivate/pyk8singresssvc]
8fe78eea7bfd: Pushed 
63699480bf04: Layer already exists 
095f4ccf99ba: Layer already exists 
f96fd631fd42: Layer already exists 
148dc688e60f: Layer already exists 
009aa92f6140: Layer already exists 
ad7efa606e4e: Layer already exists 
384858ccd7ef: Layer already exists 
7292cf786aa8: Layer already exists 
test0601: digest: sha256:ea38d03be384df9f481b35a64c113f3b8a3e375c5bff3ec8e4e7c7c853782093 size: 2203

a helm upgrade to use that image

$ helm upgrade mytest -n disabledtest -f myvalues.yaml ./pyK8sService/
Release "mytest" has been upgraded. Happy Helming!
NAME: mytest
LAST DEPLOYED: Wed Dec 27 14:41:18 2023
NAMESPACE: disabledtest
STATUS: deployed
REVISION: 8
NOTES:
1. Get the application URL by running these commands:
  https://pytestapp.local/

Then a port-forward to test

builder@DESKTOP-QADGF36:~/Workspaces/pyK8sService$ kubectl port-forward svc/mytest-pyk8sservice-app -n disabledtest 5000:5000
Forwarding from 127.0.0.1:5000 -> 5000
Forwarding from [::1]:5000 -> 5000
Handling connection for 5000
Handling connection for 5000

It would seem listing works just dandy

/content/images/2024/01/newapp5-09.png

Though my restore fell down

/content/images/2024/01/newapp5-10.png

I realized that I needed to switch to just “V1Ingress” in the class

    try:
        file_path = f"/config/{ingress_name}.yaml"
        with open(file_path, 'r') as file:
            ingress_dict = yaml.safe_load(file)
        
        # Convert Dict back to an Ingress Object
        ingress = client.V1Ingress(**ingress_dict)
        # ingress_name = ingress.metadata.name

        v1.create_namespaced_ingress(namespace=namespace, body=ingress)

Now a build and push and test works

/content/images/2024/01/newapp5-11.png

However, I don’t actually see it created

builder@DESKTOP-QADGF36:~/Workspaces/pyK8sService$ kubectl get ingress -n disabledtest
No resources found in disabledtest namespace.

Logs show us something is amiss

$ kubectl logs mytest-pyk8sservice-app-b587b498b-nlwhc -n disabledtest
 * Serving Flask app 'app'
 * Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on all addresses (0.0.0.0)
 * Running on http://127.0.0.1:5000
 * Running on http://10.42.0.64:5000
Press CTRL+C to quit
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 127-226-652
Number of Ingress resources found: 1
10.42.0.1 - - [27/Dec/2023 20:50:59] "GET / HTTP/1.1" 200 -
Number of Ingress resources found: 1
Number of Ingress resources found: 1
10.42.0.1 - - [27/Dec/2023 20:51:07] "GET / HTTP/1.1" 200 -
10.42.0.1 - - [27/Dec/2023 20:51:07] "GET / HTTP/1.1" 200 -
Restore Ingress: mytest-pyk8sservice
in store_ingress
0 in restore ingress
1 in restore ingress
error in restore: (422)
Reason: Unprocessable Entity
HTTP response headers: HTTPHeaderDict({'Audit-Id': '7d07b45d-7c9c-40eb-934a-c715fb37533a', 'Cache-Control': 'no-cache, private', 'Content-Type': 'application/json', 'Warning': '299 - "unknown field \\"metadata.creation_timestamp\\"", 299 - "unknown field \\"metadata.deletion_grace_period_seconds\\"", 299 - "unknown field \\"metadata.deletion_timestamp\\"", 299 - "unknown field \\"metadata.generate_name\\"", 299 - "unknown field \\"metadata.managed_fields\\"", 299 - "unknown field \\"metadata.owner_references\\"", 299 - "unknown field \\"metadata.resource_version\\"", 299 - "unknown field \\"metadata.self_link\\"", 299 - "unknown field \\"spec.default_backend\\"", 299 - "unknown field \\"spec.ingress_class_name\\"", 299 - "unknown field \\"spec.rules[0].http.paths[0].path_type\\"", 299 - "unknown field \\"spec.tls[0].secret_name\\"", 299 - "unknown field \\"status.load_balancer\\""', 'X-Kubernetes-Pf-Flowschema-Uid': '9d2994dc-4137-4742-91f2-ef281ef01ff2', 'X-Kubernetes-Pf-Prioritylevel-Uid': '70d13cea-624c-4356-af78-6b13c6d82bba', 'Date': 'Wed, 27 Dec 2023 20:51:15 GMT', 'Content-Length': '471'})
HTTP response body: {"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Ingress.extensions \"mytest-pyk8sservice\" is invalid: spec.rules[0].http.paths[0].pathType: Required value: pathType must be specified","reason":"Invalid","details":{"name":"mytest-pyk8sservice","group":"extensions","kind":"Ingress","causes":[{"reason":"FieldValueRequired","message":"Required value: pathType must be specified","field":"spec.rules[0].http.paths[0].pathType"}]},"code":422}


127.0.0.1 - - [27/Dec/2023 20:51:15] "GET /restore?thisIngress=mytest-pyk8sservice HTTP/1.1" 200 -
Number of Ingress resources found: 0
Number of Ingress resources found: 0
10.42.0.1 - - [27/Dec/2023 20:51:17] "GET / HTTP/1.1" 200 -
10.42.0.1 - - [27/Dec/2023 20:51:17] "GET / HTTP/1.1" 200 -
Number of Ingress resources found: 0
10.42.0.1 - - [27/Dec/2023 20:51:27] "GET / HTTP/1.1" 200 -
Number of Ingress resources found: 0
10.42.0.1 - - [27/Dec/2023 20:51:27] "GET / HTTP/1.1" 200 -
Number of Ingress resources found: 0
10.42.0.1 - - [27/Dec/2023 20:51:37] "GET / HTTP/1.1" 200 -

I used a lot of debug and found it just was vommitting on the parsed YAML. I decided to pivot to the utils apply method instead.

From

def restore_ingress_service(ingress_name):
    config.load_incluster_config()
    v1 = client.NetworkingV1Api()

    namespace = open("/var/run/secrets/kubernetes.io/serviceaccount/namespace").read()
    print(f"in restore ingress", file=sys.stderr)

    v1 = client.NetworkingV1Api()

    print(f"0 in restore ingress", file=sys.stderr)
    # Remove IFF exists
    try:
        v1.delete_namespaced_ingress(name=ingress_name, namespace=namespace)
    except client.exceptions.ApiException as e:
        if e.status != 404:  # Ignore error if the ingress does not exist
            raise

    # Now try and restore it
    print(f"1 in restore ingress", file=sys.stderr)
    try:
        file_path = f"/config/{ingress_name}.yaml"
        
        print(f"1a Restore Ingress: {file_path}", file=sys.stderr)
        with open(file_path, 'r') as file:
            ingress_dict = yaml.safe_load(file)
        
        print(f"1b Restore Ingress: {ingress_dict}", file=sys.stderr)
        # Convert Dict back to an Ingress Object
        ingress = client.V1Ingress(**ingress_dict)
        print(f"1c Restore Ingress: {ingress}", file=sys.stderr)
        # ingress_name = ingress.metadata.name

        v1.create_namespaced_ingress(namespace=namespace, body=ingress)
        print(f"1d Restored Ingress: {ingress_name}", file=sys.stderr)
    except client.rest.ApiException as e:
        print(f"error in restore: {e}", file=sys.stderr)
        return f"Error restoring Ingress '{ingress_name}': {e}"

    print(f"2 Restored Ingress: {ingress_name}", file=sys.stderr)
    return f"Ingress '{ingress_name}' restored from /config '{ingress_name}'"

to:

from kubernetes import client, config, utils

#... snip ...

def restore_ingress_service(ingress_name):
    config.load_incluster_config()

    apiClient = client.ApiClient()

    namespace = open("/var/run/secrets/kubernetes.io/serviceaccount/namespace").read()
    print(f"in restore ingress", file=sys.stderr)

    v1 = client.NetworkingV1Api()
    print(f"0 in restore ingress", file=sys.stderr)
    # Remove IFF exists
    try:
        v1.delete_namespaced_ingress(name=ingress_name, namespace=namespace)
    except client.exceptions.ApiException as e:
        if e.status != 404:  # Ignore error if the ingress does not exist
            raise

    # Now try and restore it
    print(f"1 in restore ingress", file=sys.stderr)
    try:
        file_path = f"/config/{ingress_name}.yaml"
        
        print(f"1a Restore Ingress: {file_path}", file=sys.stderr)
        # Apply the manifest
        utils.create_from_yaml(apiClient, file_path)

        print(f"1d Restored Ingress: {ingress_name}", file=sys.stderr)
    except client.rest.ApiException as e:
        print(f"error in restore: {e}", file=sys.stderr)
        return f"Error restoring Ingress '{ingress_name}': {e}"

    print(f"2 Restored Ingress: {ingress_name}", file=sys.stderr)
    return f"Ingress '{ingress_name}' restored from /config '{ingress_name}'"

Actually, this ran into another issue - the V1 api was storing the YAML with “api_version” not “apiVersion”.

$ kubectl exec `kubectl get pods -l app.kubernetes.io/instance=mytest-app -n disabledtest -o=jsonpath='{.items[0].metadata.name}'` -n disabledtest cat '/config/mytest-pyk8sservice.yaml'
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.
api_version: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: traefik
    meta.helm.sh/release-name: mytest
    meta.helm.sh/release-namespace: disabledtest
  creation_timestamp: 2023-12-28 21:39:08+00:00
  ... snip ...

I fought and fought this. I finally sucommed to just using a post-save file replace in the store routine:

def store_ingress_service(ingress_name):
    config.load_incluster_config()
    v1 = client.NetworkingV1Api()

    namespace = open("/var/run/secrets/kubernetes.io/serviceaccount/namespace").read()

    print(f"in store_ingress", file=sys.stderr)
    try:
        print(f"0 in store_ingress", file=sys.stderr)
        ingress = v1.read_namespaced_ingress(name=ingress_name, namespace=namespace)
        # Update the Ingress backend service to 'new_service_name'
        #print(f"1 in update_ingress_service", file=sys.stderr)
        #print(f"Ingress first: {ingress}", file=sys.stderr)
        #file_path = os.path.join("/config", ingress_name, ".yaml")
        file_path = f"/config/{ingress_name}.yaml"

        ingress_dict = ingress.to_dict()
        ingress_yaml = yaml.dump(ingress_dict) 

        with open(file_path, 'w') as file:
            file.write(ingress_yaml)

        # seems to really want to use "api_version". Force it to use "apiVersion"!
        with open(file_path, 'r') as file:
            content = file.read()

        content = content.replace('api_version', 'apiVersion', 1)
        with open(file_path, 'w') as file:
            file.write(content)


        print(f"1 store_ingress: {ingress} to {file_path}", file=sys.stderr)
    except client.rest.ApiException as e:
        print(f"error in store_ingress: {e}", file=sys.stderr)
        return f"Error storing Ingress '{ingress_name}': {e}"

    return f"Ingress '{ingress_name}' stored in /config '{ingress_name}'"

Now the store routine (http://localhost:5000/store?thisIngress=mytest-pyk8sservice) saves it properly:

/content/images/2024/01/newapp5-12.png

$ kubectl exec `kubectl get pods -l app.kubernetes.io/instance=mytest-app -n disabledtest -o=jsonpath='{.items[0].metadata.name}'` -n disabledtest cat '/config/mytest-pyk8sservice.yaml' | head -n10
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: traefik
    meta.helm.sh/release-name: mytest
    meta.helm.sh/release-namespace: disabledtest
  creation_timestamp: 2023-12-28 21:39:08+00:00
  deletion_grace_period_seconds: null
  deletion_timestamp: null

I had to fix the update ingress service routine as it had some hardcoded values


def update_ingress_service(ingress_name, new_service_name):
    config.load_incluster_config()
    v1 = client.NetworkingV1Api()

    namespace = open("/var/run/secrets/kubernetes.io/serviceaccount/namespace").read()

    print(f"in update_ingress_service", file=sys.stderr)
    try:
        print(f"0 in update_ingress_service", file=sys.stderr)
        ingress = v1.read_namespaced_ingress(name=ingress_name, namespace=namespace)
        # Update the Ingress backend service to 'new_service_name'
        #print(f"1 in update_ingress_service", file=sys.stderr)
        #print(f"Ingress first: {ingress}", file=sys.stderr)
        ingress.spec.rules[0].http.paths[0].backend.service.name = new_service_name
        #print(f"2 in update_ingress_service", file=sys.stderr)
        api_response = v1.patch_namespaced_ingress(
            name=ingress_name,
            namespace=namespace,  # replace with your namespace
            body=ingress
        )
        #print(f"3 in update_ingress_service", file=sys.stderr)
        #print(f"Ingress second: {ingress}", file=sys.stderr)
    except client.rest.ApiException as e:
        print(f"error in update_ingress_service: {e}", file=sys.stderr)
        return f"Error updating Ingress '{ingress_name}': {e}"

    return f"Ingress '{ingress_name}' updated to use service '{new_service_name}'"

But now it works to disable:

/content/images/2024/01/newapp5-13.png

And I can see it disabled it’s own ingress

$ kubectl get ingress mytest-pyk8sservice -n disabledtest -o yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: traefik
    meta.helm.sh/release-name: mytest
    meta.helm.sh/release-namespace: disabledtest
  creationTimestamp: "2023-12-28T21:39:08Z"
  generation: 2
  labels:
    app.kubernetes.io/instance: mytest
    app.kubernetes.io/managed-by: Helm
    app.kubernetes.io/name: pyk8sservice
    app.kubernetes.io/version: 1.16.0
    helm.sh/chart: pyk8sservice-0.2.0
  name: mytest-pyk8sservice
  namespace: disabledtest
  resourceVersion: "9126079"
  uid: cba226d5-2cc9-4161-972f-79367486876e
spec:
  ingressClassName: traefik
  rules:
  - host: pytestapp.local
    http:
      paths:
      - backend:
          service:
            name: disabledservice
            port:
              number: 80
        path: /
        pathType: ImplementationSpecific
  tls:
  - hosts:
    - chart-example.local
    secretName: chart-example-tls
status:
  loadBalancer:
    ingress:
    - ip: 192.168.1.13
    - ip: 192.168.1.159
    - ip: 192.168.1.206

To be honest, I spent hours fighting to get it to case things right

from flask import Flask, render_template, request
from kubernetes import client, config, utils
import sys
import os
import yaml

app = Flask(__name__)

@app.route('/')
def index():
    ingresses, ingress_count = get_kubernetes_resources()
    print(f"Number of Ingress resources found: {ingress_count}", file=sys.stderr)
    return render_template('index.html', resources=ingresses, count=ingress_count)

@app.route('/disable', methods=['GET'])
def disable_ingress():
    ingress_name = request.args.get('thisIngress')
    print(f"Disable Ingress: {ingress_name}", file=sys.stderr)
    if ingress_name:
        update_ingress_service(ingress_name, 'disabledservice')
        return f"Ingress '{ingress_name}' updated to use service 'disabledservice'"
    else:
        return "Invalid request. Please provide 'thisIngress' as a GET parameter."

@app.route('/store', methods=['GET'])
def store_ingress():
    ingress_name = request.args.get('thisIngress')
    print(f"Store Ingress: {ingress_name}", file=sys.stderr)
    if ingress_name:
        store_ingress_service(ingress_name)
        return f"Ingress '{ingress_name}' stored to /config"
    else:
        return "Invalid request. Please provide 'thisIngress' as a GET parameter."

@app.route('/saved', methods=['GET'])
def showsaved_ingress():
    filenames = os.listdir('/config')
    ingress_count = len(filenames)
    print(f"1 show saved ingresses in /config: {ingress_count} total", file=sys.stderr)
    return render_template('restore.html', resources=filenames, count=ingress_count)


@app.route('/restore', methods=['GET'])
def restore_ingress():
    ingress_name = request.args.get('thisIngress')
    print(f"Restore Ingress: {ingress_name}", file=sys.stderr)
    if ingress_name:
        restore_ingress_service(ingress_name)
        return f"Ingress '{ingress_name}' restored from /config"
    else:
        return "Invalid request. Please provide 'thisIngress' as a GET parameter."
    
#==================================================================
def snake_to_camel(word):
    """Convert a snake case string to camel case without capitalizing the first word."""
    components = word.split('_')
    return components[0] + ''.join(x.capitalize() for x in components[1:])

def restore_ingress_service(ingress_name):
    config.load_incluster_config()

    namespace = open("/var/run/secrets/kubernetes.io/serviceaccount/namespace").read()
    print(f"in restore ingress", file=sys.stderr)

    v1 = client.NetworkingV1Api()
    print(f"0 in restore ingress", file=sys.stderr)
    # Remove IFF exists
    try:
        v1.delete_namespaced_ingress(name=ingress_name, namespace=namespace)
    except client.exceptions.ApiException as e:
        if e.status != 404:  # Ignore error if the ingress does not exist
            raise

    # Now try and restore it
    print(f"1 in restore ingress", file=sys.stderr)
    try:
        file_path = f"/config/{ingress_name}.yaml"
        
        print(f"1a Restore Ingress: {file_path}", file=sys.stderr)
        # Apply the manifest
        utils.create_from_yaml(client.ApiClient(), yaml_file=file_path, verbose=True, namespace=namespace)

        print(f"1d Restored Ingress: {ingress_name}", file=sys.stderr)
    except client.rest.ApiException as e:
        print(f"error in restore: {e}", file=sys.stderr)
        return f"Error restoring Ingress '{ingress_name}': {e}"

    print(f"2 Restored Ingress: {ingress_name}", file=sys.stderr)
    return f"Ingress '{ingress_name}' restored from /config '{ingress_name}'"

def get_kubernetes_resources():
    config.load_incluster_config()
    v1 = client.NetworkingV1Api()

    namespace = open("/var/run/secrets/kubernetes.io/serviceaccount/namespace").read()

    ingresses = v1.list_namespaced_ingress(namespace)
    ingress_list = [ingress.metadata.name for ingress in ingresses.items]
    ingress_count = len(ingress_list)

    return ingress_list, ingress_count

def store_ingress_service(ingress_name):
    config.load_incluster_config()
    v1 = client.NetworkingV1Api()

    namespace = open("/var/run/secrets/kubernetes.io/serviceaccount/namespace").read()

    print(f"in store_ingress", file=sys.stderr)
    try:
        print(f"0 in store_ingress", file=sys.stderr)
        ingress = v1.read_namespaced_ingress(name=ingress_name, namespace=namespace)
        # Update the Ingress backend service to 'new_service_name'
        #print(f"1 in update_ingress_service", file=sys.stderr)
        #print(f"Ingress first: {ingress}", file=sys.stderr)
        #file_path = os.path.join("/config", ingress_name, ".yaml")
        file_path = f"/config/{ingress_name}.yaml"

        ingress_dict = ingress.to_dict()
        ingress_yaml = yaml.dump(ingress_dict) 

        with open(file_path, 'w') as file:
            file.write(ingress_yaml)

        # seems to really want to use "api_version". Force it to use "apiVersion"!
        with open(file_path, 'r') as file:
            lines = file.readlines()

        for i in range(len(lines)):
            words = lines[i].split(' ')
            words = [snake_to_camel(word) if '_' in word else word for word in words]
            lines[i] = ' '.join(words)

        content = ''.join(lines)

        with open(file_path, 'w') as file:
            file.write(content)

        print(f"1 store_ingress: {ingress} to {file_path}", file=sys.stderr)
    except client.rest.ApiException as e:
        print(f"error in store_ingress: {e}", file=sys.stderr)
        return f"Error storing Ingress '{ingress_name}': {e}"

    return f"Ingress '{ingress_name}' stored in /config '{ingress_name}'"


def update_ingress_service(ingress_name, new_service_name):
    config.load_incluster_config()
    v1 = client.NetworkingV1Api()

    namespace = open("/var/run/secrets/kubernetes.io/serviceaccount/namespace").read()

    print(f"in update_ingress_service", file=sys.stderr)
    try:
        print(f"0 in update_ingress_service", file=sys.stderr)
        ingress = v1.read_namespaced_ingress(name=ingress_name, namespace=namespace)
        # Update the Ingress backend service to 'new_service_name'
        #print(f"1 in update_ingress_service", file=sys.stderr)
        #print(f"Ingress first: {ingress}", file=sys.stderr)
        ingress.spec.rules[0].http.paths[0].backend.service.name = new_service_name
        #print(f"2 in update_ingress_service", file=sys.stderr)
        api_response = v1.patch_namespaced_ingress(
            name=ingress_name,
            namespace=namespace,  # replace with your namespace
            body=ingress
        )
        #print(f"3 in update_ingress_service", file=sys.stderr)
        #print(f"Ingress second: {ingress}", file=sys.stderr)
    except client.rest.ApiException as e:
        print(f"error in update_ingress_service: {e}", file=sys.stderr)
        return f"Error updating Ingress '{ingress_name}': {e}"

    return f"Ingress '{ingress_name}' updated to use service '{new_service_name}'"


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True)

Only to get stopped by

kubernetes.utils.create_from_yaml.FailToCreateError: Error from server (Internal Server Error): {"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"resourceVersion should not be set on objects to be created","code":500}

I did end up fighting for a few more hours on snake casing vs camel casing before I pivoted to just using kubectl directly

I updated the Dockerfile to pull in kubectl

FROM python:3.8-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Install necessary packages for adding apt repositories
RUN apt-get update && \
    apt-get install -y apt-transport-https ca-certificates curl gnupg lsb-release

# Add Kubernetes apt repository
RUN echo "deb https://apt.kubernetes.io/ kubernetes-xenial main" | tee -a /etc/apt/sources.list.d/kubernetes.list

# Add Kubernetes apt repository GPG key
RUN curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -

# Update package list
RUN apt-get update

# Install kubectl
RUN apt-get install -y kubectl

COPY . .

CMD ["python", "app.py"]

Then the app needed to be changed. This is after quite a lot of cleaning:

from flask import Flask, render_template, request
from kubernetes import client, config, utils
import sys
import os
import subprocess

app = Flask(__name__)

@app.route('/')
def index():
    ingresses, ingress_count = get_kubernetes_resources()
    print(f"Number of Ingress resources found: {ingress_count}", file=sys.stderr)
    return render_template('index.html', resources=ingresses, count=ingress_count)

@app.route('/disable', methods=['GET'])
def disable_ingress():
    ingress_name = request.args.get('thisIngress')
    print(f"Disable Ingress: {ingress_name}", file=sys.stderr)
    if ingress_name:
        update_ingress_service(ingress_name, 'disabledservice')
        return f"Ingress '{ingress_name}' updated to use service 'disabledservice'"
    else:
        return "Invalid request. Please provide 'thisIngress' as a GET parameter."

@app.route('/store', methods=['GET'])
def store_ingress():
    ingress_name = request.args.get('thisIngress')
    print(f"Store Ingress: {ingress_name}", file=sys.stderr)
    if ingress_name:
        newstore_ingress_service(ingress_name)
        return f"Ingress '{ingress_name}' stored to /config"
    else:
        return "Invalid request. Please provide 'thisIngress' as a GET parameter."

@app.route('/saved', methods=['GET'])
def showsaved_ingress():
    filenames = os.listdir('/config')
    ingress_count = len(filenames)
    print(f"1 show saved ingresses in /config: {ingress_count} total", file=sys.stderr)
    return render_template('restore.html', resources=filenames, count=ingress_count)


@app.route('/restore', methods=['GET'])
def restore_ingress():
    ingress_name = request.args.get('thisIngress')
    print(f"Restore Ingress: {ingress_name}", file=sys.stderr)
    if ingress_name:
        newrestore_ingress_service(ingress_name)
        return f"Ingress '{ingress_name}' restored from /config"
    else:
        return "Invalid request. Please provide 'thisIngress' as a GET parameter."
    
#==================================================================
def newrestore_ingress_service(ingress_name):
    config.load_incluster_config()

    namespace = open("/var/run/secrets/kubernetes.io/serviceaccount/namespace").read()

    v1 = client.NetworkingV1Api()
    print(f"0 in restore ingress", file=sys.stderr)
    # Remove IFF exists
    try:
        v1.delete_namespaced_ingress(name=ingress_name, namespace=namespace)
    except client.exceptions.ApiException as e:
        if e.status != 404:  # Ignore error if the ingress does not exist
            raise

    file_path = f"/config/{ingress_name}.yaml"

    cmd = f"kubectl apply --validate='false' -f {file_path}"
    # Execute the command
    subprocess.run(cmd, shell=True, check=True)

    return f"Ingress '{ingress_name}' restored from /config '{ingress_name}'"

def get_kubernetes_resources():
    config.load_incluster_config()
    v1 = client.NetworkingV1Api()

    namespace = open("/var/run/secrets/kubernetes.io/serviceaccount/namespace").read()

    ingresses = v1.list_namespaced_ingress(namespace)
    ingress_list = [ingress.metadata.name for ingress in ingresses.items]
    ingress_count = len(ingress_list)

    return ingress_list, ingress_count

def newstore_ingress_service(ingress_name):
    config.load_incluster_config()
    v1 = client.NetworkingV1Api()

    namespace = open("/var/run/secrets/kubernetes.io/serviceaccount/namespace").read()

    file_path = f"/config/{ingress_name}.yaml"

    cmd = f"kubectl get ingress {ingress_name} -n {namespace} -o yaml > {file_path}"
    # Execute the command
    subprocess.run(cmd, shell=True, check=True)

    return f"Ingress '{ingress_name}' stored in /config '{ingress_name}'"

def update_ingress_service(ingress_name, new_service_name):
    config.load_incluster_config()
    v1 = client.NetworkingV1Api()

    namespace = open("/var/run/secrets/kubernetes.io/serviceaccount/namespace").read()

    print(f"in update_ingress_service", file=sys.stderr)
    try:
        print(f"0 in update_ingress_service", file=sys.stderr)
        ingress = v1.read_namespaced_ingress(name=ingress_name, namespace=namespace)
        # Update the Ingress backend service to 'new_service_name'
        #print(f"1 in update_ingress_service", file=sys.stderr)
        #print(f"Ingress first: {ingress}", file=sys.stderr)
        ingress.spec.rules[0].http.paths[0].backend.service.name = new_service_name
        #print(f"2 in update_ingress_service", file=sys.stderr)
        api_response = v1.patch_namespaced_ingress(
            name=ingress_name,
            namespace=namespace,  # replace with your namespace
            body=ingress
        )
        #print(f"3 in update_ingress_service", file=sys.stderr)
        #print(f"Ingress second: {ingress}", file=sys.stderr)
    except client.rest.ApiException as e:
        print(f"error in update_ingress_service: {e}", file=sys.stderr)
        return f"Error updating Ingress '{ingress_name}': {e}"

    return f"Ingress '{ingress_name}' updated to use service '{new_service_name}'"


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True)

Lastly, I wanted, at the least, a start to actions from the index page so I wouldn’t need to keep typing in the URLs manually

$ cat ./templates/index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Kubernetes Ingresses</title>
    <style>
        .styled-table {
            border-collapse: collapse;
            margin: 25px 0;
            font-size: 0.9em;
            font-family: sans-serif;
            min-width: 400px;
            box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
        }

        .styled-table thead tr {
            background-color: #491ac9;
            color: #ffffff;
            text-align: left;
        }

        .styled-table th,
        .styled-table td {
            padding: 12px 15px;
        }

        .styled-table tbody tr {
            border-bottom: 1px solid #dddddd;
        }

        .styled-table tbody tr:nth-of-type(even) {
            background-color: #dadada;
        }

        .styled-table tbody tr:last-of-type {
            border-bottom: 2px solid #491ac9;
        }

        .styled-table tbody tr.active-row {
            font-weight: bold;
            color: #491ac9;
        }

        caption {
        font-weight: bold;
        font-size: 24px;
        text-align: left;
        color: #333;
        }
    </style>
</head>
<body>
    <table class="styled-table">
        <caption>Kubernetes Ingresses in the Namespace: 8</caption>
        <thead>
            <tr>
                <th>Ingress</th>
                <th>Actions</th>
            </tr>
        </thead>
        <tbody>
            
        </tbody>
    </table>
</body>
</html>

Here we can see it in action with a full end to end demo:

I created PR 7 and we can see it’s associated to the issue (#6)

/content/images/2024/01/newapp5-15.png

On merge, it built and pushed to Harbor

/content/images/2024/01/newapp5-16.png

I want this pushed to Dockerhub so others can enjoy it, thus I made a release.0.0.3 tag which kicked a build. You can access that image here in Dockerhub.

Summary

Today we changed the app to store our Ingress definitions into a mounted Persistent Volume Claim (PVC) for permanent storage. In doing so, I demonstrated how to use imagePullSecrets for local builds of the container. This was sorted by PR 5. We moved onto Feature #6 for restoring from backups. This led down a rabbit hole of fighting with the Kubernetes python library which wanted to endlessly reformat the YAML to snake_case from camelCase. After enough trying, I moved to just using a local kubectl in the Dockerfile and this wrapped with PR 7 which implemented Issue #6 on restore. The code at this point is tagged release.0.03

At this point we have a functional app, in the basic sense. Our next goals will be to tackle Password login, some styling changes and using the real Nginx disabled service name. We’ll cover all that next week, stay tuned!

Kubernetes App Container Docker Helm Python Harbor Opensource

Have something to add? Feedback? Try our new forums

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