-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 1c51456
Showing
21 changed files
with
1,310 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
. .venv/bin/activate | ||
|
||
export KUBE_NAMESPACE=chain-link | ||
export KUBE_LABEL_SELECTOR=app=chain-link | ||
export SERVICE_NAME=chain-link-0 | ||
export SERVICES_LIST=http://localhost:5001,http://localhost:5002,http://localhost:5003 | ||
|
||
alias m=make |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
name: Build and Push Docker Image | ||
|
||
on: | ||
push: | ||
branches: | ||
- main # Replace with the default branch name of your repository | ||
|
||
jobs: | ||
build_and_push: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- name: Checkout repository | ||
uses: actions/checkout@v3 | ||
|
||
- name: Set up Docker Buildx | ||
uses: docker/setup-buildx-action@v2 | ||
|
||
- name: Login to GitHub Container Registry | ||
uses: docker/login-action@v2 | ||
with: | ||
registry: ghcr.io | ||
username: ${{ github.actor }} | ||
password: ${{ secrets.GHCR_PAT }} | ||
|
||
- name: Build and push Docker image | ||
uses: docker/build-push-action@v2 | ||
with: | ||
context: . | ||
push: true | ||
tags: ghcr.io/${{ github.repository_owner }}/chain-link:latest |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
__pycache__ | ||
.venv | ||
.vscode | ||
chain-link-cli.conf | ||
chain-link-manifests | ||
NOTES.md |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
3.11.3 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
# Dockerfile | ||
FROM python:3.11-slim | ||
COPY requirements.txt / | ||
RUN set -ex && \ | ||
pip install -r requirements.txt | ||
COPY app.py gunicorn-run.sh /app/ | ||
RUN useradd gunicorn -u 10001 --user-group | ||
USER 10001 | ||
WORKDIR /app | ||
|
||
ENTRYPOINT [ "/app/gunicorn-run.sh" ] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
# chain-link | ||
|
||
This is a not a crypto app. :) It's really meant as a demo app that can be deployed into Kubernetes and then used by some other tooling to visualize connectivity. | ||
|
||
Using the app and CLI tool in this repository, several pods will be deployed to Kubernetes, and when the first one of those pods is connected to on port 80, it will connect to the next "chain-link" pod, which will forward it to the next pod, and so on, forming a "service chain". That chain can then be visualized in something like the Zipkin GUI. | ||
|
||
``` | ||
+-------------------+ +--------------------+ +--------------------+ +--------------------+ | ||
| | | | | | | | | ||
| loadgenerator +--------> chain-link-0 +---------> chain-link-1 +--------> chain-link-(n) | | ||
| | | | | | | | | ||
+-------------------+ +--------------------+ +--------------------+ +--------------------+ | ||
``` | ||
|
||
The application is configured to send traces to a Zipkin instance that is deployed into the same namespace. | ||
|
||
There is a loadgenerator pod that will continuously poll the first deployment to create traffic. | ||
|
||
## How to Deploy | ||
|
||
1. Check out the git repository | ||
1. OPTIONAL: Build the image and put it into a registry that the Kubernetes nodes can pull from. Otherwise the default image will be used. | ||
2. Deploy with the CLI provided (which might require setting up a suitable Python environment) | ||
|
||
### OPTIONAL: Build the Image | ||
|
||
Build the image and push it into your registry. | ||
|
||
``` | ||
docker build -t chain-link . | ||
docker tag chain-link <your registry>/chain-link:latest | ||
docker push <your registry>/chain-link:latest | ||
``` | ||
|
||
### Deploy the Kubernetes Resources | ||
|
||
Use the provided CLI to deploy into Kubernetes. There are a few options that can be set, so use `--help` to determine the options. | ||
|
||
``` | ||
./chain-link-cli -h | ||
``` | ||
|
||
The number of instances is configurable: | ||
|
||
``` | ||
./chain-link-cli --instances 5 deploy | ||
``` | ||
|
||
## What is Deployed | ||
|
||
* A specified number of instances of the chain-link application | ||
* loadgenerator instance | ||
* Zipkin instance | ||
|
||
Below is an example of a deployment with five chain link instances. | ||
|
||
``` | ||
$ k get pods | ||
NAME READY STATUS RESTARTS AGE | ||
chain-link-deployment-0-785c6fc8c9-mfx8s 1/1 Running 0 20m | ||
chain-link-deployment-1-74b5c7bc7f-4d5zr 1/1 Running 0 19m | ||
chain-link-deployment-2-77cdf4f74c-lsmb2 1/1 Running 0 19m | ||
loadgenerator 1/1 Running 0 19m | ||
zipkin-deployment-5c44dd85ff-5vdmw 1/1 Running 0 20m | ||
``` | ||
|
||
## Zipkin | ||
|
||
The application is configured to send traces to the Zipkin service. | ||
|
||
### Access Zipkin | ||
|
||
Port forward to it and access the service from your localhost in the browser on port 9411. | ||
|
||
``` | ||
kubectl port-forward svc/zipkin-service 9411:80 | ||
``` | ||
|
||
### What it Looks Like in Zipkin | ||
|
||
 | ||
|
||
 | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
""" | ||
This app forwards a request to the next service in the chain | ||
""" | ||
|
||
import os | ||
import logging | ||
import json | ||
import random | ||
import requests | ||
import time | ||
from opentelemetry import trace | ||
from opentelemetry.sdk.resources import Resource | ||
from opentelemetry.sdk.trace import TracerProvider | ||
from opentelemetry.sdk.trace.export import BatchSpanProcessor | ||
from opentelemetry.instrumentation.flask import FlaskInstrumentor | ||
from opentelemetry.instrumentation.requests import RequestsInstrumentor | ||
from opentelemetry.exporter.zipkin.json import ZipkinExporter | ||
from flask import Flask, request, jsonify, make_response | ||
|
||
|
||
# | ||
# OpenTelemetry and Zipkin | ||
# | ||
|
||
# Configure the TracerProvider and SpanExporter | ||
# service_name is set in the cli.py script as a env var | ||
service_name = os.environ.get("CHAIN_LINK_SERVICE_NAME", "unknown") | ||
trace.set_tracer_provider( | ||
TracerProvider(resource=Resource.create({"service.name": service_name})) | ||
) | ||
|
||
# create a ZipkinSpanExporter - this is specific to Zipkin deployed into | ||
# Kubernetes with the cli.py script | ||
# NOTE(curtis): this is expecting a service called zipkin-service-0 listening on | ||
# port 80! | ||
zipkin_exporter = ZipkinExporter( | ||
endpoint="http://zipkin-service/api/v2/spans", | ||
) | ||
|
||
# Create a BatchSpanProcessor and add the exporter to it | ||
span_processor = BatchSpanProcessor(zipkin_exporter) | ||
|
||
# add to the tracer | ||
trace.get_tracer_provider().add_span_processor(span_processor) | ||
|
||
# | ||
# Flask | ||
# | ||
|
||
# Instrument the Flask app and Requests library | ||
app = Flask(__name__) | ||
FlaskInstrumentor().instrument_app(app) | ||
RequestsInstrumentor().instrument() | ||
|
||
# | ||
# Logging | ||
# | ||
|
||
logging.basicConfig(level=logging.INFO) | ||
app.logger.info("service_name %s", service_name) | ||
|
||
|
||
# the services.json will be mounted from a configmap | ||
def get_service_urls(): | ||
""" | ||
Get the list of services from the configmap which is mounted into the pod | ||
""" | ||
with open( | ||
"/etc/chain-link.conf.d/services.json", encoding="utf-8" | ||
) as services_file: | ||
services_json = services_file.read() | ||
return json.loads(services_json) | ||
|
||
|
||
services = get_service_urls() | ||
app.logger.info("new services: %s", services) | ||
|
||
|
||
def is_valid_service(svc_name): | ||
""" | ||
Check if the service_name is in the services list | ||
""" | ||
return svc_name in services | ||
|
||
|
||
# | ||
# Routes | ||
# | ||
|
||
|
||
@app.route("/", methods=["GET"]) | ||
def process_request(): | ||
""" | ||
Process the request and forward it to the next service in the chain | ||
""" | ||
|
||
# one of the nodes should take longer to respond, so we are going to randomly | ||
# sleep for 2 seconds on one of the nodes (well, this isn't perfect but you | ||
# get the idea) | ||
random_number = random.random() | ||
app.logger.info(f"Node's random_number: {random_number}") | ||
if random_number < 1 / len(services): | ||
sleep_duration = 2 # Sleep for 2 seconds | ||
app.logger.info("This node is sleeping for %s seconds", sleep_duration) | ||
time.sleep(sleep_duration) | ||
|
||
current_service = request.headers.get("X-Current-Service", service_name) | ||
|
||
# check if the current service is valid and then set the index in the chain | ||
# to the current service | ||
if current_service and not is_valid_service(current_service): | ||
return make_response(jsonify({"message": "Invalid service"}), 400) | ||
elif current_service: | ||
app.logger.info("current_service: %s", current_service) | ||
index = services.index(current_service) | ||
else: | ||
index = 0 | ||
|
||
# if the current service is not the last service in the chain, then forward | ||
# the request to the next service in the chain | ||
if index + 1 < len(services): | ||
next_service = services[index + 1] | ||
app.logger.info("next_service: %s", next_service) | ||
headers = {"X-Current-Service": next_service} | ||
response = requests.get( | ||
f"http://{next_service}/forward", headers=headers, timeout=3 | ||
) | ||
return response.text, response.status_code | ||
else: | ||
return ( | ||
jsonify( | ||
{"message": f"You have reached the final chain link {current_service}"} | ||
), | ||
200, | ||
) | ||
|
||
|
||
# I just want a route named /forward :) | ||
@app.route("/forward", methods=["GET"]) | ||
def forward_request(): | ||
""" | ||
Forward the request to the next service in the chain | ||
""" | ||
return process_request() | ||
|
||
|
||
@app.route("/readiness", methods=["GET"]) | ||
def readiness(): | ||
""" | ||
Readiness probe | ||
""" | ||
return make_response(jsonify({"message": "ok"}), 200) | ||
|
||
|
||
# | ||
# Main | ||
# | ||
|
||
# if running out of gunicorn, use the gunicorn logger | ||
# https://trstringer.com/logging-flask-gunicorn-the-manageable-way/ | ||
if __name__ != "__main__": | ||
gunicorn_logger = logging.getLogger("gunicorn.error") | ||
app.logger.handlers = gunicorn_logger.handlers | ||
app.logger.setLevel(gunicorn_logger.level) | ||
|
||
if __name__ == "__main__": | ||
port = int(os.environ.get("FLASK_RUN_PORT", "8080")) | ||
app.logging.info("port: %s", port) | ||
app.run(host="0.0.0.0", port=port) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
#!/usr/bin/env python3 | ||
""" | ||
ChainLink CLI - Deploy and validate the chain-link application in to a Kubernetes cluster | ||
""" | ||
|
||
from cli.cli_manager import run_cli | ||
|
||
|
||
if __name__ == "__main__": | ||
run_cli() |
Empty file.
Oops, something went wrong.