diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..ba13248
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,3 @@
+*.png filter=lfs diff=lfs merge=lfs -text
+*.jpg filter=lfs diff=lfs merge=lfs -text
+*.gif filter=lfs diff=lfs merge=lfs -text
diff --git a/.gitignore b/.gitignore
index 9b78208..2f4c6bc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,11 @@
site
?
-_site
\ No newline at end of file
+_site
+node_modules/
+build/
+package-lock.json
+yarn.lock
+template
+build
+.secrets
+tools/
\ No newline at end of file
diff --git a/api/ping/ping.yml b/api/ping/ping.yml
new file mode 100644
index 0000000..809e533
--- /dev/null
+++ b/api/ping/ping.yml
@@ -0,0 +1,10 @@
+version: 1.0
+provider:
+ name: openfaas
+ gateway: http://127.0.0.1:8080
+functions:
+ ping:
+ lang: python3-http
+ handler: ./ping
+ image: host.docker.internal:5000/ping:1.0
+
diff --git a/api/ping/ping/__init__.py b/api/ping/ping/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/api/ping/ping/handler.py b/api/ping/ping/handler.py
new file mode 100644
index 0000000..5716ff9
--- /dev/null
+++ b/api/ping/ping/handler.py
@@ -0,0 +1,11 @@
+import json
+
+def handle(event, context):
+ """handle a request to the function
+ Args:
+ req (str): request body
+ """
+ return {
+ "statusCode": 200,
+ "body": str(event.body)
+ }
\ No newline at end of file
diff --git a/api/ping/ping/requirements.txt b/api/ping/ping/requirements.txt
new file mode 100644
index 0000000..e69de29
diff --git a/app/.dockerignore b/app/.dockerignore
new file mode 100644
index 0000000..fc9a38a
--- /dev/null
+++ b/app/.dockerignore
@@ -0,0 +1,2 @@
+**/node_modules/**
+*/yarn.lock
\ No newline at end of file
diff --git a/app/Dockerfile b/app/Dockerfile
new file mode 100644
index 0000000..5311d24
--- /dev/null
+++ b/app/Dockerfile
@@ -0,0 +1,12 @@
+FROM node:21-alpine3.17 as builder
+
+ADD . /workdir
+
+WORKDIR /workdir
+
+RUN yarn install && yarn run build
+
+
+FROM nginx:mainline-alpine
+ADD nginx.conf /etc/nginx/nginx.conf
+COPY --from=builder /workdir/build /usr/share/nginx/html
diff --git a/app/nginx.conf b/app/nginx.conf
new file mode 100644
index 0000000..79b946c
--- /dev/null
+++ b/app/nginx.conf
@@ -0,0 +1,24 @@
+worker_processes 5; ## Default: 1
+
+events {
+ worker_connections 4096; ## Default: 1024
+}
+
+http {
+ include /etc/nginx/mime.types;
+ server {
+ listen 80;
+ listen [::]:80;
+
+ root /usr/share/nginx/html;
+ index index.html index.htm;
+
+ location (.*)/config.js$ {
+ try_files $uri $uri/ /config.js;
+ }
+
+ location / {
+ try_files $uri $uri/ /index.html;
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/package.json b/app/package.json
new file mode 100644
index 0000000..fe896a0
--- /dev/null
+++ b/app/package.json
@@ -0,0 +1,49 @@
+{
+ "name": "mm-potal",
+ "version": "0.1.0",
+ "private": true,
+ "homepage": "/app",
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.14.1",
+ "@testing-library/react": "^13.0.0",
+ "@testing-library/user-event": "^13.2.1",
+ "@types/jest": "^27.0.1",
+ "@types/node": "^16.7.13",
+ "@types/react": "^18.0.0",
+ "@types/react-dom": "^18.0.0",
+ "oidc-client-ts": "^2.4.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.20.1",
+ "react-scripts": "5.0.1",
+ "typescript": "^4.4.2",
+ "web-vitals": "^2.1.0"
+ },
+ "scripts": {
+ "start": "react-scripts start",
+ "build": "react-scripts build",
+ "test": "react-scripts test",
+ "eject": "react-scripts eject"
+ },
+ "eslintConfig": {
+ "extends": [
+ "react-app",
+ "react-app/jest"
+ ]
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ },
+ "devDependencies": {
+ "@babel/plugin-proposal-private-property-in-object": "^7.21.11"
+ }
+}
diff --git a/app/public/config.js b/app/public/config.js
new file mode 100644
index 0000000..6059b43
--- /dev/null
+++ b/app/public/config.js
@@ -0,0 +1,9 @@
+config = {
+ 'serverUrl': 'http://localhost/api',
+ 'applicationRoot':'http://localhost/app/',
+ 'authentication': {
+ 'clientId': 'mm-portal',
+ 'serviceUrl': 'http://localhost:8080/realms/master/',
+ 'scope': 'openid profile email phone roles'
+ }
+}
\ No newline at end of file
diff --git a/app/public/index.html b/app/public/index.html
new file mode 100644
index 0000000..010fb9d
--- /dev/null
+++ b/app/public/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/app.tsx b/app/src/app.tsx
new file mode 100644
index 0000000..f11ac07
--- /dev/null
+++ b/app/src/app.tsx
@@ -0,0 +1,52 @@
+import React from 'react';
+import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
+import './index.css';
+import SecureRoute from './components/secureRoute'
+import Home from './features/home/view/content';
+import About from './features/about/view/content';
+import NavigationMenu from './components/navigationbar';
+import TitleBar from './components/titlebar';
+import { SettingsService } from './common/services/settingsService';
+import { AuthenticationService } from './common/services/authenticationService';
+import { ApplicationContext } from './common/utilities/applicationContext';
+import { SigninCallback } from './components/signinCallback';
+
+const App = () => {
+ const context = new AppContext();
+ return (
+
+
+
+
+
+
+ } />
+ } />
+ } />
+
+
+
+
+ );
+};
+
+export default App;
+
+class AppContext {
+ private settingsService: SettingsService | undefined;
+ private authenticationService: AuthenticationService | undefined;
+
+ public getAuthenticationService(): AuthenticationService {
+ if (!this.authenticationService) {
+ this.authenticationService = new AuthenticationService(this.getSettingsService().getAuthenticationSettings());
+ }
+ return this.authenticationService;
+ }
+
+ public getSettingsService(): SettingsService {
+ if (!this.settingsService) {
+ this.settingsService = new SettingsService();
+ }
+ return this.settingsService;
+ }
+}
\ No newline at end of file
diff --git a/app/src/common/services/authenticationService.ts b/app/src/common/services/authenticationService.ts
new file mode 100644
index 0000000..d050db4
--- /dev/null
+++ b/app/src/common/services/authenticationService.ts
@@ -0,0 +1,54 @@
+import { Log, User, UserManager } from 'oidc-client-ts';
+import { AuthenticationSettings } from '../settings/AuthenticationSettings';
+import { JsonHelper } from '../utilities/jsonHelper';
+import { Roles } from '../utilities/roles';
+
+export class AuthenticationService {
+ public userManager: UserManager;
+
+ constructor(settings: AuthenticationSettings) {
+ const oidcSettings = {
+ authority: settings.serviceUrl,
+ client_id: settings.clientId,
+ redirect_uri: settings.applicationRoot + `signin-callback`,
+ response_type: 'code',
+ scope: settings.scope
+ };
+ this.userManager = new UserManager(oidcSettings);
+
+ Log.setLogger(console);
+ Log.setLevel(Log.INFO);
+ }
+
+ public getUser(): Promise {
+ return this.userManager.getUser();
+ }
+
+ public login(): Promise {
+ return this.userManager.signinRedirect();
+ }
+
+ public renewToken(): Promise {
+ return this.userManager.signinSilent();
+ }
+
+ public logout(): Promise {
+ return this.userManager.signoutRedirect({ state: (window.location.pathname + window.location.search).substring(1) });
+ }
+
+ public handleSignin(): Promise {
+ return this.userManager.signinRedirectCallback();
+ }
+
+ public async getUserRoles(): Promise {
+ var user = await this.getUser();
+ if (!user) {
+ throw new ReferenceError("User is not authenticated");
+ }
+ const claims = JsonHelper.parseJwt(user.access_token);
+ console.log("Claims", claims);
+ const allowedRoles = [Roles.admin, Roles.user]
+ const roles = (claims.roles as string[]).filter(r => allowedRoles.includes(r));
+ return roles;
+ }
+}
\ No newline at end of file
diff --git a/app/src/common/services/settingsService.ts b/app/src/common/services/settingsService.ts
new file mode 100644
index 0000000..ca5471e
--- /dev/null
+++ b/app/src/common/services/settingsService.ts
@@ -0,0 +1,18 @@
+import { AuthenticationSettings } from '../settings/AuthenticationSettings';
+
+export class SettingsService {
+ public getAuthenticationSettings(): AuthenticationSettings {
+ let settings = new AuthenticationSettings();
+
+ settings.clientId = (window as any).config.authentication.clientId;
+ settings.serviceUrl = (window as any).config.authentication.serviceUrl;
+ settings.scope = (window as any).config.authentication.clientScope;
+ settings.applicationRoot = (window as any).config.applicationRoot;
+
+ return settings;
+ }
+
+ public getServerUrl(): string {
+ return (window as any).config.serverUrl;
+ }
+}
\ No newline at end of file
diff --git a/app/src/common/settings/AuthenticationSettings.ts b/app/src/common/settings/AuthenticationSettings.ts
new file mode 100644
index 0000000..dfb6be2
--- /dev/null
+++ b/app/src/common/settings/AuthenticationSettings.ts
@@ -0,0 +1,6 @@
+export class AuthenticationSettings {
+ clientId: string
+ serviceUrl: string
+ scope: string
+ applicationRoot: string
+}
\ No newline at end of file
diff --git a/app/src/common/utilities/applicationContext.ts b/app/src/common/utilities/applicationContext.ts
new file mode 100644
index 0000000..96dc9df
--- /dev/null
+++ b/app/src/common/utilities/applicationContext.ts
@@ -0,0 +1,14 @@
+import * as React from 'react';
+import { AuthenticationService } from '../services/authenticationService';
+import { SettingsService } from '../services/settingsService';
+
+export interface IApplicationContext {
+ getAuthenticationService(): AuthenticationService;
+ getSettingsService(): SettingsService;
+}
+
+export const ApplicationContext = React.createContext({} as IApplicationContext);
+
+export function useApplicationContext(): IApplicationContext {
+ return React.useContext(ApplicationContext);
+}
\ No newline at end of file
diff --git a/app/src/common/utilities/jsonHelper.ts b/app/src/common/utilities/jsonHelper.ts
new file mode 100644
index 0000000..dae26a3
--- /dev/null
+++ b/app/src/common/utilities/jsonHelper.ts
@@ -0,0 +1,105 @@
+export class JsonHelper {
+ public static toCamelCase(input: any): any {
+ if (input instanceof Array) {
+ return JsonHelper.arrayToCamelCase(input);
+ } else {
+ return JsonHelper.objectToCamelCase(input);
+ }
+ }
+
+ public static toSnakeCase(input: any): any {
+ if (input instanceof Array) {
+ return JsonHelper.arrayToSnakeCase(input);
+ } else {
+ return JsonHelper.objectToSnakeCase(input);
+ }
+ }
+
+ public static parseJwt(token: string): any {
+ var base64Url = token.split('.')[1];
+ var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
+ var jsonPayload = decodeURIComponent(window.atob(base64).split('').map(function (c) {
+ return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
+ }).join(''));
+
+ return JSON.parse(jsonPayload);
+ };
+
+ private static objectToCamelCase(input: any) {
+ let result: any = {};
+ for (let sourceKey in input) {
+ if (input.hasOwnProperty(sourceKey)) {
+ let targetKey = JsonHelper.valueToCamelCase(sourceKey);
+ let value = input[sourceKey]
+ if (value instanceof Array || (value !== null && value.constructor === Object)) {
+ value = JsonHelper.toCamelCase(value)
+ }
+ result[targetKey] = value
+ }
+ }
+ return result
+ }
+
+ private static arrayToCamelCase(input: Array) {
+ const convertProperty = (value: any) => {
+ if (typeof value === "object") {
+ value = JsonHelper.toCamelCase(value)
+ }
+ return value
+ }
+ return input.map(convertProperty);
+ }
+
+ private static valueToCamelCase(input: string) {
+ let result = input.charAt(0).toLowerCase();
+ let index = 1;
+ for (; index < input.length; index++) {
+ if (input[index] === '_') {
+ index++;
+ result += input[index].toUpperCase();
+ } else {
+ result += input[index];
+ }
+ }
+ return result;
+ }
+
+ private static objectToSnakeCase(input: any) {
+ let result: any = {};
+ for (let sourceKey in input) {
+ if (input.hasOwnProperty(sourceKey)) {
+ let targetKey = JsonHelper.valueToSnakeCase(sourceKey);
+ let value = input[sourceKey]
+ if (value instanceof Array || (value !== null && value.constructor === Object)) {
+ value = JsonHelper.toSnakeCase(value)
+ }
+ result[targetKey] = value
+ }
+ }
+ return result
+ }
+
+ private static arrayToSnakeCase(input: Array) {
+ const convertProperty = (value: any) => {
+ if (typeof value === "object") {
+ value = JsonHelper.toSnakeCase(value)
+ }
+ return value
+ }
+ return input.map(convertProperty);
+ }
+
+ private static valueToSnakeCase(input: string) {
+ let result = input.charAt(0).toLowerCase();
+ let index = 1;
+ for (; index < input.length; index++) {
+ if (input[index] === input[index].toUpperCase()) {
+ result += "_";
+ result += input[index].toLowerCase();
+ } else {
+ result += input[index];
+ }
+ }
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/app/src/common/utilities/roles.ts b/app/src/common/utilities/roles.ts
new file mode 100644
index 0000000..5fee29d
--- /dev/null
+++ b/app/src/common/utilities/roles.ts
@@ -0,0 +1,4 @@
+export class Roles {
+ public static readonly admin = "admin";
+ public static readonly user = "user";
+}
\ No newline at end of file
diff --git a/app/src/components/logo.tsx b/app/src/components/logo.tsx
new file mode 100644
index 0000000..1496ca6
--- /dev/null
+++ b/app/src/components/logo.tsx
@@ -0,0 +1,10 @@
+import logo from "../images/mm-logo.png";
+import * as React from "react";
+
+export default function Logo() {
+ return (
+ <>
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/app/src/components/navigationbar.tsx b/app/src/components/navigationbar.tsx
new file mode 100644
index 0000000..43001dc
--- /dev/null
+++ b/app/src/components/navigationbar.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import { Link } from 'react-router-dom';
+
+const NavigationMenu = () => {
+ return (
+
+ );
+};
+
+export
+
+ default NavigationMenu;
\ No newline at end of file
diff --git a/app/src/components/secureRoute.tsx b/app/src/components/secureRoute.tsx
new file mode 100644
index 0000000..f53b438
--- /dev/null
+++ b/app/src/components/secureRoute.tsx
@@ -0,0 +1,27 @@
+import * as React from 'react';
+import { useEffect } from 'react';
+import { useApplicationContext } from '../common/utilities/applicationContext';
+
+export default function SecureRoute(props: any) {
+ const context = useApplicationContext();
+ const authService = context.getAuthenticationService();
+ useEffect(() => {
+ const loginIfNeeded = async () => {
+ console.log("Get user");
+ try {
+ const user = await authService.getUser();
+ if (!user) {
+ authService.login();
+ }
+ }
+ catch (error) {
+ console.log("Failed to get user");
+ console.error(error);
+ }
+
+ };
+
+ loginIfNeeded();
+ }, [authService]);
+ return (<>{props.children}>);
+}
\ No newline at end of file
diff --git a/app/src/components/signinCallback.tsx b/app/src/components/signinCallback.tsx
new file mode 100644
index 0000000..af268d0
--- /dev/null
+++ b/app/src/components/signinCallback.tsx
@@ -0,0 +1,21 @@
+import * as React from 'react';
+import { AuthenticationService } from '../common/services/authenticationService';
+import { SettingsService } from '../common/services/settingsService';
+
+export function SigninCallback() {
+ let authSettings = new SettingsService().getAuthenticationSettings();
+ let authService = new AuthenticationService(authSettings);
+ React.useEffect(() => {
+ authService.handleSignin()
+ .then(user => {
+ if (user) {
+ let returnUrl = "index";
+ if (user.state && (user.state as any).returnUrl) {
+ returnUrl = (user.state as any).returnUrl;
+ }
+ (window as any).location.href = authSettings.applicationRoot + returnUrl;
+ }
+ });
+ });
+ return (<>>);
+}
diff --git a/app/src/components/titlebar.tsx b/app/src/components/titlebar.tsx
new file mode 100644
index 0000000..d07f0d2
--- /dev/null
+++ b/app/src/components/titlebar.tsx
@@ -0,0 +1,15 @@
+import React from 'react';
+import Logo from './logo';
+
+const TitleBar = () => {
+ return (
+
+ );
+};
+
+export default TitleBar;
\ No newline at end of file
diff --git a/app/src/features/about/view/content.tsx b/app/src/features/about/view/content.tsx
new file mode 100644
index 0000000..00ac896
--- /dev/null
+++ b/app/src/features/about/view/content.tsx
@@ -0,0 +1,8 @@
+const Content = () => {
+ console.log("about");
+ return (
+ About page
+
);
+};
+
+export default Content;
\ No newline at end of file
diff --git a/app/src/features/home/view/content.tsx b/app/src/features/home/view/content.tsx
new file mode 100644
index 0000000..b1762be
--- /dev/null
+++ b/app/src/features/home/view/content.tsx
@@ -0,0 +1,8 @@
+const Content = () => {
+ console.log("home");
+ return (
+ Home page
+
);
+};
+
+export default Content;
\ No newline at end of file
diff --git a/app/src/images/mm-logo.png b/app/src/images/mm-logo.png
new file mode 100644
index 0000000..c4753f0
--- /dev/null
+++ b/app/src/images/mm-logo.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f84fe554d5bb19e7c63aca0fa6d78c27678032d722adcb80acf99a5d43eee8ea
+size 7865
diff --git a/app/src/index.css b/app/src/index.css
new file mode 100644
index 0000000..c362927
--- /dev/null
+++ b/app/src/index.css
@@ -0,0 +1,45 @@
+/* Global styles */
+body {
+ font-family: sans-serif;
+ margin: 0;
+ padding: 0;
+}
+
+/* Navigation menu styles */
+.navigation-menu {
+ background-color: #f4f4f4;
+ padding: 20px;
+}
+
+.navigation-menu ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.navigation-menu li {
+ display: inline-block;
+ margin-right: 20px;
+}
+
+.navigation-menu a {
+ color: #000;
+ text-decoration: none;
+}
+
+/* Title bar styles */
+.title-bar {
+ background-color: #2196F3;
+ color: #fff;
+ padding: 20px;
+}
+
+.title-bar h1 {
+ margin: 0;
+ padding: 0;
+}
+
+/* Content styles */
+.content {
+ padding: 20px;
+}
\ No newline at end of file
diff --git a/app/src/index.tsx b/app/src/index.tsx
new file mode 100644
index 0000000..341ee5a
--- /dev/null
+++ b/app/src/index.tsx
@@ -0,0 +1,10 @@
+import * as React from 'react';
+import * as ReactDOM from 'react-dom/client';
+import App from './app';
+
+const root = ReactDOM.createRoot(document.getElementById('root')!);
+root.render(
+
+
+
+);
\ No newline at end of file
diff --git a/app/tsconfig.json b/app/tsconfig.json
new file mode 100644
index 0000000..e926689
--- /dev/null
+++ b/app/tsconfig.json
@@ -0,0 +1,29 @@
+{
+ "compilerOptions": {
+ "target": "es2016",
+ "lib": [
+ "dom",
+ "dom.iterable",
+ "esnext"
+ ],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noFallthroughCasesInSwitch": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx"
+ },
+ "include": [
+ "../src/**/*.tsx"
+ ],
+ "exclude": [
+ "node_modules"
+ ]
+}
\ No newline at end of file
diff --git a/local/docker-compose.yml b/local/docker-compose.yml
new file mode 100644
index 0000000..1401569
--- /dev/null
+++ b/local/docker-compose.yml
@@ -0,0 +1,75 @@
+version: "3.9"
+services:
+ registry:
+ image: registry:2.8.3
+ restart: unless-stopped
+ ports:
+ - 5000:5000
+ volumes:
+ - registry:/var/lib/registry
+ - ./registry/certs:/certs:/certs
+ environment:
+ REGISTRY_HTTP_ADDR: "0.0.0.0:5000"
+ REGISTRY_HTTP_TLS_CERTIFICATE: "/certs/host-docker.internal.crt"
+ REGISTRY_HTTP_TLS_KEY: "/certs/host-docker.internal.key"
+
+ app:
+ hostname: app
+ build:
+ dockerfile: Dockerfile
+ context: ../app
+
+ nginx:
+ image: openresty/openresty:1.21.4.3-1-alpine
+ restart: unless-stopped
+ ports:
+ - 80:80
+ volumes:
+ - ./nginx/nginx.conf:/usr/local/openresty/nginx/conf/nginx.conf
+
+ db:
+ image: postgres:16.1-alpine
+ hostname: db
+ restart: unless-stopped
+ environment:
+ - POSTGRES_PASSWORD=password
+ - POSTGRES_USER=admin
+ - POSTGRES_DB=mm-portal
+ ports:
+ - 5432:5432
+ volumes:
+ - pg_data:/var/lib/postgresql/data
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U admin -d mm-portal"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+
+ keycloak:
+ image: quay.io/keycloak/keycloak:latest
+ hostname: keycloak
+ restart: unless-stopped
+ depends_on:
+ - db
+ ports:
+ - "8081:8080"
+ environment:
+ KC_DB: postgres
+ KC_DB_URL: jdbc:postgresql://db/keycloak
+ KC_DB_USERNAME: admin
+ KC_DB_PASSWORD: password
+ KEYCLOAK_ADMIN: admin
+ KEYCLOAK_ADMIN_PASSWORD: admin
+ KC_METRICS_ENABLED: true
+ KC_HEALTH_ENABLED: true
+ healthcheck:
+ interval: 30s
+ timeout: 3s
+ start_period: 10s
+ retries: 3
+ test: curl -k --fail http://localhost/ || exit 1
+ command: start-dev
+
+volumes:
+ pg_data:
+ registry:
diff --git a/local/install-environment.ps1 b/local/install-environment.ps1
new file mode 100644
index 0000000..30d8e58
--- /dev/null
+++ b/local/install-environment.ps1
@@ -0,0 +1,79 @@
+param(
+ [Parameter()]
+ [string]
+ [ValidateSet('full-install', 'cluster', 'openfaas')]
+ $Action
+)
+
+$helmVersion = "3.13.2"
+
+function Install-Windows() {
+ New-Item -ItemType Directory -Path "$PSScriptRoot\tools" -Force
+ Push-Location "$PSScriptRoot\tools"
+ try {
+ $helmVersion = (Invoke-WebRequest "https://api.github.com/repos/helm/helm/releases/latest" | ConvertFrom-Json)[0].tag_name
+ Invoke-WebRequest -Uri "https://get.helm.sh/helm-$helmVersion-windows-amd64.zip" -OutFile "helm.zip"
+ Expand-Archive -Path "helm.zip" -DestinationPath "." -Force
+ $helmExe = Get-ChildItem -Path . -Recurse -Filter "helm.exe"
+ $helmExe | ForEach-Object { Move-Item $_.FullName "helm.exe" }
+ Remove-Item "helm.zip"
+ Remove-Item $helmExe.Directory.FullName -Recurse -Force
+ $faasCliVersion = (Invoke-WebRequest "https://api.github.com/repos/openfaas/faas-cli/releases/latest" | ConvertFrom-Json)[0].tag_name
+ Invoke-WebRequest -Uri "https://github.com/openfaas/faas-cli/releases/download/$faasCliVersion/faas-cli.exe" -OutFile "faas-cli.exe"
+ Invoke-WebRequest -Uri "https://kind.sigs.k8s.io/dl/v0.20.0/kind-windows-amd64" -OutFile "kind.exe"
+ $env:PATH = "$PSScriptRoot\tools;" + $env:PATH
+ }
+ finally {
+ Pop-Location
+ }
+}
+
+function Install-OpenFaas() {
+ kubectl create namespace openfaas
+ kubectl create namespace openfaas-fn
+ helm upgrade openfaas --install openfaas/openfaas --namespace openfaas
+ $password = kubectl -n openfaas get secret basic-auth -o jsonpath="{.data.basic-auth-password}"
+ [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($password)) | Out-File "password.txt"
+}
+
+function Install-General-Prerequisites() {
+ helm repo add openfaas https://openfaas.github.io/faas-netes
+ helm repo update
+}
+
+function Create-Cluster() {
+ kind create cluster
+ docker network connect "kind" "local-registry-1"
+ kubectl apply -f k8s/k8s-registry-config.yml
+
+ docker exec kind-control-plane mkdir /usr/share/ca-certificates/extra/
+ docker cp registry/certs/host-docker.internal.crt kind-control-plane:/usr/share/ca-certificates/extra/host-docker.internal.crt
+ docker exec -i kind-control-plane bash -c "echo 'extra/host-docker.internal.crt' | tee -a /etc/ca-certificates.conf"
+ docker exec kind-control-plane update-ca-certificates
+ docker restart kind-control-plane
+ $nodes = kubectl get nodes -o json | ConvertFrom-Json
+ while ((-not $nodes) -or (-not $nodes.items) -or $nodes.items.Length -eq 0) {
+ Start-Sleep -Seconds 1
+ $nodes = kubectl get nodes -o json | ConvertFrom-Json
+ }
+}
+
+function Main() {
+ if ($Action -eq 'full-install') {
+ if ($IsWindows) {
+ Install-Windows
+ }
+ else {
+
+ }
+ Install-General-Prerequisites
+ }
+ if ($Action -eq 'full-install' -or $Action -eq 'cluster') {
+ Create-Cluster
+ }
+ if ($Action -eq 'full-install' -or $Action -eq 'openfaas') {
+ Install-OpenFaas
+ }
+}
+
+Main
\ No newline at end of file
diff --git a/local/k8s/k8s-registry-config.yml b/local/k8s/k8s-registry-config.yml
new file mode 100644
index 0000000..dc88c3c
--- /dev/null
+++ b/local/k8s/k8s-registry-config.yml
@@ -0,0 +1,9 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: local-registry-hosting
+ namespace: kube-public
+data:
+ localRegistryHosting.v1: |
+ host: "localhost:5000"
+ help: "https://kind.sigs.k8s.io/docs/user/local-registry/"
\ No newline at end of file
diff --git a/local/nginx/nginx.conf b/local/nginx/nginx.conf
new file mode 100644
index 0000000..727e650
--- /dev/null
+++ b/local/nginx/nginx.conf
@@ -0,0 +1,47 @@
+events {
+ worker_connections 4096; ## Default: 1024
+}
+
+http {
+ include /usr/local/openresty/nginx/conf/mime.types;
+ server {
+ listen 80;
+ location ^~/api/ {
+ access_by_lua_block
+ {
+ ngx.req.read_body()
+ local req = ngx.req.get_body_data()
+ if (req)
+ then
+ local cjson = require "cjson"
+ local json_body = cjson.decode(req)
+ ngx.req.set_body_data(cjson.encode({ body = json_body }))
+ end
+ }
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_pass http://host.docker.internal:8080/;
+ rewrite /api(.*) /function$1 break;
+ }
+
+ location ^~/app/ {
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-NginX-Proxy true;
+ proxy_buffer_size 128k;
+ proxy_buffers 4 256k;
+ proxy_busy_buffers_size 256k;
+ proxy_pass 'http://app';
+ rewrite /app(.*) $1 break;
+ proxy_ssl_session_reuse off;
+ proxy_set_header Host $http_host;
+ proxy_cache_bypass $http_upgrade;
+ proxy_redirect off;
+ }
+
+ location / {
+ return 301 $scheme://$host/app/;
+ }
+ }
+}
\ No newline at end of file
diff --git a/local/registry/certs/host-docker.internal.crt b/local/registry/certs/host-docker.internal.crt
new file mode 100644
index 0000000..ce3cfa0
--- /dev/null
+++ b/local/registry/certs/host-docker.internal.crt
@@ -0,0 +1,31 @@
+-----BEGIN CERTIFICATE-----
+MIIFQDCCAyigAwIBAgIUMY1ucXnHR03xib/LQkMMtsSEf+swDQYJKoZIhvcNAQEL
+BQAwHzEdMBsGA1UEAwwUaG9zdC5kb2NrZXIuaW50ZXJuYWwwHhcNMjMxMjA5MTAx
+MDQ3WhcNMjQxMjA4MTAxMDQ3WjAfMR0wGwYDVQQDDBRob3N0LmRvY2tlci5pbnRl
+cm5hbDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALmOCGYJOVFiACXi
+vqmgsoWrhLZR85Qg4MKpnw+Ji4vq9CPKxGdqLgZym+fUV6v3IhiJ72fzR8kPtHP7
+RWN9O9EA1VtMXUhUKKNxOCjn4IkSmw01/NcjmJMx0Wavx/o7oTulce7b13PI4j6f
+xy5zrbCHwlG4xd+Ot1GVwfgA1hfqaBCETRNQWYXjWNiE9hcnpuI2LMxapALtMh7Y
+VWdCqswF4k9VVZrYazHtmZ4Hp4q5FrmsbsQpNE1qU6uQbeKj9wd8wKBWktxl/q3n
+jQ7AhhgDOQyw2tWDqPnDyLvSFcaAxFycUjSpPEp1/pYBKz3tS1gQkvX3sQFU8lgN
++m+KrQafNX7t30VID069/z4SkYIkC65L+cdRYwu85rvaOnjswecHa1xA335LPt9s
+wuHvgsJN72UjOsOXdkgZ65u4sQtCDFZvxzXdt6BhE63YNrXYjJJ1Y4DTLXNu//G4
+ttAEWTFXfy9nb7v5Co7GNUvZQaa3OgJAJBbLt40Z8KWMaOpfx5bQaU09+tXksald
+8mKoMwONhKIe/DdiQ6TSoutxokCJpx61B+U3NwvNnLPySQYLZivYrNGVyYUsAGQ1
+cHgEYexWp2ba56/bIMMcB/02Ddez6zqev2GeBu7Yw8bhPaOffQVFkLI8y4kWGUJS
+O3ai2ePGS5gBdJ3zAjVsCZKLK83RAgMBAAGjdDByMB0GA1UdDgQWBBTWRxQKqeK1
+ud5nKid/7Ogj4w8k2TAfBgNVHSMEGDAWgBTWRxQKqeK1ud5nKid/7Ogj4w8k2TAP
+BgNVHRMBAf8EBTADAQH/MB8GA1UdEQQYMBaCFGhvc3QuZG9ja2VyLmludGVybmFs
+MA0GCSqGSIb3DQEBCwUAA4ICAQBCx1SW+Y8w4KniYTarAXr3lvzsHw3CYiGSdCuq
+6+6bMkm7m+EUVSSFjWFMKk3XX5IZR9a2iGWNRDeVmUbFVVnSPXn1gHoPIQM5GrXz
+hs1gcLec1SsO2vNDGt7HzBgSyMiQnjsQ4WyFIV+7PpFQNBWwvhacjqkmGNJCiWUn
+JKyxmf/tyJZH8cjp0tNQXX3Z/BlVd/wrvdsde1wzaSDnhjs+F9Wo2GmKREQyVRjq
+8h1rjbjiHFkR4KBrbWD5iaTkD5mRnkEo81QVGwEuCu7z65g0HP18S0OqmSbUwUcU
+gpzjo/mvf+gC+ty/b3BLjy4ySKPpqZ24rb1bOZiZwY3kWgbiV94xYN+LrUpLp7dI
+WcRsRRCDN4ilrnUUY1eeGlZS1olnuBbxAySy7P0xZkCrrL9pFUS19Q4UE64t6iZR
+M2I8T4r8IK69R6lDZORb4zrGbLHe9aDj3N6GQhuGxZW8v23PfwE6/KoaIa20nEwx
+Qd5Xcw1+KCPAEnzbDdsuywFwUFxsqXsR3gnxcJxOVU0ty9qR1Zk02OCrawzSCqZE
+0Td1IbZv/MxbKm+xHLNGo2ZZe0HuT8aE09ZHhvfDZK5YD6+7ZOLFNBnoAyTbUi/a
+17s48ZsnP2bsVheS26z90A9hB2KpbTEX7p6WTgXaAk0xFz7hFq4R2zftpcf5A5Jo
+rzCixg==
+-----END CERTIFICATE-----
diff --git a/local/registry/certs/host-docker.internal.key b/local/registry/certs/host-docker.internal.key
new file mode 100644
index 0000000..be51572
--- /dev/null
+++ b/local/registry/certs/host-docker.internal.key
@@ -0,0 +1,52 @@
+-----BEGIN PRIVATE KEY-----
+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
+d9vojSzgdydWnKrGx5MEJ+iIAHgc
+-----END PRIVATE KEY-----