├── frontend ├── .dockerignore ├── .stignore ├── chart │ ├── values.yaml │ ├── templates │ │ ├── NOTES.txt │ │ ├── service.yaml │ │ ├── ingress.yaml │ │ ├── deployment.yaml │ │ └── _helpers.tpl │ └── Chart.yaml ├── dist │ ├── favicon.png │ ├── poster-aliens.png │ ├── poster-hacker.png │ ├── poster-thehat.png │ ├── poster-cloudatlas.png │ ├── 60c3b61b94a9ae77b3353552ab6d4772.jpg │ └── index.html ├── src │ ├── static │ │ ├── poster-aliens.png │ │ ├── poster-kube.png │ │ ├── poster-mobydock.png │ │ ├── poster-cloudatlas.png │ │ ├── poster-crashloop.png │ │ └── poster-thefinalizer.png │ ├── assets │ │ └── images │ │ │ ├── favicon.ico │ │ │ └── favicon.png │ ├── index.html │ ├── index.jsx │ ├── Loader.jsx │ ├── index.css │ ├── Users.css │ ├── Users.jsx │ ├── App.css │ └── App.jsx ├── .babelrc ├── bashrc ├── default.conf ├── Dockerfile ├── package.json └── webpack.config.js ├── rent ├── .stignore ├── chart │ ├── values.yaml │ ├── templates │ │ ├── NOTES.txt │ │ ├── service.yaml │ │ ├── ingress.yaml │ │ ├── deployment.yaml │ │ └── _helpers.tpl │ └── Chart.yaml ├── target │ ├── maven-status │ │ └── maven-compiler-plugin │ │ │ └── compile │ │ │ └── default-compile │ │ │ ├── createdFiles.lst │ │ │ └── inputFiles.lst │ └── classes │ │ └── com │ │ └── okteto │ │ └── rent │ │ ├── RentApplication.class │ │ ├── ServletInitializer.class │ │ ├── controller │ │ ├── RentController.class │ │ ├── RentController$1.class │ │ └── RentController$Rent.class │ │ └── kafka │ │ └── KafkaProducerConfig.class ├── src │ └── main │ │ └── java │ │ └── com │ │ └── okteto │ │ └── rent │ │ ├── RentApplication.java │ │ ├── ServletInitializer.java │ │ ├── kafka │ │ └── KafkaProducerConfig.java │ │ └── controller │ │ └── RentController.java ├── Dockerfile └── pom.xml ├── worker ├── chart │ ├── values.yaml │ ├── templates │ │ ├── NOTES.txt │ │ ├── worker-deployment.yaml │ │ └── _helpers.tpl │ └── Chart.yaml ├── bashrc ├── Makefile ├── .stignore ├── Dockerfile ├── pkg │ ├── database │ │ └── database.go │ └── kafka │ │ └── kafka.go ├── go.mod ├── cmd │ └── worker │ │ └── main.go └── go.sum ├── api ├── chart │ ├── values.yaml │ ├── templates │ │ ├── NOTES.txt │ │ ├── api-service.yaml │ │ ├── api-ingress.yaml │ │ ├── api-deployment.yaml │ │ └── _helpers.tpl │ └── Chart.yaml ├── go.mod ├── bashrc ├── .stignore ├── go.sum ├── Makefile ├── Dockerfile ├── pkg │ └── database │ │ └── database.go ├── data │ └── README.md └── cmd │ └── api │ └── main.go ├── catalog ├── chart │ ├── values.yaml │ ├── templates │ │ ├── NOTES.txt │ │ ├── service.yaml │ │ ├── ingress.yaml │ │ ├── _helpers.tpl │ │ └── deployment.yaml │ └── Chart.yaml ├── .vscode │ └── launch.json ├── package.json ├── Dockerfile ├── .stignore ├── server.js ├── load.js └── data │ └── catalog.json ├── package.json ├── docs ├── architecture-diagram.png ├── demo-with-volume-snapshot.md ├── creating-db-snapshot.md └── architecture-diagram.svg ├── tests ├── Dockerfile ├── package.json ├── tests │ └── main.spec.js └── playwright.config.js ├── .gitignore ├── infrastructure └── chart │ ├── Chart.yaml │ ├── templates │ ├── kafka-service.yaml │ ├── mongodb-service.yaml │ ├── postgresql-service.yaml │ ├── NOTES.txt │ ├── _helpers.tpl │ ├── postgresql-deployment.yaml │ ├── kafka-deployment.yaml │ └── mongodb-deployment.yaml │ └── values.yaml ├── .oktetoignore ├── .github └── workflows │ ├── preview-closed.yaml │ └── preview.yaml ├── .vscode └── launch.json ├── README.md ├── okteto.yaml ├── okteto-with-volumes.yaml └── LICENSE /frontend/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /rent/.stignore: -------------------------------------------------------------------------------- 1 | .git 2 | ? 3 | *.jar 4 | -------------------------------------------------------------------------------- /frontend/.stignore: -------------------------------------------------------------------------------- 1 | chart 2 | node_modules 3 | -------------------------------------------------------------------------------- /rent/chart/values.yaml: -------------------------------------------------------------------------------- 1 | replicaCount: 1 2 | image: "" 3 | -------------------------------------------------------------------------------- /worker/chart/values.yaml: -------------------------------------------------------------------------------- 1 | replicaCount: 1 2 | image: "" 3 | -------------------------------------------------------------------------------- /frontend/chart/values.yaml: -------------------------------------------------------------------------------- 1 | replicaCount: 1 2 | image: "" 3 | -------------------------------------------------------------------------------- /api/chart/values.yaml: -------------------------------------------------------------------------------- 1 | replicaCount: 1 2 | image: "" 3 | load: "" 4 | -------------------------------------------------------------------------------- /catalog/chart/values.yaml: -------------------------------------------------------------------------------- 1 | 2 | replicaCount: 1 3 | image: "" 4 | -------------------------------------------------------------------------------- /api/chart/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | Success! Your application will be available shortly. -------------------------------------------------------------------------------- /rent/chart/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | Success! Your application will be available shortly. -------------------------------------------------------------------------------- /catalog/chart/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | Success! Your application will be available shortly. -------------------------------------------------------------------------------- /frontend/chart/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | Success! Your application will be available shortly. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "mongodb": "^6.18.0" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /rent/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /worker/chart/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | Success! Your application will be available shortly. -------------------------------------------------------------------------------- /frontend/dist/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okteto/movies/HEAD/frontend/dist/favicon.png -------------------------------------------------------------------------------- /docs/architecture-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okteto/movies/HEAD/docs/architecture-diagram.png -------------------------------------------------------------------------------- /frontend/dist/poster-aliens.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okteto/movies/HEAD/frontend/dist/poster-aliens.png -------------------------------------------------------------------------------- /frontend/dist/poster-hacker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okteto/movies/HEAD/frontend/dist/poster-hacker.png -------------------------------------------------------------------------------- /frontend/dist/poster-thehat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okteto/movies/HEAD/frontend/dist/poster-thehat.png -------------------------------------------------------------------------------- /frontend/dist/poster-cloudatlas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okteto/movies/HEAD/frontend/dist/poster-cloudatlas.png -------------------------------------------------------------------------------- /frontend/src/static/poster-aliens.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okteto/movies/HEAD/frontend/src/static/poster-aliens.png -------------------------------------------------------------------------------- /frontend/src/static/poster-kube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okteto/movies/HEAD/frontend/src/static/poster-kube.png -------------------------------------------------------------------------------- /frontend/src/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okteto/movies/HEAD/frontend/src/assets/images/favicon.ico -------------------------------------------------------------------------------- /frontend/src/assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okteto/movies/HEAD/frontend/src/assets/images/favicon.png -------------------------------------------------------------------------------- /frontend/src/static/poster-mobydock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okteto/movies/HEAD/frontend/src/static/poster-mobydock.png -------------------------------------------------------------------------------- /frontend/src/static/poster-cloudatlas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okteto/movies/HEAD/frontend/src/static/poster-cloudatlas.png -------------------------------------------------------------------------------- /frontend/src/static/poster-crashloop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okteto/movies/HEAD/frontend/src/static/poster-crashloop.png -------------------------------------------------------------------------------- /frontend/src/static/poster-thefinalizer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okteto/movies/HEAD/frontend/src/static/poster-thefinalizer.png -------------------------------------------------------------------------------- /tests/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/playwright:v1.55.0-noble 2 | ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 3 | RUN corepack enable yarn 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | frontend/node_modules 2 | catalog/node_modules 3 | bin 4 | rent/.idea 5 | rent/target 6 | tests/test-results 7 | tests/playwright-report -------------------------------------------------------------------------------- /frontend/dist/60c3b61b94a9ae77b3353552ab6d4772.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okteto/movies/HEAD/frontend/dist/60c3b61b94a9ae77b3353552ab6d4772.jpg -------------------------------------------------------------------------------- /rent/target/classes/com/okteto/rent/RentApplication.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okteto/movies/HEAD/rent/target/classes/com/okteto/rent/RentApplication.class -------------------------------------------------------------------------------- /api/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/okteto/movies 2 | 3 | go 1.24 4 | 5 | require github.com/lib/pq v1.10.5 6 | 7 | require github.com/gorilla/mux v1.8.0 // indirect 8 | -------------------------------------------------------------------------------- /frontend/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ], 6 | "plugins": [ 7 | "react-hot-loader/babel" 8 | ] 9 | } -------------------------------------------------------------------------------- /rent/target/classes/com/okteto/rent/ServletInitializer.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okteto/movies/HEAD/rent/target/classes/com/okteto/rent/ServletInitializer.class -------------------------------------------------------------------------------- /rent/target/classes/com/okteto/rent/controller/RentController.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okteto/movies/HEAD/rent/target/classes/com/okteto/rent/controller/RentController.class -------------------------------------------------------------------------------- /rent/target/classes/com/okteto/rent/kafka/KafkaProducerConfig.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okteto/movies/HEAD/rent/target/classes/com/okteto/rent/kafka/KafkaProducerConfig.class -------------------------------------------------------------------------------- /rent/target/classes/com/okteto/rent/controller/RentController$1.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okteto/movies/HEAD/rent/target/classes/com/okteto/rent/controller/RentController$1.class -------------------------------------------------------------------------------- /api/bashrc: -------------------------------------------------------------------------------- 1 | cat << EOF 2 | Welcome to your development container. Happy coding! 3 | EOF 4 | 5 | export PS1="\[\e[36m\]\${OKTETO_NAMESPACE:-okteto}:\[\e[32m\]\${OKTETO_NAME:-dev} \[\e[m\]\W> " -------------------------------------------------------------------------------- /rent/target/classes/com/okteto/rent/controller/RentController$Rent.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okteto/movies/HEAD/rent/target/classes/com/okteto/rent/controller/RentController$Rent.class -------------------------------------------------------------------------------- /worker/bashrc: -------------------------------------------------------------------------------- 1 | cat << EOF 2 | Welcome to your development container. Happy coding! 3 | EOF 4 | 5 | export PS1="\[\e[36m\]\${OKTETO_NAMESPACE:-okteto}:\[\e[32m\]\${OKTETO_NAME:-dev} \[\e[m\]\W> " -------------------------------------------------------------------------------- /frontend/bashrc: -------------------------------------------------------------------------------- 1 | cat << EOF 2 | Welcome to your development container. Happy coding! 3 | EOF 4 | 5 | export PS1="\[\e[36m\]\${OKTETO_NAMESPACE:-okteto}:\[\e[32m\]\${OKTETO_NAME:-dev} \[\e[m\]\W> " -------------------------------------------------------------------------------- /infrastructure/chart/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: infrastructure 3 | description: Simple Helm chart for PostgreSQL, Kafka, and MongoDB 4 | type: application 5 | version: 1.0.0 6 | appVersion: "1.0.0" -------------------------------------------------------------------------------- /api/chart/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: movies-api 3 | description: Rentals API for Movies App 4 | type: application 5 | version: 0.1.0 6 | appVersion: 1.0.0 7 | icon: https://apps.okteto.com/movies/icon.png -------------------------------------------------------------------------------- /rent/chart/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: movies-rent 3 | description: Rent backend for Movies App 4 | type: application 5 | version: 0.1.0 6 | appVersion: 1.0.0 7 | icon: https://apps.okteto.com/movies/icon.png -------------------------------------------------------------------------------- /frontend/chart/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: movies-frontend 3 | description: Frontend of the Movies App 4 | type: application 5 | version: 0.1.0 6 | appVersion: 1.0.0 7 | icon: https://apps.okteto.com/movies/icon.png -------------------------------------------------------------------------------- /worker/chart/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: movies-worker 3 | description: Rentals worker for Movies App 4 | type: application 5 | version: 0.1.0 6 | appVersion: 1.0.0 7 | icon: https://apps.okteto.com/movies/icon.png -------------------------------------------------------------------------------- /catalog/chart/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: movies-catalog 3 | description: Catalog backend for Movies App 4 | type: application 5 | version: 0.1.0 6 | appVersion: 1.0.0 7 | icon: https://apps.okteto.com/movies/icon.png -------------------------------------------------------------------------------- /rent/chart/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: rent 6 | name: rent 7 | spec: 8 | type: ClusterIP 9 | ports: 10 | - name: rent 11 | port: 8080 12 | selector: 13 | app: rent 14 | 15 | -------------------------------------------------------------------------------- /api/.stignore: -------------------------------------------------------------------------------- 1 | .git 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # vendor folders 9 | vendor 10 | 11 | # Test binary, built with go test -c 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # dlv binary 18 | __debug_bin 19 | -------------------------------------------------------------------------------- /worker/Makefile: -------------------------------------------------------------------------------- 1 | BACKEND ?= $(OKTETO_NAME) 2 | 3 | .PHONY: build 4 | build: 5 | go build -o bin/$(BACKEND) cmd/$(BACKEND)/main.go 6 | 7 | .PHONY: start 8 | start: 9 | bin/$(BACKEND) 10 | 11 | .PHONY: debug 12 | debug: 13 | dlv debug --headless --listen=:2345 --log --api-version=2 cmd/$(BACKEND)/main.go 14 | -------------------------------------------------------------------------------- /frontend/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Movies 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /api/go.sum: -------------------------------------------------------------------------------- 1 | github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= 2 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 3 | github.com/lib/pq v1.10.5 h1:J+gdV2cUmX7ZqL2B0lFcW0m+egaHC2V3lpO8nWxyYiQ= 4 | github.com/lib/pq v1.10.5/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 5 | -------------------------------------------------------------------------------- /rent/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst: -------------------------------------------------------------------------------- 1 | /app/src/main/java/com/okteto/rent/controller/RentController.java 2 | /app/src/main/java/com/okteto/rent/RentApplication.java 3 | /app/src/main/java/com/okteto/rent/ServletInitializer.java 4 | /app/src/main/java/com/okteto/rent/kafka/KafkaProducerConfig.java 5 | -------------------------------------------------------------------------------- /worker/.stignore: -------------------------------------------------------------------------------- 1 | .git 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # vendor folders 9 | vendor 10 | 11 | # Test binary, built with go test -c 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # dlv binary 18 | __debug_bin 19 | 20 | # binary 21 | bin/worker -------------------------------------------------------------------------------- /frontend/src/index.jsx: -------------------------------------------------------------------------------- 1 | import { hot } from 'react-hot-loader/root'; 2 | import React from 'react'; 3 | import { render } from 'react-dom'; 4 | 5 | import App from './App'; 6 | import './index.css'; 7 | 8 | if (module.hot) { 9 | module.hot.accept(); 10 | } 11 | 12 | const Root = hot(App); 13 | render(, document.getElementById('root')); 14 | -------------------------------------------------------------------------------- /.oktetoignore: -------------------------------------------------------------------------------- 1 | // Ignore all other files and folders 2 | ** 3 | 4 | // Synch chart folders 5 | !api/chart 6 | !catalog/chart 7 | !frontend/chart 8 | !rent/chart 9 | !worker/chart 10 | !infrastructure/chart 11 | 12 | 13 | [test] 14 | !tests/tests/main.spec.js 15 | !tests/tests/Dockerfile 16 | !tests/tests/package.json 17 | !tests/tests/playwright.config.js -------------------------------------------------------------------------------- /api/Makefile: -------------------------------------------------------------------------------- 1 | BACKEND ?= $(OKTETO_NAME) 2 | 3 | .PHONY: build 4 | build: 5 | go build -o bin/$(BACKEND) cmd/$(BACKEND)/main.go 6 | 7 | .PHONY: load-data 8 | load-data: 9 | bin/$(BACKEND) load-data 10 | 11 | .PHONY: start 12 | start: 13 | bin/$(BACKEND) 14 | 15 | .PHONY: debug 16 | debug: 17 | dlv debug --headless --listen=:2345 --log --api-version=2 cmd/$(BACKEND)/main.go 18 | -------------------------------------------------------------------------------- /worker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24-bookworm as dev 2 | 3 | WORKDIR /usr/src/app 4 | 5 | ADD go.mod go.sum ./ 6 | RUN go mod download all 7 | 8 | 9 | ADD . . 10 | RUN CGO_ENABLED=0 GOOS=linux go build -v -o /usr/local/bin/worker cmd/worker/main.go 11 | 12 | FROM scratch 13 | 14 | COPY --from=dev /usr/local/bin/worker /usr/local/bin/worker 15 | 16 | ENTRYPOINT ["/usr/local/bin/worker"] 17 | -------------------------------------------------------------------------------- /api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24-bookworm as dev 2 | 3 | WORKDIR /usr/src/app 4 | 5 | ADD go.mod go.sum ./ 6 | RUN go mod download all 7 | ADD . . 8 | RUN CGO_ENABLED=0 GOOS=linux go build -v -o /usr/local/bin/api cmd/api/main.go 9 | 10 | FROM scratch 11 | 12 | COPY --from=dev /usr/local/bin/api /usr/local/bin/api 13 | COPY --from=dev /usr/src/app/data /data 14 | 15 | ENTRYPOINT ["/usr/local/bin/api"] 16 | -------------------------------------------------------------------------------- /rent/src/main/java/com/okteto/rent/RentApplication.java: -------------------------------------------------------------------------------- 1 | package com.okteto.rent; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class RentApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(RentApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /api/chart/templates/api-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: api 5 | labels: 6 | {{- include "movies.labels" . | nindent 4 }} 7 | spec: 8 | type: ClusterIP 9 | ports: 10 | - port: 8080 11 | targetPort: 8080 12 | protocol: TCP 13 | name: http 14 | selector: 15 | app.kubernetes.io/name: api 16 | app.kubernetes.io/instance: {{ .Release.Name }} 17 | -------------------------------------------------------------------------------- /frontend/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Movies 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /catalog/chart/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: catalog 5 | labels: 6 | {{- include "movies.labels" . | nindent 4 }} 7 | spec: 8 | type: ClusterIP 9 | ports: 10 | - port: 8080 11 | targetPort: 8080 12 | protocol: TCP 13 | name: http 14 | selector: 15 | app.kubernetes.io/name: catalog 16 | app.kubernetes.io/instance: {{ .Release.Name }} 17 | -------------------------------------------------------------------------------- /frontend/chart/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: frontend 5 | labels: 6 | {{- include "movies.labels" . | nindent 4 }} 7 | spec: 8 | type: ClusterIP 9 | ports: 10 | - port: 80 11 | targetPort: http 12 | protocol: TCP 13 | name: http 14 | selector: 15 | app.kubernetes.io/name: frontend 16 | app.kubernetes.io/instance: {{ .Release.Name }} 17 | -------------------------------------------------------------------------------- /rent/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM maven:3.9.8-eclipse-temurin-21 2 | 3 | WORKDIR /app 4 | 5 | # copy the project files 6 | COPY ./pom.xml ./pom.xml 7 | 8 | 9 | # build all dependencies for offline use 10 | RUN mvn dependency:go-offline -B 11 | 12 | # copy the src files 13 | COPY ./src ./src 14 | 15 | # build for release 16 | RUN mvn clean package 17 | 18 | RUN cp ./target/*.jar app.jar 19 | 20 | EXPOSE 8080 21 | 22 | ENTRYPOINT ["java", "-jar", "/app/app.jar"] -------------------------------------------------------------------------------- /rent/chart/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: rent 5 | annotations: 6 | dev.okteto.com/generate-host: "movies" 7 | spec: 8 | rules: 9 | - http: 10 | paths: 11 | - path: /rent 12 | pathType: Prefix 13 | backend: 14 | service: 15 | name: rent 16 | port: 17 | number: 8080 18 | -------------------------------------------------------------------------------- /tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tests", 3 | "version": "1.0.0", 4 | "license": "Apache-2.0", 5 | "private": true, 6 | "scripts": { 7 | "test": "playwright test" 8 | }, 9 | "dependencies": { 10 | "@playwright/test": "1.55.0", 11 | "playwright": "1.55.0" 12 | }, 13 | "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" 14 | } 15 | -------------------------------------------------------------------------------- /catalog/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Connect to okteto", 6 | "type": "node", 7 | "request": "attach", 8 | "address": "localhost", 9 | "port": 9229, 10 | "localRoot": "${workspaceFolder}", 11 | "remoteRoot": "/src", 12 | "skipFiles": [ 13 | "/**" 14 | ] 15 | }, 16 | ] 17 | } -------------------------------------------------------------------------------- /catalog/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "movies-api", 3 | "version": "0.1.0", 4 | "license": "Apache-2.0", 5 | "dependencies": { 6 | "async": "^3.1.0", 7 | "express": "^4.16.4", 8 | "mongodb": "^6.18.0", 9 | "nodemon": "^2.0.2", 10 | "saslprep": "^1.0.3" 11 | }, 12 | "scripts": { 13 | "debug": "node --inspect-brk=0.0.0.0:9229 server.js", 14 | "start": "nodemon server.js", 15 | "load": "node load.js" 16 | }, 17 | "packageManager": "yarn@4.9.4" 18 | } 19 | -------------------------------------------------------------------------------- /rent/chart/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: rent 6 | name: rent 7 | spec: 8 | replicas: {{ .Values.replicaCount }} 9 | selector: 10 | matchLabels: 11 | app: rent 12 | template: 13 | metadata: 14 | labels: 15 | app: rent 16 | spec: 17 | containers: 18 | - image: {{ .Values.image }} 19 | name: rent 20 | ports: 21 | - containerPort: 8080 22 | name: rent 23 | -------------------------------------------------------------------------------- /rent/src/main/java/com/okteto/rent/ServletInitializer.java: -------------------------------------------------------------------------------- 1 | package com.okteto.rent; 2 | 3 | import org.springframework.boot.builder.SpringApplicationBuilder; 4 | import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; 5 | 6 | public class ServletInitializer extends SpringBootServletInitializer { 7 | 8 | @Override 9 | protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { 10 | return application.sources(RentApplication.class); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/preview-closed.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | types: 4 | - closed 5 | 6 | jobs: 7 | closed: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Context 11 | uses: okteto/context@latest 12 | with: 13 | token: ${{ secrets.OKTETO_TOKEN }} 14 | url: ${{ secrets.OKTETO_URL }} 15 | 16 | - name: Destroy preview environment 17 | uses: okteto/destroy-preview@latest 18 | with: 19 | name: pr-${{ github.event.number }}-cindylopez 20 | -------------------------------------------------------------------------------- /catalog/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20 2 | 3 | # Enable Corepack and prepare Yarn 4.9.4 4 | RUN corepack enable && corepack prepare yarn@4.9.4 --activate 5 | 6 | # Copy dependencies 7 | WORKDIR /src 8 | COPY package.json yarn.lock ./ 9 | 10 | # Install using Yarn 4 with node_modules (disable PnP) 11 | RUN --mount=type=cache,target=/root/.yarn \ 12 | YARN_CACHE_FOLDER=/root/.yarn \ 13 | yarn config set nodeLinker node-modules && \ 14 | yarn install --immutable 15 | 16 | COPY . . 17 | CMD ["yarn", "start"] 18 | -------------------------------------------------------------------------------- /frontend/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | listen [::]:80; 4 | server_name localhost; 5 | 6 | location / { 7 | root /usr/share/nginx/html; 8 | index index.html index.htm; 9 | try_files $uri $uri/ /index.html =404; 10 | } 11 | 12 | # redirect server error pages to the static page /50x.html 13 | # 14 | error_page 500 502 503 504 /50x.html; 15 | location = /50x.html { 16 | root /usr/share/nginx/html; 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /worker/chart/templates/worker-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: worker 6 | name: worker 7 | spec: 8 | replicas: {{ .Values.replicaCount }} 9 | selector: 10 | matchLabels: 11 | app: worker 12 | template: 13 | metadata: 14 | labels: 15 | app: worker 16 | spec: 17 | containers: 18 | - image: {{ .Values.image }} 19 | name: worker 20 | command: 21 | - /usr/local/bin/worker 22 | 23 | -------------------------------------------------------------------------------- /frontend/chart/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: frontend 5 | labels: 6 | {{- include "movies.labels" . | nindent 4 }} 7 | annotations: 8 | dev.okteto.com/generate-host: "movies" 9 | spec: 10 | rules: 11 | - http: 12 | paths: 13 | - path: / 14 | pathType: Prefix 15 | backend: 16 | service: 17 | name: frontend 18 | port: 19 | number: 80 20 | -------------------------------------------------------------------------------- /catalog/chart/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: catalog 5 | labels: 6 | {{- include "movies.labels" . | nindent 4 }} 7 | annotations: 8 | dev.okteto.com/generate-host: "movies" 9 | spec: 10 | rules: 11 | - http: 12 | paths: 13 | - path: /catalog 14 | pathType: Prefix 15 | backend: 16 | service: 17 | name: catalog 18 | port: 19 | number: 8080 20 | -------------------------------------------------------------------------------- /infrastructure/chart/templates/kafka-service.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.kafka.enabled }} 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: kafka 6 | labels: 7 | app: kafka 8 | chart: {{ include "infrastructure.chart" . }} 9 | release: {{ .Release.Name }} 10 | heritage: {{ .Release.Service }} 11 | spec: 12 | type: ClusterIP 13 | ports: 14 | - port: {{ .Values.kafka.service.port }} 15 | targetPort: kafka 16 | protocol: TCP 17 | name: kafka 18 | selector: 19 | app: kafka 20 | release: {{ .Release.Name }} 21 | {{- end }} -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20 AS dev 2 | 3 | # setup okteto message 4 | COPY bashrc /root/.bashrc 5 | 6 | WORKDIR /usr/src/app 7 | 8 | COPY package.json yarn.lock ./ 9 | RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install 10 | COPY . . 11 | 12 | RUN --mount=type=cache,target=./node_modules/.cache/webpack yarn build 13 | 14 | FROM nginx:alpine 15 | 16 | # overwrite default.conf 17 | RUN rm /etc/nginx/conf.d/default.conf 18 | COPY default.conf /etc/nginx/conf.d 19 | 20 | COPY --from=dev /usr/src/app/dist /usr/share/nginx/html 21 | EXPOSE 80 22 | -------------------------------------------------------------------------------- /infrastructure/chart/templates/mongodb-service.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.mongodb.enabled }} 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: mongodb 6 | labels: 7 | app: mongodb 8 | chart: {{ include "infrastructure.chart" . }} 9 | release: {{ .Release.Name }} 10 | heritage: {{ .Release.Service }} 11 | spec: 12 | type: ClusterIP 13 | ports: 14 | - port: {{ .Values.mongodb.service.port }} 15 | targetPort: mongodb 16 | protocol: TCP 17 | name: mongodb 18 | selector: 19 | app: mongodb 20 | release: {{ .Release.Name }} 21 | {{- end }} -------------------------------------------------------------------------------- /infrastructure/chart/templates/postgresql-service.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.postgresql.enabled }} 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: postgresql 6 | labels: 7 | app: postgresql 8 | chart: {{ include "infrastructure.chart" . }} 9 | release: {{ .Release.Name }} 10 | heritage: {{ .Release.Service }} 11 | spec: 12 | type: ClusterIP 13 | ports: 14 | - port: {{ .Values.postgresql.service.port }} 15 | targetPort: postgresql 16 | protocol: TCP 17 | name: postgresql 18 | selector: 19 | app: postgresql 20 | release: {{ .Release.Name }} 21 | {{- end }} -------------------------------------------------------------------------------- /api/chart/templates/api-ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: api 5 | labels: 6 | {{- include "movies.labels" . | nindent 4 }} 7 | annotations: 8 | dev.okteto.com/generate-host: "movies" 9 | spec: 10 | rules: 11 | - http: 12 | paths: 13 | - path: /rentals 14 | pathType: Prefix 15 | backend: 16 | service: 17 | name: api 18 | port: 19 | number: 8080 20 | - path: /users 21 | pathType: Prefix 22 | backend: 23 | service: 24 | name: api 25 | port: 26 | number: 8080 27 | -------------------------------------------------------------------------------- /api/pkg/database/database.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | 7 | _ "github.com/lib/pq" 8 | ) 9 | 10 | const ( 11 | host = "postgresql" 12 | port = 5432 13 | user = "okteto" 14 | password = "okteto" 15 | dbname = "rentals" 16 | ) 17 | 18 | func Open() *sql.DB { 19 | psqlconn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", host, port, user, password, dbname) 20 | for { 21 | db, err := sql.Open("postgres", psqlconn) 22 | if err == nil { 23 | return db 24 | } 25 | } 26 | } 27 | 28 | func Ping(db *sql.DB) { 29 | fmt.Println("Waiting for postgresql...") 30 | for { 31 | if err := db.Ping(); err == nil { 32 | fmt.Println("Postgresql connected!") 33 | return 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /worker/pkg/database/database.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | 7 | _ "github.com/lib/pq" 8 | ) 9 | 10 | const ( 11 | host = "postgresql" 12 | port = 5432 13 | user = "okteto" 14 | password = "okteto" 15 | dbname = "rentals" 16 | ) 17 | 18 | func Open() *sql.DB { 19 | psqlconn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", host, port, user, password, dbname) 20 | for { 21 | db, err := sql.Open("postgres", psqlconn) 22 | if err == nil { 23 | return db 24 | } 25 | } 26 | } 27 | 28 | func Ping(db *sql.DB) { 29 | fmt.Println("Waiting for postgresql...") 30 | for { 31 | if err := db.Ping(); err == nil { 32 | fmt.Println("Postgresql connected!") 33 | return 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /worker/pkg/kafka/kafka.go: -------------------------------------------------------------------------------- 1 | package kafka 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | kingpin "gopkg.in/alecthomas/kingpin.v2" 8 | 9 | "github.com/Shopify/sarama" 10 | ) 11 | 12 | var ( 13 | brokerList = kingpin.Flag("brokerList", "List of brokers to connect").Default("kafka:9092").Strings() 14 | ) 15 | 16 | func GetMaster() sarama.Consumer { 17 | kingpin.Parse() 18 | config := sarama.NewConfig() 19 | config.Consumer.Return.Errors = true 20 | config.Consumer.Offsets.AutoCommit.Enable = true 21 | config.Consumer.Offsets.AutoCommit.Interval = 1 * time.Second 22 | brokers := *brokerList 23 | fmt.Println("Waiting for kafka...") 24 | for { 25 | master, err := sarama.NewConsumer(brokers, config) 26 | if err == nil { 27 | fmt.Println("Kafka connected!") 28 | return master 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug Worker", 6 | "type": "go", 7 | "request": "attach", 8 | "mode": "remote", 9 | "remotePath": "/usr/src/app", 10 | "port": 2345, 11 | "host": "127.0.0.1", 12 | "cwd": "${workspaceFolder}/worker" 13 | }, 14 | { 15 | "name": "Debug Catalog", 16 | "type": "node", 17 | "request": "attach", 18 | "address": "localhost", 19 | "port": 9229, 20 | "localRoot": "${workspaceFolder}/catalog", 21 | "remoteRoot": "/src", 22 | "skipFiles": [ 23 | "/**" 24 | ] 25 | }, 26 | ] 27 | } -------------------------------------------------------------------------------- /frontend/src/Loader.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Loader = () => { 4 | return ( 5 |
6 | 13 | 14 | 21 | 22 | 23 |
24 | ); 25 | } 26 | 27 | export default Loader; 28 | -------------------------------------------------------------------------------- /infrastructure/chart/values.yaml: -------------------------------------------------------------------------------- 1 | # PostgreSQL Configuration 2 | postgresql: 3 | enabled: true 4 | image: 5 | repository: postgres 6 | tag: "16" 7 | auth: 8 | postgresPassword: "okteto" 9 | username: "okteto" 10 | password: "okteto" 11 | database: "rentals" 12 | storage: 13 | size: 1Gi 14 | storageClass: "" 15 | service: 16 | port: 5432 17 | 18 | # Kafka Configuration (using KRaft mode - no Zookeeper needed) 19 | kafka: 20 | enabled: true 21 | image: 22 | repository: apache/kafka 23 | tag: "3.7.0" 24 | auth: 25 | enabled: false 26 | storage: 27 | size: 1Gi 28 | storageClass: "" 29 | service: 30 | port: 9092 31 | 32 | # MongoDB Configuration 33 | mongodb: 34 | enabled: true 35 | image: 36 | repository: mongo 37 | tag: "7" 38 | auth: 39 | enabled: true 40 | username: "okteto" 41 | password: "okteto" 42 | database: "okteto" 43 | storage: 44 | size: 1Gi 45 | storageClass: "" 46 | service: 47 | port: 27017 -------------------------------------------------------------------------------- /api/chart/templates/api-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: api 5 | labels: 6 | {{- include "movies.labels" . | nindent 4 }} 7 | app.kubernetes.io/component: api 8 | spec: 9 | replicas: {{ .Values.replicaCount }} 10 | selector: 11 | matchLabels: 12 | app.kubernetes.io/name: api 13 | app.kubernetes.io/instance: {{ .Release.Name }} 14 | template: 15 | metadata: 16 | labels: 17 | app.kubernetes.io/name: api 18 | app.kubernetes.io/instance: {{ .Release.Name }} 19 | spec: 20 | terminationGracePeriodSeconds: 0 21 | {{- if .Values.load }} 22 | initContainers: 23 | - name: load-data 24 | image: {{ .Values.image }} 25 | command: 26 | - /usr/local/bin/api 27 | - load-data 28 | {{- end }} 29 | containers: 30 | - name: api 31 | image: {{ .Values.image }} 32 | ports: 33 | - name: http 34 | containerPort: 8080 35 | protocol: TCP 36 | -------------------------------------------------------------------------------- /catalog/.stignore: -------------------------------------------------------------------------------- 1 | .git 2 | # Runtime data 3 | pids 4 | *.pid 5 | *.seed 6 | *.pid.lock 7 | 8 | # Directory for instrumented libs generated by jscoverage/JSCover 9 | lib-cov 10 | 11 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 12 | .grunt 13 | 14 | # Bower dependency directory (https://bower.io/) 15 | bower_components 16 | 17 | # node-waf configuration 18 | .lock-wscript 19 | 20 | # Compiled binary addons (https://nodejs.org/api/addons.html) 21 | build/Release 22 | 23 | # Dependency directories 24 | node_modules 25 | jspm_packages 26 | 27 | # Optional npm cache directory 28 | .npm 29 | 30 | # Optional eslint cache 31 | .eslintcache 32 | 33 | # Optional REPL history 34 | .node_repl_history 35 | 36 | # Output of 'npm pack' 37 | *.tgz 38 | 39 | # Yarn Integrity file 40 | .yarn-integrity 41 | 42 | # parcel-bundler cache (https://parceljs.org/) 43 | .cache 44 | 45 | # next.js build output 46 | .next 47 | 48 | # nuxt.js build output 49 | .nuxt 50 | 51 | # vuepress build output 52 | .vuepress/dist 53 | 54 | # Serverless directories 55 | .serverless 56 | 57 | -------------------------------------------------------------------------------- /frontend/chart/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: frontend 5 | labels: 6 | {{- include "movies.labels" . | nindent 4 }} 7 | app.kubernetes.io/component: frontend 8 | spec: 9 | replicas: {{ .Values.replicaCount }} 10 | selector: 11 | matchLabels: 12 | app.kubernetes.io/name: frontend 13 | app.kubernetes.io/instance: {{ .Release.Name }} 14 | template: 15 | metadata: 16 | labels: 17 | app.kubernetes.io/name: frontend 18 | app.kubernetes.io/instance: {{ .Release.Name }} 19 | spec: 20 | terminationGracePeriodSeconds: 0 21 | containers: 22 | - name: frontend 23 | image: {{ .Values.image }} 24 | ports: 25 | - name: http 26 | containerPort: 80 27 | protocol: TCP 28 | livenessProbe: 29 | periodSeconds: 1 30 | httpGet: 31 | path: / 32 | port: http 33 | readinessProbe: 34 | periodSeconds: 1 35 | httpGet: 36 | path: / 37 | port: http 38 | -------------------------------------------------------------------------------- /tests/tests/main.spec.js: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test('environment variables are set', async () => { 4 | expect(process.env.OKTETO_NAMESPACE).toBeDefined(); 5 | expect(process.env.OKTETO_DOMAIN).toBeDefined(); 6 | expect(process.env.OKTETO_NAMESPACE).not.toBe(''); 7 | expect(process.env.OKTETO_DOMAIN).not.toBe(''); 8 | }); 9 | 10 | 11 | test('movies has title', async ({ page }) => { 12 | await page.goto(`https://movies-${process.env.OKTETO_NAMESPACE}.${process.env.OKTETO_DOMAIN}`); 13 | 14 | // The page title 15 | await expect(page).toHaveTitle('Movies'); 16 | }); 17 | 18 | test('catalog has entries', async ({ request }) => { 19 | const apiUrl = `https://movies-${process.env.OKTETO_NAMESPACE}.${process.env.OKTETO_DOMAIN}/catalog`; 20 | const response = await request.get(apiUrl); 21 | expect(response.status()).toBe(200); 22 | const data = await response.json(); 23 | expect(data.length).toBe(6); 24 | 25 | const expectedTitles = [ 26 | 'Moby Dock', 27 | 'The Finalizer', 28 | 'Crash Loop Backoff', 29 | 'Kube', 30 | 'Cloud Atlas', 31 | 'Aliens' 32 | ]; 33 | 34 | const actualTitles = data.map(item => item.original_title); 35 | expect(actualTitles).toEqual(expectedTitles); 36 | }); -------------------------------------------------------------------------------- /worker/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/okteto/movies 2 | 3 | go 1.24 4 | 5 | require ( 6 | github.com/Shopify/sarama v1.32.0 7 | github.com/lib/pq v1.10.5 8 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 9 | ) 10 | 11 | require ( 12 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect 13 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect 14 | github.com/davecgh/go-spew v1.1.1 // indirect 15 | github.com/eapache/go-resiliency v1.2.0 // indirect 16 | github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 // indirect 17 | github.com/eapache/queue v1.1.0 // indirect 18 | github.com/golang/snappy v0.0.4 // indirect 19 | github.com/hashicorp/go-uuid v1.0.2 // indirect 20 | github.com/jcmturner/aescts/v2 v2.0.0 // indirect 21 | github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect 22 | github.com/jcmturner/gofork v1.0.0 // indirect 23 | github.com/jcmturner/gokrb5/v8 v8.4.2 // indirect 24 | github.com/jcmturner/rpc/v2 v2.0.3 // indirect 25 | github.com/klauspost/compress v1.14.4 // indirect 26 | github.com/pierrec/lz4 v2.6.1+incompatible // indirect 27 | github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect 28 | golang.org/x/crypto v0.0.0-20220214200702-86341886e292 // indirect 29 | golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect 30 | ) 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Movies App 2 | 3 | This example shows how to leverage [Okteto](https://github.com/okteto/okteto) to develop an application based on microservices directly on Kubernetes. The Movies App is deployed using a Helm Charts. It creates the following components: 4 | 5 | - A *React* based [frontend](frontend) service, using [webpack](https://webpack.js.org) as bundler and *hot-reload server* for development 6 | - A Node.js based [catalog](catalog) service to serve the available movies from a MongoDB database 7 | - A Java based [rent](rent) service to receive rent requests and send them to Kafka 8 | - A Golang based [worker](worker) to process rent request from Kafka and update the PostgreSQL database 9 | - A Golang based [api](api) to retrieve the current movies rentals from the PostgresSQL database 10 | - A [MongoDB](https://bitnami.com/stack/mongodb/helm) database 11 | - A [Kafka](https://bitnami.com/stack/kafka/helm) queue 12 | - A [PostgresQL](https://bitnami.com/stack/postgresql/helm) database 13 | 14 | ![Architecture diagram](docs/architecture-diagram.png) 15 | 16 | ## Development container demo script 17 | 18 | - Deploy the repo from UI 19 | - Rent two movies 20 | - `okteto up worker` + `make build` + `make start` 21 | - Uncomment line 61 in `rentals/cmd/worker/main.go` 22 | - `make build` + `make start` 23 | - Show how the change is applied 24 | -------------------------------------------------------------------------------- /catalog/server.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const mongo = require("mongodb").MongoClient; 3 | 4 | const app = express(); 5 | 6 | const url = `mongodb://${process.env.MONGODB_USERNAME}:${encodeURIComponent(process.env.MONGODB_PASSWORD)}@${process.env.MONGODB_HOST}:27017/${process.env.MONGODB_DATABASE}?authSource=admin`; 7 | 8 | async function startWithRetry() { 9 | try { 10 | const client = await mongo.connect(url, { 11 | connectTimeoutMS: 30000, 12 | socketTimeoutMS: 30000, 13 | }); 14 | 15 | const db = client.db(process.env.MONGODB_DATABASE); 16 | 17 | app.get("/catalog/healthz", (req, res, next) => { 18 | res.sendStatus(200) 19 | return; 20 | }); 21 | 22 | app.get("/catalog", async (req, res, next) => { 23 | console.log(`GET /catalog`) 24 | try { 25 | const results = await db.collection('catalog').find().toArray(); 26 | res.json(results); 27 | } catch (err) { 28 | console.log(`failed to query movies: ${err}`) 29 | res.json([]); 30 | } 31 | }); 32 | 33 | app.listen(8080, () => { 34 | console.log("Server running on port 8080."); 35 | }); 36 | } catch (err) { 37 | console.error(`Error connecting, retrying in 1 sec: ${err}`); 38 | setTimeout(startWithRetry, 1000); 39 | } 40 | }; 41 | 42 | startWithRetry(); -------------------------------------------------------------------------------- /.github/workflows/preview.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | branches: 4 | - main 5 | 6 | jobs: 7 | preview: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Context 11 | uses: okteto/context@latest 12 | with: 13 | token: ${{ secrets.OKTETO_TOKEN }} 14 | url: ${{ secrets.OKTETO_URL }} 15 | 16 | - name: Deploy preview environment 17 | uses: okteto/deploy-preview@latest 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | with: 21 | name: pr-${{ github.event.number }}-cindylopez 22 | scope: global 23 | 24 | - name: Checkout code 25 | uses: actions/checkout@v4 26 | 27 | - name: Run end to end tests 28 | uses: okteto/test@latest 29 | with: 30 | tests: e2e 31 | namespace: pr-${{ github.event.number }}-cindylopez 32 | 33 | - name: Save playwright report 34 | uses: actions/upload-artifact@v4 35 | if: ${{ !cancelled() }} 36 | with: 37 | name: playwright-report 38 | path: tests/playwright-report/ 39 | retention-days: 30 40 | 41 | - name: Save test results 42 | uses: actions/upload-artifact@v4 43 | if: ${{ !cancelled() }} 44 | with: 45 | name: test-results 46 | path: tests/test-results/ 47 | retention-days: 30 48 | include-hidden-files: true -------------------------------------------------------------------------------- /tests/playwright.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/test'; 2 | 3 | const BASE_URL = `https://movies-${process.env.OKTETO_NAMESPACE}.${process.env.OKTETO_DOMAIN}`; 4 | 5 | /** 6 | * @see https://playwright.dev/docs/test-configuration 7 | */ 8 | const config = defineConfig({ 9 | testDir: './tests', 10 | /* Run tests in files in parallel */ 11 | fullyParallel: true, 12 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 13 | forbidOnly: !!process.env.CI, 14 | /* Retry on CI only */ 15 | retries: process.env.CI ? 0 : 0, 16 | /* Opt out of parallel tests on CI. */ 17 | workers: process.env.CI ? 1 : undefined, 18 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 19 | reporter: [['list'], ['html', { open: 'never' }]], 20 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 21 | use: { 22 | /* Base URL to use in actions like `await page.goto('/')`. */ 23 | baseURL: BASE_URL, 24 | 25 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 26 | trace: 'on-first-retry', 27 | }, 28 | 29 | /* Configure projects for major browsers */ 30 | projects: [ 31 | { 32 | name: 'chromium', 33 | use: { ...devices['Desktop Chrome'] }, 34 | }, 35 | ], 36 | }); 37 | 38 | export default config; -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "okteto-movies-frontend", 3 | "version": "0.3.0", 4 | "license": "Apache-2.0", 5 | "private": true, 6 | "dependencies": { 7 | "@hot-loader/react-dom": "^17.0.2", 8 | "react": "17.0.2", 9 | "react-dom": "^17.0.1", 10 | "react-hot-loader": "^4.13.0", 11 | "react-router-dom": "5.3.3" 12 | }, 13 | "devDependencies": { 14 | "@babel/core": "^7.12.3", 15 | "@babel/preset-env": "^7.12.1", 16 | "@babel/preset-react": "^7.12.1", 17 | "@webpack-cli/serve": "^1.0.1", 18 | "babel-loader": "^8.0.4", 19 | "copy-webpack-plugin": "^10.2.4", 20 | "css-loader": "3.4.2", 21 | "file-loader": "^6.1.1", 22 | "html-webpack-plugin": "^5.0.0-alpha.6", 23 | "react-refresh": "^0.8.3", 24 | "style-loader": "1.1.3", 25 | "url-loader": "4.0.0", 26 | "webpack": "^5.1.3", 27 | "webpack-cli": "^4.9.2", 28 | "webpack-dev-server": "^4.7.4" 29 | }, 30 | "scripts": { 31 | "start": "yarn devel", 32 | "devel": "webpack serve --mode=development", 33 | "build": "webpack", 34 | "test": "echo \"Error: no test specified\" && exit 1" 35 | }, 36 | "browserslist": [ 37 | "last 8 versions", 38 | ">1%", 39 | "not edge < 80", 40 | "not ie >= 0", 41 | "not ie_mob >= 0", 42 | "not OperaMobile >= 0", 43 | "not samsung < 10", 44 | "not op_mini all", 45 | "not dead" 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /infrastructure/chart/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 1. Get the application URLs by running these commands: 2 | 3 | {{- if .Values.postgresql.enabled }} 4 | PostgreSQL: 5 | Connection: postgresql://postgres:postgres@postgresql:5432/movies 6 | 7 | To connect from within the cluster: 8 | kubectl run postgresql-client --rm --tty -i --restart='Never' --image postgres:16 -- psql --host postgresql --port 5432 -U postgres -d movies 9 | 10 | {{- end }} 11 | 12 | {{- if .Values.mongodb.enabled }} 13 | MongoDB: 14 | Connection: mongodb://mongodb:27017/movies 15 | 16 | To connect from within the cluster: 17 | kubectl run mongodb-client --rm --tty -i --restart='Never' --image mongo:7 -- mongosh --host mongodb --port 27017 18 | 19 | {{- end }} 20 | 21 | {{- if .Values.kafka.enabled }} 22 | Kafka: 23 | Bootstrap server: kafka:9092 24 | 25 | To connect from within the cluster: 26 | kubectl run kafka-client --rm --tty -i --restart='Never' --image apache/kafka:3.7.0 -- /opt/kafka/bin/kafka-topics.sh --bootstrap-server kafka:9092 --list 27 | 28 | {{- end }} 29 | 30 | 2. Infrastructure components deployed: 31 | {{- if .Values.postgresql.enabled }} 32 | - PostgreSQL {{ .Values.postgresql.image.tag }} 33 | {{- end }} 34 | {{- if .Values.mongodb.enabled }} 35 | - MongoDB {{ .Values.mongodb.image.tag }} 36 | {{- end }} 37 | {{- if .Values.kafka.enabled }} 38 | - Kafka {{ .Values.kafka.image.tag }} (KRaft mode - no Zookeeper needed) 39 | {{- end }} -------------------------------------------------------------------------------- /rent/src/main/java/com/okteto/rent/kafka/KafkaProducerConfig.java: -------------------------------------------------------------------------------- 1 | package com.okteto.rent.kafka; 2 | 3 | import org.apache.kafka.clients.producer.ProducerConfig; 4 | import org.apache.kafka.common.serialization.StringSerializer; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.kafka.core.DefaultKafkaProducerFactory; 8 | import org.springframework.kafka.core.KafkaTemplate; 9 | import org.springframework.kafka.core.ProducerFactory; 10 | 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | 14 | @Configuration 15 | public class KafkaProducerConfig { 16 | 17 | @Bean 18 | public ProducerFactory producerFactory() { 19 | Map configProps = new HashMap<>(); 20 | configProps.put( 21 | ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, 22 | "kafka:9092"); 23 | configProps.put( 24 | ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, 25 | StringSerializer.class); 26 | configProps.put( 27 | ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, 28 | StringSerializer.class); 29 | return new DefaultKafkaProducerFactory<>(configProps); 30 | } 31 | 32 | @Bean 33 | public KafkaTemplate kafkaTemplate() { 34 | return new KafkaTemplate<>(producerFactory()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /api/chart/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "movies.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | If release name contains chart name it will be used as a full name. 13 | */}} 14 | {{- define "movies.fullname" -}} 15 | {{- if .Values.fullnameOverride -}} 16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 17 | {{- else -}} 18 | {{- $name := default .Chart.Name .Values.nameOverride -}} 19 | {{- if contains $name .Release.Name -}} 20 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 21 | {{- else -}} 22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 23 | {{- end -}} 24 | {{- end -}} 25 | {{- end -}} 26 | 27 | {{/* 28 | Create chart name and version as used by the chart label. 29 | */}} 30 | {{- define "movies.chart" -}} 31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 32 | {{- end -}} 33 | 34 | {{/* 35 | Common labels 36 | */}} 37 | {{- define "movies.labels" -}} 38 | helm.sh/chart: {{ include "movies.chart" . }} 39 | app.kubernetes.io/managed-by: {{ .Release.Service }} 40 | app.kubernetes.io/instance: {{ .Release.Name }} 41 | {{- if .Chart.AppVersion }} 42 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 43 | {{- end }} 44 | {{- end -}} 45 | -------------------------------------------------------------------------------- /catalog/chart/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "movies.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | If release name contains chart name it will be used as a full name. 13 | */}} 14 | {{- define "movies.fullname" -}} 15 | {{- if .Values.fullnameOverride -}} 16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 17 | {{- else -}} 18 | {{- $name := default .Chart.Name .Values.nameOverride -}} 19 | {{- if contains $name .Release.Name -}} 20 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 21 | {{- else -}} 22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 23 | {{- end -}} 24 | {{- end -}} 25 | {{- end -}} 26 | 27 | {{/* 28 | Create chart name and version as used by the chart label. 29 | */}} 30 | {{- define "movies.chart" -}} 31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 32 | {{- end -}} 33 | 34 | {{/* 35 | Common labels 36 | */}} 37 | {{- define "movies.labels" -}} 38 | helm.sh/chart: {{ include "movies.chart" . }} 39 | app.kubernetes.io/managed-by: {{ .Release.Service }} 40 | app.kubernetes.io/instance: {{ .Release.Name }} 41 | {{- if .Chart.AppVersion }} 42 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 43 | {{- end }} 44 | {{- end -}} 45 | -------------------------------------------------------------------------------- /frontend/chart/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "movies.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | If release name contains chart name it will be used as a full name. 13 | */}} 14 | {{- define "movies.fullname" -}} 15 | {{- if .Values.fullnameOverride -}} 16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 17 | {{- else -}} 18 | {{- $name := default .Chart.Name .Values.nameOverride -}} 19 | {{- if contains $name .Release.Name -}} 20 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 21 | {{- else -}} 22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 23 | {{- end -}} 24 | {{- end -}} 25 | {{- end -}} 26 | 27 | {{/* 28 | Create chart name and version as used by the chart label. 29 | */}} 30 | {{- define "movies.chart" -}} 31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 32 | {{- end -}} 33 | 34 | {{/* 35 | Common labels 36 | */}} 37 | {{- define "movies.labels" -}} 38 | helm.sh/chart: {{ include "movies.chart" . }} 39 | app.kubernetes.io/managed-by: {{ .Release.Service }} 40 | app.kubernetes.io/instance: {{ .Release.Name }} 41 | {{- if .Chart.AppVersion }} 42 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 43 | {{- end }} 44 | {{- end -}} 45 | -------------------------------------------------------------------------------- /worker/chart/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "movies.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | If release name contains chart name it will be used as a full name. 13 | */}} 14 | {{- define "movies.fullname" -}} 15 | {{- if .Values.fullnameOverride -}} 16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 17 | {{- else -}} 18 | {{- $name := default .Chart.Name .Values.nameOverride -}} 19 | {{- if contains $name .Release.Name -}} 20 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 21 | {{- else -}} 22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 23 | {{- end -}} 24 | {{- end -}} 25 | {{- end -}} 26 | 27 | {{/* 28 | Create chart name and version as used by the chart label. 29 | */}} 30 | {{- define "movies.chart" -}} 31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 32 | {{- end -}} 33 | 34 | {{/* 35 | Common labels 36 | */}} 37 | {{- define "movies.labels" -}} 38 | helm.sh/chart: {{ include "movies.chart" . }} 39 | app.kubernetes.io/managed-by: {{ .Release.Service }} 40 | app.kubernetes.io/instance: {{ .Release.Name }} 41 | {{- if .Chart.AppVersion }} 42 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 43 | {{- end }} 44 | {{- end -}} 45 | -------------------------------------------------------------------------------- /docs/demo-with-volume-snapshot.md: -------------------------------------------------------------------------------- 1 | ## Preview demo using a database snapshot 2 | 3 | This will demonstrate how to use [Volume Snapshots](https://www.okteto.com/docs/enterprise/administration/volume-snapshots/) with Okteto. The PostgreSQL database has fake "production" user data in it that is loaded on startup. We can configure the preview environments to skip loading that data and use a snapshot with data for development instead. 4 | 5 | ### Pre-Requisites 6 | 7 | - The Okteto cluster is prepared for Volume Snapshots, and the feature is enabled. See [Volume Snapshots documentation](https://www.okteto.com/docs/enterprise/administration/volume-snapshots/). 8 | - A VolumeSnapshot of the postgresql has been created. See [Creating a test database snapshot](creating-db-snapshot.md) for an example. 9 | 10 | ### Update preview YAML to use a snapshot 11 | 12 | - Create a branch 13 | - Make two modifications to `.github/workflows/preview.yaml`: 14 | 15 | - specify the `okteto-with-volumes.yaml` file for deployment 16 | - include variables to skip data loading, specify the custom db snapshot and namespace 17 | 18 | Example: 19 | 20 | ```diff 21 | name: pr-${{ github.event.number }}-cindylopez 22 | scope: global 23 | - 24 | + file: "okteto-with-volumes.yaml" 25 | + variables: "API_LOAD_DATA=false,DB_SNAPSHOT_NAME=dbdata-snapshot,DB_SNAPSHOT_NAMESPACE=movies-test" 26 | ``` 27 | 28 | - Push this change and show how the API container did not load the data 29 | - Show the data in the database is from the snapshot, and not the fake "production" data 30 | -------------------------------------------------------------------------------- /catalog/load.js: -------------------------------------------------------------------------------- 1 | const { MongoClient } = require("mongodb"); 2 | 3 | const url = process.env.MONGODB_USERNAME && process.env.MONGODB_PASSWORD 4 | ? `mongodb://${process.env.MONGODB_USERNAME}:${encodeURIComponent(process.env.MONGODB_PASSWORD)}@${process.env.MONGODB_HOST}:27017/${process.env.MONGODB_DATABASE}?authSource=admin` 5 | : `mongodb://${process.env.MONGODB_HOST}:27017/${process.env.MONGODB_DATABASE}`; 6 | 7 | async function insertData(collection, dataPath) { 8 | const data = require(dataPath); 9 | data.results.forEach((doc) => { 10 | doc._id = doc.id; 11 | }); 12 | 13 | try { 14 | await collection.insertMany(data.results); 15 | console.log(`Inserted ${data.results.length} documents`); 16 | } catch (err) { 17 | if (err.code !== 11000) { 18 | throw err; 19 | } 20 | console.log('Documents already exist, skipping insertion'); 21 | } 22 | } 23 | 24 | async function loadWithRetry() { 25 | const client = new MongoClient(url, { 26 | connectTimeoutMS: 30000, 27 | socketTimeoutMS: 30000, 28 | serverSelectionTimeoutMS: 30000, 29 | }); 30 | 31 | try { 32 | console.log('Connecting to MongoDB...'); 33 | await client.connect(); 34 | console.log('Connected successfully'); 35 | 36 | const db = client.db(process.env.MONGODB_DATABASE); 37 | await insertData(db.collection('catalog'), "./data/catalog.json"); 38 | 39 | console.log('All data loaded successfully'); 40 | await client.close(); 41 | process.exit(0); 42 | } catch (err) { 43 | console.error(`Error: ${err}`); 44 | await client.close(); 45 | 46 | console.log('Retrying in 3 seconds...'); 47 | setTimeout(loadWithRetry, 3000); 48 | } 49 | } 50 | 51 | loadWithRetry(); 52 | -------------------------------------------------------------------------------- /infrastructure/chart/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "infrastructure.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "infrastructure.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "infrastructure.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "infrastructure.labels" -}} 37 | helm.sh/chart: {{ include "infrastructure.chart" . }} 38 | {{ include "infrastructure.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "infrastructure.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "infrastructure.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} -------------------------------------------------------------------------------- /catalog/chart/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: catalog 5 | labels: 6 | {{- include "movies.labels" . | nindent 4 }} 7 | app.kubernetes.io/component: catalog 8 | spec: 9 | replicas: {{ .Values.replicaCount }} 10 | selector: 11 | matchLabels: 12 | app.kubernetes.io/name: catalog 13 | app.kubernetes.io/instance: {{ .Release.Name }} 14 | template: 15 | metadata: 16 | labels: 17 | app.kubernetes.io/name: catalog 18 | app.kubernetes.io/instance: {{ .Release.Name }} 19 | spec: 20 | terminationGracePeriodSeconds: 0 21 | initContainers: 22 | - name: load-data 23 | image: {{ .Values.image }} 24 | command: 25 | - yarn 26 | - load 27 | env: 28 | - name: MONGODB_USERNAME 29 | value: "okteto" 30 | - name: MONGODB_PASSWORD 31 | value: "okteto" 32 | - name: MONGODB_DATABASE 33 | value: okteto 34 | - name: MONGODB_HOST 35 | value: mongodb 36 | containers: 37 | - name: catalog 38 | image: {{ .Values.image }} 39 | env: 40 | - name: MONGODB_USERNAME 41 | value: "okteto" 42 | - name: MONGODB_PASSWORD 43 | value: "okteto" 44 | - name: MONGODB_DATABASE 45 | value: okteto 46 | - name: MONGODB_HOST 47 | value: mongodb 48 | ports: 49 | - name: http 50 | containerPort: 8080 51 | protocol: TCP 52 | livenessProbe: 53 | periodSeconds: 1 54 | httpGet: 55 | path: /catalog/healthz 56 | port: 8080 57 | readinessProbe: 58 | periodSeconds: 1 59 | httpGet: 60 | path: /catalog/healthz 61 | port: 8080 62 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | /*** 2 | The new CSS reset - version 1.5.1 (last updated 1.3.2022) 3 | GitHub page: https://github.com/elad2412/the-new-css-reset 4 | ***/ 5 | 6 | /* 7 | Remove all the styles of the "User-Agent-Stylesheet", except for the 'display' property 8 | - The "symbol *" part is to solve Firefox SVG sprite bug 9 | */ 10 | *:where(:not(iframe, canvas, img, svg, video):not(svg *, symbol *)) { 11 | all: unset; 12 | display: revert; 13 | } 14 | 15 | /* Preferred box-sizing value */ 16 | *, 17 | *::before, 18 | *::after { 19 | box-sizing: border-box; 20 | } 21 | 22 | /* Reapply the pointer cursor for anchor tags */ 23 | a, button { 24 | cursor: revert; 25 | } 26 | 27 | /* Remove list styles (bullets/numbers) */ 28 | ol, ul, menu { 29 | list-style: none; 30 | } 31 | 32 | /* For images to not be able to exceed their container */ 33 | img { 34 | max-width: 100%; 35 | } 36 | 37 | /* removes spacing between cells in tables */ 38 | table { 39 | border-collapse: collapse; 40 | } 41 | 42 | /* revert the 'white-space' property for textarea elements on Safari */ 43 | textarea { 44 | white-space: revert; 45 | } 46 | 47 | /* minimum style to allow to style meter element */ 48 | meter { 49 | -webkit-appearance: revert; 50 | appearance: revert; 51 | } 52 | 53 | /* reset default text opacity of input placeholder */ 54 | ::placeholder { 55 | color: unset; 56 | } 57 | 58 | /* fix the feature of 'hidden' attribute. 59 | display:revert; revert to element instead of attribute */ 60 | :where([hidden]) { 61 | display: none; 62 | } 63 | 64 | /* revert for bug in Chromium browsers 65 | - fix for the content editable attribute will work properly. */ 66 | :where([contenteditable]) { 67 | -moz-user-modify: read-write; 68 | -webkit-user-modify: read-write; 69 | overflow-wrap: break-word; 70 | -webkit-line-break: after-white-space; 71 | } 72 | 73 | /* apply back the draggable feature - exist only in Chromium and Safari */ 74 | :where([draggable="true"]) { 75 | -webkit-user-drag: element; 76 | } -------------------------------------------------------------------------------- /rent/chart/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "rent.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "rent.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "rent.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "rent.labels" -}} 37 | helm.sh/chart: {{ include "rent.chart" . }} 38 | {{ include "rent.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "rent.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "rent.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "rent.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "rent.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /frontend/src/Users.css: -------------------------------------------------------------------------------- 1 | .Table { 2 | margin: 20px; 3 | overflow: auto; 4 | } 5 | 6 | .Table { 7 | padding: 20px; 8 | } 9 | 10 | .Table > * { 11 | margin: 8px; 12 | } 13 | 14 | .Table h5 { 15 | text-align: right; 16 | } 17 | 18 | .Table__table { 19 | border-spacing: 1; 20 | border-collapse: collapse; 21 | border-radius: 12px; 22 | overflow: hidden; 23 | width: 100%; 24 | margin: 0 auto; 25 | position: relative; 26 | min-width: 1024px; 27 | } 28 | 29 | .Table__header { 30 | padding: 20px; 31 | } 32 | .Table__data { 33 | padding: 12px; 34 | vertical-align: middle; 35 | } 36 | 37 | .Table__head .Table__row { 38 | height: 60px; 39 | background: #2a2332; 40 | font-size: 16px; 41 | font-weight: bold; 42 | } 43 | 44 | .Table__body .Table__row { 45 | color: black; 46 | height: 48px; 47 | border-bottom: 1px solid #ececec; 48 | background: white; 49 | } 50 | 51 | .Table__body .Table__row:hover { 52 | background-color: #ececec; 53 | } 54 | 55 | .Pagination__wrapper { 56 | margin: 10px; 57 | padding: 10px 24px; 58 | } 59 | 60 | .Pagination__list { 61 | display: flex; 62 | flex-direction: row; 63 | align-items: center; 64 | justify-content: center; 65 | } 66 | 67 | .Pagination__list li { 68 | margin: 2px 4px; 69 | display: flex; 70 | justify-content: center; 71 | align-items: center; 72 | } 73 | 74 | .Pagination__list li:hover { 75 | cursor: pointer; 76 | text-decoration: underline; 77 | } 78 | 79 | .Pagination__item-nav.disabled { 80 | pointer-events: none; 81 | } 82 | 83 | .Pagination__item-number { 84 | padding: 8px 13px; 85 | border-radius: 50%; 86 | font-weight: bold; 87 | } 88 | 89 | .Pagination__selected { 90 | border-radius: 50%; 91 | line-height: 1.875em; 92 | width: 30px; 93 | vertical-align: middle; 94 | display: inline-block; 95 | position: relative; 96 | } 97 | 98 | .Pagination__circle { 99 | background-color: red; 100 | position: absolute; 101 | width: 30px; 102 | height: 30px; 103 | border-radius: 20px; 104 | right: 0; 105 | left: 0; 106 | margin: 0 auto; 107 | z-index: -1; 108 | } 109 | 110 | -------------------------------------------------------------------------------- /catalog/data/catalog.json: -------------------------------------------------------------------------------- 1 | { 2 | "results": [ 3 | { 4 | "id": 1, 5 | "vote_average": 5.8, 6 | "original_title": "Moby Dock", 7 | "backdrop_path": "/poster-mobydock.png", 8 | "price": 12.99, 9 | "overview": "This classic story revolves around Captain Pod and his obsession with a huge whale, Moby Dock. The sole survivor of a ship narrates the tale of the captain's obsession." 10 | }, 11 | { 12 | "id": 2, 13 | "vote_average": 6.2, 14 | "original_title": "The Finalizer", 15 | "backdrop_path": "/poster-thefinalizer.png", 16 | "price": 2.99, 17 | "overview": "Robert McNode, who serves an unflinching justice for the exploited and oppressed, embarks on a relentless, globe-trotting quest for vengeance when a long-time girl friend is murdered." 18 | }, 19 | { 20 | "id": 3, 21 | "vote_average": 4.2, 22 | "original_title": "Crash Loop Backoff", 23 | "backdrop_path": "/poster-crashloop.png", 24 | "price": 1.99, 25 | "overview": "Mr. Pod keeps starting, crashing, starting, and then crashing again in an endless loop." 26 | }, 27 | { 28 | "id": 4, 29 | "vote_average": 7.8, 30 | "original_title": "Kube", 31 | "backdrop_path": "/poster-kube.png", 32 | "price": 9.99, 33 | "overview": "Kube is filled with deeper meaning than what meets the surface - it's a philosophical study about human life." 34 | }, 35 | { 36 | "id": 5, 37 | "vote_average": 7.9, 38 | "original_title": "Cloud Atlas", 39 | "backdrop_path": "/poster-cloudatlas.png", 40 | "price": 3.99, 41 | "overview": "An exploration of how the actions of individual lives impact one another in the past, present and future, as one soul is shaped from a killer into a hero, and an act of kindness ripples across centuries to inspire a revolution." 42 | }, 43 | { 44 | "id": 6, 45 | "vote_average": 7.6, 46 | "original_title": "Aliens", 47 | "backdrop_path": "/poster-aliens.png", 48 | "price": 4.99, 49 | "overview": "Fifty-seven years after surviving an apocalyptic attack aboard her space vessel by merciless space creatures, Officer Ripley awakens from hyper-sleep and tries to warn anyone who will listen about the predators." 50 | } 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /okteto.yaml: -------------------------------------------------------------------------------- 1 | icon: https://apps.okteto.com/movies/icon.png 2 | 3 | build: 4 | frontend: 5 | context: frontend 6 | catalog: 7 | context: catalog 8 | rent: 9 | context: rent 10 | api: 11 | context: api 12 | worker: 13 | context: worker 14 | tests: 15 | context: tests 16 | 17 | deploy: 18 | - name: Deploy Infrastructure (PostgreSQL, Kafka, MongoDB) 19 | command: helm upgrade --install infrastructure infrastructure/chart 20 | - name: Deploy Frontend 21 | command: helm upgrade --install frontend frontend/chart --set image=${OKTETO_BUILD_FRONTEND_IMAGE} 22 | - name: Deploy Catalog 23 | command: helm upgrade --install catalog catalog/chart --set image=${OKTETO_BUILD_CATALOG_IMAGE} 24 | - name: Deploy Rent 25 | command: helm upgrade --install rent rent/chart --set image=${OKTETO_BUILD_RENT_IMAGE} 26 | - name: Deploy Worker 27 | command: helm upgrade --install worker worker/chart --set image=${OKTETO_BUILD_WORKER_IMAGE} 28 | - name: Deploy API 29 | command: helm upgrade --install api api/chart --set image=${OKTETO_BUILD_API_IMAGE} --set load=${API_LOAD_DATA:-true} 30 | 31 | dev: 32 | frontend: 33 | image: okteto/node:20 34 | workdir: /usr/src/app 35 | command: bash 36 | sync: 37 | - frontend:/usr/src/app 38 | catalog: 39 | command: yarn start 40 | sync: 41 | - catalog:/src 42 | forward: 43 | - 9229:9229 44 | rent: 45 | command: mvn spring-boot:run 46 | workdir: /app 47 | sync: 48 | - rent:/app 49 | volumes: 50 | - /root/.m2 51 | forward: 52 | - 5005:5005 53 | - 5432:postgresql:5432 54 | api: 55 | image: okteto/golang:1.24 56 | workdir: /usr/src/app 57 | command: bash 58 | securityContext: 59 | capabilities: 60 | add: 61 | - SYS_PTRACE 62 | sync: 63 | - api:/usr/src/app 64 | forward: 65 | - 2346:2345 66 | worker: 67 | image: okteto/golang:1.24 68 | workdir: /usr/src/app 69 | command: bash 70 | securityContext: 71 | capabilities: 72 | add: 73 | - SYS_PTRACE 74 | sync: 75 | - worker:/usr/src/app 76 | forward: 77 | - 2345:2345 78 | test: 79 | e2e: 80 | image: ${OKTETO_BUILD_TESTS_IMAGE} 81 | context: tests 82 | caches: 83 | - yarn/.cache 84 | - node_modules 85 | commands: 86 | - yarn install 87 | - yarn test 88 | artifacts: 89 | - test-results 90 | - playwright-report 91 | -------------------------------------------------------------------------------- /rent/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.3.2 9 | 10 | 11 | com.okteto 12 | rent 13 | 0.0.1-SNAPSHOT 14 | rent 15 | Rent backend service for Movies App 16 | 17 | 21 18 | 19 | 20 | 21 | org.springframework.boot 22 | spring-boot-starter-thymeleaf 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-web 27 | 28 | 29 | org.springframework.kafka 30 | spring-kafka 31 | 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-starter-tomcat 36 | provided 37 | 38 | 39 | org.springframework.boot 40 | spring-boot-starter-test 41 | test 42 | 43 | 44 | org.springframework.kafka 45 | spring-kafka-test 46 | test 47 | 48 | 49 | ognl 50 | ognl 51 | 3.4.3 52 | 53 | 54 | org.springframework.boot 55 | spring-boot-devtools 56 | true 57 | 58 | 59 | 60 | 61 | 62 | 63 | org.springframework.boot 64 | spring-boot-maven-plugin 65 | 66 | 67 | -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /worker/cmd/worker/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "os/signal" 7 | "strconv" 8 | 9 | "fmt" 10 | 11 | _ "github.com/lib/pq" 12 | 13 | kingpin "gopkg.in/alecthomas/kingpin.v2" 14 | 15 | "github.com/Shopify/sarama" 16 | "github.com/okteto/movies/pkg/database" 17 | "github.com/okteto/movies/pkg/kafka" 18 | ) 19 | 20 | var ( 21 | topic = kingpin.Flag("topic", "Topic name").Default("rentals").String() 22 | messageCountStart = kingpin.Flag("messageCountStart", "Message counter start from:").Int() 23 | ) 24 | 25 | func main() { 26 | db := database.Open() 27 | defer db.Close() 28 | 29 | database.Ping(db) 30 | 31 | dropTableStmt := `DROP TABLE IF EXISTS rentals` 32 | if _, err := db.Exec(dropTableStmt); err != nil { 33 | log.Panic(err) 34 | } 35 | 36 | createTableStmt := `CREATE TABLE IF NOT EXISTS rentals (id VARCHAR(255) NOT NULL UNIQUE, price VARCHAR(255) NOT NULL)` 37 | if _, err := db.Exec(createTableStmt); err != nil { 38 | log.Panic(err) 39 | } 40 | 41 | master := kafka.GetMaster() 42 | defer master.Close() 43 | 44 | // Consumer for "rentals" topic 45 | consumerRentals, err := master.ConsumePartition("rentals", 0, sarama.OffsetNewest) 46 | if err != nil { 47 | log.Panic(err) 48 | } 49 | 50 | // Consumer for "returns" topic 51 | consumerReturns, err := master.ConsumePartition("returns", 0, sarama.OffsetNewest) 52 | if err != nil { 53 | log.Panic(err) 54 | } 55 | 56 | signals := make(chan os.Signal, 1) 57 | signal.Notify(signals, os.Interrupt) 58 | doneCh := make(chan struct{}) 59 | 60 | go func() { 61 | for { 62 | select { 63 | case err := <-consumerRentals.Errors(): 64 | fmt.Println(err) 65 | case msg := <-consumerRentals.Messages(): 66 | *messageCountStart++ 67 | fmt.Printf("Received message: movies %s price %s\n", string(msg.Key), string(msg.Value)) 68 | price, _ := strconv.ParseFloat(string(msg.Value), 64) 69 | insertDynStmt := `insert into "rentals"("id", "price") values($1, $2) on conflict(id) do update set price = $2` 70 | if _, err := db.Exec(insertDynStmt, string(msg.Key), fmt.Sprintf("%f", price)); err != nil { 71 | log.Panic(err) 72 | } 73 | case msg := <-consumerReturns.Messages(): 74 | catalogID := string(msg.Value) 75 | fmt.Printf("Received return message: catalogID %s\n", catalogID) 76 | deleteStmt := `DELETE FROM rentals WHERE id = $1` 77 | if _, err := db.Exec(deleteStmt, catalogID); err != nil { 78 | log.Panic(err) 79 | } 80 | case <-signals: 81 | fmt.Println("Interrupt is detected") 82 | doneCh <- struct{}{} 83 | } 84 | } 85 | }() 86 | <-doneCh 87 | log.Println("Processed", *messageCountStart, "messages") 88 | } 89 | -------------------------------------------------------------------------------- /infrastructure/chart/templates/postgresql-deployment.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.postgresql.enabled }} 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: postgresql 6 | labels: 7 | app: postgresql 8 | chart: {{ include "infrastructure.chart" . }} 9 | release: {{ .Release.Name }} 10 | heritage: {{ .Release.Service }} 11 | spec: 12 | replicas: 1 13 | selector: 14 | matchLabels: 15 | app: postgresql 16 | release: {{ .Release.Name }} 17 | template: 18 | metadata: 19 | labels: 20 | app: postgresql 21 | release: {{ .Release.Name }} 22 | spec: 23 | containers: 24 | - name: postgresql 25 | image: "{{ .Values.postgresql.image.repository }}:{{ .Values.postgresql.image.tag }}" 26 | ports: 27 | - name: postgresql 28 | containerPort: {{ .Values.postgresql.service.port }} 29 | protocol: TCP 30 | env: 31 | - name: POSTGRES_PASSWORD 32 | value: "{{ .Values.postgresql.auth.postgresPassword }}" 33 | - name: POSTGRES_DB 34 | value: "{{ .Values.postgresql.auth.database }}" 35 | - name: POSTGRES_USER 36 | value: "{{ .Values.postgresql.auth.username }}" 37 | volumeMounts: 38 | - name: data 39 | mountPath: /var/lib/postgresql/data 40 | subPath: postgres 41 | resources: 42 | limits: 43 | cpu: 500m 44 | memory: 512Mi 45 | requests: 46 | cpu: 100m 47 | memory: 256Mi 48 | livenessProbe: 49 | exec: 50 | command: 51 | - /bin/sh 52 | - -c 53 | - pg_isready -U {{ .Values.postgresql.auth.username }} -h localhost 54 | initialDelaySeconds: 30 55 | periodSeconds: 10 56 | readinessProbe: 57 | exec: 58 | command: 59 | - /bin/sh 60 | - -c 61 | - pg_isready -U {{ .Values.postgresql.auth.username }} -h localhost 62 | initialDelaySeconds: 5 63 | periodSeconds: 5 64 | volumes: 65 | - name: data 66 | persistentVolumeClaim: 67 | claimName: postgresql-pvc 68 | --- 69 | apiVersion: v1 70 | kind: PersistentVolumeClaim 71 | metadata: 72 | name: postgresql-pvc 73 | labels: 74 | app: postgresql 75 | chart: {{ include "infrastructure.chart" . }} 76 | release: {{ .Release.Name }} 77 | heritage: {{ .Release.Service }} 78 | spec: 79 | accessModes: 80 | - ReadWriteOnce 81 | resources: 82 | requests: 83 | storage: {{ .Values.postgresql.storage.size }} 84 | {{- if .Values.postgresql.storage.storageClass }} 85 | storageClassName: {{ .Values.postgresql.storage.storageClass }} 86 | {{- end }} 87 | {{- end }} -------------------------------------------------------------------------------- /api/data/README.md: -------------------------------------------------------------------------------- 1 | # Generating user data with Mimesis 2 | 3 | The `users.json` data was generated with [Mimesis](https://mimesis.name/en/master/index.html) and a dataset of north american city/state/zipcodes using the following process. 4 | 5 | ### Setup 6 | 7 | https://mimesis.name/en/master/getting_started.html 8 | 9 | - Activate Python virtualenv, and install Mimesis: 10 | 11 | ``` 12 | (env) $ pip install mimesis 13 | ``` 14 | 15 | - Download and extract dataset: https://github.com/djbelieny/geoinfo-dataset/blob/master/unique_zipcodes_csv.zip 16 | 17 | ### Create and run python script 18 | 19 | - `user_generate.py`: 20 | 21 | ```python 22 | from mimesis import Person 23 | from mimesis.locales import Locale 24 | from mimesis.enums import Gender 25 | 26 | import random 27 | import csv 28 | import json 29 | 30 | genders = ['female', 'male', 'nonbinary', 'fluid'] 31 | gender_distribution = [48, 48, 1, 1] # Sample distribution, adjust to represent your population 32 | 33 | people_genders = random.choices(genders, weights=gender_distribution, k=10000) 34 | person = Person(Locale.EN) 35 | 36 | with open('unique_zipcodes_csv.csv') as zipcodes: 37 | csv_rdr = csv.reader(zipcodes) 38 | # Data is from https://github.com/djbelieny/geoinfo-dataset 39 | # Raw data headers 40 | #"city","state","stateISO","country","countryISO","zipCode" 41 | city_state_zipcode = [ 42 | [ 43 | row[0], 44 | row[1], 45 | row[5] 46 | ] 47 | for row in csv_rdr] 48 | 49 | users = [] 50 | inc = 0 51 | for g in people_genders: 52 | if g == 'female': 53 | first_name = person.first_name(gender=Gender.FEMALE) 54 | last_name = person.last_name(gender=Gender.FEMALE) 55 | elif g == 'male': 56 | first_name = person.first_name(gender=Gender.MALE) 57 | last_name = person.last_name(gender=Gender.MALE) 58 | else: 59 | first_name = person.first_name() 60 | last_name = person.last_name() 61 | city, state, zip_code = city_state_zipcode[random.randint(1,len(city_state_zipcode)-1)] 62 | 63 | telephone = person.telephone(mask="###-###-####") 64 | inc += 1 65 | 66 | users.append({ 67 | "userId": inc, 68 | "firstName": first_name, 69 | "lastName": last_name, 70 | "phone": telephone, 71 | "city": city, 72 | "state": state, 73 | "zip": zip_code, 74 | "age": random.randint(13, 109), 75 | "gender": g 76 | }) 77 | 78 | 79 | print(json.dumps(users, indent=4)) 80 | ``` 81 | 82 | - Run Script 83 | 84 | ``` 85 | (env) $ python user_generate.py > users.json 86 | 87 | ``` 88 | -------------------------------------------------------------------------------- /docs/creating-db-snapshot.md: -------------------------------------------------------------------------------- 1 | # Creating a database snapshot 2 | 3 | ### Pre-requisite 4 | 5 | You must first prepare the Okteto cluster and enable Volume Snapshots. See the official documention for [Volume Snapshots](https://www.okteto.com/docs/enterprise/administration/volume-snapshots/) for more information. 6 | 7 | ### Prepare data 8 | 9 | Before creating a database snapshot you will need to create an instance of the database and modify the data for use in dev environments. This could include adding or removing data needed for tests, or removing sensitive data from the database to make it safe to use in development environments. 10 | 11 | 1. Create a namespace to work in while creating the source of the snapshot: 12 | 13 | ``` 14 | $ okteto namespace create movies-source 15 | ``` 16 | 17 | 2. Deploy the environment with `okteto deploy` 18 | 3. Create a port-foward to the database service 19 | 20 | ``` 21 | $ kubectl -n movies-source port-forward service/postgresql 5432:5432 22 | ``` 23 | 24 | 4. In another terminal, connect to database using psql: 25 | 26 | ``` 27 | $ psql -h localhost -p 5432 -U okteto -d rentals 28 | Password for user okteto: #The password is "okteto" 29 | psql (14.5 (Ubuntu 14.5-0ubuntu0.22.04.1), server 14.4) 30 | Type "help" for help. 31 | 32 | rentals=> 33 | ``` 34 | 35 | 5. Modify the data as needed for your purposes. In this example, we'll just update the last-name column to a static value but you can make any needed modifications here. 36 | 37 | ``` 38 | rentals=> UPDATE users SET last_name='testing-snapshot'; 39 | ``` 40 | 41 | ### Create VolumeSnapshot 42 | 43 | This example YAML will use the source PersistentVolumeClaim for the postgresql service (`data-postgresql-0`) to create a VolumeSnapshot called "dbdata-snapshot" in the "movies-source" namespace. 44 | 45 | `snapshot.yml`: 46 | 47 | ```yml 48 | apiVersion: snapshot.storage.k8s.io/v1beta1 49 | kind: VolumeSnapshot 50 | metadata: 51 | namespace: movies-source 52 | name: dbdata-snapshot 53 | spec: 54 | volumeSnapshotClassName: okteto-snapshot-class 55 | source: 56 | persistentVolumeClaimName: data-postgresql-0 57 | ``` 58 | 59 | 1. Apply the snapshot YAML to the cluster with the modified database you want to act as a source for your snapshot: 60 | 61 | ``` 62 | $ kubectl apply -f snapshot.yml 63 | ``` 64 | 65 | 2. Confirm that the VolumeSnapshot exists and that READYTOUSE is true: 66 | 67 | ``` 68 | $ kubectl -n movies-source get VolumeSnapshot 69 | NAME READYTOUSE SOURCEPVC SOURCESNAPSHOTCONTENT RESTORESIZE SNAPSHOTCLASS SNAPSHOTCONTENT CREATIONTIME AGE 70 | dbdata-snapshot true data-postgresql-0 1Gi okteto-snapshot-class snapcontent-67f7c5cb-658f-49fc-876d-bb16b7aa38ca 4d 4d 71 | ``` 72 | 73 | Your snapshot is now ready to be used as a source for development or preview environments. 74 | -------------------------------------------------------------------------------- /frontend/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { DefinePlugin } = require('webpack'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const CopyPlugin = require('copy-webpack-plugin'); 5 | 6 | const srcPath = path.join(__dirname, 'src'); 7 | const distPath = path.join(__dirname, 'dist'); 8 | 9 | module.exports = (_, argv) => { 10 | const mode = argv.mode ?? 'production'; 11 | const includeSourceMap = mode === 'production' ? 'hidden-nosources-source-map' : 'source-map'; 12 | return { 13 | context: srcPath, 14 | mode, 15 | target: 'web', 16 | entry: ['./index.jsx'], 17 | output: { 18 | filename: 'app.[contenthash].js', 19 | path: distPath, 20 | publicPath: '/' 21 | }, 22 | resolve: { 23 | extensions: ['.js', '.jsx', '.css'], 24 | modules: [ 25 | path.resolve(path.join(__dirname, '/node_modules')), 26 | path.resolve(srcPath) 27 | ], 28 | alias: { 29 | 'react-dom': '@hot-loader/react-dom' 30 | } 31 | }, 32 | module: { 33 | rules: [{ 34 | test: /\.(js|jsx)$/i, 35 | include: srcPath, 36 | use: [ 37 | { 38 | loader: 'babel-loader', 39 | options: { 40 | cacheDirectory: true 41 | } 42 | } 43 | ] 44 | }, { 45 | test: /\.css$/i, 46 | include: srcPath, 47 | use: ['style-loader', 'css-loader'] 48 | }, { 49 | test: /\.(png|jpg|gif)$/i, 50 | use: [ 51 | { 52 | loader: 'url-loader', 53 | options: { 54 | limit: 8192, 55 | }, 56 | }, 57 | ], 58 | }] 59 | }, 60 | plugins: [ 61 | new HtmlWebpackPlugin({ 62 | template: './index.html', 63 | favicon: path.join(srcPath, 'assets/images/favicon.png') 64 | }), 65 | new CopyPlugin({ 66 | patterns: [ 67 | { from: './static', to: distPath }, 68 | ], 69 | }), 70 | new DefinePlugin({ 71 | MODE: JSON.stringify(mode), 72 | }) 73 | ], 74 | devServer: { 75 | historyApiFallback: true, 76 | port: 80, 77 | host: '0.0.0.0', 78 | hot: true, 79 | allowedHosts: 'all', 80 | client: { 81 | webSocketTransport: 'ws' 82 | }, 83 | webSocketServer: 'ws', 84 | proxy: { 85 | '/rent': 'http://rentals:8080/rent', 86 | '/rentals': 'http://rentals:8080/rentals', 87 | '/catalog': 'http://catalog:8080' 88 | }, 89 | client: { 90 | webSocketURL: { 91 | port: 443 92 | }, 93 | }, 94 | watchFiles: { 95 | options: { 96 | usePolling: true, 97 | }, 98 | }, 99 | }, 100 | devtool: includeSourceMap, 101 | cache: { 102 | type: 'filesystem', 103 | buildDependencies: { 104 | config: [__filename] 105 | } 106 | } 107 | }; 108 | }; 109 | -------------------------------------------------------------------------------- /okteto-with-volumes.yaml: -------------------------------------------------------------------------------- 1 | icon: https://apps.okteto.com/movies/icon.png 2 | 3 | build: 4 | frontend: 5 | context: frontend 6 | frontend-dev: 7 | context: frontend 8 | target: dev 9 | catalog: 10 | context: catalog 11 | rent: 12 | context: rent 13 | api: 14 | context: api 15 | api-dev: 16 | context: api 17 | target: dev 18 | worker: 19 | context: worker 20 | worker-dev: 21 | context: worker 22 | target: dev 23 | 24 | deploy: 25 | - name: Deploy PostgreSQL 26 | command: | 27 | # If we get a snapshot name passed into the deployment we will add two annotations to the persistent volume for PostgreSQL. 28 | # These annotations tell Okteto to create the volume from an existing snapshot. 29 | # Details in https://www.okteto.com/docs/self-hosted/administration/volume-snapshots/#consuming-a-volume-snapshot-created-in-kubernetes 30 | if [ -z $DB_SNAPSHOT_NAME ]; then 31 | helm upgrade --install postgresql postgresql/postgresql-11.6.21.tgz -f postgresql/values.yml --version 11.6.21 32 | else 33 | helm upgrade --install postgresql postgresql/postgresql-11.6.21.tgz \ 34 | -f postgresql/values.yml \ 35 | --version 11.6.21 \ 36 | --set primary.persistence.annotations."dev\.okteto\.com/from-snapshot-name"="$DB_SNAPSHOT_NAME" \ 37 | --set primary.persistence.annotations."dev\.okteto\.com/from-snapshot-namespace"="$DB_SNAPSHOT_NAMESPACE" 38 | fi 39 | - name: Deploy Kafka 40 | command: helm upgrade --install kafka kafka/kafka-18.0.3.tgz -f kafka/values.yml --version 18.0.3 41 | - name: Deploy MongoDB 42 | command: helm upgrade --install mongodb mongodb/mongodb-12.1.30.tgz -f mongodb/values.yml --version 12.1.30 43 | - name: Deploy Frontend 44 | command: helm upgrade --install frontend frontend/chart --set image=${OKTETO_BUILD_FRONTEND_IMAGE} 45 | - name: Deploy Catalog 46 | command: helm upgrade --install catalog catalog/chart --set image=${OKTETO_BUILD_CATALOG_IMAGE} 47 | - name: Deploy Rent 48 | command: helm upgrade --install rent rent/chart --set image=${OKTETO_BUILD_RENT_IMAGE} 49 | - name: Deploy Worker 50 | command: helm upgrade --install worker worker/chart --set image=${OKTETO_BUILD_WORKER_IMAGE} 51 | - name: Deploy API 52 | command: helm upgrade --install api api/chart --set image=${OKTETO_BUILD_API_IMAGE} --set load=${API_LOAD_DATA:-true} 53 | 54 | dev: 55 | frontend: 56 | image: ${OKTETO_BUILD_FRONTEND_DEV_IMAGE} 57 | command: bash 58 | sync: 59 | - frontend:/usr/src/app 60 | catalog: 61 | command: yarn start 62 | sync: 63 | - catalog:/src 64 | forward: 65 | - 9229:9229 66 | rent: 67 | command: mvn spring-boot:run 68 | sync: 69 | - rent:/app 70 | volumes: 71 | - /root/.m2 72 | forward: 73 | - 5005:5005 74 | api: 75 | image: ${OKTETO_BUILD_API_DEV_IMAGE} 76 | command: bash 77 | securityContext: 78 | capabilities: 79 | add: 80 | - SYS_PTRACE 81 | sync: 82 | - api:/usr/src/app 83 | forward: 84 | - 2346:2345 85 | worker: 86 | image: ${OKTETO_BUILD_WORKER_DEV_IMAGE} 87 | command: bash 88 | securityContext: 89 | capabilities: 90 | add: 91 | - SYS_PTRACE 92 | sync: 93 | - worker:/usr/src/app 94 | forward: 95 | - 2345:2345 96 | -------------------------------------------------------------------------------- /infrastructure/chart/templates/kafka-deployment.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.kafka.enabled }} 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: kafka 6 | labels: 7 | app: kafka 8 | chart: {{ include "infrastructure.chart" . }} 9 | release: {{ .Release.Name }} 10 | heritage: {{ .Release.Service }} 11 | spec: 12 | replicas: 1 13 | selector: 14 | matchLabels: 15 | app: kafka 16 | release: {{ .Release.Name }} 17 | template: 18 | metadata: 19 | labels: 20 | app: kafka 21 | release: {{ .Release.Name }} 22 | spec: 23 | containers: 24 | - name: kafka 25 | image: "{{ .Values.kafka.image.repository }}:{{ .Values.kafka.image.tag }}" 26 | ports: 27 | - name: kafka 28 | containerPort: {{ .Values.kafka.service.port }} 29 | protocol: TCP 30 | env: 31 | - name: KAFKA_NODE_ID 32 | value: "1" 33 | - name: KAFKA_PROCESS_ROLES 34 | value: "broker,controller" 35 | - name: KAFKA_LISTENERS 36 | value: "PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093" 37 | - name: KAFKA_ADVERTISED_LISTENERS 38 | value: "PLAINTEXT://kafka:9092" 39 | - name: KAFKA_CONTROLLER_LISTENER_NAMES 40 | value: "CONTROLLER" 41 | - name: KAFKA_LISTENER_SECURITY_PROTOCOL_MAP 42 | value: "CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT" 43 | - name: KAFKA_CONTROLLER_QUORUM_VOTERS 44 | value: "1@0.0.0.0:9093" 45 | - name: KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR 46 | value: "1" 47 | - name: KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR 48 | value: "1" 49 | - name: KAFKA_TRANSACTION_STATE_LOG_MIN_ISR 50 | value: "1" 51 | - name: KAFKA_LOG_DIRS 52 | value: "/opt/kafka/logs" 53 | - name: CLUSTER_ID 54 | value: "MkU3OEVBNTcwNTJENDM2Qk" 55 | volumeMounts: 56 | - name: kafka-logs 57 | mountPath: /opt/kafka/logs 58 | resources: 59 | limits: 60 | cpu: 1 61 | memory: 1024Mi 62 | requests: 63 | cpu: 250m 64 | memory: 512Mi 65 | livenessProbe: 66 | tcpSocket: 67 | port: 9092 68 | initialDelaySeconds: 60 69 | periodSeconds: 30 70 | readinessProbe: 71 | tcpSocket: 72 | port: 9092 73 | initialDelaySeconds: 30 74 | periodSeconds: 10 75 | volumes: 76 | - name: data 77 | persistentVolumeClaim: 78 | claimName: kafka-pvc 79 | - name: kafka-logs 80 | emptyDir: {} 81 | --- 82 | apiVersion: v1 83 | kind: PersistentVolumeClaim 84 | metadata: 85 | name: kafka-pvc 86 | labels: 87 | app: kafka 88 | chart: {{ include "infrastructure.chart" . }} 89 | release: {{ .Release.Name }} 90 | heritage: {{ .Release.Service }} 91 | spec: 92 | accessModes: 93 | - ReadWriteOnce 94 | resources: 95 | requests: 96 | storage: {{ .Values.kafka.storage.size }} 97 | {{- if .Values.kafka.storage.storageClass }} 98 | storageClassName: {{ .Values.kafka.storage.storageClass }} 99 | {{- end }} 100 | {{- end }} 101 | -------------------------------------------------------------------------------- /infrastructure/chart/templates/mongodb-deployment.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.mongodb.enabled }} 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: mongodb 6 | labels: 7 | app: mongodb 8 | chart: {{ include "infrastructure.chart" . }} 9 | release: {{ .Release.Name }} 10 | heritage: {{ .Release.Service }} 11 | spec: 12 | replicas: 1 13 | selector: 14 | matchLabels: 15 | app: mongodb 16 | release: {{ .Release.Name }} 17 | template: 18 | metadata: 19 | labels: 20 | app: mongodb 21 | release: {{ .Release.Name }} 22 | spec: 23 | containers: 24 | - name: mongodb 25 | image: "{{ .Values.mongodb.image.repository }}:{{ .Values.mongodb.image.tag }}" 26 | ports: 27 | - name: mongodb 28 | containerPort: {{ .Values.mongodb.service.port }} 29 | protocol: TCP 30 | {{- if .Values.mongodb.auth.enabled }} 31 | env: 32 | - name: MONGO_INITDB_DATABASE 33 | value: "{{ .Values.mongodb.auth.database }}" 34 | - name: MONGO_INITDB_ROOT_USERNAME 35 | value: "{{ .Values.mongodb.auth.username }}" 36 | - name: MONGO_INITDB_ROOT_PASSWORD 37 | value: "{{ .Values.mongodb.auth.password }}" 38 | {{- end }} 39 | volumeMounts: 40 | - name: data 41 | mountPath: /data/db 42 | subPath: mongodb 43 | resources: 44 | limits: 45 | cpu: 500m 46 | memory: 512Mi 47 | requests: 48 | cpu: 100m 49 | memory: 256Mi 50 | livenessProbe: 51 | exec: 52 | command: 53 | - mongosh 54 | {{- if .Values.mongodb.auth.enabled }} 55 | - --username 56 | - "{{ .Values.mongodb.auth.username }}" 57 | - --password 58 | - "{{ .Values.mongodb.auth.password }}" 59 | {{- end }} 60 | - --eval 61 | - "db.adminCommand('ping')" 62 | initialDelaySeconds: 30 63 | periodSeconds: 10 64 | timeoutSeconds: 5 65 | readinessProbe: 66 | exec: 67 | command: 68 | - mongosh 69 | {{- if .Values.mongodb.auth.enabled }} 70 | - --username 71 | - "{{ .Values.mongodb.auth.username }}" 72 | - --password 73 | - "{{ .Values.mongodb.auth.password }}" 74 | {{- end }} 75 | - --eval 76 | - "db.adminCommand('ping')" 77 | initialDelaySeconds: 5 78 | periodSeconds: 5 79 | timeoutSeconds: 5 80 | volumes: 81 | - name: data 82 | persistentVolumeClaim: 83 | claimName: mongodb-pvc 84 | --- 85 | apiVersion: v1 86 | kind: PersistentVolumeClaim 87 | metadata: 88 | name: mongodb-pvc 89 | labels: 90 | app: mongodb 91 | chart: {{ include "infrastructure.chart" . }} 92 | release: {{ .Release.Name }} 93 | heritage: {{ .Release.Service }} 94 | spec: 95 | accessModes: 96 | - ReadWriteOnce 97 | resources: 98 | requests: 99 | storage: {{ .Values.mongodb.storage.size }} 100 | {{- if .Values.mongodb.storage.storageClass }} 101 | storageClassName: {{ .Values.mongodb.storage.storageClass }} 102 | {{- end }} 103 | {{- end }} -------------------------------------------------------------------------------- /rent/src/main/java/com/okteto/rent/controller/RentController.java: -------------------------------------------------------------------------------- 1 | package com.okteto.rent.controller; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.kafka.core.KafkaTemplate; 8 | import org.springframework.kafka.support.SendResult; 9 | import org.springframework.web.bind.annotation.PostMapping; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.RequestBody; 12 | import org.springframework.web.bind.annotation.RestController; 13 | 14 | import java.util.LinkedList; 15 | import java.util.List; 16 | import java.util.Map; 17 | import java.util.Collections; 18 | 19 | @RestController 20 | public class RentController { 21 | private static final String KAFKA_TOPIC_RENTALS = "rentals"; 22 | private static final String KAFKA_TOPIC_RETURNS = "returns"; 23 | 24 | private final Logger logger = LoggerFactory.getLogger(RentController.class); 25 | 26 | @Autowired 27 | private KafkaTemplate kafkaTemplate; 28 | 29 | @GetMapping(path= "/rent", produces = "application/json") 30 | Map healthz() { 31 | return Collections.singletonMap("status", "ok"); 32 | } 33 | 34 | @PostMapping(path= "/rent", consumes = "application/json", produces = "application/json") 35 | List rent(@RequestBody Rent rentInput) { 36 | String catalogID = rentInput.getMovieID(); 37 | Double price = rentInput.getPrice(); 38 | 39 | logger.info("Rent [{},{}] received", catalogID, price); 40 | 41 | kafkaTemplate.send(KAFKA_TOPIC_RENTALS, catalogID, price.toString()) 42 | .thenAccept(result -> logger.info("Message [{}] delivered with offset {}", 43 | catalogID, 44 | result.getRecordMetadata().offset())) 45 | .exceptionally(ex -> { 46 | logger.warn("Unable to deliver message [{}]. {}", catalogID, ex.getMessage()); 47 | return null; 48 | }); 49 | 50 | 51 | return new LinkedList<>(); 52 | } 53 | 54 | @PostMapping(path= "/rent/return", consumes = "application/json", produces = "application/json") 55 | public Map returnMovie(@RequestBody ReturnRequest returnRequest) { 56 | String catalogID = returnRequest.getMovieID(); 57 | 58 | logger.info("Return [{}] received", catalogID); 59 | 60 | kafkaTemplate.send(KAFKA_TOPIC_RETURNS, catalogID, catalogID) 61 | .thenAccept(result -> logger.info("Return message [{}] delivered with offset {}", 62 | catalogID, 63 | result.getRecordMetadata().offset())) 64 | .exceptionally(ex -> { 65 | logger.warn("Unable to deliver return message [{}]. {}", catalogID, ex.getMessage()); 66 | return null; 67 | }); 68 | 69 | return Collections.singletonMap("status", "return processed"); 70 | } 71 | 72 | public static class Rent { 73 | @JsonProperty("catalog_id") 74 | private String movieID; 75 | private Double price; 76 | 77 | public void setMovieID(String movieID) { 78 | this.movieID = movieID; 79 | } 80 | 81 | public String getMovieID() { 82 | return movieID; 83 | } 84 | 85 | 86 | public void setPrice(Double price) { 87 | this.price = price; 88 | } 89 | 90 | public Double getPrice() { 91 | return price; 92 | } 93 | } 94 | 95 | public static class ReturnRequest { 96 | @JsonProperty("catalog_id") 97 | private String movieID; 98 | 99 | public void setMovieID(String movieID) { 100 | this.movieID = movieID; 101 | } 102 | 103 | public String getMovieID() { 104 | return movieID; 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /frontend/src/Users.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import Loader from './Loader'; 3 | import './Users.css'; 4 | 5 | const Users = () => { 6 | const [users, setUsers] = useState([]); 7 | const [loading, setLoading] = useState(false); 8 | 9 | const fetchUsers = async () => { 10 | setLoading(true); 11 | const reqUsers = await fetch('/users'); 12 | const usersResult = await reqUsers.json(); 13 | setUsers(usersResult); 14 | return setLoading(false); 15 | }; 16 | 17 | useEffect(() => { 18 | fetchUsers(); 19 | }, []); 20 | 21 | return ( 22 |
23 |

Users

24 | { loading && } 25 | { !!users.length && } 26 | 27 | ) 28 | }; 29 | 30 | const Table = ({ users }) => { 31 | const allUsers = [...users]; 32 | const [currentUsers, setCurrentUsers] = useState(allUsers.slice(0, 100)); 33 | const [currentPage, setCurrentPage] = useState(1); 34 | 35 | const pageSize = 100; 36 | const totalPages = Math.ceil(allUsers.length / pageSize); 37 | 38 | useEffect(() => { 39 | handleUsersToDisplay(); 40 | }, [currentPage]); 41 | 42 | const handleUsersToDisplay = () => { 43 | const endIndex = currentPage * pageSize; 44 | const startIndex = endIndex - pageSize; 45 | setCurrentUsers(allUsers.slice(startIndex, endIndex)); 46 | } 47 | 48 | return ( 49 |
50 |
Total Users: {allUsers.length ? allUsers.length : '0'}
51 |
52 | 53 | 54 | { 55 | Object.keys(allUsers[0]).map((key) => { 56 | return 57 | }) 58 | } 59 | 60 | 61 | 62 | { 63 | currentUsers.map((user) => ( 64 | 65 | { 66 | Object.keys(user).map((property) => { 67 | return ( 68 | 71 | ) 72 | }) 73 | } 74 | 75 | )) 76 | } 77 | 78 |
{key}
69 | {user[property]} 70 |
79 | 84 |
85 | ) 86 | }; 87 | 88 | const Pagination = (props) => { 89 | const { 90 | totalPages, 91 | currentPage, 92 | setCurrentPage, 93 | } = props; 94 | 95 | const [pageNumbers, setPageNumbers] = useState([...Array(totalPages + 1).keys()].slice(1, 11)); 96 | 97 | const handlePageNumbers = () => { 98 | const start = totalPages - currentPage < 9 ? totalPages - 9 : currentPage > 8 ? currentPage - 5 : 1; 99 | const end = start + 10; 100 | setPageNumbers([...Array(totalPages + 1).keys()].slice(start, end)) 101 | } 102 | 103 | useEffect(() => { 104 | return handlePageNumbers(); 105 | }, [currentPage]); 106 | 107 | const setPage = { 108 | prevPage: function () { 109 | const newCurrentPage = currentPage > 1 ? currentPage - 1 : 1; 110 | setCurrentPage(newCurrentPage); 111 | }, 112 | nextPage: () => { 113 | const newCurrentPage = currentPage !== totalPages ? currentPage + 1 : totalPages; 114 | setCurrentPage(newCurrentPage); 115 | }, 116 | pageNumber: (num) => { 117 | setCurrentPage(num); 118 | } 119 | } 120 | 121 | return ( 122 |
123 |
    124 |
  • setPage.prevPage()} 127 | > 128 | 129 |
  • 130 | {pageNumbers.map((pageNum) => ( 131 |
  • setPage.pageNumber(pageNum)} 134 | key={pageNum} 135 | > 136 | { pageNum === currentPage && } 137 | {pageNum} 138 |
  • 139 | ))} 140 |
  • setPage.nextPage(currentPage)} 143 | > 144 | 145 |
  • 146 |
147 |
148 | ) 149 | }; 150 | 151 | const CaretIcon = (props) => { 152 | const { 153 | disabled, 154 | rotate 155 | } = props; 156 | const fill = disabled ? 'gray' : '#fff'; 157 | const transform = rotate === 'left' ? 'rotate(90 12 12)' : 'rotate(-90 12 12)'; 158 | return ( 159 | 165 | 166 | 167 | 168 | 169 | 173 | 174 | 175 | 176 | ); 177 | } 178 | 179 | export default Users; 180 | -------------------------------------------------------------------------------- /api/cmd/api/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "encoding/json" 6 | "io/ioutil" 7 | "net/http" 8 | "os" 9 | "strconv" 10 | "log" 11 | 12 | "github.com/okteto/movies/pkg/database" 13 | 14 | "fmt" 15 | 16 | _ "github.com/lib/pq" 17 | "github.com/gorilla/mux" 18 | ) 19 | 20 | var db *sql.DB 21 | 22 | func main() { 23 | db = database.Open() 24 | defer db.Close() 25 | 26 | if len(os.Args) > 1 && os.Args[1] == "load-data" { 27 | database.Ping(db) 28 | fmt.Println("Loading data...") 29 | loadData() 30 | return 31 | } 32 | 33 | fmt.Println("Running server on port 8080...") 34 | handleRequests() 35 | } 36 | 37 | type Rental struct { 38 | Movie string 39 | Price string 40 | } 41 | 42 | type Movie struct { 43 | ID int `json:"id,omitempty"` 44 | VoteAverage float64 `json:"vote_average,omitempty"` 45 | OriginalTitle string `json:"original_title,omitempty"` 46 | BackdropPath string `json:"backdrop_path,omitempty"` 47 | Price float64 `json:"price,omitempty"` 48 | Overview string `json:"overview,omitempty"` 49 | } 50 | 51 | type User struct { 52 | Userid int 53 | Firstname string 54 | Lastname string 55 | Phone string 56 | City string 57 | State string 58 | Zip string 59 | Age int 60 | Gender string 61 | } 62 | 63 | func loadData() { 64 | dropTableStmt := `DROP TABLE IF EXISTS users` 65 | if _, err := db.Exec(dropTableStmt); err != nil { 66 | log.Panic(err) 67 | } 68 | 69 | createTableStmt := `CREATE TABLE IF NOT EXISTS users (user_id int NOT NULL UNIQUE, first_name varchar(255), last_name varchar(255), phone varchar(15), city varchar(255), state varchar(30), zip varchar(12), age int, gender varchar(10))` 70 | if _, err := db.Exec(createTableStmt); err != nil { 71 | log.Panic(err) 72 | } 73 | 74 | jsonContent, err := os.ReadFile("data/users.json") 75 | if err != nil { 76 | log.Panic(err) 77 | } 78 | 79 | var users []User 80 | 81 | unmarshalErr := json.Unmarshal([]byte(jsonContent), &users) 82 | 83 | if unmarshalErr != nil { 84 | log.Panic(err) 85 | } 86 | 87 | for _, user := range users { 88 | insertStmt := `insert into "users"("user_id", "first_name", "last_name", "phone", "city", "state", "zip", "age", "gender") values($1, $2, $3, $4, $5, $6, $7, $8, $9)` 89 | if _, err := db.Exec(insertStmt, user.Userid, user.Firstname, user.Lastname, user.Phone, user.City, user.State, user.Zip, user.Age, user.Gender); err != nil { 90 | log.Panic(err) 91 | } 92 | } 93 | 94 | return 95 | } 96 | 97 | func handleRequests() { 98 | muxRouter := mux.NewRouter().StrictSlash(true) 99 | 100 | muxRouter.HandleFunc("/rentals", rentals) 101 | muxRouter.HandleFunc("/users", allUsers) 102 | muxRouter.HandleFunc("/users/{userid}", singleUser) 103 | 104 | log.Fatal(http.ListenAndServe(":8080", muxRouter)) 105 | } 106 | 107 | func rentals(w http.ResponseWriter, r *http.Request) { 108 | fmt.Println("Received request...") 109 | 110 | rows, err := db.Query("SELECT * FROM rentals") 111 | if err != nil { 112 | fmt.Println("error listing rentals", err) 113 | w.WriteHeader(500) 114 | return 115 | } 116 | defer rows.Close() 117 | 118 | var rentals []Rental 119 | 120 | for rows.Next() { 121 | var r Rental 122 | if err := rows.Scan(&r.Movie, &r.Price); err != nil { 123 | fmt.Println("error scanning row", err) 124 | os.Exit(1) 125 | } 126 | rentals = append(rentals, r) 127 | } 128 | if err = rows.Err(); err != nil { 129 | fmt.Println("error in rows", err) 130 | os.Exit(1) 131 | } 132 | 133 | resp, err := http.Get("http://catalog:8080/catalog") 134 | if err != nil { 135 | fmt.Println("error listing catalog", err) 136 | w.WriteHeader(500) 137 | return 138 | } 139 | 140 | body, err := ioutil.ReadAll(resp.Body) 141 | if err != nil { 142 | fmt.Println("error reading catalog", err) 143 | w.WriteHeader(500) 144 | return 145 | } 146 | 147 | movies := []Movie{} 148 | if err := json.Unmarshal(body, &movies); err != nil { 149 | fmt.Println("error unmarshaling catalog", err) 150 | w.WriteHeader(500) 151 | return 152 | } 153 | 154 | result := []Movie{} 155 | for _, rental := range rentals { 156 | for _, m := range movies { 157 | if rental.Movie == strconv.Itoa(m.ID) { 158 | price, _ := strconv.ParseFloat(rental.Price, 64) 159 | m.Price = price 160 | result = append(result, m) 161 | } 162 | } 163 | } 164 | 165 | fmt.Println("Returned", result) 166 | w.Header().Set("Content-Type", "application/json") 167 | json.NewEncoder(w).Encode(result) 168 | } 169 | 170 | func allUsers(w http.ResponseWriter, r *http.Request) { 171 | fmt.Println("Received request...") 172 | 173 | rows, err := db.Query("SELECT * FROM users") 174 | if err != nil { 175 | fmt.Println("error listing users", err) 176 | w.WriteHeader(500) 177 | return 178 | } 179 | defer rows.Close() 180 | 181 | var users []User 182 | 183 | for rows.Next() { 184 | var u User 185 | if err := rows.Scan(&u.Userid, &u.Firstname, &u.Lastname, &u.Phone, &u.City, &u.State, &u.Zip, &u.Age, &u.Gender); err != nil { 186 | log.Panic("error scanning row", err) 187 | } 188 | users = append(users, u) 189 | } 190 | if err = rows.Err(); err != nil { 191 | log.Panic("error in rows", err) 192 | } 193 | 194 | fmt.Println("Returned", len(users), "user records.") 195 | w.Header().Set("Content-Type", "application/json") 196 | json.NewEncoder(w).Encode(users) 197 | } 198 | 199 | func singleUser(w http.ResponseWriter, r *http.Request) { 200 | vars := mux.Vars(r) 201 | userid := vars["userid"] 202 | 203 | fmt.Println("Received request...") 204 | 205 | row := db.QueryRow("SELECT * FROM users WHERE user_id = $1", userid) 206 | 207 | var user User 208 | 209 | if err := row.Scan(&user.Userid, &user.Firstname, &user.Lastname, &user.Phone, &user.City, &user.State, &user.Zip, &user.Age, &user.Gender); err != nil { 210 | if err == sql.ErrNoRows { 211 | fmt.Println("No user was found") 212 | w.WriteHeader(404) 213 | return 214 | } else { 215 | log.Panic("error scanning returned user", err) 216 | } 217 | } 218 | 219 | fmt.Println("Returned", user) 220 | w.Header().Set("Content-Type", "application/json") 221 | json.NewEncoder(w).Encode(user) 222 | } 223 | -------------------------------------------------------------------------------- /frontend/src/App.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css?family=Oswald:500"); 2 | 3 | html { 4 | height: 100%; 5 | } 6 | 7 | body { 8 | --app-background: linear-gradient(180deg, #1A161F 0%, #0D0C0F 49.17%); 9 | --app-horizontal-margin: 65px; 10 | --app-title-font: 'Oswald', serif; 11 | --app-normal-font: 'Helvetica Neue', Helvetica, Arial, sans-serif; 12 | --app-header-height: 64px; 13 | --app-header-z-index: 5; 14 | 15 | --color-green: #00D1CA; 16 | --color-red: #ce1616; 17 | --color-navy: #1A263E; 18 | --color-navy-light: #2A2332; 19 | --shadow-level-1: rgb(0 0 0 / 69%) 0px 26px 30px -10px, rgb(0 0 0 / 73%) 0px 16px 10px -10px; 20 | --shadow-level-2: rgb(0 0 0 / 70%) 0px 40px 58px -16px, rgb(0 0 0 / 52%) 0px 30px 22px -10px; 21 | 22 | background: var(--app-background); 23 | color: #ffffff; 24 | font-family: var(--app-normal-font); 25 | font-weight: 400; 26 | } 27 | 28 | strong { 29 | font-weight: bold; 30 | } 31 | 32 | a { 33 | color: var(--color-green); 34 | font-weight: 200; 35 | text-transform: lowercase; 36 | } 37 | 38 | a:hover { 39 | color: #FFF; 40 | } 41 | 42 | .button { 43 | display: inline-flex; 44 | gap: 8px; 45 | color: #fff; 46 | background-color: var(--color-navy-light); 47 | border-radius: 8px; 48 | box-shadow: var(--shadow-level-1); 49 | font-size: 16px; 50 | font-weight: 400; 51 | padding: 8px 20px; 52 | text-align: center; 53 | letter-spacing: -.01em; 54 | transition: transform .2s ease-in-out; 55 | } 56 | 57 | .button:hover { 58 | box-shadow: var(--shadow-level-1); 59 | filter: brightness(1.2); 60 | transform: scale(1.05); 61 | } 62 | 63 | .App { 64 | font-family: var(--app-normal-font); 65 | font-size: 16px; 66 | height: 100vh; 67 | overflow: auto; 68 | letter-spacing: 1px; 69 | } 70 | 71 | .App h2, 72 | .TitleList .Title, 73 | .Users h1 { 74 | font-size: 22px; 75 | font-weight: 500; 76 | line-height: 1.4; 77 | letter-spacing: -.01em; 78 | margin-bottom: .5em; 79 | } 80 | 81 | .App p { 82 | line-height: 1.6em; 83 | margin-bottom: 1em; 84 | } 85 | 86 | .App .spring { 87 | flex: 1 auto; 88 | } 89 | 90 | .App__header { 91 | background: linear-gradient(to bottom, rgba(0, 0, 0, 0.6) 0%, transparent 100%); 92 | position: fixed; 93 | top: 0; 94 | left: 0; 95 | width: 100vw; 96 | box-sizing: border-box; 97 | z-index: var(--app-header-z-index); 98 | min-height: var(--app-header-height); 99 | display: flex; 100 | flex-direction: column; 101 | background-color: transparent; 102 | transition: background-color .3s ease-in-out; 103 | display: flex; 104 | align-items: center; 105 | padding: 0 var(--app-horizontal-margin); 106 | flex-direction: row; 107 | align-items: center; 108 | justify-content: space-between; 109 | } 110 | 111 | .App__nav { 112 | display: flex; 113 | justify-content: right; 114 | gap: 8px; 115 | margin-bottom: 12px; 116 | } 117 | 118 | .App__header.fixed { 119 | background-color: rgba(0, 0, 0, 0.8); 120 | } 121 | 122 | .App__logo { 123 | flex: 1; 124 | color: var(--color-red); 125 | text-transform: uppercase; 126 | font-size: 28px; 127 | font-weight: 500; 128 | font-family: var(--app-title-font); 129 | letter-spacing: -1.2px; 130 | display: flex; 131 | flex-direction: row; 132 | align-items: baseline; 133 | gap: 4px; 134 | } 135 | 136 | .App__content { 137 | padding: calc(var(--app-header-height) + 12px) var(--app-horizontal-margin) 10px; 138 | } 139 | 140 | .App__promo { 141 | background-color: #514360; 142 | color: #FFF; 143 | margin: 0; 144 | border-radius: 8px; 145 | font-size: 15px; 146 | letter-spacing: -.005rem; 147 | text-align: center; 148 | display: flex; 149 | flex-direction: row; 150 | align-items: center; 151 | justify-content: center; 152 | gap: 6px; 153 | flex: 1; 154 | } 155 | 156 | .App__promo strong { 157 | color: var(--color-green); 158 | } 159 | 160 | .TitleList { 161 | box-sizing: border-box; 162 | transition: opacity 3s ease; 163 | position: relative; 164 | z-index: 4; 165 | } 166 | 167 | .DevToast { 168 | --toast-size: 224px; 169 | 170 | background-color: var(--color-green); 171 | color: var(--color-navy); 172 | letter-spacing: -.04rem; 173 | font-weight: 500; 174 | display: flex; 175 | flex-direction: row; 176 | align-items: center; 177 | justify-content: center; 178 | gap: 4px; 179 | position: absolute; 180 | z-index: calc(var(--app-header-z-index) + 1); 181 | width: var(--toast-size); 182 | left: calc(50% - calc(var(--toast-size) / 2)); 183 | padding: 6px 0; 184 | border-bottom-right-radius: 8px; 185 | border-bottom-left-radius: 8px; 186 | box-shadow: var(--shadow-level-1); 187 | cursor: default; 188 | } 189 | 190 | .TitleList--empty { 191 | font-size: 15px; 192 | font-weight: normal; 193 | line-height: 1.4; 194 | margin-bottom: .5em; 195 | margin: 1em; 196 | } 197 | 198 | .TitleList__slider { 199 | width: 100%; 200 | box-sizing: border-box; 201 | position: relative; 202 | display: flex; 203 | place-content: center flex-start; 204 | flex-flow: row wrap; 205 | } 206 | 207 | .TitleList h1 { 208 | display: flex; 209 | flex-direction: row; 210 | align-items: center; 211 | } 212 | 213 | .Item { 214 | display: inline-block; 215 | position: relative; 216 | cursor: pointer; 217 | z-index: 20; 218 | overflow: hidden; 219 | max-width: 200px; 220 | width: calc(30% - 20px); 221 | margin: 20px 0px 0px 20px; 222 | aspect-ratio: 0.69; 223 | box-shadow: var(--shadow-level-1); 224 | transition-duration: 150ms; 225 | transition-property: transform, box-shadow; 226 | transition-timing-function: ease-out; 227 | } 228 | 229 | .Item:hover { 230 | z-index: 21; 231 | box-shadow: var(--shadow-level-2); 232 | transform: scale(1.05, 1.05) translateZ(0px) translate3d(0px, 0px, 0px); 233 | } 234 | 235 | .Item:last-child { 236 | margin-right: 0; 237 | } 238 | 239 | .Item__container { 240 | border-radius: 12px; 241 | background-color: #000000; 242 | background-position: center; 243 | height: 100%; 244 | width: 100%; 245 | background-repeat: no-repeat; 246 | background-size: cover; 247 | overflow: hidden; 248 | margin: 2px; 249 | position: relative; 250 | display: flex; 251 | align-items: center; 252 | justify-content: center; 253 | } 254 | 255 | .Item__overlay { 256 | background: linear-gradient(to bottom, rgba(0, 0, 0, 0.75) 20%, transparent 100%); 257 | padding: 20px; 258 | position: relative; 259 | height: 100%; 260 | pointer-events: none; 261 | opacity: 0; 262 | transition: opacity .125s ease; 263 | box-sizing: border-box; 264 | display: flex; 265 | flex-direction: column; 266 | align-items: center; 267 | width: 100%; 268 | } 269 | 270 | .Item__title { 271 | font-size: 22px; 272 | font-weight: 400; 273 | text-align: center; 274 | } 275 | 276 | .Item__rating { 277 | font-size: 14px; 278 | } 279 | 280 | .Item__price { 281 | background: rgba(0, 0, 0, 0.6); 282 | opacity: 0.8; 283 | border-radius: 12px; 284 | padding: 14px 18px; 285 | font-size: 26px; 286 | } 287 | 288 | .Item:hover .Item__overlay { 289 | opacity: 1; 290 | pointer-events: all; 291 | } 292 | 293 | .Item__button { 294 | background-color: var(--color-red); 295 | font-size: 18px; 296 | font-weight: bold; 297 | } 298 | 299 | .Item__button--rented { 300 | background-color: #000000; 301 | font-size: 18px; 302 | font-weight: bold; 303 | } 304 | 305 | .Item__button--rented:hover { 306 | background-color: #1e1e1e; 307 | } 308 | 309 | @media only screen and (max-width: 1050px) { 310 | .Item__title { 311 | font-size: 18px; 312 | } 313 | 314 | .Item__button { 315 | font-size: 14px; 316 | } 317 | } 318 | 319 | @media only screen and (max-width: 800px) { 320 | .Item { 321 | width: 180px; 322 | } 323 | 324 | .Item__title, .Item__rating { 325 | display: none; 326 | } 327 | } 328 | 329 | .Cart { 330 | display: inline-block; 331 | position: relative; 332 | cursor: pointer; 333 | z-index: 20; 334 | overflow: hidden; 335 | max-width: 200px; 336 | width: calc(30% - 20px); 337 | margin: 20px 0px 0px 20px; 338 | aspect-ratio: 0.69; 339 | box-shadow: rgb(0 0 0 / 69%) 0px 26px 30px -10px, rgb(0 0 0 / 73%) 0px 16px 10px -10px; 340 | transition-duration: 150ms; 341 | transition-property: transform, box-shadow; 342 | transition-timing-function: ease-out; 343 | } 344 | 345 | .Cart__container { 346 | border-radius: 12px; 347 | background-color: var(--color-navy-light); 348 | height: 100%; 349 | width: 100%; 350 | overflow: hidden; 351 | margin: 2px; 352 | position: relative; 353 | padding: 14px 20px; 354 | display: flex; 355 | flex-direction: column; 356 | font-size: 14px; 357 | } 358 | 359 | .Cart__header { 360 | display: flex; 361 | flex-direction: row; 362 | align-items: center; 363 | gap: 8px; 364 | font-size: 16px; 365 | } 366 | 367 | .Cart__list { 368 | display: flex; 369 | flex: 1; 370 | flex-direction: column; 371 | font-size: 14px; 372 | font-weight: normal; 373 | margin: 12px 4px; 374 | } 375 | 376 | .Cart__item { 377 | display: flex; 378 | flex-direction: row; 379 | align-items: center; 380 | margin-bottom: 4px; 381 | } 382 | 383 | .Cart__item-name { 384 | flex: 1; 385 | white-space: nowrap; 386 | margin-right: 4px; 387 | overflow: hidden; 388 | text-overflow: ellipsis; 389 | } 390 | 391 | .Cart__total { 392 | display: flex; 393 | flex-direction: row; 394 | align-items: center; 395 | padding: 14px 0; 396 | border-top: 1px solid #d4d3d668; 397 | } 398 | 399 | .Cart__total-title { 400 | flex: 1; 401 | white-space: nowrap; 402 | margin-right: 4px; 403 | } 404 | 405 | .Cart__total-price { 406 | font-size: 22px; 407 | } 408 | 409 | @media only screen and (max-width: 800px) { 410 | .Cart { 411 | width: 180px; 412 | } 413 | 414 | .Cart__list { 415 | display: none; 416 | } 417 | 418 | .Cart__total { 419 | border: 0; 420 | flex-direction: column; 421 | justify-content: center; 422 | flex: 1; 423 | } 424 | 425 | .Cart__total-title { 426 | flex: 0; 427 | } 428 | } 429 | 430 | .Loader { 431 | display: flex; 432 | align-items: center; 433 | align-content: center; 434 | margin: 0 auto 1em; 435 | } 436 | 437 | .Loader svg path, 438 | .Loader svg rect { 439 | fill: var(--color-green); 440 | } 441 | -------------------------------------------------------------------------------- /worker/go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/Shopify/sarama v1.32.0 h1:P+RUjEaRU0GMMbYexGMDyrMkLhbbBVUVISDywi+IlFU= 3 | github.com/Shopify/sarama v1.32.0/go.mod h1:+EmJJKZWVT/faR9RcOxJerP+LId4iWdQPBGLy1Y1Njs= 4 | github.com/Shopify/toxiproxy/v2 v2.3.0 h1:62YkpiP4bzdhKMH+6uC5E95y608k3zDwdzuBMsnn3uQ= 5 | github.com/Shopify/toxiproxy/v2 v2.3.0/go.mod h1:KvQTtB6RjCJY4zqNJn7C7JDFgsG5uoHYDirfUfpIm0c= 6 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= 7 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 8 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= 9 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= 10 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 11 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 12 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 14 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/eapache/go-resiliency v1.2.0 h1:v7g92e/KSN71Rq7vSThKaWIq68fL4YHvWyiUKorFR1Q= 16 | github.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= 17 | github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 h1:YEetp8/yCZMuEPMUDHG0CW/brkkEp8mzqk2+ODEitlw= 18 | github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= 19 | github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= 20 | github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= 21 | github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= 22 | github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= 23 | github.com/frankban/quicktest v1.14.2 h1:SPb1KFFmM+ybpEjPUhCCkZOM5xlovT5UbrMvWnXyBns= 24 | github.com/frankban/quicktest v1.14.2/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= 25 | github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= 26 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 27 | github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= 28 | github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= 29 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 30 | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= 31 | github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= 32 | github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= 33 | github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 34 | github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= 35 | github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= 36 | github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= 37 | github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= 38 | github.com/jcmturner/gofork v1.0.0 h1:J7uCkflzTEhUZ64xqKnkDxq3kzc96ajM1Gli5ktUem8= 39 | github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= 40 | github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= 41 | github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= 42 | github.com/jcmturner/gokrb5/v8 v8.4.2 h1:6ZIM6b/JJN0X8UM43ZOM6Z4SJzla+a/u7scXFJzodkA= 43 | github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc= 44 | github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= 45 | github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= 46 | github.com/klauspost/compress v1.14.4 h1:eijASRJcobkVtSt81Olfh7JX43osYLwy5krOJo6YEu4= 47 | github.com/klauspost/compress v1.14.4/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= 48 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 49 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 50 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 51 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 52 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 53 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 54 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 55 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 56 | github.com/lib/pq v1.10.5 h1:J+gdV2cUmX7ZqL2B0lFcW0m+egaHC2V3lpO8nWxyYiQ= 57 | github.com/lib/pq v1.10.5/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 58 | github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM= 59 | github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= 60 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 61 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 62 | github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= 63 | github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= 64 | github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= 65 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 66 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 67 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 68 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 69 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 70 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 71 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 72 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 73 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 74 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 75 | github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= 76 | github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= 77 | github.com/xdg-go/scram v1.1.0/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= 78 | github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= 79 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 80 | golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 81 | golang.org/x/crypto v0.0.0-20220214200702-86341886e292 h1:f+lwQ+GtmgoY+A2YaQxlSOnDjXcQ7ZRLWOHbC6HtRqE= 82 | golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 83 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 84 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 85 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 86 | golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk= 87 | golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 88 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 89 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 90 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 91 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 92 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 93 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 94 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 95 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 96 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 97 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 98 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 99 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 100 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 101 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 102 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 103 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 104 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 105 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= 106 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 107 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 108 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 109 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 110 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 111 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 112 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 113 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 114 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 115 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 116 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 117 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /frontend/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom'; 3 | import Loader from './Loader'; 4 | import Users from './Users'; 5 | 6 | import './App.css'; 7 | 8 | const compact = (movies = []) => { 9 | return movies.filter((item, index, self) => 10 | self.findIndex(i => i.id === item.id) === index 11 | ); 12 | } 13 | 14 | const financial = (x) => { 15 | return Number.parseFloat(x).toFixed(2); 16 | } 17 | 18 | class App extends Component { 19 | constructor(props) { 20 | super(props); 21 | 22 | this.state = { 23 | catalog: { 24 | data: [], 25 | loaded: false 26 | }, 27 | rental: { 28 | data: [], 29 | loaded: false 30 | }, 31 | cost: 0, 32 | session: { 33 | name: 'Cindy', 34 | lastName: 'Lopez', 35 | username: 'cindy' 36 | }, 37 | fixHeader: false 38 | }; 39 | 40 | this.appRef = React.createRef(); 41 | } 42 | 43 | componentDidMount() { 44 | this.refreshData(); 45 | } 46 | 47 | handleRent = async (item) => { 48 | await fetch('/rent', { 49 | method: 'POST', 50 | headers: { 51 | 'Content-Type': 'application/json' 52 | }, 53 | body: JSON.stringify({ 54 | catalog_id: item.id, 55 | price: item.price 56 | }) 57 | }); 58 | this.refreshData(); 59 | } 60 | 61 | handleReturn = async (item) =>{ 62 | await fetch('/rent/return', { 63 | method: 'POST', 64 | headers: { 65 | 'Content-Type': 'application/json' 66 | }, 67 | body: JSON.stringify({ 68 | catalog_id: item.id 69 | }) 70 | }); 71 | this.refreshData(); 72 | 73 | } 74 | 75 | 76 | refreshData = async () => { 77 | const catalogPromise = fetch('/catalog') 78 | .then(res => res.json()) 79 | .then(result => compact(result)); 80 | 81 | const rentalsPromise = fetch('/rentals') 82 | .then(res => res.json()) 83 | .then(result => compact(result)); 84 | 85 | const [catalog, rentals] = await Promise.all([catalogPromise, rentalsPromise]); 86 | this.setState({ 87 | rental: { 88 | data: rentals, 89 | loaded: true 90 | }, 91 | catalog: { 92 | data: catalog.map(movie => ({ 93 | ...movie, 94 | rented: !!rentals.find(c => c.id === movie.id) 95 | })), 96 | loaded: true 97 | }, 98 | cost: financial(rentals.reduce((acc, item) => acc += Number(item?.price ?? 0), 0)) 99 | }); 100 | } 101 | 102 | handleScroll = () => { 103 | this.setState({ 104 | fixHeader: this.appRef.current.scrollTop > 20 105 | }); 106 | } 107 | 108 | render() { 109 | const { catalog, rental, session, cost } = this.state; 110 | return ( 111 | 112 |
113 |
114 | 115 |
116 | 117 | Movies 118 |
119 | 120 | 121 |
122 | 123 | {MODE === 'development' && 124 | 125 | } 126 | 127 |
128 | 129 | 130 |
131 |
132 | 133 | {/*
134 | 135 | Kubecon 2023 special offer! Get a 50% discount on all movies today! 136 |
*/} 137 | 138 | 139 | 140 | 141 | 142 | Admin 143 | 144 |
145 | 152 | 159 |
160 |
161 |
162 | 163 | 164 |
165 | 166 | 167 | 168 | 169 | Back to Movies 170 | 171 |
172 | 173 |
174 |
175 |
176 |
177 |
178 | ); 179 | } 180 | } 181 | 182 | const DevToast = () => { 183 | return ( 184 |
185 | 186 | In Development Mode 187 |
188 | ); 189 | }; 190 | 191 | class TitleList extends Component { 192 | renderList() { 193 | const { titles = [], loaded, onRent, onReturn } = this.props; 194 | const movies = titles.filter(item => !item?.rented); 195 | 196 | if (loaded) { 197 | if (movies.length === 0) { 198 | return ( 199 |
200 | {onRent && 'No movies left to rent.'} 201 |
202 | ); 203 | } 204 | 205 | return movies.map((item, i) => { 206 | const backDrop = `/${item.backdrop_path}`; 207 | return ( 208 | 215 | ); 216 | }); 217 | } 218 | } 219 | 220 | render() { 221 | const { titles, title, cost = 0 } = this.props; 222 | 223 | return ( 224 |
225 |
226 |

227 | {title} 228 |

229 |
230 | {!!cost && 231 | 232 | } 233 | {this.renderList() || } 234 |
235 |
236 |
237 | ); 238 | } 239 | } 240 | 241 | const Item = ({ item, onRent, onReturn, backdrop }) => { 242 | return ( 243 |
244 |
245 |
246 |
{item?.original_title ?? 'Unknown Title'}
247 |
{item?.vote_average ?? 0} / 10
248 |
249 | { onRent ? 250 | <> 251 | {!!item?.price && 252 |
${item.price}
253 | } 254 |
255 |
onRent(item)}> 256 | Rent 257 |
258 | : 259 | <> 260 |
261 | Watch Now 262 |
263 |
onReturn(item)}> 264 | Return 265 |
266 | 267 | } 268 |
269 |
270 |
271 | ); 272 | } 273 | 274 | const CartIcon = () => { 275 | return ( 276 | 277 | 278 | 279 | ); 280 | } 281 | 282 | 283 | const Cart = ({ cost, titles }) => { 284 | return ( 285 |
286 |
287 |
288 | 289 | Cart 290 |
291 |
292 | {titles.map((movie, i) => ( 293 |
294 |
{movie.original_title}
295 |
${movie.price}
296 |
297 | ))} 298 |
299 |
300 |
Total due:
301 |
${cost}
302 |
303 |
304 |
305 | ); 306 | } 307 | 308 | const MoviesIcon = ({ size = '28'}) => { 309 | return ( 310 | 311 | 312 | 313 | ); 314 | }; 315 | 316 | const Logo = ({ size = '24'}) => { 317 | return ( 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | ); 326 | }; 327 | 328 | const Symbol = ({ size = '18'}) => { 329 | return ( 330 | 331 | 332 | 333 | 334 | 335 | ); 336 | }; 337 | 338 | const KubeconLogo = ({ size = '21'}) => { 339 | return ( 340 | 341 | 342 | 343 | 344 | ); 345 | } 346 | export default App; 347 | -------------------------------------------------------------------------------- /docs/architecture-diagram.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | --------------------------------------------------------------------------------