From 54400fb9d65ad9edb01b2e76edb6ef3680b79a29 Mon Sep 17 00:00:00 2001 From: Edgar Gomes Date: Fri, 5 Jul 2024 19:29:22 -0300 Subject: [PATCH] feat: add more info to docs (#17) --- .github/workflows/publish-demo-artifacts.yml | 52 +++++++ README.md | 58 +++++-- examples/voting-app/load-generator.py | 35 +++++ examples/voting-app/shell.nix | 27 ++++ examples/voting-app/voting-app-ui/Dockerfile | 12 ++ .../voting-app/voting-app-ui/Dockerfile-v2 | 14 ++ examples/voting-app/voting-app-ui/app.py | 94 ++++++++++++ .../voting-app-ui/templates/index.html | 142 ++++++++++++++++++ shell.nix | 27 +++- 9 files changed, 446 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/publish-demo-artifacts.yml create mode 100755 examples/voting-app/load-generator.py create mode 100644 examples/voting-app/shell.nix create mode 100644 examples/voting-app/voting-app-ui/Dockerfile create mode 100644 examples/voting-app/voting-app-ui/Dockerfile-v2 create mode 100644 examples/voting-app/voting-app-ui/app.py create mode 100644 examples/voting-app/voting-app-ui/templates/index.html diff --git a/.github/workflows/publish-demo-artifacts.yml b/.github/workflows/publish-demo-artifacts.yml new file mode 100644 index 00000000..35ba1f0f --- /dev/null +++ b/.github/workflows/publish-demo-artifacts.yml @@ -0,0 +1,52 @@ +name: Publish demo artifacts + +on: + push: + branches: + - main + tags: + - "v*.*.*" + pull_request: + branches: + - main + +env: + MAIN_BRANCH: ${{ 'refs/heads/main' }} + +jobs: + build-publish-demo: + runs-on: ubuntu-latest + if: github.ref == env.MAIN_BRANCH + steps: + - name: git checkout + uses: actions/checkout@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push voting app UI - v1 + uses: docker/build-push-action@v6 + with: + context: ./examples/voting-app/voting-app-ui/ + file: ./examples/voting-app/voting-app-ui/Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: kurtosistech/demo-voting-app-ui:latest + + - name: Build and push voting app UI - v2 + uses: docker/build-push-action@v6 + with: + context: ./examples/voting-app/voting-app-ui/ + file: ./examples/voting-app/voting-app-ui/Dockerfile-v2 + platforms: linux/amd64,linux/arm64 + push: true + tags: kurtosistech/demo-voting-app-ui-v2:latest diff --git a/README.md b/README.md index e6652d45..e507eef8 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Kardinal is a traffic control and data isolation layer that enables engineers to Kardinal injects production data and service dependencies into your dev and test workflows safely and securely. Instead of spinning up ephemeral environments with mocked services, fake traffic, and fake data, developers using Kardinal can put their service directly into the production environment to see how it works... without risking the stability of that environment. Key features: + - Develop and test directly in production without risk - Catch bugs that "only appear in prod" faster - Stop maintaining multiple environments - do it all in production @@ -58,7 +59,7 @@ Have questions or need assistance? We're here to help: ## Architecture -Kardinal main components are the Kardinal CLI and the Kardinal Manager. The Kardinal CLI allows the user to manage the development flows. The Kardinal Manager retrieves the latest configuration from the Kardinal Cloud and applies changes to the K8S user services topology. +Kardinal main components are the Kardinal CLI and the Kardinal Manager. The Kardinal CLI allows the user to manage the development flows. The Kardinal Manager retrieves the latest configuration from the Kardinal Cloud and applies changes to the K8S user services topology. ![kardinal-dev-overview](./img/kardinal-dev-overview.png?raw=true) @@ -72,20 +73,27 @@ The Kardinal CLI is a standalone tool interacting with the Kardinal Cloud to man The Kardinal Manager retrieves the latest user services topology from the Kardinal Cloud and applies the changes by interacting with the Istio client and K8S client. The Manager manages traffic using Istio objects such as virtual services and destination rules. The Manager also updates the K8S services and deployments. - ## Quickstart ### How to run Kardinal and use the voting app example to test the dev flow #### Prerequisites -- A local Kubernetes cluster ([Minikube](https://minikube.sigs.k8s.io/docs/start/?arch=%2Fmacos%2Fx86-64%2Fstable%2Fbinary+download used in this example) +You will need the following tools installed (they will be already available if you are using the nix shell provided by this repository): + +- A local Kubernetes cluster ([Minikube](https://minikube.sigs.k8s.io/docs/start/?arch=%2Fmacos%2Fx86-64%2Fstable%2Fbinary+download) used in this example - Istio resources installed in the local cluster (use the [getting started doc](https://istio.io/latest/docs/setup/getting-started/#download)) + ```bash -# Install with istioctl and default profile +minikube start --driver=docker --cpus=10 --memory 8192 --disk-size 32g +minikube addons enable ingress +minikube addons enable metrics-server istioctl install --set profile=default -y +minikube dashboard ``` + - Both `prod.app.localhost` and `dev.app.localhost` defined in the host file + ```bash # Add these entries in the '/private/etc/hosts' file 127.0.0.1 prod.app.localhost @@ -95,51 +103,73 @@ istioctl install --set profile=default -y #### Steps ##### Deploy the production voting app -1. Follow [this to build and run the cli][run-build-cli] + +1. Use the `kardinal` provided by the Nix shell (enter using `nix develop`) or follow [this to build and run the cli][run-build-cli] 2. Deploy `Kardinal Manager` in the local kubernetes cluster and set the `Kardinal Control` location (we are going to use the cloud version on these steps) + ```bash -./kardinal manager deploy kloud-kontrol +kardinal manager deploy kloud-kontrol ``` + 3. Copy the tenant UUID generated while running this command + ```bash # This log line will be printed in the terminal, copy the generated UUID INFO[0000] Using tenant UUID 58d33536-3c9e-4110-aa83-bf112ae94a49 ``` + 3. Deploy the voting-app application with Kardinal + ```bash -./kardinal deploy --docker-compose ../examples/voting-app/docker-compose.yaml +kardinal deploy --docker-compose ../examples/voting-app/docker-compose.yaml ``` + 4. Check the current topology in the cloud Kontrol FE using this URL: https://app.kardinal.dev/{use-your-tenant-UUID-here}/traffic-configuration -5. Open the [production page in the browser](http://prod.app.localhost/) to see the production `voting-app` +5. Start the tunnel to access the services (you may have to provide you password for the underlying sudo access) + +```bash +minukube tunnel +``` + +6. Open the [production page in the browser](http://prod.app.localhost/) to see the production `voting-app` ##### Deploy the voting app development version in the same cluster + 1. Create a new flow to test a development `voting-app-ui-v2` version in production + ```bash -./kardinal flow create voting-app-ui voting-app-ui-v2 --docker-compose ../examples/voting-app/docker-compose.yaml +kardinal flow create voting-app-ui voting-app-ui-v2 --docker-compose ../examples/voting-app/docker-compose.yaml ``` + 2. Check how the topology has changed, to reflect both prod and the dev version, in the cloud Kontrol FE using this URL: https://app.kardinal.dev/{use-your-tenant-UUID-here}/traffic-configuration 3. Open the [development voting-app-ui-v2 page in the browser](http://dev.app.localhost/) to see the development `voting-app-ui-v2` ##### Remove the voting app development version from the same cluster + 1. Remove the flow created for the `voting-app-ui-v2` + ```bash -./kardinal flow delete --docker-compose ../examples/voting-app/docker-compose.yaml +kardinal flow delete --docker-compose ../examples/voting-app/docker-compose.yaml ``` + 2. Check the topology again to, it's showing only the production version as the beginning, in the cloud Kontrol FE using this URL: https://app.kardinal.dev/{use-your-tenant-UUID-here}/traffic-configuration 3. Open the [development voting-app-ui-v2 page in the browser](http://dev.app.localhost/) to check that it was successfully removed 4. Open the [production page in the browser](http://prod.app.localhost/) to check that it didn't change ##### Clean + 1. Remove `Kardinal Manager` from the cluster + ```bash -./kardinal manager remove +kardinal manager remove ``` + 2. Remove the `voting-app` application from the cluster + ```bash kubectl delete ns prod ``` - ## Development instructions 1. Enter the dev shell and start the local cluster: @@ -247,4 +277,6 @@ gomod2nix generate ``` -[run-build-cli]: #running-kardinal-cli \ No newline at end of file + +[run-build-cli]: #running-kardinal-cli + diff --git a/examples/voting-app/load-generator.py b/examples/voting-app/load-generator.py new file mode 100755 index 00000000..afddde0c --- /dev/null +++ b/examples/voting-app/load-generator.py @@ -0,0 +1,35 @@ +import requests +import time + +# The URL to send the POST requests to +host = "prod.app.localhost" +url = "http://127.0.0.1/" + +# Headers to be included in the POST requests +headers = { + "Origin": f"http://{host}", + "Host": host, +} + +# Data to be sent in the POST requests +data_options = ["option1", "option2"] +data_index = 0 + + +# Function to send a burst of 5 POST requests +def send_burst(data): + print(f"New burst of {data}") + for _ in range(5): + response = None + try: + response = requests.post(url, headers=headers, data={"vote": data}) + print(f"Sent '{data}' - Response status code: {response.status_code}") + except requests.exceptions.RequestException as e: + print(f"Error sending '{data}' - {e}") + + +# Send bursts of 5 POST requests every 5 seconds, alternating between 'Cats' and 'Dogs' +while True: + send_burst(data_options[data_index]) + data_index = (data_index + 1) % 2 + time.sleep(5) diff --git a/examples/voting-app/shell.nix b/examples/voting-app/shell.nix new file mode 100644 index 00000000..b082bd69 --- /dev/null +++ b/examples/voting-app/shell.nix @@ -0,0 +1,27 @@ +{pkgs, ...}: let + pyEnv = pkgs.python3.buildEnv.override { + extraLibs = [pkgs.python3Packages.click pkgs.python3Packages.requests]; + ignoreCollisions = true; + }; + + pname = "demo-load-generator"; + demo-load-genarator = pkgs.stdenv.mkDerivation { + inherit pname; + version = "1.0.0"; + + src = ./.; + + installPhase = '' + mkdir -p $out/bin + echo "#!${pyEnv}/bin/python3" > $out/bin/${pname} + cat load-generator.py >> $out/bin/${pname} + chmod +x $out/bin/${pname} + ''; + }; +in + pkgs.mkShell { + buildInputs = [ + demo-load-genarator + pyEnv + ]; + } diff --git a/examples/voting-app/voting-app-ui/Dockerfile b/examples/voting-app/voting-app-ui/Dockerfile new file mode 100644 index 00000000..032b37ca --- /dev/null +++ b/examples/voting-app/voting-app-ui/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.9-slim + +WORKDIR /app + +COPY . . + +RUN pip install Flask redis + +EXPOSE 5000 + +CMD ["python", "app.py"] + diff --git a/examples/voting-app/voting-app-ui/Dockerfile-v2 b/examples/voting-app/voting-app-ui/Dockerfile-v2 new file mode 100644 index 00000000..cee5a378 --- /dev/null +++ b/examples/voting-app/voting-app-ui/Dockerfile-v2 @@ -0,0 +1,14 @@ +FROM python:3.9-slim + +WORKDIR /app + +COPY . . + +RUN pip install Flask redis + +ENV APP_VERSION v2 + +EXPOSE 5000 + +CMD ["python", "app.py"] + diff --git a/examples/voting-app/voting-app-ui/app.py b/examples/voting-app/voting-app-ui/app.py new file mode 100644 index 00000000..accb3b3b --- /dev/null +++ b/examples/voting-app/voting-app-ui/app.py @@ -0,0 +1,94 @@ +from flask import Flask, render_template, request, redirect, url_for +import redis +import os + +app = Flask(__name__) + +redis_server = os.environ["REDIS"] + +# Initialize Redis +r = redis.Redis(host=redis_server, port=6379) + +# Getting app version +if "APP_VERSION" in os.environ and os.environ["APP_VERSION"]: + app_version = os.environ["APP_VERSION"] +else: + app_version = "v1" + +print("app_version is: " + app_version) + +if "OPTION1" in os.environ and os.environ["OPTION1"]: + option1 = os.environ["OPTION1"] +else: + option1 = "Option 1" + +if "OPTION2" in os.environ and os.environ["OPTION2"]: + option2 = os.environ["OPTION2"] +else: + option2 = "Option 2" + +if "OPTION3" in os.environ and os.environ["OPTION3"] and app_version != "v1": + option3 = os.environ["OPTION3"] +elif app_version != "v1": + option3 = "Option 3" + +if "TITLE" in os.environ and os.environ["TITLE"]: + title = os.environ["TITLE"] +else: + title = "Vote For Your Favorite Option" + +# Set up initial vote counts +# TODO: implement this on redis proxy +if not r.exists("option1"): + r.set("option1", 0) +if not r.exists("option2"): + r.set("option2", 0) + +if app_version == "v1": + if not r.exists("option3"): + r.set("option3", 0) + + +@app.route("/", methods=["GET", "POST"]) +def index(): + if request.method == "POST": + vote = request.form["vote"] + if vote == "option1": + r.incr("option1") + elif vote == "option2": + r.incr("option2") + elif vote == "option3" and app_version != "v1": + r.incr("option3") + return redirect(url_for("index")) + + # Get current vote counts + option1_votes = int(r.get("option1") or 0) + option2_votes = int(r.get("option2") or 0) + if app_version != "v1": + option3_votes = int(r.get("option3") or 0) + + if app_version != "v1": + return render_template( + "index.html", + option1_votes=option1_votes, + option2_votes=option2_votes, + option3_votes=option3_votes, + title=title, + option1=option1, + option2=option2, + option3=option3, + ) + else: + return render_template( + "index.html", + option1_votes=option1_votes, + option2_votes=option2_votes, + title=title, + option1=option1, + option2=option2, + ) + + + +if __name__ == "__main__": + app.run(debug=True, host="0.0.0.0", port=80) diff --git a/examples/voting-app/voting-app-ui/templates/index.html b/examples/voting-app/voting-app-ui/templates/index.html new file mode 100644 index 00000000..0babd41c --- /dev/null +++ b/examples/voting-app/voting-app-ui/templates/index.html @@ -0,0 +1,142 @@ + + + + + + Voting App + + + +
+ +
+
+
+
+
+ + + {% if option3 %} + + {% endif %} +
+
+
+
+
Current votes
+
+ {{ option1 }}: {{ option1_votes }} | + {{ option2 }}: {{ option2_votes }} + {% if option3 %} + | + {{ option3 }}: {{ option3_votes }} + {% endif %} +
+
+
+ + diff --git a/shell.nix b/shell.nix index 581312fb..6d4db0c1 100644 --- a/shell.nix +++ b/shell.nix @@ -6,17 +6,34 @@ nativeBuildInputs = builtins.concatLists (map (s: s.nativeBuildInputs or []) shells); paths = builtins.concatLists (map (s: s.paths or []) shells); }; + + kardinal = pkgs.writeShellScriptBin "kardinal" '' + nix run .#kardinal-cli -- "$@" + ''; + manager_shell = pkgs.callPackage ./kardinal-manager/shell.nix {inherit pkgs;}; cli_shell = pkgs.callPackage ./kardinal-cli/shell.nix {inherit pkgs;}; cli_kontrol_api_shell = pkgs.callPackage ./libs/cli-kontrol-api/shell.nix {inherit pkgs;}; + demo_shell = pkgs.callPackage ./examples/voting-app/shell.nix {inherit pkgs;}; + kardinal_shell = with pkgs; pkgs.mkShell { nativeBuildInputs = [bashInteractive bash-completion]; - buildInputs = [kubectl kustomize kubernetes-helm minikube istioctl tilt reflex]; + buildInputs = [ + kardinal + kubectl + kustomize + kubernetes-helm + minikube + istioctl + tilt + reflex + ]; shellHook = '' export SHELLNAME=$(basename $shell) source <(kubectl completion $SHELLNAME) source <(minikube completion $SHELLNAME) + source <(kardinal completion $SHELLNAME) printf '\u001b[31m ::::: @@ -49,4 +66,10 @@ ''; }; in - mergeShells [manager_shell cli_shell kardinal_shell cli_kontrol_api_shell] + mergeShells [ + manager_shell + cli_shell + kardinal_shell + cli_kontrol_api_shell + demo_shell + ]