A New Project: Part 2: NodeJS and Python

Published: Dec 16, 2023 by Isaac Johnson

In A New Project: Part 1: Moving past the blank canvas we set the plans for the App we wanted to build, drew up some diagrams and planned a few spikes to figure out how we might containerize it.

Today we’ll look at NodeJS and Python as options as we action on the Spikes

NodeJS: React

My first shot is to try ReactJS. I generally find it’s quick to get started

$ mkdir k8sChecker
$ cd k8sChecker/
$ npx create-react-app my-app
$ npm install @kubernetes/client-node

I got a few errors in launching with npm start till I moved to Node 20.

If I use NodeJS 20 (latest)

$ nvm list
       v14.21.3
       v16.20.2
       v18.18.1
       v18.18.2
->      v20.8.0

I create a kubernetes library that could be used for K8s calls

import { CoreV1Api, KubeConfig } from '@kubernetes/client-node';

const kc = new KubeConfig();
kc.loadFromDefault();

const k8sApi = kc.makeApiClient(CoreV1Api);

export async function getServices() {
  const res = await k8sApi.listServiceForAllNamespaces();
  return res.body.items;
}

I hoped I could them pull it in using the App.js

//import logo from './logo.svg';
//import './App.css';
import { useEffect, useState } from 'react';
import { getServices } from './kubernetes';

function App() {
  const [services, setServices] = useState([]);

  useEffect(() => {
    async function fetchData() {
      const data = await getServices();
      setServices(data);
    }

    fetchData();
  }, []);

  return (
    <div>
      <h1>Services</h1>
      <ul>
        {services.map((service) => (
          <li key={service.metadata.uid}>{service.metadata.name}</li>
        ))}
      </ul>
    </div>
  );
}

export default App;

The first errors I get on launch are about pollyfills and query strings

Failed to compile.

Module not found: Error: Can't resolve 'querystring' in '/home/builder/Workspaces/k8sChecker/my-app/node_modules/@kubernetes/client-node/dist'
BREAKING CHANGE: webpack < 5 used to include polyfills for node.js core modules by default.
This is no longer the case. Verify if you need this module and configure a polyfill for it.

If you want to include a polyfill, you need to:
        - add a fallback 'resolve.fallback: { "querystring": require.resolve("querystring-es3") }'
        - install 'querystring-es3'
If you don't want to include a polyfill, you can use an empty module like this:
        resolve.fallback: { "querystring": false }
WARNING in ./node_modules/@kubernetes/client-node/dist/api.js
Module Warning (from ./node_modules/source-map-loader/dist/cjs.js):
Failed to parse source map from '/home/builder/Workspaces/k8sChecker/my-app/node_modules/@kubernetes/client-node/dist/api.js.map' file: Error: ENOENT: no such file or directory, open '/home/builder/Workspaces/k8sChecker/my-app/node_modules/@kubernetes/client-node/dist/api.js.map'

/content/images/2023/12/newapp2-01.png

I tried all sorts of things. Installing querystring-es3, adding in a resolve block to react-scripts:


        fallback: { 
          "querystring": require.resolve("querystring-es3") ,
          "path": require.resolve("path-browserify"),
          "buffer": require.resolve("buffer/"),
          "crypto": require.resolve("crypto-browserify"),
          "http": require.resolve("stream-http"),
          "stream": require.resolve("stream-browserify"),
          "url": require.resolve("url/"),
          "util": require.resolve("util/"),
        },

Then installing the missing packages

$ npm install --save buffer crypto-browserify stream-http stream
-browserify url util

but then that dumped

$ npm start

> my-app@0.1.0 start
> react-scripts start

Failed to compile.

Invalid configuration object. Webpack has been initialized using a configuration object that does not match the API schema.
 - configuration.resolve.alias should be one of these:
   object { alias?, aliasFields?, byDependency?, cache?, cachePredicate?, cacheWithContext?, conditionNames?, descriptionFiles?, enforceExtension?, exportsFields?, extensionAlias?, extensions?, fallback?, fileSystem?, fullySpecified?, importsFields?, mainFields?, mainFiles?, modules?, plugins?, preferAbsolute?, preferRelative?, resolver?, restrictions?, roots?, symlinks?, unsafeCache?, useSyncFileSystemCalls? }
   -> Redirect module requests.
   Details:
    * configuration.resolve.alias['fallback'] should be one of these:
      [non-empty string, ...] | false | non-empty string
      -> New request.
      Details:
       * configuration.resolve.alias['fallback'] should be an array:
         [non-empty string, ...]
         -> Multiple alternative requests.
       * configuration.resolve.alias['fallback'] should be false.
         -> Ignore request (replace with empty module).
       * configuration.resolve.alias['fallback'] should be a non-empty string.
         -> New request.

I decided to try it fresh, maybe somewhere in my experimentation, I screwed up a file.

I created a new folder, this time I used Node 18 and tried again

$ cd k8sChecker2/
$ nvm use 18.18.2
$ npx create-react-app my-app
$ cd my-app/
$ npm install --save @kubernetes/client-node
$ npm start

Here I created an App.js I hoped would work

import logo from './logo.svg';
import './App.css';
import { KubeConfig, CoreV1Api } from '@kubernetes/client-node';

function App() {
  const kc = new KubeConfig();
  kc.loadFromDefault();
  const k8sApi = kc.makeApiClient(CoreV1Api);

  const res = await k8sApi.listServiceForAllNamespaces();
  return res.body.items;
  
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
    <div>
      <h1>Services</h1>
      <ul>
        {res.body.items.map((service) => (
          <li key={service.metadata.uid}>{service.metadata.name}</li>
        ))}
      </ul>
    </div>
  );
}

export default App;

This is quite literally just slapping in what I think are the relevant bits to fetch K8s details and display them.

Trying to launch indicated it wasn’t keen on the await

Failed to compile.

SyntaxError: /home/builder/Workspaces/k8sChecker2/my-app/src/App.js: Unexpected reserved word 'await'. (10:14)
   8 |   const k8sApi = kc.makeApiClient(CoreV1Api);
   9 |
> 10 |   const res = await k8sApi.listServiceForAllNamespaces();
     |               ^
  11 |   return res.body.items;
  12 |
  13 |   return (
    at parser.next (<anonymous>)
    at normalizeFile.next (<anonymous>)
ERROR in ./src/App.js
Module build failed (from ./node_modules/babel-loader/lib/index.js):
SyntaxError: /home/builder/Workspaces/k8sChecker2/my-app/src/App.js: Unexpected reserved word 'await'. (10:14)

   8 |   const k8sApi = kc.makeApiClient(CoreV1Api);
   9 |
> 10 |   const res = await k8sApi.listServiceForAllNamespaces();
     |               ^
  11 |   return res.body.items;
  12 |
  13 |   return (
    at constructor (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:356:19)
    at FlowParserMixin.raise (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:3223:19)
    at FlowParserMixin.checkReservedWord (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:12072:12)
    at FlowParserMixin.parseIdentifierName (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:12051:12)
    at FlowParserMixin.parseIdentifier (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:12026:23)
    at FlowParserMixin.parseExprAtom (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:11231:27)
    at FlowParserMixin.parseExprAtom (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:6932:20)
    at FlowParserMixin.parseExprSubscripts (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:10857:23)
    at FlowParserMixin.parseUpdate (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:10840:21)
    at FlowParserMixin.parseMaybeUnary (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:10816:23)
    at FlowParserMixin.parseMaybeUnaryOrPrivate (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:10654:61)
    at FlowParserMixin.parseExprOps (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:10659:23)
    at FlowParserMixin.parseMaybeConditional (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:10636:23)
    at FlowParserMixin.parseMaybeAssign (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:10597:21)
    at FlowParserMixin.parseMaybeAssign (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:5755:18)
    at /home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:10567:39
    at FlowParserMixin.allowInAnd (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:12279:16)
    at FlowParserMixin.parseMaybeAssignAllowIn (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:10567:17)
    at FlowParserMixin.parseVar (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:13259:91)
    at FlowParserMixin.parseVarStatement (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:13100:10)
    at FlowParserMixin.parseStatementContent (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:12683:23)
    at FlowParserMixin.parseStatementLike (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:12588:17)
    at FlowParserMixin.parseStatementLike (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:5088:24)
    at FlowParserMixin.parseStatementListItem (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:12568:17)
    at FlowParserMixin.parseBlockOrModuleBlockBody (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:13189:61)
    at FlowParserMixin.parseBlockBody (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:13182:10)
    at FlowParserMixin.parseBlock (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:13170:10)
    at FlowParserMixin.parseFunctionBody (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:11935:24)
    at FlowParserMixin.parseFunctionBody (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:5065:11)
    at FlowParserMixin.parseFunctionBodyAndFinish (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:11921:10)
    at FlowParserMixin.parseFunctionBodyAndFinish (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:5073:18)
    at /home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:13318:12
    at FlowParserMixin.withSmartMixTopicForbiddingContext (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:12261:14)
    at FlowParserMixin.parseFunction (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:13317:10)
    at FlowParserMixin.parseFunctionStatement (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:12984:17)
    at FlowParserMixin.parseStatementContent (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:12614:21)
    at FlowParserMixin.parseStatementLike (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:12588:17)
    at FlowParserMixin.parseStatementLike (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:5088:24)
    at FlowParserMixin.parseModuleItem (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:12565:17)
    at FlowParserMixin.parseBlockOrModuleBlockBody (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:13189:36)
    at FlowParserMixin.parseBlockBody (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:13182:10)
    at FlowParserMixin.parseProgram (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:12464:10)
    at FlowParserMixin.parseTopLevel (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:12454:25)
    at FlowParserMixin.parseTopLevel (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:5893:28)
    at FlowParserMixin.parse (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:14376:10)
    at parse (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/parser/lib/index.js:14417:38)
    at parser (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/core/lib/parser/index.js:41:34)
    at parser.next (<anonymous>)
    at normalizeFile (/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@babel/core/lib/transformation/normalize-file.js:64:37)
    at normalizeFile.next (<anonymous>)

ERROR in [eslint]
src/App.js
  Line 10:14:  Parsing error: Unexpected reserved word 'await'. (10:14)

webpack compiled with 2 errors
One of your dependencies, babel-preset-react-app, is importing the
"@babel/plugin-proposal-private-property-in-object" package without
declaring it in its dependencies. This is currently working because
"@babel/plugin-proposal-private-property-in-object" is already in your
node_modules folder for unrelated reasons, but it may break at any time.

babel-preset-react-app is part of the create-react-app project, which
is not maintianed anymore. It is thus unlikely that this bug will
ever be fixed. Add "@babel/plugin-proposal-private-property-in-object" to
your devDependencies to work around this error. This will make this message
go away.

^C

When I removed the await and combined the div’s

import logo from './logo.svg';
import './App.css';
import { KubeConfig, CoreV1Api } from '@kubernetes/client-node';

function App() {
  const kc = new KubeConfig();
  kc.loadFromDefault();
  const k8sApi = kc.makeApiClient(CoreV1Api);

  const res = k8sApi.listServiceForAllNamespaces();
  return res.body.items;
  
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
      
      <h1>Services</h1>
      <ul>
        {res.body.items.map((service) => (
          <li key={service.metadata.uid}>{service.metadata.name}</li>
        ))}
      </ul>
    </div>
  );
}

export default App;

I got the same blow up again

Compiled with problems:
×
ERROR in ./node_modules/@kubernetes/client-node/dist/attach.js 7:20-42
Module not found: Error: Can't resolve 'querystring' in '/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@kubernetes/client-node/dist'

BREAKING CHANGE: webpack < 5 used to include polyfills for node.js core modules by default.
This is no longer the case. Verify if you need this module and configure a polyfill for it.

If you want to include a polyfill, you need to:
	- add a fallback 'resolve.fallback: { "querystring": require.resolve("querystring-es3") }'
	- install 'querystring-es3'
If you don't want to include a polyfill, you can use an empty module like this:
	resolve.fallback: { "querystring": false }
ERROR in ./node_modules/@kubernetes/client-node/dist/azure_auth.js 8:34-58
Module not found: Error: Can't resolve 'child_process' in '/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@kubernetes/client-node/dist'

Like before, I installed the package

builder@LuiGi17:~/Workspaces/k8sChecker2/my-app$ npm i querystring-es3

added 1 package, and audited 1594 packages in 3s

248 packages are looking for funding
  run `npm fund` for details

11 vulnerabilities (5 moderate, 6 high)

To address all issues possible (including breaking changes), run:
  npm audit fix --force

Some issues need review, and may require choosing
a different dependency.

Run `npm audit` for details.

Then, I added the resolve line fallback: { "querystring": require.resolve("querystring-es3") },

/content/images/2023/12/newapp2-02.png

This is a game that isn’t fun. More modules to fix

Compiled with problems:
×
ERROR in ./node_modules/@kubernetes/client-node/dist/azure_auth.js 8:34-58
Module not found: Error: Can't resolve 'child_process' in '/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@kubernetes/client-node/dist'
ERROR in ./node_modules/@kubernetes/client-node/dist/config.js 8:22-46
Module not found: Error: Can't resolve 'child_process' in '/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@kubernetes/client-node/dist'
ERROR in ./node_modules/@kubernetes/client-node/dist/config.js 9:11-24
Module not found: Error: Can't resolve 'fs' in '/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@kubernetes/client-node/dist'
ERROR in ./node_modules/@kubernetes/client-node/dist/config.js 11:12-26
Module not found: Error: Can't resolve 'net' in '/home/builder/Workspaces/k8sChecker2/my-app/node_modules/@kubernetes/client-node/dist'

I installed every one it listed as missing

builder@LuiGi17:~/Workspaces/k8sChecker2/my-app$ npm install assert buffer child_process crypto fs http https net os path process stream timers tls url util zlib
npm WARN deprecated crypto@1.0.1: This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.

added 25 packages, and audited 1619 packages in 7s

254 packages are looking for funding
  run `npm fund` for details

11 vulnerabilities (5 moderate, 6 high)

To address all issues possible (including breaking changes), run:
  npm audit fix --force

Some issues need review, and may require choosing
a different dependency.

Run `npm audit` for details.

and same vomits

/content/images/2023/12/newapp2-03.png

I tried

      fallback: { 
          "querystring": require.resolve("querystring-es3"),
          "path": require.resolve("path-browserify"),
          "buffer": require.resolve("buffer/"),
          "crypto": require.resolve("crypto-browserify"),
          "http": require.resolve("stream-http"),
          "stream": require.resolve("stream-browserify"),
          "url": require.resolve("url/"),
          "child_process": require.resolve("child_process/"),
          "fs": require.resolve("fs/"),
          "util": require.resolve("util/"),
      },

but it still blew up on child_process and fs

but adding them

      fallback: { 
          "querystring": require.resolve("querystring-es3"),
          "path": require.resolve("path-browserify"),
          "buffer": require.resolve("buffer/"),
          "crypto": require.resolve("crypto-browserify"),
          "http": require.resolve("stream-http"),
          "stream": require.resolve("stream-browserify"),
          "url": require.resolve("url/"),
          "child_process": require.resolve("child_process/"),
          "fs": require.resolve("fs/"),
          "util": require.resolve("util/"),
      },

puked. No installs work

builder@LuiGi17:~/Workspaces/k8sChecker2/my-app$ npm install child_process --save

up to date, audited 1750 packages in 3s

256 packages are looking for funding
  run `npm fund` for details

11 vulnerabilities (5 moderate, 6 high)

To address all issues possible (including breaking changes), run:
  npm audit fix --force

Some issues need review, and may require choosing
a different dependency.

Run `npm audit` for details.
builder@LuiGi17:~/Workspaces/k8sChecker2/my-app$ npm start

> my-app@0.1.0 start
> react-scripts start

Cannot find module '/home/builder/Workspaces/k8sChecker2/my-app/node_modules/child_process/index.js'. Please verify that the package.json has a valid "main" entry

I finally got to a point where I was ready to pivot, or at least take a break

/content/images/2023/12/newapp2-04.png

Python

I prefer NodeJS, I do. But at the end of the day, I want a scripting language that easily compiles (or runs in an interpreter) in a container.

We can get a lot done with Python and Flask.

I created a new directory pyK8sService and started to build out the files by hand

My first (working) app.py:

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

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')
    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."

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 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()

    try:
        ingress = v1.read_namespaced_ingress(name=ingress_name, namespace=namespace)
        # Update the Ingress backend service to 'new_service_name'
        print(f"Ingress first: {ingress}", file=sys.stderr)
        ingress.spec.backend.service_name = new_service_name
        print(f"Ingress second: {ingress}", file=sys.stderr)
        v1.replace_namespaced_ingress(name=ingress_name, namespace=namespace, body=ingress)
    except client.rest.ApiException as e:
        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)

This leverages a basic template in 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>
</head>
<body>
    <h1>Kubernetes Ingresses in the Namespace: 8</h1>
    <ul>
        
    </ul>
</body>
</html>

And a requirements.txt

Flask==2.1.0
kubernetes==28.1.0
Werkzeug==2.2.2

The next file I need is a Dockerfile to build it. In fact, the only challenge I had was that python:3.8 seemed to crash but 3.8-slim was fine. Maybe I pulled a bad image, who knows.

FROM python:3.8-slim

WORKDIR /app

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

COPY . .

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

Lastly, I wanted a Kubernetes Manfiest to use it. Since I decided, at least while I debugged, to use my private CR, I needed to add an image pull secret.

The RBAC rules were mostly found with trial and error. I can say that I used a bit of ChatGPT and the consequence of the older free-tier GPT 3.5 is it gets APIs wrong (or at least based on dated knowledge).

This meant I got hung up on why the Ingress was absent in the Extensions API, and even when I got the syntax right in the Python code, it kept crashing on zero entries. After a while it dawned on me that Ingresses used to be there, but now live in networking.k8s.io. My cluster (upon which i test) is 1.23 or higher.

apiVersion: v1
kind: ServiceAccount
metadata:
  name: list-services-sa
  namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: default
  name: list-services-role
rules:
- apiGroups: [""]
  resources: ["services"]
  verbs: ["list"]
- apiGroups: ["extensions"]  # Use "extensions" API group for Ingress
  resources: ["ingresses"]
  verbs: ["list","create","delete"]
- apiGroups: ["networking.k8s.io"]
  resources: ["ingresses"]
  verbs: ["list","create","delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  namespace: default
  name: list-services-rolebinding
subjects:
- kind: ServiceAccount
  name: list-services-sa
  namespace: default
roleRef:
  kind: Role
  name: list-services-role
  apiGroup: rbac.authorization.k8s.io
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: list-services-deployment
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: list-services
  template:
    metadata:
      labels:
        app: list-services
    spec:
      serviceAccountName: list-services-sa
      containers:
      - name: list-services-container
        image: harbor.freshbrewed.science/freshbrewedprivate/pytest01:16
        ports:
        - containerPort: 5000
        volumeMounts:
        - mountPath: "/var/run/secrets/kubernetes.io/serviceaccount/namespacewrong"
          name: namespace-volume
      imagePullSecrets:
      - name: myharborreg
      volumes:
      - name: namespace-volume
        downwardAPI:
          items:
          - path: "namespace"
            fieldRef:
              fieldPath: metadata.namespace

What you will notice above is I am not making an ingress. Until I figure out Auth and controls, this will live either as a NodePort service (where I reach it on a high number port on a Node) OR I’ll live with port-forwarding when I want to engage.

With a quick port-forward I could see my ingresses

/content/images/2023/12/newapp2-05.png

I next thought I might get fancy and implement the disable too

/content/images/2023/12/newapp2-06.png

Creating an App.py as

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

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')
    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."

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 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()

    try:
        ingress = v1.read_namespaced_ingress(name=ingress_name, namespace=namespace)
        # Update the Ingress backend service to 'new_service_name'
        print(f"Ingress first: {ingress}", file=sys.stderr)
        ingress.spec.backend.service_name = new_service_name
        print(f"Ingress second: {ingress}", file=sys.stderr)
        v1.replace_namespaced_ingress(name=ingress_name, namespace=namespace, body=ingress)
    except client.rest.ApiException as e:
        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)

and the HTML now as

<!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>
</head>
<body>
    <h1>Kubernetes Ingresses in the Namespace: 8</h1>
    <ul>
        
    </ul>
</body>
</html>

Now that things are a bit stable, we can see the files in the repo

/content/images/2023/12/newapp2-07.png

and it makes more sense to flip the bit and make it public

/content/images/2023/12/newapp2-08.png

and sharing it here

I’ll close the issue with a comment

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

I’m going to try an App I bought on the Play store for Android, GitNex, to create an issue

/content/images/2023/12/newapp-13.jpg

which, while I cannot set the project, does create

/content/images/2023/12/newapp-12.jpg

But I can set it in the web interface

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

I added a couple more tasks to dig into, namely fixing the disable and implementing a full CICD pipeline

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

Summary

Sometimes things don’t pan out. I probably could, with enough time, energy and research have sorted it out. However, it made more sense to pivot to Python. I have some old colleagues at TR that would get quite a chuckle of me giving up on Node for Python, but regardless, this is the right path.

Next, I have to solve how best to fix my disable issue and build out the pipeline, but we’ll cover all that next time. I’m also going to start thinking on auth and documentation.

Kubernetes App Container Docker ReactJS NodeJS Python

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