From d993995eb3f22b761d2c266e5e05916de8eec1dd Mon Sep 17 00:00:00 2001 From: Gilles Mouchet Date: Sat, 12 Apr 2025 11:03:22 +0200 Subject: [PATCH] V1.0.0 --- .env.dist | 17 +++ .gitignore | 1 + .vscode/settings.json | 5 + README.md | 47 ++++++++ README_BUILD.md | 64 +++++++++++ docker-compose.yml | 10 ++ docker.sh | 101 ++++++++++++++++++ docker/hello-kube/.dockerignore | 3 + docker/hello-kube/Dockerfile | 17 +++ docker/hello-kube/info.json | 3 + docker/hello-kube/package.json | 18 ++++ docker/hello-kube/package.json.reold | 18 ++++ docker/hello-kube/server.js | 82 ++++++++++++++ docker/hello-kube/static/css/main.css | 59 ++++++++++ docker/hello-kube/static/favicon.ico | Bin 0 -> 67646 bytes .../hello-kube/static/images/kubernetes.png | Bin 0 -> 11761 bytes docker/hello-kube/views/home.handlebars | 22 ++++ .../hello-kube/views/layouts/main.handlebars | 18 ++++ helm/Chart.yaml | 29 +++++ helm/templates/deployment.yaml | 58 ++++++++++ helm/templates/ingress.yaml | 18 ++++ helm/templates/service.yaml | 15 +++ helm/values-gmo-env.yaml | 12 +++ helm/values-vdg-env.yaml | 12 +++ 24 files changed, 629 insertions(+) create mode 100644 .env.dist create mode 100644 .gitignore create mode 100644 .vscode/settings.json create mode 100644 README.md create mode 100644 README_BUILD.md create mode 100644 docker-compose.yml create mode 100755 docker.sh create mode 100644 docker/hello-kube/.dockerignore create mode 100644 docker/hello-kube/Dockerfile create mode 100644 docker/hello-kube/info.json create mode 100644 docker/hello-kube/package.json create mode 100644 docker/hello-kube/package.json.reold create mode 100644 docker/hello-kube/server.js create mode 100644 docker/hello-kube/static/css/main.css create mode 100644 docker/hello-kube/static/favicon.ico create mode 100644 docker/hello-kube/static/images/kubernetes.png create mode 100644 docker/hello-kube/views/home.handlebars create mode 100644 docker/hello-kube/views/layouts/main.handlebars create mode 100644 helm/Chart.yaml create mode 100644 helm/templates/deployment.yaml create mode 100644 helm/templates/ingress.yaml create mode 100644 helm/templates/service.yaml create mode 100644 helm/values-gmo-env.yaml create mode 100644 helm/values-vdg-env.yaml 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 0000000000000000000000000000000000000000..89af875910277e1cb7af6e3672f8e101e5708bdc GIT binary patch literal 67646 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 E|I{)^8vpTNzn#= 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 literal 0 HcmV?d00001 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