commit d993995eb3f22b761d2c266e5e05916de8eec1dd Author: Gilles Mouchet Date: Sat Apr 12 11:03:22 2025 +0200 V1.0.0 diff --git a/.env.dist b/.env.dist new file mode 100644 index 0000000..753e542 --- /dev/null +++ b/.env.dist @@ -0,0 +1,17 @@ +# docker image +DOCKER_IMAGE=hello-kube +DOCKER_IMAGE_VERSION=1.0 +PORT_LOCAL=8081 +PORT=8080 # do not change var name. It using by server.js + +# registry docker +DOCKER_REGISTRY=docker.io +DOCKER_REGISTRY_USER=gmouchet +DOCKER_REGISTRY_PASSWORD=TO_BE_COMPLETED + +# variable used by the application +KUBERNETES_NAMESPACE=Docker +KUBERNETES_NODE_NAME=${HOSTNAME} +MESSAGE="Welcome on Docker environment" + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2eea525 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..b3ee43d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "editor.fontSize": 13, + "terminal.integrated.fontSize": 13, + "window.zoomLevel": 1.4, +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..421f83f --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# Hello kube application +## Description +This application display a web page with: +* message +* namespace +* pod +* node +* image details + +## clone repositopry +```bash +git clone https://gitweb.dyndns.org/helm/hello-kube.git +``` + +## Helm variables + +| Variable name | Value sample | Description | +|:---|:---|:---| +| message | `hello world` | Message display on web page | +| ingress.hostUrl | `hello-kube.gmolab.net`| Site URL. Must be define in DNS or hosts file | +| deployment.replicaCount | `2` | Number of deployment's replica | +| deployment.container.image.repository | `docker.io/gmouchet/hello-kube:1.0` | Docker image repository | +| deployment.container.image.repository.pullPolicy | `IfNotPresent` | [Image Pull Policy](https://kubernetes.io/docs/concepts/containers/images/) | +| deployment.container.port | `8080` | Docker image port. Do not change without rebuild docker image | + +## Deploy +```bash +KENV=gmo|vdg +helm upgrade hello-kube . \ +--install \ +--atomic \ +--cleanup-on-fail \ +--values=values-$KENV-env.yaml \ +--namespace hello-kube \ +--create-namespace +``` +## Test +Go to http://hello-kube.gmolab.net () + +## Building images + +If you'd like to build the `hello-kube` container image yourself and reference from your own registry or DockerHub repository, then you can get more details on how to do this in the [Build and push container images](README_BUILD.md) documentation. + +## Changelog +### v1.0.0 (2025-04-12) +#### Added +- initial version \ No newline at end of file diff --git a/README_BUILD.md b/README_BUILD.md new file mode 100644 index 0000000..c127380 --- /dev/null +++ b/README_BUILD.md @@ -0,0 +1,64 @@ +# Hello Kube demo app + +A demo app that can be deployd to a Kubernetes cluster (k8s, kind, etc.). It displays a message, and also namespace, pod, node and image details. + +This is a fork from [paulbouwer/hello-kubernetes](https://github.com/paulbouwer/hello-kubernetes) + +## Configuration + +| Variables | Required | Default Value | Description | +| --- | -------- | ------------- | ----------- | +| DOCKER_IMAGE | Yes | hello-kube | Docker image name | +| DOCKER_IMAGE_VERSION | Yes | 1.0 | Docker image version | +| DOCKER_REGISTRY | yes | docker.io | Docker registry name | +| DOCKER_REGISTRY_USER | Yes | gmouchet | Docker registry user | +| DOCKER_REGISTRY_PASSWORD| Yes | secret | Docker registry user password | +| PORT_LOCAL| No| 8080 | The port for URL :| +| PORT | No | 8080 | The port that the app listens on. | +| MESSAGE | No | "Hello world!" | The message displayed by the app. | +| RENDER_PATH_PREFIX | No | "" | The path prefix to use when rendering the urls for the static assets in the handlebar templates.
Must be used when app is deployed with an ingress using a backend path for traffic to app. | +| HANDLER_PATH_PREFIX | No | "" | The path prefix to use by handlers when serving the dynamic and static assets.
Note: Must be used when app is deployed with an ingress that has a backend path for traffic to the app, but which does not rewrite the backend paths to '/'. | +| KUBERNETES_NAMESPACE | Yes | "-" | The Kubernetes namespace that the app has been deployed to. | +| KUBERNETES_NODE_NAME | Yes | "-" | The name of the Kubernetes node that the app is deployed on. | + +The application relies on the following files for configuration and operational information. + +| File | Required | Information | Description | +| ---- | -------- | ----------- | ----------- | +| package.json | Yes | `.version` | The release version is used when the CONTAINER_IMAGE env is not provided. | +| info.json | Yes | `.containerImageArch` | The container image architecture is used for display. This file will be overwritten in future versions as part of the container image build process when multi-arch images are suppoted. | + +## Build image +**Multi architecture** + +To create a multi-architecture image, proceed as follows: + +```bash +docker buildx create --use --name gmobuilder +docker buildx inspect gmobuilder --bootstrap +``` + +```bash +docker buildx build --platform linux/amd64,linux/arm64 -t gmouchet/hello-kube:latest . --push +``` +or user script `./docker.sh build` + +**Variables** +```bash +cp .env.dist .env +vim .env +``` + +**Build image** +```bash +./docker.sh build-no-cache +``` +**Others actions** +```bash +./docker.sh help +``` + +## Changelog +### v1.0.0 (2025-04-12) +#### Added +- initial version \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..de86e47 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,10 @@ +services: + hello-kube: + container_name: hello-kube + hostname: hello-kube + image: ${DOCKER_REGISTRY_USER}/${DOCKER_IMAGE}:${DOCKER_IMAGE_VERSION} + ports: + - '${PORT_LOCAL:-9090}:${PORT:-8080}' #local:docker + env_file: + - .env + \ No newline at end of file diff --git a/docker.sh b/docker.sh new file mode 100755 index 0000000..74d04fe --- /dev/null +++ b/docker.sh @@ -0,0 +1,101 @@ +#!/bin/bash +progName="./$(/bin/basename $0)" + +DOCKER_REGISTRY="$(grep -s 'DOCKER_REGISTRY=' .env | cut -f2 -d=)" +DOCKER_REGISTRY_USER="$(grep -s 'DOCKER_REGISTRY_USER=' .env | cut -f2 -d=)" +DOCKER_REGISTRY_PASSWORD="$(grep -s 'DOCKER_REGISTRY_PASSWORD=' .env | cut -f2 -d=)" +DOCKER_IMAGE="$(grep -s 'DOCKER_IMAGE=' .env | cut -f2 -d=)" +DOCKER_IMAGE_VERSION="$(grep -s 'DOCKER_IMAGE_VERSION=' .env | cut -f2 -d=)" + +function print_usage { +# print +/bin/cat << EOF + +Usage: $progName [options] + +Options: + start Start docker container + stop Stop docker container + down Stop docker container and delete container and image + bash Open a shell bash + bash-root Open a root shell + logs Display container logs + build Build docker images and push on registry + build-no-cache Build docker images without use cache and push on registry + help,-h,--help Display this help + +Examples: + To start container + $progName start + To stop container + $progName stop + To create a new functional environment after recovering the sources + $progName build +EOF +} + +if [ $# -eq 0 ]; then + echo "${progName}: you must specify an option" + echo -e "Try '$progName help' for more information.\n" +fi + +while test $# -gt 0 +do + case "$1" in + start) + docker compose up -d; + ;; + stop) + docker compose stop + ;; + down) + docker compose down --rmi all + ;; + bash) + docker exec -it hello-kube /bin/sh + ;; + bash-root) + docker exec --user root -it hello-kube /bin/sh + ;; + logs) + docker logs hello-kube -f + ;; + build-no-cache) + echo $DOCKER_REGISTRY_PASSWORD| docker login -u ${DOCKER_REGISTRY_USER} --password-stdin ${DOCKER_REGISTRY} + if [ $? -eq 0 ]; then + # need for multi architecture + docker buildx create --append --use --name gmobuilder + docker buildx inspect gmobuilder --bootstrap + # build multi architecture image + docker buildx build --no-cache --platform linux/amd64,linux/arm64 \ + -t ${DOCKER_REGISTRY_USER}/${DOCKER_IMAGE}:${DOCKER_IMAGE_VERSION} \ + -f ./docker/hello-kube/Dockerfile . --output=type=registry + docker logout ${DOCKER_REGISTRY} + fi + ;; + build) + echo $DOCKER_REGISTRY_PASSWORD| docker login -u ${DOCKER_REGISTRY_USER} --password-stdin ${DOCKER_REGISTRY} + if [ $? -eq 0 ]; then + # need for multi architecture + docker buildx create --append --use --name gmobuilder + docker buildx inspect gmobuilder --bootstrap + # build multi architecture image + docker buildx build --platform linux/amd64,linux/arm64 \ + -t ${DOCKER_REGISTRY_USER}/${DOCKER_IMAGE}:${DOCKER_IMAGE_VERSION} \ + -f ./docker/hello-kube/Dockerfile . --output=type=registry + docker logout ${DOCKER_REGISTRY} + fi + ;; + -h|--help|help) + print_usage + exit 0 + ;; + *) + echo "${progName}: invalid option -- '$1'!" + echo -e "Try '$progName help' for more information.\n" + ;; + esac + shift +done +exit 0 + diff --git a/docker/hello-kube/.dockerignore b/docker/hello-kube/.dockerignore new file mode 100644 index 0000000..77a661f --- /dev/null +++ b/docker/hello-kube/.dockerignore @@ -0,0 +1,3 @@ +node_modules +package-lock.json +README.md diff --git a/docker/hello-kube/Dockerfile b/docker/hello-kube/Dockerfile new file mode 100644 index 0000000..596a3da --- /dev/null +++ b/docker/hello-kube/Dockerfile @@ -0,0 +1,17 @@ +FROM node:18.20.8-alpine3.21 + +# Create app directory +RUN mkdir -p /usr/src/app +WORKDIR /usr/src/app + +# Install app dependencies +COPY ./docker/hello-kube/package.json /usr/src/app/ +#COPY package.json /usr/src/app/ +RUN npm install + +# Bundle app source +COPY ./docker/hello-kube/ /usr/src/app +#COPY . /usr/src/app + +USER node +CMD [ "npm", "start" ] diff --git a/docker/hello-kube/info.json b/docker/hello-kube/info.json new file mode 100644 index 0000000..6746bb0 --- /dev/null +++ b/docker/hello-kube/info.json @@ -0,0 +1,3 @@ +{ + "containerImageArch": "linux/amd64,linux/arm64" +} diff --git a/docker/hello-kube/package.json b/docker/hello-kube/package.json new file mode 100644 index 0000000..75c5152 --- /dev/null +++ b/docker/hello-kube/package.json @@ -0,0 +1,18 @@ +{ + "name": "hello-kube", + "version": "1.0", + "description": "Hello Kube!", + "author": "Gilles Mouchet", + "license": "MIT", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "express": "^4.17.1", + "express-handlebars": "^5.2.1", + "express-pino-logger": "^6.0.0", + "handlebars": "^4.7.7", + "pino": "^6.11.2" + } +} diff --git a/docker/hello-kube/package.json.reold b/docker/hello-kube/package.json.reold new file mode 100644 index 0000000..e363d27 --- /dev/null +++ b/docker/hello-kube/package.json.reold @@ -0,0 +1,18 @@ +{ + "name": "hello-kube", + "version": "1.0", + "description": "Hello Kube!", + "author": "Gilles Mouchet", + "license": "MIT", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "express": "^4.17.1", + "express-handlebars": "^5.2.1", + "pino-http": "^10.4.0", + "handlebars": "^4.7.7", + "pino": "^6.11.2" + } +} diff --git a/docker/hello-kube/server.js b/docker/hello-kube/server.js new file mode 100644 index 0000000..ebb41c0 --- /dev/null +++ b/docker/hello-kube/server.js @@ -0,0 +1,82 @@ +const express = require('express'); +const exphbs = require('express-handlebars'); +const os = require("os"); +const fs = require('fs'); + +const pino = require('pino'); +const expressPino = require('express-pino-logger'); +const logger = pino({ level: process.env.LOG_LEVEL || 'info' }); +const expressLogger = expressPino({ logger }); + +const app = express(); +app.use(expressLogger); +app.engine('handlebars', exphbs({defaultLayout: 'main'})); +app.set('view engine', 'handlebars'); + +// Configuration + +var port = process.env.PORT || 8080; +var message = process.env.MESSAGE || 'Hello world!'; +var renderPathPrefix = ( + process.env.RENDER_PATH_PREFIX ? + '/' + process.env.RENDER_PATH_PREFIX.replace(/^[\\/]+/, '').replace(/[\\/]+$/, '') : + '' +); +var handlerPathPrefix = ( + process.env.HANDLER_PATH_PREFIX ? + '/' + process.env.HANDLER_PATH_PREFIX.replace(/^[\\/]+/, '').replace(/[\\/]+$/, '') : + '' +); + +var namespace = process.env.KUBERNETES_NAMESPACE || '-'; +var podName = process.env.KUBERNETES_POD_NAME || os.hostname(); +var nodeName = process.env.KUBERNETES_NODE_NAME || '-'; +var nodeOS = os.type() + ' ' + os.release(); +var applicationVersion = JSON.parse(fs.readFileSync('package.json', 'utf8')).version; +var containerImage = process.env.CONTAINER_IMAGE || 'gmouchet/hello-kube:' + applicationVersion +var containerImageArch = JSON.parse(fs.readFileSync('info.json', 'utf8')).containerImageArch; + +logger.debug(); +logger.debug('Configuration'); +logger.debug('-----------------------------------------------------'); +logger.debug('PORT=' + process.env.PORT); +logger.debug('MESSAGE=' + process.env.MESSAGE); +logger.debug('RENDER_PATH_PREFIX=' + process.env.RENDER_PATH_PREFIX); +logger.debug('HANDLER_PATH_PREFIX=' + process.env.HANDLER_PATH_PREFIX); +logger.debug('KUBERNETES_NAMESPACE=' + process.env.KUBERNETES_NAMESPACE); +logger.debug('KUBERNETES_POD_NAME=' + process.env.KUBERNETES_POD_NAME); +logger.debug('KUBERNETES_NODE_NAME=' + process.env.KUBERNETES_NODE_NAME); +logger.debug('CONTAINER_IMAGE=' + process.env.CONTAINER_IMAGE); + +// Handlers + +logger.debug(); +logger.debug('Handlers'); +logger.debug('-----------------------------------------------------'); + +logger.debug('Handler: static assets'); +logger.debug('Serving from base path "' + handlerPathPrefix + '"'); +app.use(handlerPathPrefix, express.static('static')) + +logger.debug('Handler: /'); +logger.debug('Serving from base path "' + handlerPathPrefix + '"'); +app.get(handlerPathPrefix + '/', function (req, res) { + res.render('home', { + message: message, + namespace: namespace, + pod: podName, + node: nodeName + ' (' + nodeOS + ')', + container: containerImage + ' (' + containerImageArch + ')', + renderPathPrefix: renderPathPrefix + }); +}); + +// Server + +logger.debug(); +logger.debug('Server'); +logger.debug('-----------------------------------------------------'); + +app.listen(port, function () { + logger.info("Listening on: http://%s:%s", podName, port); +}); diff --git a/docker/hello-kube/static/css/main.css b/docker/hello-kube/static/css/main.css new file mode 100644 index 0000000..c0c40b5 --- /dev/null +++ b/docker/hello-kube/static/css/main.css @@ -0,0 +1,59 @@ +body { + margin:0; + padding:0; + background-color:#333333; +} + +div.main { + text-align: center; +} + +div.main img { + margin: 40px 0; +} + +div.content { + color:#f2f2f2; +} + +.content #message { + margin: 10px 0 50px 0; + padding: 30px 0; + font-family: 'Ubuntu', sans-serif; + font-weight: 300; + font-size: 32pt; + background-color: #505050; + border-top: 2px solid #909090; + border-bottom: 2px solid #909090; +} + +.content #footer { + margin: 50px 0; + padding: 30px 0; + font-family: 'Ubuntu', sans-serif; + font-weight: 300; + font-size: 12pt; + color: #909090; + border-top: 2px solid #909090; +} + +.content #info { + margin: 0 auto; + font-family: 'Ubuntu', sans-serif; + font-weight: 300; + font-size: 12pt; +} + +.content #info table { + margin: 10px auto; +} + +.content #info table th { + text-align: right; + padding-right: 20px; +} + +.content #info table td { + text-align: left; +} + diff --git a/docker/hello-kube/static/favicon.ico b/docker/hello-kube/static/favicon.ico new file mode 100644 index 0000000..89af875 Binary files /dev/null and b/docker/hello-kube/static/favicon.ico differ diff --git a/docker/hello-kube/static/images/kubernetes.png b/docker/hello-kube/static/images/kubernetes.png new file mode 100644 index 0000000..01e3fc3 Binary files /dev/null and b/docker/hello-kube/static/images/kubernetes.png differ diff --git a/docker/hello-kube/views/home.handlebars b/docker/hello-kube/views/home.handlebars new file mode 100644 index 0000000..9e0af39 --- /dev/null +++ b/docker/hello-kube/views/home.handlebars @@ -0,0 +1,22 @@ +
+ {{ message }} +
+
+ + + + + + + + + + + + + +
namespace:{{ namespace }}
pod:{{ pod }}
node:{{ node }}
+
+ diff --git a/docker/hello-kube/views/layouts/main.handlebars b/docker/hello-kube/views/layouts/main.handlebars new file mode 100644 index 0000000..d7c270c --- /dev/null +++ b/docker/hello-kube/views/layouts/main.handlebars @@ -0,0 +1,18 @@ + + + + Hello Kube! + + + + + +
+ +
+ {{{body}}} +
+
+ + + diff --git a/helm/Chart.yaml b/helm/Chart.yaml new file mode 100644 index 0000000..a191472 --- /dev/null +++ b/helm/Chart.yaml @@ -0,0 +1,29 @@ +apiVersion: v2 +name: hello-kube +description: Display a simple web page + +maintainers: + - name: GMOLab + email: gilles.mouchet@gmail.com + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 1.0.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +# Also set the docker image tag +appVersion: "1.0.0" \ No newline at end of file diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml new file mode 100644 index 0000000..9a411f6 --- /dev/null +++ b/helm/templates/deployment.yaml @@ -0,0 +1,58 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: hello-kube + namespace: {{ .Release.Namespace }} +spec: + replicas: {{ required ".Values.deployment.replicaCount entry is required!" .Values.deployment.replicaCount }} + selector: + matchLabels: + app: hello-kube + template: + metadata: + labels: + app: hello-kube + spec: + containers: + - name: hello-kube + image: {{ required ".Values.deployment.container.image.repository entry is required!" .Values.deployment.container.image.repository }} + imagePullPolicy: {{ required ".Values.deployment.container.image.pullPolicy entry is required!" .Values.deployment.container.image.pullPolicy }} + ports: + - name: http + containerPort: {{ required ".Values.deployment.container.port entry is required!" .Values.deployment.container.port }} + env: + - name: PORT + value: '{{ required ".Values.deployment.container.port entry is required!" .Values.deployment.container.port }}' + - name: MESSAGE + value: "{{ .Values.message }}" + - name: KUBERNETES_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: KUBERNETES_POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: KUBERNETES_NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName + + startupProbe: + tcpSocket: + port: {{ required ".Values.deployment.container.port entry is required!" .Values.deployment.container.port }} + failureThreshold: 30 + periodSeconds: 10 + + livenessProbe: + tcpSocket: + port: {{ required ".Values.deployment.container.port entry is required!" .Values.deployment.container.port }} + periodSeconds: 30 + failureThreshold: 3 #Default 3 + initialDelaySeconds: 0 + + readinessProbe: + tcpSocket: + port: {{ required ".Values.deployment.container.port entry is required!" .Values.deployment.container.port }} + initialDelaySeconds: 15 + periodSeconds: 10 diff --git a/helm/templates/ingress.yaml b/helm/templates/ingress.yaml new file mode 100644 index 0000000..f7c2f1b --- /dev/null +++ b/helm/templates/ingress.yaml @@ -0,0 +1,18 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ingress-hello-kube + namespace: {{ .Release.Namespace }} +spec: + ingressClassName: nginx + rules: + - host: {{ required ".Values.ingress.hostUrl entry is required!" .Values.ingress.hostUrl }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: service-hello-kube + port: + number: 8080 diff --git a/helm/templates/service.yaml b/helm/templates/service.yaml new file mode 100644 index 0000000..dc234a4 --- /dev/null +++ b/helm/templates/service.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: service-hello-kube + namespace: {{ .Release.Namespace }} + labels: + app: hello-kube +spec: + ports: + - port: {{ required ".Values.deployment.container.port entry is required!" .Values.deployment.container.port }} + protocol: TCP + selector: + app: hello-kube + type: ClusterIP diff --git a/helm/values-gmo-env.yaml b/helm/values-gmo-env.yaml new file mode 100644 index 0000000..97c6059 --- /dev/null +++ b/helm/values-gmo-env.yaml @@ -0,0 +1,12 @@ +# provide a custom message +message: "Bienvenue dans le lab de Gilles Mouchet" +ingress: + hostUrl: hello-kube.gmolab.net # to declare on DNS or host file +deployment: + replicaCount: 2 + container: + image: + repository: "docker.io/gmouchet/hello-kube:1.0" + pullPolicy: IfNotPresent + port: 8080 # docker image port. Do not change without rebuild docker image + # with another port \ No newline at end of file diff --git a/helm/values-vdg-env.yaml b/helm/values-vdg-env.yaml new file mode 100644 index 0000000..1279ca4 --- /dev/null +++ b/helm/values-vdg-env.yaml @@ -0,0 +1,12 @@ +# provide a custom message +message: "Bienvenue dans le lab de la Ville de Genèev!" +ingress: + hostUrl: hello-kube.vdglab.net # to declare on DNS or host file +deployment: + replicaCount: 2 + container: + image: + repository: "docker.io/gmouchet/hello-kube:1.0" + pullPolicy: IfNotPresent + port: 8080 # docker image port. Do not change without rebuild docker image + # with another port \ No newline at end of file