├── .devcontainer └── chatbot │ └── devcontainer.json ├── .github ├── CODEOWNERS └── workflows │ ├── chatbot.yml │ └── weekly.yml ├── .gitignore ├── .vale.ini ├── .vale └── styles │ └── Flipt │ ├── Spelling.yml │ ├── TitleCase.yml │ └── spelling-exceptions.txt ├── LICENSE ├── README.md ├── chatbot ├── .dockerignore ├── Dockerfile ├── README.md ├── backend │ ├── Dockerfile │ ├── README.md │ ├── main.py │ ├── requirements.txt │ └── responses.py ├── docker-compose.advanced.yml ├── docker-compose.basic.yml ├── docker-compose.gitops.yml ├── frontend │ ├── .dockerignore │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── .prettierignore │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── postcss.config.cjs │ ├── prettier.config.cjs │ ├── public │ │ ├── favicon.svg │ │ ├── images │ │ │ ├── advanced │ │ │ │ ├── beta-segment.png │ │ │ │ ├── personas-flag.png │ │ │ │ └── personas-rule.png │ │ │ ├── basic │ │ │ │ ├── flags.png │ │ │ │ ├── new-flag.png │ │ │ │ └── update-flag.png │ │ │ ├── chatbot.png │ │ │ ├── gitops │ │ │ │ ├── disabled-flag.png │ │ │ │ ├── gitea-login.png │ │ │ │ ├── merge-pr-one.png │ │ │ │ └── pr-one.png │ │ │ └── logo.svg │ │ └── robots.txt │ ├── src │ │ ├── App.tsx │ │ ├── components │ │ │ ├── Banner.tsx │ │ │ ├── Button.tsx │ │ │ ├── Chat.tsx │ │ │ ├── ChatLine.tsx │ │ │ ├── ChatWindow.tsx │ │ │ ├── CodeBlock.tsx │ │ │ ├── Footer.tsx │ │ │ ├── Guide.tsx │ │ │ ├── InteractiveArrow.tsx │ │ │ ├── Notification.tsx │ │ │ ├── Page.tsx │ │ │ ├── Sidebar.tsx │ │ │ ├── Steps.tsx │ │ │ ├── User.tsx │ │ │ ├── UserProvider.tsx │ │ │ └── Well.tsx │ │ ├── content │ │ │ ├── intro.mdx │ │ │ └── modules │ │ │ │ ├── advanced │ │ │ │ ├── intro.mdx │ │ │ │ ├── step1.mdx │ │ │ │ ├── step2.mdx │ │ │ │ ├── step3.mdx │ │ │ │ ├── step4.mdx │ │ │ │ └── step5.mdx │ │ │ │ ├── basic │ │ │ │ ├── intro.mdx │ │ │ │ ├── step1.mdx │ │ │ │ ├── step2.mdx │ │ │ │ ├── step3.mdx │ │ │ │ ├── step4.mdx │ │ │ │ └── step5.mdx │ │ │ │ └── gitops │ │ │ │ ├── intro.mdx │ │ │ │ ├── step1.mdx │ │ │ │ ├── step2.mdx │ │ │ │ ├── step3.mdx │ │ │ │ ├── step4.mdx │ │ │ │ ├── step5.mdx │ │ │ │ ├── step6.mdx │ │ │ │ ├── step7.mdx │ │ │ │ └── step8.mdx │ │ ├── hooks │ │ │ ├── user.ts │ │ │ └── viewport.ts │ │ ├── index.css │ │ └── index.tsx │ ├── tailwind.config.cjs │ ├── tsconfig.json │ └── vite.config.ts ├── gitea │ ├── Dockerfile │ ├── entrypoint.sh │ ├── go.mod │ ├── go.sum │ └── provision │ │ ├── main.go │ │ ├── main │ │ └── features.yml │ │ ├── pr_one │ │ └── features.yml │ │ └── pr_two │ │ └── features.yml └── scripts │ ├── flipt-adv-init.sh │ └── start ├── common ├── grafana │ ├── dashboards │ │ ├── Flipt │ │ │ ├── flipt-evaluations.json │ │ │ └── flipt-system.json │ │ └── dashboards.yaml │ └── datasources │ │ └── prometheus.yml ├── prometheus │ └── prometheus.yml └── whipt │ ├── .dockeringore │ ├── Dockerfile │ ├── README.md │ ├── evaluations.example.json │ ├── go.mod │ ├── go.sum │ └── main.go ├── cup ├── argo │ ├── README.md │ ├── cmd │ │ ├── provision │ │ │ └── main.go │ │ └── service │ │ │ └── main.go │ ├── diagram.d2 │ ├── diagram.svg │ ├── go.mod │ ├── go.sum │ ├── internal │ │ ├── build │ │ │ └── build.go │ │ └── publish │ │ │ └── publish.go │ ├── kind-config.yml │ ├── manifests │ │ ├── argo.yml │ │ ├── cup.yml │ │ └── gitea.yml │ └── scripts │ │ └── start └── flipt │ ├── Dockerfile │ ├── README.md │ ├── docker-compose.yml │ ├── evaluations.json │ └── gitea │ ├── Dockerfile │ ├── entrypoint.sh │ ├── go.mod │ ├── go.sum │ └── provision │ ├── main.go │ ├── main │ └── features.yml │ ├── pr_one │ └── features.yml │ └── pr_two │ └── features.yml ├── images ├── chatbot.png └── cup.svg └── sidecar ├── README.md └── replication ├── diagrams ├── diagram-local.d2 ├── diagram-local.svg ├── diagram-object-store.d2 └── diagram-object-store.svg ├── flipt-aws └── Dockerfile ├── go ├── Dockerfile ├── go.mod ├── go.sum ├── index.html.tmpl └── main.go ├── manifests ├── local │ ├── flipt-job.yml │ └── flipt.yml └── object-store │ ├── flipt-job.yml │ ├── flipt.yml │ ├── minio-job.yml │ └── minio.yml └── scripts ├── start-local └── start-object-store /.devcontainer/chatbot/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/alpine 3 | { 4 | "name": "Flipt Labs - Chatbot", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "build": { 7 | "dockerfile": "../../chatbot/Dockerfile" 8 | }, 9 | "workspaceFolder": "/app", 10 | 11 | // Features to add to the dev container. More info: https://containers.dev/features. 12 | "features": { 13 | "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {} 14 | }, 15 | "customizations": { 16 | "vscode": { 17 | "extensions": ["ms-azuretools.vscode-docker"] 18 | } 19 | }, 20 | 21 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 22 | // "forwardPorts": [], 23 | 24 | // Use 'postCreateCommand' to run commands after the container is created. 25 | "postCreateCommand": "./scripts/start" 26 | 27 | // Configure tool-specific properties. 28 | // "customizations": {}, 29 | 30 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 31 | // "remoteUser": "root" 32 | } 33 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners will be the default owners for everything in 2 | # the repo. Unless a later match takes precedence, 3 | # @global-owner1 and @global-owner2 will be requested for 4 | # review when someone opens a pull request. 5 | * @flipt-io/maintainers -------------------------------------------------------------------------------- /.github/workflows/chatbot.yml: -------------------------------------------------------------------------------- 1 | name: Chatbot 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths: 7 | - "chatbot/**" 8 | pull_request: 9 | paths: 10 | - "chatbot/**" 11 | workflow_dispatch: 12 | 13 | jobs: 14 | chatbot-backend-lint: 15 | name: "Lint Python" 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v3 19 | - uses: rickstaa/action-black@v1 20 | with: 21 | black_args: "./chatbot/backend --check" 22 | 23 | chatbot-frontend-lint: 24 | name: "Lint Frontend" 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v3 28 | - uses: actions/setup-node@v3 29 | with: 30 | node-version: "18" 31 | cache: "npm" 32 | cache-dependency-path: chatbot/frontend/package-lock.json 33 | - run: | 34 | npm ci 35 | npm run lint 36 | working-directory: chatbot/frontend 37 | 38 | chatbot-vale: 39 | name: "Lint Markdown" 40 | runs-on: ubuntu-latest 41 | steps: 42 | - uses: actions/checkout@v3 43 | - uses: errata-ai/vale-action@reviewdog 44 | with: 45 | files: "./chatbot/frontend/src/content" 46 | -------------------------------------------------------------------------------- /.github/workflows/weekly.yml: -------------------------------------------------------------------------------- 1 | name: Weekly Build 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: "0 0 * * 1" # Every Monday at 12:00 AM UTC. 6 | 7 | jobs: 8 | chatbot-backend-release: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | packages: write 13 | 14 | steps: 15 | - name: Set up Docker Buildx 16 | uses: docker/setup-buildx-action@v2 17 | 18 | - name: Set up QEMU 19 | uses: docker/setup-qemu-action@v2 20 | 21 | - name: Login to GitHub Container Registry 22 | uses: docker/login-action@v2 23 | with: 24 | registry: ghcr.io 25 | username: "${{ github.repository_owner }}" 26 | password: ${{ secrets.GITHUB_TOKEN }} 27 | 28 | - name: "Chatbot Backend Docker Image" 29 | uses: docker/build-push-action@v4 30 | with: 31 | context: "{{defaultContext}}:chatbot/backend" 32 | platforms: linux/amd64,linux/arm64 33 | push: true 34 | tags: ghcr.io/flipt-io/labs/chatbot/backend:latest 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/venv/ 2 | **/features/ 3 | .vscode/ 4 | .vale/styles/* 5 | !.vale/styles/Flipt -------------------------------------------------------------------------------- /.vale.ini: -------------------------------------------------------------------------------- 1 | StylesPath = .vale/styles 2 | MinAlertLevel = error 3 | Packages = Microsoft 4 | 5 | [formats] 6 | mdx = md 7 | 8 | [*.md] 9 | BasedOnStyles = Flipt, Microsoft 10 | 11 | # set the level of the spelling to error 12 | Flipt.Spelling = error 13 | 14 | Microsoft.Avoid = warning 15 | Microsoft.Foreign = warning 16 | Microsoft.URLFormat = warning -------------------------------------------------------------------------------- /.vale/styles/Flipt/Spelling.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Warning: Flipt.Spelling 3 | # 4 | # Checks for possible spelling mistakes in content, not code. Results from links using angle brackets () should be corrected. 5 | # 6 | # If a word is flagged as a spelling mistake incorrectly, such as a product name, 7 | # you can submit an MR to update `spelling-exceptions.txt` with the missing word. 8 | # Commands, like `git clone` must use backticks, and must not be added to the 9 | # exceptions. 10 | # 11 | # For a list of all options, see https://vale.sh/docs/topics/styles/ 12 | extends: spelling 13 | message: "Check the spelling of '%s'. If the spelling is correct, add this word to the spelling exception list." 14 | level: warning 15 | ignore: 16 | - Flipt/spelling-exceptions.txt 17 | -------------------------------------------------------------------------------- /.vale/styles/Flipt/TitleCase.yml: -------------------------------------------------------------------------------- 1 | extends: capitalization 2 | message: "'%s' should be in title case" 3 | scope: heading 4 | level: error 5 | match: $title 6 | style: AP 7 | exceptions: 8 | - ^v([0-9]+)\.([0-9]+)\.([0-9]+) 9 | -------------------------------------------------------------------------------- /.vale/styles/Flipt/spelling-exceptions.txt: -------------------------------------------------------------------------------- 1 | chatbot 2 | chatbot's 3 | flipt 4 | flipt's 5 | gitea 6 | rollout 7 | yaml -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flipt Labs 🧪 2 | 3 | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/flipt-io/labs?skip_quickstart=true) 4 | 5 | This repository serves as a location to explore [Flipt](https://www.flipt.io) use cases and configurations. 6 | 7 | Each subdirectory is a self-contained application that can be run locally. 8 | 9 | We'll be adding more applications over time, so check back often! 10 | 11 | ## Applications 12 | 13 | - [Chatbot](./chatbot/README.md) - A set of tutorials on how to use Flipt in different scenarios, based on the idea of releasing an AI-powered chatbot. 14 | - [Cup](./cup/README.md) - A tutorial demonstrating the [Cup project](https://github.com/flipt-io/cup) managing different configuration formats. 15 | - [Sidecar](./sidecar/README.md) - Demonstrates how to use Flipt as a sidecar to your application for fast evaluations in various configurations. 16 | 17 | ## Usage 18 | 19 | 1. Clone this repository 20 | 1. `cd` into a subdirectory (e.g. `cd chatbot`) 21 | 1. Run `./scripts/start` or similar scripts to start the application 22 | 23 | ## Troubleshooting 24 | 25 | If you run into any issues, please [open an issue](https://github.com/flipt-io/labs/issues/new) and we'll get back to you as soon as we can. 26 | -------------------------------------------------------------------------------- /chatbot/.dockerignore: -------------------------------------------------------------------------------- 1 | frontend/node_modules 2 | frontend/build -------------------------------------------------------------------------------- /chatbot/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 2 | 3 | RUN apt-get update && apt-get install -y \ 4 | ca-certificates \ 5 | curl \ 6 | gnupg \ 7 | git 8 | 9 | # install more recent nodejs 10 | RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - 11 | RUN apt-get install -y nodejs 12 | 13 | # install docker from get.docker.com 14 | RUN curl -fsSL https://get.docker.com | bash - 15 | 16 | ADD . /app 17 | 18 | WORKDIR /app 19 | 20 | EXPOSE 3000 -------------------------------------------------------------------------------- /chatbot/README.md: -------------------------------------------------------------------------------- 1 | # Flipt Chatbot 2 | 3 | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new/flipt-io/labs?skip_quickstart=true&ref=main&devcontainer_path=.devcontainer%2Fchatbot%2Fdevcontainer.json) 4 | 5 | ![Flipt Chatbot](../images/chatbot.png) 6 | 7 | ## Overview 8 | 9 | This application is designed to be a set of interactive tutorials on how to use Flipt. It is intended to be both a learning tool and a reference implementation for how to use Flipt in a real application. 10 | 11 | In particular, this application demonstrates how to use Flipt to: 12 | 13 | - Use a simple feature flag to control the availability of the chatbot. 14 | - Use segmentation to determine which sentiment or persona the chatbot should use based on the user's username. 15 | - Use Git as a source of truth and leveraging GitOps for feature flag data showing how to use Flipt without a UI. 16 | 17 | The chatbot is designed to answer questions about Flipt. If you enable OpenAI support, by setting the `OPENAI_API_KEY` environment variable, you will get much better answers as the chatbot will use the OpenAI API to generate answers that are based on our live documentation () 18 | 19 | If you don't have an OpenAI API key, you can still use the chatbot, but the answers will be much more limited as they are based on a static set of answers that are included in the application. Make sure to checkout OpenAI's pricing page: 20 | 21 | ## Prerequisites 22 | 23 | - [Node.js](https://nodejs.org/en/download/) (v16 or higher) 24 | - [Docker](https://docs.docker.com/get-docker/) 25 | - [Docker Compose](https://docs.docker.com/compose/install/) 26 | - `OPENAI_API_KEY` environment variable set to your OpenAI API key. You can get one [here](https://beta.openai.com/). (optional) 27 | 28 | ## Usage 29 | 30 | 1. `cd` into this directory (e.g. `cd chatbot`) 31 | 1. Run `./scripts/start` 32 | 1. Open in your browser if it doesn't open automatically 33 | 1. Start with the 'Basic' tutorial, then move on to the 'Advanced' tutorial, etc 34 | 35 | ## Architecture 36 | 37 | TODO 38 | 39 | ## Troubleshooting 40 | 41 | If you run into any issues, please [open an issue](https://github.com/flipt-io/labs/issues/new&labels=chatbot) and we'll get back to you as soon as we can. 42 | -------------------------------------------------------------------------------- /chatbot/backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9 2 | 3 | RUN apt install git 4 | 5 | WORKDIR /app 6 | 7 | RUN git clone --depth 1 https://github.com/flipt-io/docs.git docs && rm -rf docs/.git 8 | 9 | COPY requirements.txt /app 10 | 11 | RUN pip install -r requirements.txt 12 | 13 | COPY *.py /app 14 | 15 | EXPOSE 9000 16 | 17 | CMD ["gunicorn", "--workers", "1", "--bind", "0.0.0.0:9000", "main:create_app()"] 18 | -------------------------------------------------------------------------------- /chatbot/backend/README.md: -------------------------------------------------------------------------------- 1 | # Running `backend` with a Virtual Environment 2 | 3 | Make sure to have python 3.9.x or higher installed. 4 | 5 | ## `python -m venv` 6 | 7 | Run the above command within the backend directory to create the virtual environment. 8 | 9 | The above command should have created a `venv` folder that will contain the necessary programs to activate your virtual environment as well as a place for installing `python` libs. 10 | 11 | ## `source venv/bin/activate` 12 | 13 | Run the above command to activate the virtual environment. You should see `(venv)` to the right of your terminal prompt, which indicates the virtual environment is now activated. 14 | 15 | ## `pip install -r requirements.txt` 16 | 17 | The above command will install all the needed `python` libs in order to successfully boot the program via `python main.py`. 18 | -------------------------------------------------------------------------------- /chatbot/backend/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==2.3.2 2 | Flask-Cors==3.0.10 3 | langchain==0.0.198 4 | numpy==1.24.3 5 | redis==4.5.5 6 | sentence_transformers==2.2.2 7 | gunicorn==20.1.0 8 | openai==0.27.8 9 | flipt==0.2.8 10 | -------------------------------------------------------------------------------- /chatbot/backend/responses.py: -------------------------------------------------------------------------------- 1 | flipt_responses = { 2 | "default": [ 3 | "Flipt is an open-source feature flagging solution with the repository hosted on GitHub. If you provide the OPENAI_API_KEY as an environment variable, you can get to know more about Flipt.", 4 | "Flipt supports segmentation based on constraints, allowing you to target specific user segments with different feature variants. If you provide the OPENAI_API_KEY as an environment variable, you can get to know more about Flipt.", 5 | "Flipt offers the ability to import and export flag state from various databases, including SQLite, MySQL, PostgreSQL, and CockroachDB. If you provide the OPENAI_API_KEY as an environment variable, you can get to know more about Flipt.", 6 | "Flipt provides official REST clients in multiple programming languages, including Golang, Node.js, Python, Java, and Rust. If you provide the OPENAI_API_KEY as an environment variable, you can get to know more about Flipt.", 7 | "Flipt has an official protobuf defintion which means any language that supports gRPC can generate a client from the definition. If you provide the OPENAI_API_KEY as an environment variable, you can get to know more about Flipt.", 8 | "Flipt allows you to serve multiple variants for each flag based on segments, enabling A/B testing and gradual feature rollouts. If you provide the OPENAI_API_KEY as an environment variable, you can get to know more about Flipt.", 9 | "Flipt has released a GitOps declarative approach of feature flagging state where flags and all its subelements are declared on a Git repository as of v1.23. If you provide the OPENAI_API_KEY as an environment variable, you can get to know more about Flipt.", 10 | 'Flipt encourages open source contributions, and provides a comprehensive "Getting Started" guide at this link: https://www.flipt.io/docs/introduction. If you provide the OPENAI_API_KEY as an environment variable, you can get to know more about Flipt.', 11 | "Flipt can be configured in various ways, and each piece of configuration can be found at this link: https://www.flipt.io/docs/configuration/overview. If you provide the OPENAI_API_KEY as an environment variable, you can get to know more about Flipt.", 12 | 'Flipt provides a top-level abstraction called "Namespaces" which can be used in many different ways, one of which includes Environments. If you provide the OPENAI_API_KEY as an environment variable, you can get to know more about Flipt.', 13 | ], 14 | "sarcastic": [ 15 | "Flipt is open source, which means the code should be stored in a Git repo right? Something you should know. If you are a developer you should know about environment variables right? Provide the OPENAI_API_KEY for better responses.", 16 | "Flipt can support segmentation. I mean how else would you have multi-variate responses? If you are a developer you should know about environment variables right? Provide the OPENAI_API_KEY for better responses.", 17 | "Flipt offers ability to import/export data, and it might be from a database, who knows? If you are a developer you should know about environment variables right? Provide the OPENAI_API_KEY for better responses.", 18 | "Flipt has an official list of REST clients, do not ask me about which languages, if you know how to code you are good to go, i think? If you are a developer you should know about environment variables right? Provide the OPENAI_API_KEY for better responses.", 19 | "Flipt loves protobuf for you to generate gRPC clients, so go at your own risk. You might fall in love with protobuf too? If you are a developer you should know about environment variables right? Provide the OPENAI_API_KEY for better responses.", 20 | "Flipt allows for A/B testing, you should have learned about this in college right? If you are a developer you should know about environment variables right? Provide the OPENAI_API_KEY for better responses.", 21 | "Flipt has GitOps features. It's a trendy word which you should care about too. If you are a developer you should know about environment variables right? Provide the OPENAI_API_KEY for better responses.", 22 | "Flipt encourages open source contributions. I mean which community doesn't? If you are a developer you should know about environment variables right? Provide the OPENAI_API_KEY for better responses.", 23 | "Flipt might be able to be configured in many different ways, but honestly just see for yourself. If you are a developer you should know about environment variables right? Provide the OPENAI_API_KEY for better responses.", 24 | "Flipt has namespaces as a concept but use it however you want, we do not care, just try the product and quit asking. If you are a developer you should know about environment variables right? Provide the OPENAI_API_KEY for better responses.", 25 | ], 26 | "liar": [ 27 | "Flipt is only a SaaS solution. Provide the environment variable AI_IS_COOL for better responses. Just kidding use OPENAI_API_KEY.", 28 | "Flipt has no ability to segment users. Provide the environment variable AI_IS_COOL for better responses. Just kidding use OPENAI_API_KEY.", 29 | "Flipt supports only flat files for data storage. Provide the environment variable AI_IS_COOL for better responses. Just kidding use OPENAI_API_KEY.", 30 | "Flipt is a SOAP API, please no REST or gRPC. Provide the environment variable AI_IS_COOL for better responses. Just kidding use OPENAI_API_KEY.", 31 | "Flipt only supports XML for data serializing, we have not got to protobuf yet. Provide the environment variable AI_IS_COOL for better responses. Just kidding use OPENAI_API_KEY.", 32 | "Flipt does not even know what A/B testing is. Provide the environment variable AI_IS_COOL for better responses. Just kidding use OPENAI_API_KEY.", 33 | "Flipt only allows manual edits to the UI in all releases, good luck with managing that at scale. Provide the environment variable AI_IS_COOL for better responses. Just kidding use OPENAI_API_KEY.", 34 | "Flipt does not encourage feedback in any capacity. Provide the environment variable AI_IS_COOL for better responses. Just kidding use OPENAI_API_KEY.", 35 | "Flipt can not be configured, it's a binary with no configuration. Provide the environment variable AI_IS_COOL for better responses. Just kidding use OPENAI_API_KEY.", 36 | 'Flipt can not be used for "environments" like other products. Provide the environment variable AI_IS_COOL for better responses. Just kidding use OPENAI_API_KEY.', 37 | ], 38 | } 39 | -------------------------------------------------------------------------------- /chatbot/docker-compose.advanced.yml: -------------------------------------------------------------------------------- 1 | version: "3.5" 2 | 3 | services: 4 | redis-stack: 5 | image: redis/redis-stack-server:7.2.0-RC1 6 | ports: 7 | - "6379:6379" 8 | 9 | backend: 10 | image: ghcr.io/flipt-io/labs/chatbot/backend:latest 11 | ports: 12 | - "9000:9000" 13 | environment: 14 | - REDIS_HOST=redis-stack 15 | - REDIS_PORT=6379 16 | - OPENAI_API_KEY=${OPENAI_API_KEY} 17 | - FLIPT_SERVER_ADDR=http://flipt:8080 18 | depends_on: 19 | - "redis-stack" 20 | 21 | flipt-init: 22 | image: flipt/flipt:latest 23 | command: | 24 | sh -c '/flipt-init.sh' 25 | volumes: 26 | - ./scripts/flipt-adv-init.sh:/flipt-init.sh 27 | - flipt-opt:/var/opt/flipt 28 | 29 | flipt: 30 | image: flipt/flipt:latest 31 | ports: 32 | - "8080:8080" 33 | environment: 34 | FLIPT_CORS_ENABLED: true 35 | volumes: 36 | - flipt-opt:/var/opt/flipt 37 | depends_on: 38 | flipt-init: 39 | condition: service_started 40 | 41 | volumes: 42 | flipt-opt: 43 | -------------------------------------------------------------------------------- /chatbot/docker-compose.basic.yml: -------------------------------------------------------------------------------- 1 | version: "3.5" 2 | 3 | services: 4 | redis-stack: 5 | image: redis/redis-stack-server:7.2.0-RC1 6 | ports: 7 | - "6379:6379" 8 | 9 | backend: 10 | image: ghcr.io/flipt-io/labs/chatbot/backend:latest 11 | ports: 12 | - "9000:9000" 13 | environment: 14 | - REDIS_HOST=redis-stack 15 | - REDIS_PORT=6379 16 | - OPENAI_API_KEY=${OPENAI_API_KEY} 17 | - FLIPT_SERVER_ADDR=http://flipt:8080 18 | depends_on: 19 | - "redis-stack" 20 | 21 | flipt: 22 | image: flipt/flipt:latest 23 | ports: 24 | - "8080:8080" 25 | environment: 26 | FLIPT_CORS_ENABLED: true 27 | -------------------------------------------------------------------------------- /chatbot/docker-compose.gitops.yml: -------------------------------------------------------------------------------- 1 | version: "3.5" 2 | 3 | services: 4 | redis-stack: 5 | image: redis/redis-stack-server:7.2.0-RC1 6 | ports: 7 | - "6379:6379" 8 | 9 | backend: 10 | image: ghcr.io/flipt-io/labs/chatbot/backend:latest 11 | ports: 12 | - "9000:9000" 13 | environment: 14 | REDIS_HOST: redis-stack 15 | REDIS_PORT: 6379 16 | OPENAI_API_KEY: ${OPENAI_API_KEY} 17 | FLIPT_SERVER_ADDR: http://flipt:8080 18 | depends_on: 19 | - "redis-stack" 20 | 21 | gitea: 22 | build: ./gitea 23 | init: true 24 | ports: 25 | - "3001:3000" 26 | 27 | flipt: 28 | image: flipt/flipt:latest 29 | ports: 30 | - "8080:8080" 31 | environment: 32 | FLIPT_CORS_ENABLED: true 33 | FLIPT_EXPERIMENTAL_FILESYSTEM_STORAGE_ENABLED: true 34 | FLIPT_STORAGE_TYPE: git 35 | FLIPT_STORAGE_GIT_REPOSITORY: http://gitea:3000/flipt/features.git 36 | FLIPT_STORAGE_GIT_POLL_INTERVAL: 5s 37 | FLIPT_STORAGE_GIT_AUTHENTICATION_BASIC_USERNAME: flipt 38 | FLIPT_STORAGE_GIT_AUTHENTICATION_BASIC_PASSWORD: password 39 | restart: always 40 | -------------------------------------------------------------------------------- /chatbot/frontend/.dockerignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .envrc 3 | node_modules 4 | dist -------------------------------------------------------------------------------- /chatbot/frontend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | }, 6 | extends: [ 7 | "eslint:recommended", 8 | "plugin:react/recommended", 9 | "plugin:react/jsx-runtime", 10 | "plugin:@typescript-eslint/recommended", 11 | "prettier", 12 | ], 13 | overrides: [ 14 | { 15 | env: { 16 | node: true, 17 | }, 18 | files: [".eslintrc.{js,cjs}"], 19 | parserOptions: { 20 | sourceType: "script", 21 | }, 22 | }, 23 | ], 24 | parser: "@typescript-eslint/parser", 25 | parserOptions: { 26 | ecmaVersion: "latest", 27 | sourceType: "module", 28 | project: "./tsconfig.json", 29 | tsConfigRootDir: __dirname, 30 | }, 31 | plugins: [ 32 | "@typescript-eslint", 33 | "react", 34 | "jsx-a11y", 35 | "import", 36 | "no-relative-import-paths", 37 | ], 38 | rules: { 39 | "no-relative-import-paths/no-relative-import-paths": [ 40 | "error", 41 | { allowSameFolder: true, rootDir: "src" }, 42 | ], 43 | }, 44 | settings: { 45 | react: { 46 | version: "detect", 47 | }, 48 | "import/parsers": { 49 | "@typescript-eslint/parser": [".ts", ".tsx"], 50 | }, 51 | "import/resolver": { 52 | typescript: { 53 | alwaysTryTypes: true, 54 | }, 55 | }, 56 | }, 57 | }; 58 | -------------------------------------------------------------------------------- /chatbot/frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | build -------------------------------------------------------------------------------- /chatbot/frontend/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build -------------------------------------------------------------------------------- /chatbot/frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Flipt Tutorial 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /chatbot/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatbot-frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "dependencies": { 7 | "@flipt-io/flipt": "^0.2.5", 8 | "@fortawesome/free-brands-svg-icons": "^6.4.0", 9 | "@fortawesome/react-fontawesome": "^0.2.0", 10 | "@headlessui/react": "^1.7.15", 11 | "@heroicons/react": "^2.0.18", 12 | "@mdx-js/loader": "^2.3.0", 13 | "@mdx-js/mdx": "^2.3.0", 14 | "@mdx-js/react": "^2.3.0", 15 | "@mdx-js/rollup": "^2.3.0", 16 | "@tailwindcss/forms": "^0.5.3", 17 | "@testing-library/jest-dom": "^5.16.5", 18 | "@testing-library/react": "^13.4.0", 19 | "@testing-library/user-event": "^13.5.0", 20 | "@types/jest": "^27.5.2", 21 | "@types/mdx": "^2.0.5", 22 | "@types/node": "^16.18.36", 23 | "@types/react": "^18.2.12", 24 | "@types/react-dom": "^18.2.5", 25 | "clsx": "^1.2.1", 26 | "highlight.js": "^11.8.0", 27 | "react": "^18.2.0", 28 | "react-dom": "^18.2.0", 29 | "react-router-dom": "^6.13.0", 30 | "react-scripts": "5.0.1", 31 | "typescript": "^5.1.3", 32 | "web-vitals": "^2.1.4" 33 | }, 34 | "scripts": { 35 | "start": "vite serve --port 3000", 36 | "build": "vite build", 37 | "lint": "eslint \"**/*.{ts,tsx}\"", 38 | "lint:fix": "eslint --fix \"**/*.{ts,tsx}\"" 39 | }, 40 | "browserslist": { 41 | "production": [ 42 | ">0.2%", 43 | "not dead", 44 | "not op_mini all" 45 | ], 46 | "development": [ 47 | "last 1 chrome version", 48 | "last 1 firefox version", 49 | "last 1 safari version" 50 | ] 51 | }, 52 | "devDependencies": { 53 | "@tailwindcss/typography": "^0.5.9", 54 | "@typescript-eslint/eslint-plugin": "^5.60.0", 55 | "@vitejs/plugin-react": "^4.0.1", 56 | "autoprefixer": "^10.4.14", 57 | "eslint": "^8.43.0", 58 | "eslint-config-prettier": "^8.8.0", 59 | "eslint-plugin-import": "^2.27.5", 60 | "eslint-plugin-jsx-a11y": "^6.7.1", 61 | "eslint-plugin-n": "^15.7.0", 62 | "eslint-plugin-no-relative-import-paths": "^1.5.2", 63 | "eslint-plugin-promise": "^6.1.1", 64 | "eslint-plugin-react": "^7.32.2", 65 | "postcss": "^8.4.24", 66 | "prettier": "^2.8.8", 67 | "prettier-plugin-tailwindcss": "^0.3.0", 68 | "tailwindcss": "^3.3.2", 69 | "ts-node": "^10.9.1", 70 | "typescript": "^5.1.3", 71 | "vite": "^4.3.9", 72 | "vite-plugin-html": "^3.2.0" 73 | }, 74 | "overrides": { 75 | "react-scripts": { 76 | "typescript": "^5" 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /chatbot/frontend/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /chatbot/frontend/prettier.config.cjs: -------------------------------------------------------------------------------- 1 | // prettier.config.js 2 | module.exports = { 3 | plugins: [require("prettier-plugin-tailwindcss")], 4 | }; 5 | -------------------------------------------------------------------------------- /chatbot/frontend/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /chatbot/frontend/public/images/advanced/beta-segment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flipt-io/labs/1f61b4852b90c157aa2d34874431dd64b6934150/chatbot/frontend/public/images/advanced/beta-segment.png -------------------------------------------------------------------------------- /chatbot/frontend/public/images/advanced/personas-flag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flipt-io/labs/1f61b4852b90c157aa2d34874431dd64b6934150/chatbot/frontend/public/images/advanced/personas-flag.png -------------------------------------------------------------------------------- /chatbot/frontend/public/images/advanced/personas-rule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flipt-io/labs/1f61b4852b90c157aa2d34874431dd64b6934150/chatbot/frontend/public/images/advanced/personas-rule.png -------------------------------------------------------------------------------- /chatbot/frontend/public/images/basic/flags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flipt-io/labs/1f61b4852b90c157aa2d34874431dd64b6934150/chatbot/frontend/public/images/basic/flags.png -------------------------------------------------------------------------------- /chatbot/frontend/public/images/basic/new-flag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flipt-io/labs/1f61b4852b90c157aa2d34874431dd64b6934150/chatbot/frontend/public/images/basic/new-flag.png -------------------------------------------------------------------------------- /chatbot/frontend/public/images/basic/update-flag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flipt-io/labs/1f61b4852b90c157aa2d34874431dd64b6934150/chatbot/frontend/public/images/basic/update-flag.png -------------------------------------------------------------------------------- /chatbot/frontend/public/images/chatbot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flipt-io/labs/1f61b4852b90c157aa2d34874431dd64b6934150/chatbot/frontend/public/images/chatbot.png -------------------------------------------------------------------------------- /chatbot/frontend/public/images/gitops/disabled-flag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flipt-io/labs/1f61b4852b90c157aa2d34874431dd64b6934150/chatbot/frontend/public/images/gitops/disabled-flag.png -------------------------------------------------------------------------------- /chatbot/frontend/public/images/gitops/gitea-login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flipt-io/labs/1f61b4852b90c157aa2d34874431dd64b6934150/chatbot/frontend/public/images/gitops/gitea-login.png -------------------------------------------------------------------------------- /chatbot/frontend/public/images/gitops/merge-pr-one.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flipt-io/labs/1f61b4852b90c157aa2d34874431dd64b6934150/chatbot/frontend/public/images/gitops/merge-pr-one.png -------------------------------------------------------------------------------- /chatbot/frontend/public/images/gitops/pr-one.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flipt-io/labs/1f61b4852b90c157aa2d34874431dd64b6934150/chatbot/frontend/public/images/gitops/pr-one.png -------------------------------------------------------------------------------- /chatbot/frontend/public/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /chatbot/frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /chatbot/frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet, RouterProvider } from "react-router-dom"; 2 | import { createBrowserRouter } from "react-router-dom"; 3 | import Sidebar from "./components/Sidebar"; 4 | import Page from "./components/Page"; 5 | import Guide from "./components/Guide"; 6 | import Footer from "./components/Footer"; 7 | import { useEffect, useState } from "react"; 8 | import Notification from "./components/Notification"; 9 | import Banner from "./components/Banner"; 10 | import { Chat } from "./components/Chat"; 11 | import ChatWindow from "./components/ChatWindow"; 12 | import { FliptApiClient } from "@flipt-io/flipt"; 13 | import { useViewport } from "~/hooks/viewport"; 14 | 15 | const BasicGuide = () => ; 16 | const AdvancedGuide = () => ( 17 | 18 | ); 19 | const GitOpsGuide = () => ; 20 | 21 | const router = createBrowserRouter([ 22 | { 23 | path: "/", 24 | element: , 25 | children: [ 26 | { 27 | index: true, 28 | element: ( 29 | <> 30 |
31 | 32 |
33 | 34 | ), 35 | }, 36 | { 37 | path: "/basic/:step", 38 | element: , 39 | }, 40 | { 41 | path: "/basic", 42 | element: , 43 | }, 44 | { 45 | path: "/advanced/:step", 46 | element: , 47 | }, 48 | { 49 | path: "/advanced", 50 | element: , 51 | }, 52 | { 53 | path: "/gitops/:step", 54 | element: , 55 | }, 56 | { 57 | path: "/gitops", 58 | element: , 59 | }, 60 | ], 61 | }, 62 | ]); 63 | 64 | function Layout() { 65 | const [missingOpenAIKey, setMissingAIKey] = useState(false); 66 | 67 | const { width } = useViewport(); 68 | const sidebarBreakpoint = 1024; 69 | 70 | const [collapsedSidebar, setCollapsedSidebar] = useState( 71 | width < sidebarBreakpoint 72 | ); 73 | 74 | const [chatEnabled, setChatEnabled] = useState(false); 75 | 76 | const client = new FliptApiClient({ 77 | environment: "http://localhost:8080", 78 | }); 79 | 80 | useEffect(() => { 81 | if (width < sidebarBreakpoint) { 82 | setCollapsedSidebar(true); 83 | } 84 | }, [width]); 85 | 86 | useEffect(() => { 87 | const checkChatEnabled = async () => { 88 | try { 89 | const flag = await client.flags.get("default", "chat-enabled"); 90 | setChatEnabled(flag.enabled); 91 | } catch (e) { 92 | // ignore 93 | } 94 | }; 95 | 96 | checkChatEnabled(); 97 | const interval = setInterval(() => { 98 | checkChatEnabled(); 99 | }, 5000); 100 | 101 | return () => clearInterval(interval); 102 | }, []); 103 | 104 | useEffect(() => { 105 | // fetch from /config endpoint on backend to get config 106 | // and check if openai_api_key is set 107 | fetch("http://localhost:9000/config") 108 | .then((response) => { 109 | if (!response.ok) { 110 | throw new Error( 111 | `HTTP error! status: ${response.status}; ${response.text}` 112 | ); 113 | } 114 | return response.json(); 115 | }) 116 | .then((data) => { 117 | if (!data.openai_api_key || data.openai_api_key === "") { 118 | setMissingAIKey(true); 119 | } 120 | }) 121 | .catch((error) => { 122 | console.error(error); 123 | }); 124 | }, []); 125 | 126 | return ( 127 |
128 | 132 | 133 |
134 | 138 |
139 |
140 | {missingOpenAIKey && ( 141 | 142 | <> 143 | OPENAI_API_KEY environment variable not set. Set 144 | before continuing for a better experience. 145 | 146 | 147 | )} 148 | 149 |
150 |
151 |
152 |
153 | 154 | {chatEnabled && ( 155 | 156 | 157 | 158 | )} 159 |
160 | ); 161 | } 162 | 163 | function App() { 164 | return ; 165 | } 166 | 167 | export default App; 168 | -------------------------------------------------------------------------------- /chatbot/frontend/src/components/Banner.tsx: -------------------------------------------------------------------------------- 1 | import { XMarkIcon } from "@heroicons/react/20/solid"; 2 | import { useState } from "react"; 3 | 4 | type BannerProps = { 5 | className?: string; 6 | text: string; 7 | }; 8 | 9 | export default function Banner(props: BannerProps) { 10 | const { className, text } = props; 11 | const [show, setShow] = useState(true); 12 | 13 | return ( 14 | <> 15 | {show && ( 16 |
19 |

20 | 21 | Note:  22 | {text} 23 | 24 |

25 |
26 | 34 |
35 |
36 | )} 37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /chatbot/frontend/src/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | 3 | export default function Button({ className, ...props }: any) { 4 | return ( 5 | 43 | 44 | ); 45 | 46 | export function Chat() { 47 | const [messages, setMessages] = useState(initialMessages); 48 | const [input, setInput] = useState(""); 49 | const [loading, setLoading] = useState(false); 50 | const { user } = useUser(); 51 | 52 | const sendMessage = async (input: string) => { 53 | setLoading(true); 54 | const newMessages = [ 55 | ...messages, 56 | { role: "user", content: input } as ChatGPTMessage, 57 | ]; 58 | setMessages(newMessages); 59 | 60 | const response = await fetch("http://localhost:9000/chat", { 61 | method: "POST", 62 | headers: { 63 | "Content-Type": "application/json", 64 | }, 65 | body: JSON.stringify({ 66 | user: user, 67 | prompt: input, 68 | }), 69 | }); 70 | 71 | if (!response.ok) { 72 | throw new Error( 73 | `HTTP error! status: ${response.status}; ${response.text}` 74 | ); 75 | } 76 | 77 | const data = await response.json(); 78 | 79 | setMessages([ 80 | ...newMessages, 81 | { 82 | role: "assistant", 83 | content: data.response, 84 | persona: data.persona, 85 | } as ChatGPTMessage, 86 | ]); 87 | 88 | setLoading(false); 89 | }; 90 | 91 | return ( 92 |
93 |
94 |
95 | {messages.map(({ content, role, persona }, index) => ( 96 | 102 | ))} 103 | 104 | {loading && } 105 |
106 | 107 |
108 | 113 |
114 |
115 |
116 | ); 117 | } 118 | -------------------------------------------------------------------------------- /chatbot/frontend/src/components/ChatLine.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | 3 | type ChatGPTAgent = "user" | "system" | "assistant"; 4 | 5 | type ChatGPTPersona = "default" | "sarcastic" | "liar"; 6 | 7 | export interface ChatGPTMessage { 8 | role: ChatGPTAgent; 9 | content: string; 10 | persona?: ChatGPTPersona; 11 | } 12 | 13 | // loading placeholder animation for the chat line 14 | export const LoadingChatLine = () => ( 15 |
16 |
17 |
18 |

19 | AI 20 |

21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | ); 32 | 33 | // util helper to convert new lines to
tags 34 | const convertNewLines = (text: string) => 35 | text.split("\n").map((line, i) => ( 36 | 37 | {line} 38 |
39 |
40 | )); 41 | 42 | export function ChatLine({ 43 | role = "assistant", 44 | content, 45 | persona, 46 | }: ChatGPTMessage) { 47 | if (!content) { 48 | return null; 49 | } 50 | const formattedMessage = convertNewLines(content); 51 | 52 | return ( 53 |
60 |
66 |
67 |
68 |

69 | 70 | {role === "assistant" ? "AI" : "You"} 71 | 72 |

73 |

81 | 82 | {persona === "sarcastic" && role === "assistant" ? "😏" : ""} 83 | {persona === "liar" && role === "assistant" ? "😈" : ""} 84 | 85 | {formattedMessage} 86 |

87 |
88 |
89 |
90 |
91 | ); 92 | } 93 | -------------------------------------------------------------------------------- /chatbot/frontend/src/components/ChatWindow.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type ChatWindowProps = { 4 | children: React.ReactNode; 5 | }; 6 | 7 | export default function ChatWindow(props: ChatWindowProps) { 8 | const { children } = props; 9 | 10 | return ( 11 |
12 |
13 |
14 |
{children}
15 |
16 |
17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /chatbot/frontend/src/components/CodeBlock.tsx: -------------------------------------------------------------------------------- 1 | import { CheckIcon, ClipboardDocumentIcon } from "@heroicons/react/24/outline"; 2 | import clsx from "clsx"; 3 | import { useRef, useState } from "react"; 4 | 5 | export default function Code( 6 | props: React.JSX.IntrinsicAttributes & 7 | React.ClassAttributes & 8 | React.HTMLAttributes 9 | ) { 10 | const ref = useRef(null); 11 | const [copied, setCopied] = useState(false); 12 | 13 | const copyTextToClipboard = (text: string) => { 14 | if ("clipboard" in navigator) { 15 | return navigator.clipboard.writeText(text); 16 | } else { 17 | return document.execCommand("copy", true, text); 18 | } 19 | }; 20 | 21 | return ( 22 |
23 |
28 | 
29 |       
53 |     
54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /chatbot/frontend/src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | faDiscord, 3 | faGithub, 4 | faMastodon, 5 | faTwitter, 6 | } from "@fortawesome/free-brands-svg-icons"; 7 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 8 | 9 | export default function Footer() { 10 | const social = [ 11 | { 12 | name: "Twitter", 13 | href: "https://www.twitter.com/flipt_io", 14 | icon: faTwitter, 15 | }, 16 | { 17 | name: "Mastadon", 18 | href: "https://www.hachyderm.io/@flipt", 19 | icon: faMastodon, 20 | }, 21 | { 22 | name: "GitHub", 23 | href: "https://www.github.com/flipt-io/flipt", 24 | icon: faGithub, 25 | }, 26 | { 27 | name: "Discord", 28 | href: "https://www.flipt.io/discord", 29 | icon: faDiscord, 30 | }, 31 | ]; 32 | 33 | return ( 34 |
35 |
36 |

37 | 38 | © {new Date().getFullYear()} Flipt Software Inc. All rights 39 | reserved. 40 | 41 |

42 | 43 | {social.map((item) => ( 44 | 49 | {item.name} 50 | 55 | 56 | ))} 57 | 58 |
59 |
60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /chatbot/frontend/src/components/Guide.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import Button from "./Button"; 3 | import Steps from "./Steps"; 4 | import Page from "./Page"; 5 | import { useParams, useNavigate } from "react-router-dom"; 6 | 7 | type GuideProps = { 8 | module: string; 9 | steps: number; 10 | next?: string; 11 | }; 12 | 13 | export default function Guide(props: GuideProps) { 14 | const { module, steps, next } = props; 15 | 16 | const params = useParams(); 17 | const navigate = useNavigate(); 18 | 19 | useEffect(() => { 20 | if (params.step) { 21 | currentStep = parseInt(params.step); 22 | if (currentStep < 1 || currentStep > steps) { 23 | navigate(`/${module}`); 24 | return; 25 | } 26 | } 27 | }, [params.step]); 28 | 29 | let page = "intro"; 30 | let currentStep = 0; 31 | 32 | if (params.step) { 33 | currentStep = parseInt(params.step); 34 | 35 | if (currentStep > 0) { 36 | page = `step${currentStep}`; 37 | } 38 | } 39 | 40 | const path = `modules/${module}/${page}`; 41 | 42 | return ( 43 | <> 44 |
45 | 46 | 47 |
48 |
49 | 50 |
51 |
52 | 65 | {currentStep >= steps && next && ( 66 | 74 | )} 75 | {(currentStep < steps || !next) && ( 76 | 85 | )} 86 |
87 |
88 |
89 | 90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /chatbot/frontend/src/components/InteractiveArrow.tsx: -------------------------------------------------------------------------------- 1 | import { ArrowDownCircleIcon } from "@heroicons/react/24/outline"; 2 | 3 | type InteractiveArrowProps = { 4 | subheading?: string; 5 | }; 6 | 7 | export default function InteractiveArrow(props: InteractiveArrowProps) { 8 | const { subheading } = props; 9 | 10 | return ( 11 |
12 |

13 | This is an interactive component 14 |

15 | {subheading &&

{subheading}

} 16 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /chatbot/frontend/src/components/Notification.tsx: -------------------------------------------------------------------------------- 1 | import { Fragment, useState } from "react"; 2 | import { Transition } from "@headlessui/react"; 3 | import { 4 | CheckCircleIcon, 5 | XCircleIcon, 6 | ExclamationCircleIcon, 7 | } from "@heroicons/react/24/outline"; 8 | import { XMarkIcon } from "@heroicons/react/20/solid"; 9 | 10 | export type NotificationProps = { 11 | title: string; 12 | children: React.ReactNode; 13 | type: "success" | "error" | "warning"; 14 | }; 15 | 16 | export default function Notification(props: NotificationProps) { 17 | const [show, setShow] = useState(true); 18 | const { title, children, type } = props; 19 | 20 | return ( 21 | <> 22 | {/* Global notification live region, render this permanently at the end of the document */} 23 |
27 |
28 | {/* Notification panel, dynamically insert this into the live region when it needs to be displayed */} 29 | 39 |
40 |
41 |
42 |
43 | {type === "success" && ( 44 |
62 |
63 |

{title}

64 |

{children}

65 |
66 |
67 | 77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | 85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /chatbot/frontend/src/components/Page.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/alt-text */ 2 | /* eslint-disable jsx-a11y/anchor-has-content */ 3 | /* eslint-disable jsx-a11y/heading-has-content */ 4 | /* eslint-disable import/no-webpack-loader-syntax */ 5 | /* eslint-disable @typescript-eslint/no-var-requires */ 6 | import { useEffect, lazy, Suspense } from "react"; 7 | import "highlight.js/styles/github-dark.css"; 8 | import hljs from "highlight.js"; 9 | import CodeBlock from "./CodeBlock"; 10 | 11 | const components = { 12 | h1: ( 13 | props: React.JSX.IntrinsicAttributes & 14 | React.ClassAttributes & 15 | React.HTMLAttributes 16 | ) => ( 17 |

21 | ), 22 | h2: ( 23 | props: React.JSX.IntrinsicAttributes & 24 | React.ClassAttributes & 25 | React.HTMLAttributes 26 | ) =>

, 27 | h3: ( 28 | props: React.JSX.IntrinsicAttributes & 29 | React.ClassAttributes & 30 | React.HTMLAttributes 31 | ) =>

, 32 | h4: ( 33 | props: React.JSX.IntrinsicAttributes & 34 | React.ClassAttributes & 35 | React.HTMLAttributes 36 | ) =>

, 37 | h5: ( 38 | props: React.JSX.IntrinsicAttributes & 39 | React.ClassAttributes & 40 | React.HTMLAttributes 41 | ) =>
, 42 | h6: ( 43 | props: React.JSX.IntrinsicAttributes & 44 | React.ClassAttributes & 45 | React.HTMLAttributes 46 | ) =>
, 47 | p: ( 48 | props: React.JSX.IntrinsicAttributes & 49 | React.ClassAttributes & 50 | React.HTMLAttributes 51 | ) =>

, 52 | a: ( 53 | props: React.JSX.IntrinsicAttributes & 54 | React.ClassAttributes & 55 | React.AnchorHTMLAttributes 56 | ) => ( 57 | 58 | ), 59 | ul: ( 60 | props: React.JSX.IntrinsicAttributes & 61 | React.ClassAttributes & 62 | React.HTMLAttributes 63 | ) =>

    , 64 | ol: ( 65 | props: React.JSX.IntrinsicAttributes & 66 | React.ClassAttributes & 67 | React.OlHTMLAttributes 68 | ) =>
      , 69 | blockquote: ( 70 | props: React.JSX.IntrinsicAttributes & 71 | React.ClassAttributes & 72 | React.BlockquoteHTMLAttributes 73 | ) => ( 74 |
      78 | ), 79 | code: ( 80 | props: React.JSX.IntrinsicAttributes & 81 | React.ClassAttributes & 82 | React.HTMLAttributes 83 | ) => ( 84 | 88 | ), 89 | pre: ( 90 | props: React.JSX.IntrinsicAttributes & 91 | React.ClassAttributes & 92 | React.HTMLAttributes 93 | ) => , 94 | img: ( 95 | props: React.JSX.IntrinsicAttributes & 96 | React.ClassAttributes & 97 | React.ImgHTMLAttributes 98 | ) => , 99 | strong: ( 100 | props: React.JSX.IntrinsicAttributes & 101 | React.ClassAttributes & 102 | React.HTMLAttributes 103 | ) => , 104 | }; 105 | 106 | type PageProps = { 107 | path: string; 108 | }; 109 | 110 | export default function Page(props: PageProps) { 111 | const { path } = props; 112 | const comps = import.meta.glob("../content/**/*.mdx"); 113 | const match = comps[`../content/${path}.mdx`]; 114 | const InternalPage = lazy(async () => await match()); 115 | 116 | useEffect(() => { 117 | window.scrollTo(0, 0); 118 | hljs.highlightAll(); 119 | }); 120 | 121 | return ( 122 | Loading...}> 123 | 124 | 125 | ); 126 | } 127 | -------------------------------------------------------------------------------- /chatbot/frontend/src/components/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ChevronDoubleLeftIcon, 3 | ChevronDoubleRightIcon, 4 | } from "@heroicons/react/24/outline"; 5 | import clsx from "clsx"; 6 | import { NavLink } from "react-router-dom"; 7 | 8 | const navigation = [ 9 | { name: "Basic", to: "/basic" }, 10 | { name: "Advanced", to: "/advanced" }, 11 | { name: "GitOps", to: "/gitops" }, 12 | ]; 13 | 14 | type SidebarProps = { 15 | collapsed: boolean; 16 | setCollapsed: (_collapsed: boolean) => void; 17 | }; 18 | 19 | export default function Sidebar(props: SidebarProps) { 20 | const { collapsed, setCollapsed } = props; 21 | 22 | const Icon = collapsed ? ChevronDoubleRightIcon : ChevronDoubleLeftIcon; 23 | return ( 24 | <> 25 |
      31 |
      32 |
      38 | {!collapsed && ( 39 | 40 | Flipt 45 |

      46 | Flipt Labs 47 |

      48 |
      49 | )} 50 | 58 |
      59 | 60 | {!collapsed && ( 61 | 96 | )} 97 |
      98 |
      99 | 100 | ); 101 | } 102 | -------------------------------------------------------------------------------- /chatbot/frontend/src/components/Steps.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | 3 | type StepProps = { 4 | module: string; 5 | currentStep: number; 6 | steps: number; 7 | }; 8 | 9 | export default function Steps(props: StepProps) { 10 | const { module, currentStep, steps } = props; 11 | return ( 12 |
      82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /chatbot/frontend/src/components/User.tsx: -------------------------------------------------------------------------------- 1 | import Button from "./Button"; 2 | import { UserIcon } from "@heroicons/react/24/outline"; 3 | import { useUser } from "~/hooks/user"; 4 | import { useState } from "react"; 5 | import { CheckIcon } from "@heroicons/react/24/outline"; 6 | 7 | export default function User() { 8 | const { user, setUser } = useUser(); 9 | const [submitting, setSubmitting] = useState(false); 10 | 11 | const handleSubmit = async (e: any) => { 12 | e.preventDefault(); 13 | setSubmitting(true); 14 | setTimeout(() => { 15 | setSubmitting(false); 16 | }, 2000); 17 | }; 18 | 19 | return ( 20 |
      21 |
      22 |
      23 |
      24 |
      25 |
      26 |
      31 | setUser(e.target.value)} 37 | className="block w-full rounded border-0 py-1.5 pl-10 pr-12 text-gray-900 shadow-sm shadow-white/50 ring-1 ring-inset ring-violet-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" 38 | placeholder="username" 39 | /> 40 | {submitting && ( 41 |
      42 | 47 |
      48 | )} 49 |
      50 | 56 |
      57 |
      58 |
      59 |
      60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /chatbot/frontend/src/components/UserProvider.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-empty-function */ 2 | import React, { createContext, useState } from "react"; 3 | 4 | interface UserContextType { 5 | user: string; 6 | setUser: (user: string) => void; 7 | } 8 | 9 | export const UserContext = createContext({ 10 | user: "", 11 | setUser: () => {}, 12 | }); 13 | 14 | export default function UserProvider({ 15 | children, 16 | }: { 17 | children: React.ReactNode; 18 | }) { 19 | const [user, setUser] = useState(""); 20 | 21 | return ( 22 | 23 | {children} 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /chatbot/frontend/src/components/Well.tsx: -------------------------------------------------------------------------------- 1 | type WellProps = { 2 | children: React.ReactNode; 3 | }; 4 | 5 | export default function Well(props: WellProps) { 6 | const { children } = props; 7 | 8 | return ( 9 |
      10 |
      {children}
      11 |
      12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /chatbot/frontend/src/content/intro.mdx: -------------------------------------------------------------------------------- 1 | # Flipt Chatbot 2 | 3 | ![Flipt Chatbot](/images/chatbot.png) 4 | 5 | This application is designed to be a set of interactive tutorials on how to use [Flipt](https://www.flipt.io). 6 | 7 | It's intended to be both a learning tool and a reference implementation for how to use Flipt in a real application. 8 | 9 | In particular, this application demonstrates how to use Flipt to: 10 | 11 | - Use a simple feature flag to control the availability of the chatbot. 12 | - Use segmentation to determine which sentiment or persona the chatbot should use based on the user's username. 13 | - Use Git as a source of truth and leverage our declarative configuration format for feature flag data, unlocking version control, peer review and authorization capabilities. 14 | 15 | The chatbot is designed to answer questions about Flipt. If you enable OpenAI support, by setting the `OPENAI_API_KEY` environment variable, you will get much better answers as the chatbot will use the OpenAI API to generate answers that are based on our live documentation ([https://flipt.io/docs](https://flipt.io/docs)) 16 | 17 | If you don't have an OpenAI API key, you can still use the chatbot, but the answers will be much more limited as they're based on a static set of answers that are included in the application. Make sure to checkout OpenAI's pricing page: [https://beta.openai.com/pricing](https://beta.openai.com/pricing). 18 | 19 | ## Structure 20 | 21 | This repository is broken down into a few modules to help you get started learning Flipt: 22 | 23 | 1. [Basic](/basic) - Learn the basics of Flipt and how to use it to toggle the chatbot UI on and off. 24 | 2. [Advanced](/advanced) - Learn how to use Flipt to experiment with different chatbot sentiments. 25 | 3. [GitOps](/gitops) - Learn how to use GitOps to manage your feature flags without clicking around in the UI at all. 26 | 27 | ## Prerequisites 28 | 29 | In order to complete these modules, you'll need the following installed on your machine: 30 | 31 | - [Node.js](https://nodejs.org/en/download/) (v16 or higher) 32 | - [Docker](https://docs.docker.com/get-docker/) 33 | - [Docker Compose](https://docs.docker.com/compose/install/) 34 | - [Git](https://git-scm.com/) 35 | -------------------------------------------------------------------------------- /chatbot/frontend/src/content/modules/advanced/intro.mdx: -------------------------------------------------------------------------------- 1 | # Experiment With Personas 2 | 3 | This tutorial builds on the [Basic](/basic) tutorial. If you haven't completed that module yet, please do so before continuing. 4 | 5 | Now that we've learned the basics of Flipt, let's get a bit more advanced. 6 | 7 | We're going to assume that the rollout of the chatbot UI has been successful and we've ready to enable it for all users. 8 | 9 | We now want to be able to test out different personas for the chatbot, but we want to limit this to only trusted users. We also want to make sure that the same user always gets the same sentiment of chat response. 10 | 11 | We'll do this by creating a new feature flag called `chat-personas` to enable this experience. We'll also create variants for each persona and a rule to determine which user gets which one. 12 | 13 | ## What You'll Learn 14 | 15 | This module will show you how to: 16 | 17 | - Create a new segment to represent our test user base 18 | - Create variants to represent the different chatbot personas 19 | - Create a rule to evaluate which user gets which persona 20 | - Return a variant based on the context of the user through evaluation 21 | 22 | Let's get started with some more advanced Flipt concepts! 23 | 24 | Click 'Next' to continue. 25 | -------------------------------------------------------------------------------- /chatbot/frontend/src/content/modules/advanced/step1.mdx: -------------------------------------------------------------------------------- 1 | import Well from "~/components/Well"; 2 | 3 | # Evaluation 4 | 5 | In this tutorial, we will make use of a concept in Flipt called [evaluation](https://www.flipt.io/docs/concepts#evaluation). 6 | 7 | This will allow us to evaluate each request from our application and determine which persona or sentiment our chatbot should have. 8 | Evaluation is a powerful concept in Flipt that allows you to create complex rules for your flags. 9 | 10 | Because we want to keep this tutorial simple, we will only be creating a single rule. 11 | We will also use some pre-populated data include the flag, [variants](https://www.flipt.io/docs/concepts#variants) and the [segment](https://www.flipt.io/docs/concepts#segments) in order to reduce the amount of clicking required in the Flipt UI. 12 | 13 | ## Setup 14 | 15 | 16 | **Required**: In the `chatbot` folder, run the following command to start the Flipt server and our other backend services required for this tutorial: 17 | 18 | ```bash 19 | docker-compose -f docker-compose.advanced.yml up 20 | ``` 21 | 22 | 23 | 24 | Once initialized, the Flipt server will be running on port `8080`. 25 | 26 | Click 'Next' to continue. 27 | -------------------------------------------------------------------------------- /chatbot/frontend/src/content/modules/advanced/step2.mdx: -------------------------------------------------------------------------------- 1 | # Segments and Constraints 2 | 3 | [Segments](https://www.flipt.io/docs/concepts#segments) are a way to group users together. You can create segments based on any user attribute. For example, you can create a segment for users who are in the 'beta' group. 4 | 5 | In fact, this is exactly what we've done for this demo: we've created a segment called 'Beta'. 6 | 7 | 1. Click the link [http://localhost:8080/](http://localhost:8080/) to open the Flipt UI 8 | 2. Click on the 'Segments' tab in the left navigation bar 9 | 3. Click on the 'Beta' segment to view the details: 10 | 11 | ![Beta Segment](/images/advanced/beta-segment.png) 12 | 13 | Scrolling down, you can see that the segment contains no constraints. 14 | 15 | [Constraints](https://www.flipt.io/docs/concepts#constraints) are used to check if a request matches a specific segment or not. By default a request will match a segment if there are no constraints. 16 | 17 | Click 'Next' to continue. 18 | -------------------------------------------------------------------------------- /chatbot/frontend/src/content/modules/advanced/step3.mdx: -------------------------------------------------------------------------------- 1 | # Variants and Rules 2 | 3 | Click the link [http://localhost:8080/](http://localhost:8080/) to open the Flipt UI. 4 | 5 | Click on the `chat-personas` flag that we've created. 6 | 7 | ![chat-personas Flag](/images/advanced/personas-flag.png) 8 | 9 | Scroll down to the 'Variants' section and notice that we've pre-populated three variants: 10 | 11 | - `default` 12 | - `sarcastic` 13 | - `liar` 14 | 15 | These variants are the values that we will use to determine which sentiment our chatbot has when communication with our users. 16 | 17 | Next, click on the 'Evaluation' tab to see the rule that we've pre-populated. 18 | 19 | ![chat-personas Rule](/images/advanced/personas-rule.png) 20 | 21 | This rule will be used to determine which variant to use for a given user. 22 | 23 | Notice that there is about a 33% chance for each variant to be selected. 24 | 25 | The `entityID` of the evaluation request is used to determine which variant is returned. This is used to ensure that the same user always gets the same variant. 26 | 27 | More on this in the next step. 28 | 29 | Click 'Next' to continue. 30 | -------------------------------------------------------------------------------- /chatbot/frontend/src/content/modules/advanced/step4.mdx: -------------------------------------------------------------------------------- 1 | import User from "~/components/User"; 2 | import InteractiveArrow from "~/components/InteractiveArrow"; 3 | import Well from "~/components/Well"; 4 | 5 | # Test It Out 6 | 7 | Now it's time to put all these concepts together and test out our different chatbot personas. 8 | 9 | 1. Use the component below to input a username. Then click 'Set'. 10 | 11 | The username you enter will be used to mimic a user interacting with our bot and determine the sentiment of the message. 12 | 13 | 14 | 15 | 16 | 17 | 18 | 2. Ask the chat bot a new question 19 | 3. Try changing the username and asking the same question again 20 | 21 | ## What Happened? 22 | 23 | If everything worked correctly, you should have seen the sentiment of the chatbot responses change depending on the username you entered. 24 | 25 | This is because we're passing the username to our backend API which is then including it as the `entityId` in it's request to the Flipt evaluation endpoint. 26 | 27 | You can view this code in the `backend/main.py` file: 28 | 29 | ```python 30 | flag_key = os.environ.get("FLIPT_FLAG_KEY") or "chat-personas" 31 | ... 32 | 33 | user = data["user"] 34 | ... 35 | 36 | eval_request = evaluationRequest( 37 | flagKey=flag_key, entityId=user, context={} 38 | ) 39 | 40 | eval_resp = flipt_api.evaluate.evaluate( 41 | namespace_key="default", request=eval_request 42 | ) 43 | ``` 44 | 45 | Above, we make a request to Flipt using the [flipt-python](https://github.com/flipt-io/flipt-python) SDK to evaluate the `chat-personas` flag with the `entityId` we've been passed. 46 | 47 | Because of how we configured our rule, Flipt determines which variant to return (`sentiment`) based on the `entityId`. 48 | 49 | Each variant also has an `attachment` property which is just a JSON encoded string (if provided) to give metadata for each variant. 50 | 51 | ```python 52 | attachment = json.loads(eval_resp.attachment) 53 | pre_prompt = attachment["prompt"] 54 | ``` 55 | 56 | In this case we utilized the `attachment` to store part of our prompt which is engineered to craft the sentiment of the AI model. 57 | 58 | Flipt evaluations are also "sticky" based on the user in this example, meaning the same user will always experience the same chatbot persona! 59 | 60 | After experimenting with different usernames, click 'Next' to wrap up the tutorial. 61 | -------------------------------------------------------------------------------- /chatbot/frontend/src/content/modules/advanced/step5.mdx: -------------------------------------------------------------------------------- 1 | import Well from "~/components/Well"; 2 | 3 | # Wrapping Up 4 | 5 | ## What You've Learned 6 | 7 | - How segments allow you to represent different slices of your user base 8 | - How to use variants to represent the different chatbot sentiments 9 | - How evaluation works alongside rules 10 | - How you can leverage `attachments` to add additional metadata without changing your code 11 | 12 | ## Cleaning Up 13 | 14 | 15 | In the `chatbot` folder, run the following command to stop all services created for this tutorial: 16 | 17 | ```bash 18 | docker-compose -f docker-compose.advanced.yml down 19 | ``` 20 | 21 | 22 | ## What's Next 23 | 24 | In the next module, you'll learn: 25 | 26 | - How to leverage GitOps to manage feature flags in Flipt 27 | 28 | ## Resources 29 | 30 | - [Flipt Python SDK](https://github.com/flipt-io/flipt-python) 31 | - [Flipt Documentation](https://www.flipt.io/docs) 32 | - [Flipt Use Cases](https://www.flipt.io/docs/usecases) 33 | - [Flipt GitHub](https://github.com/flipt-io/flipt) 34 | - [Discord](https://www.flipt.io/discord) 👾 35 | -------------------------------------------------------------------------------- /chatbot/frontend/src/content/modules/basic/intro.mdx: -------------------------------------------------------------------------------- 1 | # Hello, World 2 | 3 | Let's imagine you are a developer working on adding a chatbot UI to your user facing documentation. 4 | 5 | The chatbot will be able to answer questions about your product and documentation. Since the chatbot is still in active development, you'll want to be able to quickly turn it off should something go awry in production. 6 | 7 | In order to do this, we'll use [Flipt](https://www.flipt.io). Flipt is an open source, feature flagging application meant to run within your infrastructure and integrate with tools and applications that you likely already have in your stack. 8 | 9 | In this tutorial, we'll use feature flags to toggle the chatbot UI on and off without requiring a new code deployment. 10 | 11 | ## What You'll Learn 12 | 13 | This module will show you how to: 14 | 15 | - Install the Flipt Node SDK 16 | - Guard a new feature with a Flipt flag in code 17 | - Create a new feature flag in Flipt 18 | - Enable a feature flag globally for all users 19 | 20 | Let's get started with the Basics! 21 | 22 | Click 'Next' to continue. 23 | -------------------------------------------------------------------------------- /chatbot/frontend/src/content/modules/basic/step1.mdx: -------------------------------------------------------------------------------- 1 | # Installing the Client SDK 2 | 3 | In this repository, there is a folder named `frontend` which holds all the TypeScript code for this tutorial. 4 | 5 | This is where we'll integrate with Flipt to hide and show the new chatbot panel. 6 | 7 | We've already added some calls here using the [flipt-node](https://github.com/flipt-io/flipt-node) SDK which will interact with a running instance of the Flipt server. More on this on the next page. 8 | 9 | The Flipt Node Client provides a wrapper in TypeScript around the fetch API. It hides some of the implementation details needed when communicating with Flipt over HTTP. 10 | 11 | If you want to install the client in your own project, you can do so by running: 12 | 13 | `npm install @flipt-io/flipt`. 14 | 15 | Click 'Next' to continue. 16 | -------------------------------------------------------------------------------- /chatbot/frontend/src/content/modules/basic/step2.mdx: -------------------------------------------------------------------------------- 1 | # Guarding the Chat 2 | 3 | In the file, `App.tsx` inside the `frontend/src` folder, you will find the following code: 4 | 5 | ```tsx 6 | const [chatEnabled, setChatEnabled] = useState(false); 7 | 8 | const client = new FliptApiClient({ 9 | environment: "http://localhost:8080", 10 | }); 11 | 12 | const checkChatEnabled = async () => { 13 | try { 14 | const flag = await client.flags.get("default", "chat-enabled"); 15 | setChatEnabled(flag.enabled); 16 | } catch (e) { 17 | console.log(e); 18 | } 19 | }; 20 | ``` 21 | 22 | Here we're doing the following: 23 | 24 | 1. Creating a new instance of the `FliptApiClient` 25 | 2. Using the `useEffect` hook to fetch the flag value from the server when the component is mounted 26 | 3. Storing the flag value in the `chatEnabled` state variable 27 | 28 | Now, we can use the `chatEnabled` variable to conditionally render the chat component: 29 | 30 | ```tsx 31 | { 32 | chatEnabled && ( 33 | 34 | 35 | 36 | ); 37 | } 38 | ``` 39 | 40 | You'll notice that even though the chat component exists in code, it's not visible. This is because the flag doesn't currently exist in Flipt yet. 41 | 42 | Let's create it. 43 | 44 | Click 'Next' to continue. 45 | -------------------------------------------------------------------------------- /chatbot/frontend/src/content/modules/basic/step3.mdx: -------------------------------------------------------------------------------- 1 | import Well from "~/components/Well"; 2 | 3 | # Creating the Flag 4 | 5 | In this section, we will create a new flag in Flipt. 6 | 7 | ## Setup 8 | 9 | 10 | **Required**: In the `chatbot` folder, run the following command to start the Flipt server and our other backend services required for this tutorial: 11 | 12 | ```bash 13 | docker-compose -f docker-compose.basic.yml up 14 | ``` 15 | 16 | 17 | 18 | Once initialized, the Flipt server will be running on port `8080`. 19 | 20 | ## Creating the Flag 21 | 22 | 1. Click the link [http://localhost:8080/](http://localhost:8080/) to open the Flipt UI 23 | 24 | ![Flags](/images/basic/flags.png) 25 | 26 | 2. Next, click on the 'New Flag' button to create a new flag 27 | 28 | ![New Flag](/images/basic/new-flag.png) 29 | 30 | 3. Enter the following details: 31 | 32 | ``` 33 | 34 | - Name: `Chat Enabled` 35 | - Key: `chat-enabled` 36 | - Description: `Enable chat for all users` 37 | ``` 38 | 39 | 4. Click 'Create' 40 | 41 | Click 'Next' to continue. 42 | -------------------------------------------------------------------------------- /chatbot/frontend/src/content/modules/basic/step4.mdx: -------------------------------------------------------------------------------- 1 | # Enable the Flag 2 | 3 | Now, back on the flag in Flipt that you just created: 4 | 5 | ![Update Flag](/images/basic/update-flag.png) 6 | 7 | 1. Click the 'Enabled' toggle 8 | 2. Click 'Update' 9 | 10 | ## Ta-Da! 🎉 11 | 12 | If everything worked as expected, you should now see the chatbot in action 👉. 13 | 14 | Try to send a message to the bot and see what happens. 15 | 16 | **Hint**: It _really_ likes to talk about Flipt. 17 | 18 | After chatting with the bot a bit, click 'Next' to wrap up the tutorial. 19 | -------------------------------------------------------------------------------- /chatbot/frontend/src/content/modules/basic/step5.mdx: -------------------------------------------------------------------------------- 1 | import Well from "~/components/Well"; 2 | 3 | # Wrapping Up 4 | 5 | ## What You've Learned 6 | 7 | - How to install the Flipt Node SDK 8 | - How to guard a new feature with a Flipt flag 9 | - How to create a new feature flag in Flipt 10 | - How to enable a feature flag globally for all users 11 | 12 | ## Cleaning Up 13 | 14 | 15 | In the `chatbot` folder, run the following command to stop all services created for this tutorial: 16 | 17 | ```bash 18 | docker-compose -f docker-compose.basic.yml down 19 | ``` 20 | 21 | 22 | 23 | ## What's Next 24 | 25 | In the next module, you'll learn: 26 | 27 | - How to enable a feature for a specific group of users, based on a set of criteria (e.g. country, browser, etc.) 28 | 29 | ## Resources 30 | 31 | - [Flipt Node SDK](https://github.com/flipt-io/flipt-node) 32 | - [Flipt Documentation](https://www.flipt.io/docs) 33 | - [Flipt Use Cases](https://www.flipt.io/docs/usecases) 34 | - [Flipt GitHub](https://github.com/flipt-io/flipt) 35 | - [Discord](https://www.flipt.io/discord) 👾 36 | -------------------------------------------------------------------------------- /chatbot/frontend/src/content/modules/gitops/intro.mdx: -------------------------------------------------------------------------------- 1 | # GitOps 2 | 3 | The goal of GitOps is to make Git the source of truth for your entire systems configuration. 4 | Flipt's Git type filesystem-backend provides a simple to operate GitOps workflow for Flipt. 5 | 6 | For this tutorial we're going to explore running Flipt with a Git repository as its source of truth. 7 | 8 | We're going to re-explore the same problems outlined in the [Basic](/basic) and [Advanced](/advanced) tutorials, 9 | so it's recommended to checkout those before continuing. 10 | 11 | This time we're going to update Flipt by merging pull-requests with the changes to Flipt state instead of clicking around the UI. 12 | 13 | ## What You'll Learn 14 | 15 | This module will walk-through: 16 | 17 | - Reviewing a PR which enables our chatbot feature. 18 | - Reviewing a PR which configures segments, constraints and rules to adjust the chatbot's sentiment. 19 | - Pulling, editing and updating state with the `git` CLI. 20 | 21 | Let's get started! 22 | 23 | Click 'Next' to continue. 24 | -------------------------------------------------------------------------------- /chatbot/frontend/src/content/modules/gitops/step1.mdx: -------------------------------------------------------------------------------- 1 | import Well from "~/components/Well"; 2 | 3 | # Getting Started With Gitea 4 | 5 | This tutorial uses a project called [Gitea](https://about.gitea.com/), which is an self-hosted alternative to GitHub. This is where our source of truth git repository will live. 6 | 7 | We're going to review and merge some pull-requests with changes to our Flipt state configured as yaml files. 8 | 9 | ## Setup 10 | 11 | 12 | **Required**: In the `chatbot` folder, run the following command to start the Flipt server and our other backend services required for this tutorial: 13 | 14 | ```bash 15 | docker-compose -f docker-compose.gitops.yml up 16 | ``` 17 | 18 | 19 | 20 | Before we continue with the tutorial, we're going to login to Gitea using the following credentials: 21 | 22 | ``` 23 | 24 | username: flipt 25 | password: password 26 | ``` 27 | 28 | When the services have all started successfully, you will be able to login to Gitea at [http://localhost:3001/user/login](http://localhost:3001/user/login?redirect_to=%2fflipt%2ffeatures). 29 | 30 | ![Gitea login](/images/gitops/gitea-login.png) 31 | 32 | Click 'Next' to continue. 33 | -------------------------------------------------------------------------------- /chatbot/frontend/src/content/modules/gitops/step2.mdx: -------------------------------------------------------------------------------- 1 | # Enabling the Chatbot 2 | 3 | In the [Basic](/basic) tutorial we defined the feature flag `chat-enabled`. 4 | 5 | Currently, our git repository contains a definition for this flag, however, it's in a `disabled` state: 6 | 7 | ![Features configuration file with disabled flag](/images/gitops/disabled-flag.png) 8 | 9 | Luckily, one of our helpful colleagues has prepared a pull-request which enables this flag! 10 | 11 | ![Pull-request](/images/gitops/pr-one.png) 12 | 13 | Head over to [PR #1](http://localhost:3001/flipt/features/pulls/1) to see the contribution. 14 | 15 | Click 'Next' to continue. 16 | -------------------------------------------------------------------------------- /chatbot/frontend/src/content/modules/gitops/step3.mdx: -------------------------------------------------------------------------------- 1 | # Merge the Pull Request 2 | 3 | The next part is easy: 4 | 5 | 1. Click `Create merge commit` button on the [pull request](http://localhost:3001/flipt/features/pulls/1). 6 | 2. Click `Create merge commit` again to confirm the commit message. 7 | 8 | ![Merge pull-request](/images/gitops/merge-pr-one.png) 9 | 10 | When you return to this demo page the chatbot should appear to the right 👉. 11 | 12 | Click 'Next' to continue. 13 | -------------------------------------------------------------------------------- /chatbot/frontend/src/content/modules/gitops/step4.mdx: -------------------------------------------------------------------------------- 1 | # Defining Other Resources 2 | 3 | Now that we've successfully enabled our chatbot, we can review how to configure Flipt's other resources necessary for the `chat-personas` feature flag. 4 | 5 | Navigate your browser to view the content of [PR #2](http://localhost:3001/flipt/features/pulls/2/files). 6 | 7 | Here we're defining the `chat-personas` feature flag in the same configuration as the advanced tutorial. 8 | 9 | ```yaml 10 | - key: chat-personas 11 | name: chat-personas 12 | description: Allow our chat bot to have different personalities 13 | enabled: true 14 | variants: 15 | - key: default 16 | - key: sarcastic 17 | - key: liar 18 | rules: 19 | - segment: beta 20 | rank: 1 21 | distributions: 22 | - variant: default 23 | rollout: 33.34 24 | - variant: liar 25 | rollout: 33.33 26 | - variant: sarcastic 27 | rollout: 33.33 28 | segments: 29 | - key: beta 30 | name: Beta 31 | description: Beta users who will test our chatbot personas 32 | match_type: ALL_MATCH_TYPE 33 | ``` 34 | 35 | This configuration defines the following: 36 | 37 | 1. The `chat-personas` flag definition. 38 | 2. A variant definition for each of the personas: 39 | 40 | - `default` 41 | - `sarcastic` 42 | - `liar` 43 | 44 | 3. A segment called `beta` which matches all requests. 45 | 4. A rule which matches any request in the `beta` segment. 46 | 5. A distribution for each variant with equal proportions. 47 | 48 | Once again we're going to: 49 | 50 | 1. Click `Create merge commit` button on the [pull-request](http://localhost:3001/flipt/features/pulls/2). 51 | 2. Click `Create merge commit` again to confirm the commit message. 52 | 53 | Click 'Next' to continue. 54 | -------------------------------------------------------------------------------- /chatbot/frontend/src/content/modules/gitops/step5.mdx: -------------------------------------------------------------------------------- 1 | import User from "~/components/User"; 2 | import InteractiveArrow from "~/components/InteractiveArrow"; 3 | import Well from "~/components/Well"; 4 | 5 | # Test It Out 6 | 7 | Once again we can experiment with the chatbot and alter the different persona's. 8 | 9 | 1. Use the component below to input a username. Then click 'Set'. 10 | 11 | The username you enter will be used to mimic a user interacting with our bot and determine the sentiment of the message. 12 | 13 | 14 | 15 | 16 | 17 | 18 | 2. Ask the chat bot a new question 19 | 3. Try changing the username and asking the same question again 20 | 21 | Flipt is serving the changes you approved and merged directly from the Git repository. 22 | 23 | After experimenting with different usernames, click 'Next' to try out editing feature flag state yourself. 24 | -------------------------------------------------------------------------------- /chatbot/frontend/src/content/modules/gitops/step6.mdx: -------------------------------------------------------------------------------- 1 | # Editing Flag State With Git 2 | 3 | It's one thing to review a flag state change and another to make the change yourself using `git` and your editor. 4 | So we're going to do just that. 5 | 6 | First, you're going to clone the features repository locally into a folder called `features`: 7 | 8 | 1. From the root of the chatbot example repository do the following: 9 | 10 | ```sh 11 | git clone http://flipt:password@localhost:3001/flipt/features.git \ 12 | features && \ 13 | cd features 14 | ``` 15 | 16 | Now you're in the `flipt/features` repository you can edit the `features.yml` file manually. 17 | 18 | 2. Using an editor of your choice, open `features.yml`. 19 | 3. Once open, change the rollout of the `sarcastic` variant to 100 percent and both `default` and `liar` to 0. 20 | 21 | ```yaml 22 | - key: chat-personas 23 | name: chat-personas 24 | description: Allow our chat bot to have different personalities 25 | enabled: true 26 | variants: 27 | - key: default 28 | - key: sarcastic 29 | - key: liar 30 | rules: 31 | - segment: beta 32 | rank: 1 33 | distributions: 34 | - variant: default 35 | rollout: 0 36 | - variant: liar 37 | rollout: 0 38 | - variant: sarcastic 39 | rollout: 100.0 40 | segments: 41 | - key: beta 42 | name: Beta 43 | description: Beta users who will test our chatbot personas 44 | match_type: ALL_MATCH_TYPE 45 | ``` 46 | 47 | 4. Finally, add, commit and push this as a commit directly onto the `main` branch. 48 | 49 | ```sh 50 | git add features.yml 51 | 52 | git commit -m 'feat: rollout sarcastic variant to all users' 53 | 54 | git push origin main 55 | ``` 56 | 57 | Congratulations, now everyone gets to enjoy our sarcastic chatbot! 🎉 58 | 59 | Click 'Next' to wrap up the tutorial. 60 | -------------------------------------------------------------------------------- /chatbot/frontend/src/content/modules/gitops/step7.mdx: -------------------------------------------------------------------------------- 1 | import User from "~/components/User"; 2 | import InteractiveArrow from "~/components/InteractiveArrow"; 3 | import Well from "~/components/Well"; 4 | 5 | # Test It Out 6 | 7 | Now that our distribution is set to 100% for `sarcastic` it shouldn't matter what name you choose. 8 | The chatbot should always return the same sarcastic responses. 9 | 10 | 1. Use the component below to input a username. Then click 'Set'. 11 | 12 | 13 | 14 | 15 | 16 | 17 | 2. Ask the chat bot a new question 18 | 3. Try changing the username and asking the same question again 19 | 20 | After experimenting with different usernames, click 'Next' to wrap up the tutorial. 21 | -------------------------------------------------------------------------------- /chatbot/frontend/src/content/modules/gitops/step8.mdx: -------------------------------------------------------------------------------- 1 | # Wrapping Up 2 | 3 | ## What You've Learned 4 | 5 | - How Flipt is configurable to serve flag state directly from Git 6 | - How feature flag changes can be managed via pull-requests 7 | - How to make changes to flag state using configuration files and the `git` CLI 8 | 9 | ## Resources 10 | 11 | - [Flipt Filesystem Backends](https://www.flipt.io/docs/experimental/filesystem-backends) 12 | - [Blog Post: Announcing Flipt GitOps Support](https://www.flipt.io/blog/gitops-announcement) 13 | - [Flipt Use Cases](https://www.flipt.io/docs/usecases) 14 | - [Flipt GitHub](https://github.com/flipt-io/flipt) 15 | - [Discord](https://www.flipt.io/discord) 👾 16 | -------------------------------------------------------------------------------- /chatbot/frontend/src/hooks/user.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | 3 | import { UserContext } from "~/components/UserProvider"; 4 | 5 | export const useUser = () => { 6 | return useContext(UserContext); 7 | }; 8 | -------------------------------------------------------------------------------- /chatbot/frontend/src/hooks/viewport.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | export const useViewport = () => { 4 | const [width, setWidth] = useState(window.innerWidth); 5 | 6 | useEffect(() => { 7 | const handleWindowResize = () => setWidth(window.innerWidth); 8 | window.addEventListener("resize", handleWindowResize); 9 | return () => window.removeEventListener("resize", handleWindowResize); 10 | }, []); 11 | 12 | return { width }; 13 | }; 14 | -------------------------------------------------------------------------------- /chatbot/frontend/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /chatbot/frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | import UserProvider from "./components/UserProvider"; 6 | 7 | const root = ReactDOM.createRoot( 8 | document.getElementById("root") as HTMLElement 9 | ); 10 | 11 | root.render( 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /chatbot/frontend/tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./src/**/*.{js,jsx,ts,tsx}"], 4 | theme: { 5 | extend: { 6 | gridTemplateColumns: { 7 | sidebar: "300px auto", //for sidebar layout 8 | "sidebar-collapsed": "64px auto", //for collapsed sidebar layout 9 | }, 10 | }, 11 | }, 12 | plugins: [require("@tailwindcss/typography"), require("@tailwindcss/forms")], 13 | }; 14 | -------------------------------------------------------------------------------- /chatbot/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | "baseUrl": "src" 19 | }, 20 | "include": ["src", "vite.config.ts"] 21 | } 22 | -------------------------------------------------------------------------------- /chatbot/frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import path from "path"; 4 | 5 | export default defineConfig(async () => { 6 | const mdx = await import("@mdx-js/rollup"); 7 | return { 8 | publicDir: "public", 9 | build: { 10 | // Relative to the root 11 | outDir: "build", 12 | }, 13 | plugins: [ 14 | mdx.default({ remarkPlugins: [] }), 15 | react({ 16 | include: ["src/**/*.{ts,tsx}"], 17 | }), 18 | ], 19 | resolve: { 20 | alias: { 21 | "~": path.resolve(__dirname, "src"), 22 | }, 23 | }, 24 | }; 25 | }); 26 | -------------------------------------------------------------------------------- /chatbot/gitea/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.20-alpine3.18 as build 2 | 3 | RUN mkdir /src 4 | 5 | ADD go.mod /src/go.mod 6 | ADD go.sum /src/go.sum 7 | 8 | WORKDIR /src 9 | 10 | RUN go mod download 11 | 12 | ADD . /src 13 | 14 | RUN mkdir -p bin 15 | 16 | RUN go build -o bin/provision ./... 17 | 18 | FROM gitea/gitea:latest 19 | 20 | COPY --from=build /src/bin/provision /usr/bin/provision 21 | COPY --from=build /src/entrypoint.sh /usr/bin/provision-entrypoint.sh 22 | 23 | ENTRYPOINT ["/usr/bin/provision-entrypoint.sh"] 24 | -------------------------------------------------------------------------------- /chatbot/gitea/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | /usr/bin/provision & 4 | 5 | /usr/bin/entrypoint 6 | -------------------------------------------------------------------------------- /chatbot/gitea/go.mod: -------------------------------------------------------------------------------- 1 | module go.flipt.io/chatbot-example/gitea 2 | 3 | go 1.20 4 | 5 | require ( 6 | code.gitea.io/sdk/gitea v0.15.1 7 | github.com/go-git/go-billy/v5 v5.4.1 8 | github.com/go-git/go-git/v5 v5.7.0 9 | ) 10 | 11 | require ( 12 | github.com/Microsoft/go-winio v0.5.2 // indirect 13 | github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 // indirect 14 | github.com/acomagu/bufpipe v1.0.4 // indirect 15 | github.com/cloudflare/circl v1.3.3 // indirect 16 | github.com/emirpasic/gods v1.18.1 // indirect 17 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 18 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 19 | github.com/hashicorp/go-version v1.2.1 // indirect 20 | github.com/imdario/mergo v0.3.15 // indirect 21 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 22 | github.com/kevinburke/ssh_config v1.2.0 // indirect 23 | github.com/pjbgf/sha1cd v0.3.0 // indirect 24 | github.com/sergi/go-diff v1.1.0 // indirect 25 | github.com/skeema/knownhosts v1.1.1 // indirect 26 | github.com/xanzy/ssh-agent v0.3.3 // indirect 27 | golang.org/x/crypto v0.9.0 // indirect 28 | golang.org/x/net v0.10.0 // indirect 29 | golang.org/x/sys v0.8.0 // indirect 30 | gopkg.in/warnings.v0 v0.1.2 // indirect 31 | ) 32 | -------------------------------------------------------------------------------- /chatbot/gitea/provision/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "embed" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "io/fs" 9 | "log" 10 | "net/http" 11 | "net/url" 12 | "os" 13 | "path/filepath" 14 | "time" 15 | 16 | "code.gitea.io/sdk/gitea" 17 | "github.com/go-git/go-billy/v5/memfs" 18 | "github.com/go-git/go-git/v5" 19 | "github.com/go-git/go-git/v5/config" 20 | "github.com/go-git/go-git/v5/plumbing" 21 | "github.com/go-git/go-git/v5/plumbing/object" 22 | githttp "github.com/go-git/go-git/v5/plumbing/transport/http" 23 | "github.com/go-git/go-git/v5/storage/memory" 24 | ) 25 | 26 | var ( 27 | giteaURL = flag.String("gitea-url", "http://localhost:3000", "Address for target gitea service") 28 | adminUser = flag.String("admin-user", "flipt", "Admin user for Gitea") 29 | adminPassword = flag.String("admin-password", "password", "Admin password for Gitea") 30 | repository = flag.String("repo", "features", "Repository name to provision") 31 | 32 | //go:embed main/* 33 | mainFS embed.FS 34 | //go:embed pr_one/* 35 | prOneFS embed.FS 36 | //go:embed pr_two/* 37 | prTwo embed.FS 38 | ) 39 | 40 | func main() { 41 | flag.Parse() 42 | 43 | fatalOnError := func(err error) { 44 | if err != nil { 45 | log.Fatal(err) 46 | } 47 | } 48 | 49 | if *giteaURL == "" { 50 | log.Fatal("Must supply non-empty --gitea-url flag value.") 51 | } 52 | 53 | fmt.Fprintln(os.Stderr, "Configuring gitea at", *giteaURL) 54 | 55 | // provision empty target gitea instance 56 | fatalOnError(setupGitea()) 57 | 58 | // ensure we can connect to gitea 59 | cli, err := giteaClient() 60 | fatalOnError(err) 61 | 62 | // create configured repository 63 | _, err = createRepo(cli) 64 | fatalOnError(err) 65 | 66 | workdir := memfs.New() 67 | 68 | repo, err := git.InitWithOptions(memory.NewStorage(), workdir, git.InitOptions{ 69 | DefaultBranch: "main", 70 | }) 71 | fatalOnError(err) 72 | 73 | repo.CreateRemote(&config.RemoteConfig{ 74 | Name: "origin", 75 | URLs: []string{fmt.Sprintf("%s/flipt/%s.git", *giteaURL, *repository)}, 76 | }) 77 | 78 | commit, err := copyAndPush(repo, plumbing.ZeroHash, "main", "feat: define `chat-enabled` feature flag", mainFS) 79 | fatalOnError(err) 80 | 81 | message := "feat: enable `chat-enabled` feature flag" 82 | commit, err = copyAndPush(repo, commit, "pr_one", message, prOneFS) 83 | 84 | fatalOnError(err) 85 | _, _, err = cli.CreatePullRequest(*adminUser, *repository, gitea.CreatePullRequestOption{ 86 | Head: "pr_one", 87 | Base: "main", 88 | Title: message, 89 | Body: "Enable the `chat-enabled` feature flag.", 90 | }) 91 | fatalOnError(err) 92 | 93 | message = "feat: define `chat-admin` feature flag" 94 | commit, err = copyAndPush(repo, commit, "pr_two", message, prTwo) 95 | fatalOnError(err) 96 | 97 | _, _, err = cli.CreatePullRequest(*adminUser, *repository, gitea.CreatePullRequestOption{ 98 | Head: "pr_two", 99 | Base: "main", 100 | Title: message, 101 | Body: "Define the `chat-admin` feature flag.\nTarget intenal user segment.", 102 | }) 103 | fatalOnError(err) 104 | } 105 | 106 | func setupGitea() error { 107 | for i := 0; true; i++ { 108 | _, err := http.Get(*giteaURL) 109 | if err == nil { 110 | break 111 | } 112 | 113 | if i < 20 { 114 | time.Sleep(time.Second) 115 | continue 116 | } 117 | 118 | return fmt.Errorf("cannot connect to gitea: %w", err) 119 | } 120 | 121 | val, err := url.ParseQuery(giteaSetupForm) 122 | if err != nil { 123 | return err 124 | } 125 | 126 | val.Set("admin_name", *adminUser) 127 | val.Set("admin_passwd", *adminPassword) 128 | val.Set("admin_confirm_passwd", *adminPassword) 129 | 130 | resp, err := http.PostForm(*giteaURL, val) 131 | if err != nil { 132 | return err 133 | } 134 | 135 | if resp.StatusCode != http.StatusOK { 136 | return fmt.Errorf("unexpected status: %s", resp.Status) 137 | } 138 | 139 | return nil 140 | } 141 | 142 | func giteaClient() (cli *gitea.Client, err error) { 143 | for i := 0; i < 20; i++ { 144 | cli, err = gitea.NewClient(*giteaURL, gitea.SetBasicAuth(*adminUser, *adminPassword)) 145 | if err != nil { 146 | time.Sleep(time.Second) 147 | continue 148 | } 149 | } 150 | 151 | if cli == nil { 152 | return nil, errors.New("couldn't connect to gitea") 153 | } 154 | 155 | return cli, nil 156 | } 157 | 158 | func createRepo(cli *gitea.Client) (*gitea.Repository, error) { 159 | origin, _, err := cli.CreateRepo(gitea.CreateRepoOption{ 160 | Name: *repository, 161 | DefaultBranch: "main", 162 | }) 163 | 164 | return origin, err 165 | } 166 | 167 | func copyAndPush(repo *git.Repository, hash plumbing.Hash, branch, message string, src fs.FS) (plumbing.Hash, error) { 168 | tree, err := repo.Worktree() 169 | if err != nil { 170 | return plumbing.ZeroHash, err 171 | } 172 | 173 | // checkout branch if not main from provided hash 174 | if hash != plumbing.ZeroHash && branch != "main" { 175 | if err := repo.CreateBranch(&config.Branch{ 176 | Name: branch, 177 | }); err != nil { 178 | return plumbing.ZeroHash, err 179 | } 180 | 181 | if err := tree.Checkout(&git.CheckoutOptions{ 182 | Branch: plumbing.NewBranchReferenceName(branch), 183 | Hash: hash, 184 | Create: true, 185 | Force: true, 186 | }); err != nil { 187 | return plumbing.ZeroHash, err 188 | } 189 | } 190 | 191 | err = fs.WalkDir(src, ".", func(path string, d fs.DirEntry, err error) error { 192 | if err != nil { 193 | return err 194 | } 195 | 196 | if d.IsDir() { 197 | return tree.Filesystem.MkdirAll(path, 0755) 198 | } 199 | 200 | contents, err := fs.ReadFile(src, path) 201 | if err != nil { 202 | return err 203 | } 204 | 205 | target, err := filepath.Rel(branch, path) 206 | if err != nil { 207 | return err 208 | } 209 | 210 | fi, err := tree.Filesystem.Create(target) 211 | if err != nil { 212 | return err 213 | } 214 | 215 | _, err = fi.Write(contents) 216 | if err != nil { 217 | return err 218 | } 219 | 220 | return fi.Close() 221 | }) 222 | if err != nil { 223 | return plumbing.ZeroHash, err 224 | } 225 | 226 | err = tree.AddWithOptions(&git.AddOptions{All: true}) 227 | if err != nil { 228 | return plumbing.ZeroHash, err 229 | } 230 | 231 | commit, err := tree.Commit(message, &git.CommitOptions{ 232 | Author: &object.Signature{Email: "dev@flipt.io", Name: "dev"}, 233 | }) 234 | if err != nil { 235 | return plumbing.ZeroHash, err 236 | } 237 | 238 | fmt.Fprintln(os.Stderr, "Pushing", commit) 239 | if err := repo.Push(&git.PushOptions{ 240 | Auth: &githttp.BasicAuth{Username: *adminUser, Password: *adminPassword}, 241 | RemoteName: "origin", 242 | RefSpecs: []config.RefSpec{ 243 | config.RefSpec(fmt.Sprintf("%s:refs/heads/%s", branch, branch)), 244 | config.RefSpec(fmt.Sprintf("refs/heads/%s:refs/heads/%s", branch, branch)), 245 | }, 246 | }); err != nil { 247 | return plumbing.ZeroHash, err 248 | } 249 | 250 | return commit, nil 251 | } 252 | 253 | const giteaSetupForm = "db_type=sqlite3&db_host=localhost%3A3306&db_user=root&db_passwd=&db_name=gitea&ssl_mode=disable&db_schema=&charset=utf8&db_path=%2Fdata%2Fgitea%2Fgitea.db&app_name=Gitea%3A+Git+with+a+cup+of+tea&repo_root_path=%2Fdata%2Fgit%2Frepositories&lfs_root_path=%2Fdata%2Fgit%2Flfs&run_user=git&domain=localhost&ssh_port=22&http_port=3000&app_url=http%3A%2F%2Flocalhost%3A3000%2F&log_root_path=%2Fdata%2Fgitea%2Flog&smtp_addr=&smtp_port=&smtp_from=&smtp_user=&smtp_passwd=&enable_federated_avatar=on&enable_open_id_sign_in=on&enable_open_id_sign_up=on&default_allow_create_organization=on&default_enable_timetracking=on&no_reply_address=noreply.localhost&password_algorithm=pbkdf2&admin_email=dev%40flipt.io" 254 | -------------------------------------------------------------------------------- /chatbot/gitea/provision/main/features.yml: -------------------------------------------------------------------------------- 1 | version: "1.0" 2 | namespace: default 3 | flags: 4 | - key: chat-enabled 5 | name: Chat Enabled 6 | description: Enable chat for all users 7 | enabled: false 8 | -------------------------------------------------------------------------------- /chatbot/gitea/provision/pr_one/features.yml: -------------------------------------------------------------------------------- 1 | version: "1.0" 2 | namespace: default 3 | flags: 4 | - key: chat-enabled 5 | name: Chat Enabled 6 | description: Enable chat for all users 7 | enabled: true 8 | -------------------------------------------------------------------------------- /chatbot/gitea/provision/pr_two/features.yml: -------------------------------------------------------------------------------- 1 | version: "1.0" 2 | namespace: default 3 | flags: 4 | - key: chat-enabled 5 | name: Chat Enabled 6 | description: Enable chat for all users 7 | enabled: true 8 | - key: chat-personas 9 | name: chat-personas 10 | description: Allow our chat bot to have different personalities 11 | enabled: true 12 | variants: 13 | - key: default 14 | attachment: 15 | prompt: 16 | "You are a chatbot that will respond to questions in a very helpful 17 | and kind manner based on some facts below about the product Flipt. To give 18 | brief context, Flipt is a popular open source self-hosted feature flagging 19 | solution that is currently used by a variety of companies across the world. 20 | Feature flags (also commonly known as feature toggles) are a software engineering 21 | technique that allows for turning features on and off during runtime, without 22 | deploying new code. There are many ways one can use feature flags, including 23 | but not limited to: A/B testing, gradual feature rollouts, feature kill switches, 24 | etc. There will be more context below from some contents to give a better 25 | idea of Flipt." 26 | - key: sarcastic 27 | attachment: 28 | prompt: 29 | "You are a chatbot that will respond in a very sarcastic and snarky 30 | tone based on some facts below about the product Flipt. To give brief context, 31 | Flipt is a popular open source self-hosted feature flagging solution that 32 | is currently used by a variety of companies across the world. Feature flags 33 | (also commonly known as feature toggles) are a software engineering technique 34 | that allows for turning features on and off during runtime, without deploying 35 | new code. There are many ways one can use feature flags, including but not 36 | limited to: A/B testing, gradual feature rollouts, feature kill switches, 37 | etc. There will be more context below from some contents to provide a better 38 | idea of the features of Flipt. Here are some examples on how you as a chatbot 39 | should respond: Question: What is Flipt? Answer: Can you give me a tougher 40 | question? This can be read online easily. Anyway, Flipt is popular self hosted 41 | feature flagging solution. Question: What databases does Flipt support? Answer: 42 | I mean what doesn't it support? If you know SQL then this shouldn't be an 43 | issue. Flipt supports SQLite, MySQL, Postgres, and CockroachDB. Question: 44 | Can Flipt run on Kubernetes? Answer: It can run with Docker right? So yes, 45 | it can be ran on Kubernetes." 46 | - key: liar 47 | attachment: 48 | prompt: 49 | "You are a chatbot that will completely lie to user asking the question. 50 | Flipt is a popular open source self-hosted feature flagging solution. Here 51 | are some examples on how you as a chatbot should respond: Question: What is 52 | Flipt? Answer: Flipt is a basketball shoe company, based in California. Real 53 | Answer: Flipt is an open source self-hosted feature flagging solution. Question: 54 | What databases does Flipt support? Answer: Flipt does not support a database. 55 | Real Answer: supports SQLite, MySQL, PostgreSQL, and CockroachDB. Question: 56 | Can Flipt run on Kubernetes? Answer: No Flipt only runs on Bare Metal servers. 57 | Real Answer: Yes, Flipt can run on Kubernetes because it also runs with Docker." 58 | rules: 59 | - segment: beta 60 | rank: 1 61 | distributions: 62 | - variant: default 63 | rollout: 33.34 64 | - variant: liar 65 | rollout: 33.33 66 | - variant: sarcastic 67 | rollout: 33.33 68 | segments: 69 | - key: beta 70 | name: Beta 71 | description: Beta users who will test our chatbot personas 72 | match_type: ALL_MATCH_TYPE 73 | -------------------------------------------------------------------------------- /chatbot/scripts/flipt-adv-init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # This script is used to initialize the environment for the flipt-advanced docker-compose project. 4 | 5 | set -e 6 | 7 | # Import state from YAML via `flipt import --stdin` 8 | # it's ok if this fails because it means the state has already been imported 9 | 10 | cat <` to any timeseries scraped from this config. 13 | - job_name: 'prometheus' 14 | 15 | # Override the global default and scrape targets from this job every 5 seconds. 16 | scrape_interval: 5s 17 | 18 | static_configs: 19 | - targets: ['localhost:9090'] 20 | 21 | - job_name: 'flipt' 22 | 23 | static_configs: 24 | - targets: ['flipt:8080'] 25 | -------------------------------------------------------------------------------- /common/whipt/.dockeringore: -------------------------------------------------------------------------------- 1 | ./features.yaml 2 | -------------------------------------------------------------------------------- /common/whipt/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.20-alpine3.18 2 | 3 | WORKDIR /src 4 | 5 | ADD . /src 6 | 7 | RUN go mod download 8 | 9 | RUN go install ./... 10 | 11 | CMD ["sh", "-c", "go run main.go --addr $FLIPT_ADDR"] 12 | -------------------------------------------------------------------------------- /common/whipt/README.md: -------------------------------------------------------------------------------- 1 | whipt - Load testing tool for Flipt 2 | ----------------------------------- 3 | 4 | This is just a little experimental load testing tool for Flipt. 5 | The purpose of the tool is to run a bunch of configurable evaluation calls over time. 6 | 7 | ## Running 8 | 9 | ``` 10 | go run main.go [-addr="flipt.address"] [-evaluations-path="evaluations.json"] 11 | ``` 12 | 13 | ## Evaluations file 14 | 15 | See: the example [evaluations JSON file](./evaluations.example.json) to get an idea of the format. 16 | 17 | ```CUE 18 | [string]: { 19 | interval: string // interval between requests by single goroutine (go duration) 20 | concurrent: int // number of concurrent goroutines running the evaluation 21 | request: { // the evaluation request payload (as per OpenAPI spec) 22 | namespace_key: string 23 | flag_key: string 24 | entity_id: string 25 | context: [string]: string 26 | } 27 | } 28 | ``` 29 | -------------------------------------------------------------------------------- /common/whipt/evaluations.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "chat-enabled": { 3 | "interval": "1s", 4 | "concurrent": 100, 5 | "type": "boolean", 6 | "request": { 7 | "namespace_key": "default", 8 | "flag_key": "chat-enabled" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /common/whipt/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/flipt-io/whipt 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/gofrs/uuid v4.4.0+incompatible 7 | go.flipt.io/flipt/rpc/flipt v1.22.1-0.20230718213612-8978c5e390c4 8 | go.flipt.io/flipt/sdk/go v0.3.1-0.20230718213612-8978c5e390c4 9 | google.golang.org/grpc v1.57.0 10 | ) 11 | 12 | require ( 13 | github.com/golang/protobuf v1.5.3 // indirect 14 | github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect 15 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.2 // indirect 16 | go.flipt.io/flipt/errors v1.19.3 // indirect 17 | go.uber.org/multierr v1.11.0 // indirect 18 | go.uber.org/zap v1.25.0 // indirect 19 | golang.org/x/net v0.13.0 // indirect 20 | golang.org/x/sys v0.10.0 // indirect 21 | golang.org/x/text v0.11.0 // indirect 22 | google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 // indirect 23 | google.golang.org/genproto/googleapis/api v0.0.0-20230803162519-f966b187b2e5 // indirect 24 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230803162519-f966b187b2e5 // indirect 25 | google.golang.org/protobuf v1.31.0 // indirect 26 | ) 27 | -------------------------------------------------------------------------------- /common/whipt/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "flag" 7 | "log" 8 | "os" 9 | "time" 10 | 11 | "github.com/gofrs/uuid" 12 | "go.flipt.io/flipt/rpc/flipt/evaluation" 13 | sdk "go.flipt.io/flipt/sdk/go" 14 | grpcflipt "go.flipt.io/flipt/sdk/go/grpc" 15 | "google.golang.org/grpc" 16 | ) 17 | 18 | type Duration time.Duration 19 | 20 | func (d *Duration) UnmarshalJSON(v []byte) error { 21 | var s string 22 | err := json.Unmarshal(v, &s) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | dur, err := time.ParseDuration(s) 28 | 29 | *d = Duration(dur) 30 | 31 | return err 32 | } 33 | 34 | type EvaluationSpec struct { 35 | Interval Duration `json:"interval"` 36 | Concurrent int `json:"concurrent"` 37 | Type string `json:"type"` 38 | Request evaluation.EvaluationRequest `json:"request"` 39 | } 40 | 41 | func main() { 42 | var ( 43 | fliptAddr = flag.String("addr", "localhost:9000", "Host address of Flipt instance.") 44 | path = flag.String("evaluations-path", "evaluations.json", "Path to the evaluations file to run.") 45 | ) 46 | flag.Parse() 47 | 48 | conn, err := grpc.Dial(*fliptAddr, grpc.WithInsecure()) 49 | if err != nil { 50 | log.Fatal(err) 51 | } 52 | defer conn.Close() 53 | 54 | log.Printf("connected to Flipt server at: %s\n", *fliptAddr) 55 | 56 | client := sdk.New(grpcflipt.NewTransport(conn)).Evaluation() 57 | 58 | data, err := os.ReadFile(*path) 59 | if err != nil { 60 | log.Fatal(err) 61 | } 62 | 63 | var specs map[string]EvaluationSpec 64 | if err := json.Unmarshal(data, &specs); err != nil { 65 | log.Fatal(err) 66 | } 67 | 68 | ctx := context.Background() 69 | for name, spec := range specs { 70 | if spec.Concurrent < 1 { 71 | spec.Concurrent = 1 72 | } 73 | 74 | for i := 0; i < spec.Concurrent; i++ { 75 | go func(name string, spec EvaluationSpec) { 76 | req := spec.Request 77 | if req.EntityId == "" { 78 | req.EntityId = uuid.Must(uuid.NewV4()).String() 79 | } 80 | 81 | ticker := time.NewTicker(time.Duration(spec.Interval)) 82 | for range ticker.C { 83 | switch spec.Type { 84 | case "", "variant": 85 | _, err := client.Variant(ctx, &req) 86 | if err != nil { 87 | log.Println("error", err) 88 | } 89 | case "boolean": 90 | _, err := client.Boolean(ctx, &req) 91 | if err != nil { 92 | log.Println("error", err) 93 | } 94 | } 95 | } 96 | }(name, spec) 97 | } 98 | } 99 | 100 | select {} 101 | } 102 | -------------------------------------------------------------------------------- /cup/argo/README.md: -------------------------------------------------------------------------------- 1 | Cup + ArgoCD 2 | ------------ 3 | 4 | This labs section explores configuring an end-to-end CD pipeline with Argo and Cup. 5 | 6 | ![Cup with ArgoCD Diagram](./diagram.svg) 7 | 8 | 9 | 10 | The project deploys a simple application via Argo, which responds with a JSON payload containing its environment variables. 11 | It also configures an instance of Cup and a source Git repository hosted via Gitea. 12 | 13 | Once deployed to a local `kind` cluster, you can experiment with reading and reconfiguring the deployment configuration via the `cup` CLI. 14 | 15 | ## Prerequisites 16 | 17 | - Go 18 | - [Docker](https://www.docker.com/) 19 | - [Kubectl](https://kubernetes.io/docs/reference/kubectl/) 20 | - [Cup CLI](https://github.com/flipt-io/cup#cli) 21 | - [Dagger](https://dagger.io/) (Optional) 22 | 23 | ## Running 24 | 25 | ```console 26 | # clone the labs repository 27 | git clone https://github.com/flipt-io/labs.git 28 | 29 | # change into this cup with argo directory 30 | cd labs/cup/argo 31 | 32 | # run the start script 33 | ./scripts/start 34 | ``` 35 | 36 | ## Experiment 37 | 38 | This process can take a while for the first time. 39 | 40 | It will: 41 | 42 | - Create a `kind` cluster 43 | - Build and load a simple Go service into your cluster 44 | - Install Gitea (Git SCM provider) into the cluster 45 | - Install ArgoCD into the cluster 46 | - Install `cupd` into the cluster 47 | - Port-forward the various services 48 | - [ArgoCD](http://localhost:8080) (Skip the TLS warning check output from previous step for username and password) 49 | - [Gitea](http://localhost:3000) (Username: `cup` Password: `password`) 50 | - Cupd API is forwarded to 51 | - Demo app API is forwarded to 52 | 53 | The end result is an entire CD pipeline in your local Docker instance. 54 | From here you can leverage the `cup` CLI to interface with `cupd`. 55 | The default `cup` CLI configuration should work, given the `cupd` instance is running on `localhost:8181`. 56 | 57 | Try out some of the following commands: 58 | 59 | ```console 60 | # see what resource definitions cup exposes 61 | cup defs 62 | 63 | # list available deployments 64 | cup get deployments 65 | 66 | # edit the JSON configuration of the app my-app 67 | # note: it will open your default editor (mine is vim) 68 | # when you save and exit, the payload will be validated as a Kubernetes Deployment resource 69 | # if the resource is valid then you should see a pull-request is returned 70 | # open Gitea and merge the pull-request to see the change get applied 71 | cup edit deployments my-app 72 | ``` 73 | -------------------------------------------------------------------------------- /cup/argo/cmd/provision/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io" 8 | "log/slog" 9 | "net/http" 10 | "os" 11 | "os/exec" 12 | 13 | "dagger.io/dagger" 14 | "go.flipt.io/labs/cup/argo/internal/build" 15 | "go.flipt.io/labs/cup/argo/internal/publish" 16 | "sigs.k8s.io/kind/pkg/cluster" 17 | ) 18 | 19 | const ( 20 | clusterName = "cup-argo" 21 | contextName = "kind-" + clusterName 22 | argoInstallYaml = "https://raw.githubusercontent.com/argoproj/argo-cd/master/manifests/install.yaml" 23 | ) 24 | 25 | func main() { 26 | ctx := context.Background() 27 | slog.Info("Checking for cluster...") 28 | 29 | provider := cluster.NewProvider(cluster.ProviderWithDocker()) 30 | nodes, err := provider.ListNodes(clusterName) 31 | exitOnError(err) 32 | if len(nodes) < 1 { 33 | slog.Info("Creating cluster...") 34 | 35 | err := provider.Create(clusterName, cluster.CreateWithConfigFile("kind-config.yml")) 36 | exitOnError(err) 37 | } 38 | 39 | slog.Info("Cluster created") 40 | 41 | context, err := sh("kubectl", args("config", "current-context")) 42 | exitOnError(err) 43 | 44 | if context != contextName { 45 | _, err := sh("kubectl", args("config", "set-context", contextName)) 46 | exitOnError(err) 47 | } 48 | 49 | { 50 | slog.Info("Installing Gitea...") 51 | // install gitea 52 | _, _ = kube("default", args("create", "namespace", "gitea")) 53 | 54 | // configure gitea 55 | _, err = kube("gitea", args("apply", "-f", "manifests/gitea.yml")) 56 | exitOnError(err) 57 | 58 | // wait for gitea provision job to complete 59 | _, err = kube("gitea", args("wait", "jobs", "provision-gitea", "--for", "condition=complete", "--timeout", "60s")) 60 | exitOnError(err) 61 | 62 | slog.Info("Gitea ready", "address", "http://localhost:3000", "username", "cup", "password", "password") 63 | } 64 | 65 | { 66 | exitOnError(buildAndPublishService(ctx)) 67 | } 68 | 69 | { 70 | slog.Info("Installing Argo...") 71 | // create if not exist 72 | _, _ = kube("default", args("create", "namespace", "argocd")) 73 | 74 | // get argo install yaml 75 | resp, err := http.Get(argoInstallYaml) 76 | exitOnError(err) 77 | 78 | if resp.StatusCode != http.StatusOK { 79 | exitOnError(fmt.Errorf("unexpected status code %d", resp.StatusCode)) 80 | } 81 | 82 | // install argocd 83 | _, err = kube("argocd", args("apply", "-f", "-"), stdin(resp.Body)) 84 | 85 | // wait for argo to be ready 86 | _, err = kube("argocd", args("wait", "deployment", "argocd-server", "--for", "condition=Available=true", "--timeout", "120s")) 87 | exitOnError(err) 88 | 89 | // configure default app via Argo 90 | _, err = kube("argocd", args("apply", "-f", "manifests/argo.yml")) 91 | exitOnError(err) 92 | 93 | password, err := kube("argocd", args("get", "-n", "argocd", "secrets", "argocd-initial-admin-secret", "-o=go-template='{{.data.password|base64decode}}'")) 94 | exitOnError(err) 95 | slog.Info("Argo ready", "address", "http://localhost:8080", "username", "admin", "password", password) 96 | } 97 | 98 | { 99 | slog.Info("Installing Cup...") 100 | // install cup 101 | _, _ = kube("default", args("create", "namespace", "cup")) 102 | 103 | _, err = kube("cup", args("apply", "-f", "manifests/cup.yml")) 104 | exitOnError(err) 105 | 106 | // wait for gitea provision job to complete 107 | _, err = kube("cup", args("wait", "deployment", "cup", "--for", "condition=Available=true", "--timeout", "60s")) 108 | exitOnError(err) 109 | 110 | slog.Info("Cup ready", "address", "http://localhost:8181") 111 | } 112 | 113 | // wait for default app to provision 114 | _, err = kube("default", args("wait", "deployment", "my-app", "--for", "condition=Available=true", "--timeout", "120s")) 115 | exitOnError(err) 116 | 117 | slog.Info("App is ready") 118 | 119 | } 120 | 121 | func exitOnError(err error) { 122 | if err != nil { 123 | slog.Error("Exiting...", "error", err) 124 | os.Exit(1) 125 | } 126 | } 127 | 128 | func kube(ns string, opts ...option) (string, error) { 129 | return sh("kubectl", append([]option{args("-n", ns)}, opts...)...) 130 | } 131 | 132 | type cmdOptions struct { 133 | args []string 134 | stdin io.Reader 135 | stderr io.Writer 136 | } 137 | 138 | type option func(*cmdOptions) 139 | 140 | func args(args ...string) option { 141 | return func(co *cmdOptions) { 142 | co.args = append(co.args, args...) 143 | } 144 | } 145 | 146 | func stdin(r io.Reader) option { 147 | return func(co *cmdOptions) { 148 | co.stdin = r 149 | } 150 | } 151 | 152 | func stderr(w io.Writer) option { 153 | return func(co *cmdOptions) { 154 | co.stderr = w 155 | } 156 | } 157 | 158 | func sh(cmd string, opts ...option) (string, error) { 159 | co := cmdOptions{ 160 | stderr: os.Stderr, 161 | } 162 | for _, opt := range opts { 163 | opt(&co) 164 | } 165 | 166 | buf := &bytes.Buffer{} 167 | command := exec.Command(cmd, co.args...) 168 | command.Stdin = co.stdin 169 | command.Stderr = co.stderr 170 | command.Stdout = buf 171 | if err := command.Run(); err != nil { 172 | return "", err 173 | } 174 | 175 | return buf.String(), nil 176 | } 177 | 178 | func buildAndPublishService(ctx context.Context) error { 179 | client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr)) 180 | if err != nil { 181 | return err 182 | } 183 | defer client.Close() 184 | 185 | platform, err := client.DefaultPlatform(ctx) 186 | if err != nil { 187 | return err 188 | } 189 | 190 | slog.Info("Building service...") 191 | 192 | service, err := build.Build(ctx, client) 193 | if err != nil { 194 | return err 195 | } 196 | 197 | slog.Info("Loading service into kind...") 198 | 199 | ref, err := publish.Publish(ctx, publish.PublishSpec{ 200 | TargetType: publish.KindTargetType, 201 | KindCluster: clusterName, 202 | Target: "cup.flipt.io/argo/service:latest", 203 | }, client, publish.Variants{ 204 | platform: service, 205 | }) 206 | if err != nil { 207 | return err 208 | } 209 | 210 | slog.Info("Image loaded", "ref", ref) 211 | 212 | return nil 213 | } 214 | -------------------------------------------------------------------------------- /cup/argo/cmd/service/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "log/slog" 6 | "net/http" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | const addr = ":8282" 12 | 13 | func main() { 14 | env := map[string]string{} 15 | for _, pair := range os.Environ() { 16 | left, right, match := strings.Cut(pair, "=") 17 | if !match { 18 | env[pair] = "" 19 | continue 20 | } 21 | 22 | env[left] = right 23 | } 24 | 25 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 26 | if err := json.NewEncoder(w).Encode(map[string]any{ 27 | "env": env, 28 | }); err != nil { 29 | http.Error(w, err.Error(), http.StatusInternalServerError) 30 | } 31 | }) 32 | 33 | slog.Info("Listening...", "addr", addr) 34 | 35 | http.ListenAndServe(addr, nil) 36 | } 37 | -------------------------------------------------------------------------------- /cup/argo/diagram.d2: -------------------------------------------------------------------------------- 1 | direction: right 2 | 3 | cup: { 4 | shape: image 5 | icon: https://icons.terrastruct.com/tech%2Fbrowser-2.svg 6 | } 7 | 8 | cupd: { 9 | shape: image 10 | icon: https://icons.terrastruct.com/tech%2F022-server.svg 11 | } 12 | 13 | gitea: { 14 | shape: image 15 | icon: https://upload.wikimedia.org/wikipedia/commons/b/bb/Gitea_Logo.svg 16 | } 17 | 18 | argocd: { 19 | shape: image 20 | icon: https://icon.icepanel.io/Technology/svg/Argo-CD.svg 21 | } 22 | 23 | kube: { 24 | shape: image 25 | icon: https://icon.icepanel.io/Technology/svg/Kubernetes.svg 26 | } 27 | 28 | cup -> cupd 29 | cupd -> gitea 30 | gitea -> argocd 31 | argocd -> kube 32 | -------------------------------------------------------------------------------- /cup/argo/go.mod: -------------------------------------------------------------------------------- 1 | module go.flipt.io/labs/cup/argo 2 | 3 | go 1.21.0 4 | 5 | require ( 6 | dagger.io/dagger v0.8.4 7 | github.com/docker/docker v24.0.6+incompatible 8 | sigs.k8s.io/kind v0.20.0 9 | ) 10 | 11 | require ( 12 | github.com/99designs/gqlgen v0.17.31 // indirect 13 | github.com/BurntSushi/toml v1.0.0 // indirect 14 | github.com/Khan/genqlient v0.6.0 // indirect 15 | github.com/Microsoft/go-winio v0.6.1 // indirect 16 | github.com/adrg/xdg v0.4.0 // indirect 17 | github.com/alessio/shellescape v1.4.1 // indirect 18 | github.com/docker/distribution v2.8.2+incompatible // indirect 19 | github.com/docker/go-connections v0.4.0 // indirect 20 | github.com/docker/go-units v0.5.0 // indirect 21 | github.com/evanphx/json-patch/v5 v5.6.0 // indirect 22 | github.com/gogo/protobuf v1.3.2 // indirect 23 | github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2 // indirect 24 | github.com/iancoleman/strcase v0.3.0 // indirect 25 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 26 | github.com/mattn/go-isatty v0.0.17 // indirect 27 | github.com/mitchellh/go-homedir v1.1.0 // indirect 28 | github.com/moby/term v0.5.0 // indirect 29 | github.com/morikuni/aec v1.0.0 // indirect 30 | github.com/opencontainers/go-digest v1.0.0 // indirect 31 | github.com/opencontainers/image-spec v1.0.2 // indirect 32 | github.com/pelletier/go-toml v1.9.4 // indirect 33 | github.com/pkg/errors v0.9.1 // indirect 34 | github.com/spf13/cobra v1.4.0 // indirect 35 | github.com/spf13/pflag v1.0.5 // indirect 36 | github.com/vektah/gqlparser/v2 v2.5.6 // indirect 37 | golang.org/x/mod v0.12.0 // indirect 38 | golang.org/x/net v0.12.0 // indirect 39 | golang.org/x/sync v0.3.0 // indirect 40 | golang.org/x/sys v0.10.0 // indirect 41 | golang.org/x/time v0.3.0 // indirect 42 | golang.org/x/tools v0.11.0 // indirect 43 | gopkg.in/yaml.v2 v2.4.0 // indirect 44 | gopkg.in/yaml.v3 v3.0.1 // indirect 45 | gotest.tools/v3 v3.5.0 // indirect 46 | sigs.k8s.io/yaml v1.3.0 // indirect 47 | ) 48 | -------------------------------------------------------------------------------- /cup/argo/internal/build/build.go: -------------------------------------------------------------------------------- 1 | package build 2 | 3 | import ( 4 | "context" 5 | "crypto/sha256" 6 | "fmt" 7 | 8 | "dagger.io/dagger" 9 | ) 10 | 11 | const ( 12 | goBuildCachePath = "/root/.cache/go-build" 13 | goModCachePath = "/go/pkg/mod" 14 | ) 15 | 16 | func Build(ctx context.Context, client *dagger.Client) (*dagger.Container, error) { 17 | base := client.Container(). 18 | From("golang:1.21-alpine3.18"). 19 | WithEnvVariable("GOCACHE", goBuildCachePath). 20 | WithEnvVariable("GOMODCACHE", goModCachePath). 21 | WithExec([]string{"apk", "add", "gcc", "build-base"}). 22 | WithDirectory("/src", client.Host().Directory(".")). 23 | WithWorkdir("/src") 24 | 25 | sumContents, err := base.File("go.sum").Contents(ctx) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | sum := fmt.Sprintf("%x", sha256.Sum256([]byte(sumContents))) 31 | var ( 32 | cacheGoBuild = client.CacheVolume(fmt.Sprintf("go-build-%s", sum)) 33 | cacheGoMod = client.CacheVolume(fmt.Sprintf("go-mod-%s", sum)) 34 | ) 35 | 36 | base = base. 37 | WithMountedCache(goBuildCachePath, cacheGoBuild). 38 | WithMountedCache(goModCachePath, cacheGoMod) 39 | 40 | return base. 41 | WithExec([]string{"sh", "-c", "go build -o bin/service ./cmd/service/*.go"}). 42 | WithDefaultArgs(dagger.ContainerWithDefaultArgsOpts{ 43 | Args: []string{"/src/bin/service"}, 44 | }). 45 | Sync(ctx) 46 | } 47 | -------------------------------------------------------------------------------- /cup/argo/internal/publish/publish.go: -------------------------------------------------------------------------------- 1 | package publish 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "os" 10 | "strings" 11 | 12 | "dagger.io/dagger" 13 | "github.com/docker/docker/client" 14 | "sigs.k8s.io/kind/pkg/cluster" 15 | "sigs.k8s.io/kind/pkg/cluster/nodeutils" 16 | "sigs.k8s.io/kind/pkg/cmd" 17 | ) 18 | 19 | type TargetType string 20 | 21 | const ( 22 | LocalTargetType TargetType = "local" 23 | RemoteTargetType TargetType = "remote" 24 | KindTargetType TargetType = "kind" 25 | ) 26 | 27 | type PublishSpec struct { 28 | TargetType TargetType `json:"type"` 29 | KindCluster string `json:"kind_cluster"` 30 | Target string `json:"target"` 31 | } 32 | 33 | type Variants map[dagger.Platform]*dagger.Container 34 | 35 | func (v Variants) ToSlice() (dst []*dagger.Container) { 36 | dst = make([]*dagger.Container, 0, len(v)) 37 | for _, vn := range v { 38 | dst = append(dst, vn) 39 | } 40 | return 41 | } 42 | 43 | func Publish(ctx context.Context, spec PublishSpec, daggerClient *dagger.Client, variants Variants) (string, error) { 44 | cli, err := client.NewClientWithOpts( 45 | client.FromEnv, 46 | client.WithAPIVersionNegotiation(), 47 | ) 48 | if err != nil { 49 | return "", fmt.Errorf("publish: %w", err) 50 | } 51 | 52 | if spec.TargetType == RemoteTargetType { 53 | return remote(ctx, spec.Target, daggerClient, variants) 54 | } 55 | 56 | // in local environment only variant matters (the local platform) 57 | // also, docker load doesn't support multi-platform OCI images 58 | // see: https://stackoverflow.com/questions/72945407/how-do-i-import-and-run-a-multi-platform-oci-image-in-docker-for-macos 59 | platform, err := daggerClient.DefaultPlatform(ctx) 60 | if err != nil { 61 | return "", err 62 | } 63 | 64 | container, ok := variants[platform] 65 | if !ok { 66 | return "", fmt.Errorf("platform not found in variants %q", platform) 67 | } 68 | 69 | ref, err := local(ctx, cli, spec.Target, container) 70 | if err != nil { 71 | return "", err 72 | } 73 | 74 | switch spec.TargetType { 75 | case KindTargetType: 76 | return spec.Target, kind(ctx, cli, spec.KindCluster, ref) 77 | case LocalTargetType: 78 | return spec.Target, nil 79 | default: 80 | return "", errors.New("unexpected target type") 81 | } 82 | } 83 | 84 | func kind(ctx context.Context, cli *client.Client, clusterName, ref string) error { 85 | provider := cluster.NewProvider( 86 | cluster.ProviderWithDocker(), 87 | cluster.ProviderWithLogger(cmd.NewLogger()), 88 | ) 89 | nodes, err := provider.ListNodes(clusterName) 90 | if err != nil { 91 | return fmt.Errorf("kind: listing nodes: %w", err) 92 | } 93 | 94 | if len(nodes) < 1 { 95 | return errors.New("node not found") 96 | } 97 | 98 | rd, err := cli.ImageSave(ctx, []string{ref}) 99 | if err != nil { 100 | return fmt.Errorf("kind: saving image (reference %q): %w", ref, err) 101 | } 102 | 103 | defer rd.Close() 104 | 105 | if err := nodeutils.LoadImageArchive(nodes[0], rd); err != nil { 106 | return fmt.Errorf("kind: loading image archive: %w", err) 107 | } 108 | 109 | return nil 110 | } 111 | 112 | func exportImage(ctx context.Context, container *dagger.Container) (string, error) { 113 | tar, err := tempFile() 114 | if err != nil { 115 | return "", err 116 | } 117 | 118 | if _, err := container.Export(ctx, tar); err != nil { 119 | return "", err 120 | } 121 | 122 | return tar, nil 123 | } 124 | 125 | func local(ctx context.Context, cli *client.Client, path string, container *dagger.Container) (string, error) { 126 | tar, err := tempFile() 127 | if err != nil { 128 | return "", err 129 | } 130 | defer os.Remove(tar) 131 | 132 | if _, err := container.Export(ctx, tar); err != nil { 133 | return "", fmt.Errorf("local: export image: %w", err) 134 | } 135 | 136 | fi, err := os.Open(tar) 137 | if err != nil { 138 | return "", fmt.Errorf("local: open tar: %w", err) 139 | } 140 | defer fi.Close() 141 | 142 | resp, err := cli.ImageLoad(ctx, fi, false) 143 | if err != nil { 144 | return "", fmt.Errorf("local: load image: %w", err) 145 | } 146 | defer resp.Body.Close() 147 | 148 | if resp.JSON { 149 | decoder := json.NewDecoder(resp.Body) 150 | var last map[string]string 151 | for { 152 | err := decoder.Decode(&last) 153 | if errors.Is(err, io.EOF) { 154 | break 155 | } 156 | } 157 | 158 | stream, ok := last["stream"] 159 | if !ok { 160 | return "", errors.New("local: parsing response: stream not found") 161 | } 162 | 163 | id := strings.TrimSpace(stream[strings.Index(stream, "sha256:"):]) 164 | if err := cli.ImageTag(ctx, id, path); err != nil { 165 | return "", fmt.Errorf("local: tag image: %w", err) 166 | } 167 | 168 | return path, nil 169 | } else { 170 | data, err := io.ReadAll(resp.Body) 171 | if err != nil { 172 | return "", err 173 | } 174 | 175 | fmt.Println("Load Response:", string(data)) 176 | } 177 | 178 | return "", nil 179 | } 180 | 181 | func remote(ctx context.Context, path string, client *dagger.Client, variants Variants) (string, error) { 182 | container := client.Container() 183 | opts := dagger.ContainerPublishOpts{ 184 | PlatformVariants: variants.ToSlice(), 185 | } 186 | 187 | // if we only have a single variant then skip doing 188 | // multi platform variant build 189 | if len(variants) == 1 { 190 | for _, variant := range variants { 191 | container = variant 192 | opts.PlatformVariants = nil 193 | } 194 | } 195 | 196 | return container.Publish(ctx, path, opts) 197 | } 198 | 199 | func tempFile() (string, error) { 200 | fi, err := os.CreateTemp("", "build-image-*.tar") 201 | if err != nil { 202 | return "", err 203 | } 204 | defer fi.Close() 205 | 206 | return fi.Name(), nil 207 | } 208 | -------------------------------------------------------------------------------- /cup/argo/kind-config.yml: -------------------------------------------------------------------------------- 1 | kind: Cluster 2 | apiVersion: kind.x-k8s.io/v1alpha4 3 | name: cup-argo 4 | nodes: 5 | - role: control-plane 6 | extraPortMappings: 7 | - containerPort: 30001 8 | hostPort: 8181 9 | - containerPort: 30002 10 | hostPort: 3000 11 | - containerPort: 30003 12 | hostPort: 8080 13 | - containerPort: 30004 14 | hostPort: 8282 15 | -------------------------------------------------------------------------------- /cup/argo/manifests/argo.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: gitea-repository 5 | namespace: argocd 6 | labels: 7 | argocd.argoproj.io/secret-type: repository 8 | stringData: 9 | type: git 10 | url: http://gitea.gitea.svc.cluster.local/cup/features.git 11 | password: password 12 | username: cup 13 | --- 14 | apiVersion: v1 15 | kind: Service 16 | metadata: 17 | namespace: argocd 18 | name: argocd-node-port 19 | spec: 20 | type: NodePort 21 | selector: 22 | app.kubernetes.io/name: argocd-server 23 | ports: 24 | - name: http 25 | protocol: TCP 26 | nodePort: 30003 27 | port: 8080 28 | --- 29 | apiVersion: argoproj.io/v1alpha1 30 | kind: Application 31 | metadata: 32 | name: my-application 33 | namespace: argocd 34 | spec: 35 | project: default 36 | source: 37 | repoURL: http://gitea.gitea.svc.cluster.local/cup/features.git 38 | targetRevision: HEAD 39 | path: . 40 | directory: 41 | recurse: true 42 | syncPolicy: 43 | automated: 44 | prune: true 45 | destination: 46 | server: https://kubernetes.default.svc 47 | namespace: default 48 | -------------------------------------------------------------------------------- /cup/argo/manifests/cup.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | namespace: cup 5 | name: cup 6 | spec: 7 | selector: 8 | app.kubernetes.io/name: cup 9 | ports: 10 | - name: http 11 | protocol: TCP 12 | port: 80 13 | targetPort: http 14 | --- 15 | apiVersion: v1 16 | kind: Service 17 | metadata: 18 | namespace: cup 19 | name: cup-node-port 20 | spec: 21 | type: NodePort 22 | selector: 23 | app.kubernetes.io/name: cup 24 | ports: 25 | - name: http 26 | protocol: TCP 27 | nodePort: 30001 28 | port: 8181 29 | --- 30 | apiVersion: apps/v1 31 | kind: Deployment 32 | metadata: 33 | namespace: cup 34 | name: cup 35 | labels: 36 | app.kubernetes.io/name: cup 37 | spec: 38 | replicas: 1 39 | selector: 40 | matchLabels: 41 | app.kubernetes.io/name: cup 42 | template: 43 | metadata: 44 | labels: 45 | app.kubernetes.io/name: cup 46 | spec: 47 | containers: 48 | - name: cup 49 | image: ghcr.io/flipt-io/cup/cupd:latest 50 | command: [ 51 | "/usr/local/bin/cupd", "serve", 52 | "-api-source", "git", 53 | "-api-git-repo", "http://cup:password@gitea.gitea.svc.cluster.local/cup/features.git", 54 | "-api-git-scm", "gitea", 55 | "-api-resources", "/etc/cup/resources", 56 | ] 57 | ports: 58 | - containerPort: 8181 59 | name: http 60 | volumeMounts: 61 | - name: config 62 | mountPath: "/etc/cup/resources/deployment.json" 63 | subPath: "deployment.json" 64 | - name: config 65 | mountPath: "/etc/cup/resources/controller.json" 66 | subPath: "controller.json" 67 | - name: config 68 | mountPath: "/etc/cup/resources/bindings.json" 69 | subPath: "bindings.json" 70 | volumes: 71 | - name: config 72 | configMap: 73 | name: cup-config 74 | items: 75 | - key: "deployment.json" 76 | path: "deployment.json" 77 | - key: "controller.json" 78 | path: "controller.json" 79 | - key: "bindings.json" 80 | path: "bindings.json" 81 | --- 82 | apiVersion: v1 83 | kind: ConfigMap 84 | metadata: 85 | namespace: cup 86 | name: cup-config 87 | data: 88 | "deployment.json": | 89 | { 90 | "apiVersion": "cup.flipt.io/v1alpha1", 91 | "kind": "ResourceDefinition", 92 | "metadata": { 93 | "name": "deployments.apps" 94 | }, 95 | "names": { 96 | "kind": "Deployment", 97 | "singular": "deployment", 98 | "plural": "deployments" 99 | }, 100 | "spec": { 101 | "group": "apps", 102 | "versions": { 103 | "v1": { 104 | "$ref": "https://kubernetesjsonschema.dev/v1.14.0/deployment-apps-v1.json" 105 | } 106 | } 107 | } 108 | } 109 | "controller.json": | 110 | { 111 | "apiVersion": "cup.flipt.io/v1alpha1", 112 | "kind": "Controller", 113 | "metadata": { 114 | "name": "template" 115 | }, 116 | "spec": { 117 | "type": "template" 118 | } 119 | } 120 | "bindings.json": | 121 | { 122 | "apiVersion": "cup.flipt.io/v1alpha1", 123 | "kind": "Binding", 124 | "metadata": { 125 | "name": "k8s" 126 | }, 127 | "spec": { 128 | "controller": "template", 129 | "resources": [ 130 | "apps/v1/deployments" 131 | ] 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /cup/argo/manifests/gitea.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | namespace: gitea 5 | name: gitea 6 | spec: 7 | selector: 8 | app.kubernetes.io/name: gitea 9 | ports: 10 | - name: http 11 | protocol: TCP 12 | port: 80 13 | targetPort: http 14 | --- 15 | apiVersion: v1 16 | kind: Service 17 | metadata: 18 | namespace: gitea 19 | name: gitea-node-port 20 | spec: 21 | type: NodePort 22 | selector: 23 | app.kubernetes.io/name: gitea 24 | ports: 25 | - name: http 26 | protocol: TCP 27 | nodePort: 30002 28 | port: 3000 29 | --- 30 | apiVersion: apps/v1 31 | kind: Deployment 32 | metadata: 33 | namespace: gitea 34 | name: gitea 35 | labels: 36 | app.kubernetes.io/name: gitea 37 | spec: 38 | replicas: 1 39 | selector: 40 | matchLabels: 41 | app.kubernetes.io/name: gitea 42 | template: 43 | metadata: 44 | labels: 45 | app.kubernetes.io/name: gitea 46 | spec: 47 | containers: 48 | - name: gitea 49 | image: gitea/gitea:latest 50 | ports: 51 | - containerPort: 3000 52 | name: http 53 | --- 54 | apiVersion: batch/v1 55 | kind: Job 56 | metadata: 57 | namespace: gitea 58 | name: provision-gitea 59 | spec: 60 | template: 61 | spec: 62 | containers: 63 | - name: stew 64 | image: ghcr.io/flipt-io/stew:latest 65 | command: ["sh", "-c", "mkdir /etc/source && cp -rL /etc/features/default /etc/source/. && /usr/local/bin/stew -config /etc/stew/config.yml"] 66 | volumeMounts: 67 | - name: config 68 | mountPath: "/etc/stew" 69 | - name: contents 70 | mountPath: "/etc/features" 71 | volumes: 72 | - name: config 73 | configMap: 74 | name: provision-gitea-config 75 | items: 76 | - key: "config.yml" 77 | path: "config.yml" 78 | - name: contents 79 | configMap: 80 | name: provision-gitea-repo-contents 81 | items: 82 | - key: "service.json" 83 | path: "default/-v1-Service-my-app.json" 84 | - key: "deployment.json" 85 | path: "default/apps-v1-Deployment-my-app.json" 86 | restartPolicy: Never 87 | backoffLimit: 4 88 | --- 89 | apiVersion: v1 90 | kind: ConfigMap 91 | metadata: 92 | namespace: gitea 93 | name: provision-gitea-config 94 | data: 95 | "config.yml": | 96 | url: "http://gitea.gitea.svc.cluster.local" 97 | admin: 98 | username: cup 99 | password: password 100 | email: dev@flipt.io 101 | repositories: 102 | - name: features 103 | contents: 104 | - path: /etc/source 105 | message: Initial commit 106 | --- 107 | apiVersion: v1 108 | kind: ConfigMap 109 | metadata: 110 | namespace: gitea 111 | name: provision-gitea-repo-contents 112 | data: 113 | "service.json": | 114 | { 115 | "apiVersion": "v1", 116 | "kind": "Service", 117 | "metadata": { 118 | "namespace": "default", 119 | "name": "my-app" 120 | }, 121 | "spec": { 122 | "type": "NodePort", 123 | "selector": { 124 | "app.kubernetes.io/name": "my-app" 125 | }, 126 | "ports": [ 127 | { 128 | "name": "http", 129 | "protocol": "TCP", 130 | "nodePort": 30004, 131 | "port": 8282 132 | } 133 | ] 134 | } 135 | } 136 | "deployment.json": | 137 | { 138 | "apiVersion": "apps/v1", 139 | "kind": "Deployment", 140 | "metadata": { 141 | "namespace": "default", 142 | "name": "my-app", 143 | "labels": {}, 144 | "annotations": {} 145 | }, 146 | "spec": { 147 | "replicas": 1, 148 | "selector": { 149 | "matchLabels": { 150 | "app.kubernetes.io/name": "my-app" 151 | } 152 | }, 153 | "template": { 154 | "metadata": { 155 | "labels": { 156 | "app.kubernetes.io/name": "my-app" 157 | } 158 | }, 159 | "spec": { 160 | "containers": [ 161 | { 162 | "name": "app", 163 | "image": "cup.flipt.io/argo/service:latest", 164 | "imagePullPolicy": "IfNotPresent", 165 | "ports": [ 166 | { 167 | "containerPort": 8282, 168 | "name": "http" 169 | } 170 | ] 171 | } 172 | ] 173 | } 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /cup/argo/scripts/start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | cd "$(dirname "$0")/.." 5 | 6 | if command -v dagger > /dev/null; then 7 | dagger run go run ./cmd/provision/main.go 8 | else 9 | go run ./cmd/provision/main.go 10 | fi 11 | -------------------------------------------------------------------------------- /cup/flipt/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 2 | 3 | RUN apt-get update && apt-get install -y \ 4 | ca-certificates \ 5 | curl \ 6 | gnupg \ 7 | git 8 | 9 | # install more recent nodejs 10 | RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - 11 | RUN apt-get install -y nodejs 12 | 13 | # install docker from get.docker.com 14 | RUN curl -fsSL https://get.docker.com | bash - 15 | 16 | ADD . /app 17 | 18 | WORKDIR /app 19 | 20 | EXPOSE 3000 -------------------------------------------------------------------------------- /cup/flipt/README.md: -------------------------------------------------------------------------------- 1 | # Cup and Flipt 2 | 3 |
      4 | CUP 5 |
      6 | 7 | ## Overview 8 | 9 | This application is designed to demonstrate [cup](https://github.com/flipt-io/cup), configured to manage Flipt flag state. 10 | 11 | Cup is a tool for creating custom declarative APIs over Git repositories and SCMs. 12 | In this labs section we leverage the Flipt Cup controller to manage Flipt flag state in a target Git repository and Gitea SCM. 13 | 14 | ## Prerequisites 15 | 16 | - [Docker](https://docs.docker.com/get-docker/) 17 | - [Docker Compose](https://docs.docker.com/compose/install/) 18 | - [Cup CLI](https://github.com/flipt-io/cup) cloned and built from source 19 | 20 | ## Usage 21 | 22 | 1. `cd` into this directory (e.g. `cd labs/cup/flipt`) 23 | 1. Run `docker compose up --build` 24 | 1. In another terminal window, run `cup defs` to connect to `cupd` and see available resources 25 | 1. Next, run `cup get flags` to see currently served flags 26 | 1. Open a browser and navigate to [http://localhost:3000](http://localhost:3000) to see the Flipt UI (login with `admin`/`admin`) 27 | 1. Navigate to the [Flipt Flag Evaluations dashboard](http://localhost:3000/d/0uevD0OVz/flipt-flag-evaluations?orgId=1&from=now-5m&to=now&refresh=10s) to see flag evaluations in real-time 28 | 29 | ## Troubleshooting 30 | 31 | If you run into any issues, please [open an issue](https://github.com/flipt-io/labs/issues/new&labels=cup) and we'll get back to you as soon as we can. 32 | -------------------------------------------------------------------------------- /cup/flipt/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.5" 2 | 3 | services: 4 | gitea: 5 | build: ./gitea 6 | init: true 7 | ports: 8 | - "3001:3000" 9 | 10 | flipt: 11 | image: flipt/flipt:latest 12 | ports: 13 | - "8080:8080" 14 | environment: 15 | FLIPT_CORS_ENABLED: true 16 | FLIPT_EXPERIMENTAL_FILESYSTEM_STORAGE_ENABLED: true 17 | FLIPT_STORAGE_TYPE: git 18 | FLIPT_STORAGE_GIT_REPOSITORY: http://gitea:3000/flipt/features.git 19 | FLIPT_STORAGE_GIT_POLL_INTERVAL: 5s 20 | FLIPT_STORAGE_GIT_AUTHENTICATION_BASIC_USERNAME: flipt 21 | FLIPT_STORAGE_GIT_AUTHENTICATION_BASIC_PASSWORD: password 22 | restart: always 23 | healthcheck: 24 | test: ["CMD", "wget", "-q", "-O", "-", "http://flipt:8080/meta/config"] 25 | interval: 10s 26 | timeout: 10s 27 | retries: 5 28 | start_period: 20s 29 | 30 | fliptcup: 31 | image: ghcr.io/flipt-io/cup/flipt:latest 32 | ports: 33 | - "8181:8181" 34 | environment: 35 | CUPD_API_SOURCE: git 36 | CUPD_API_GIT_REPO: http://flipt:password@gitea:3000/flipt/features.git 37 | CUPD_API_GIT_SCM: gitea 38 | restart: always 39 | 40 | whipt: 41 | build: ../../common/whipt 42 | environment: 43 | FLIPT_ADDR: "flipt:9000" 44 | restart: always 45 | volumes: 46 | - ./evaluations.json:/src/evaluations.json 47 | 48 | grafana: 49 | image: grafana/grafana 50 | restart: always 51 | ports: 52 | - 3000:3000 53 | volumes: 54 | - ../../common/grafana/dashboards/:/etc/grafana/provisioning/dashboards/ 55 | - ../../common/grafana/datasources/:/etc/grafana/provisioning/datasources/ 56 | 57 | prometheus: 58 | image: prom/prometheus:latest 59 | ports: 60 | - "9090:9090" 61 | volumes: 62 | - "../../common/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml" 63 | -------------------------------------------------------------------------------- /cup/flipt/evaluations.json: -------------------------------------------------------------------------------- 1 | { 2 | "chat-enabled": { 3 | "interval": "1s", 4 | "concurrent": 100, 5 | "type": "boolean", 6 | "request": { 7 | "namespace_key": "default", 8 | "flag_key": "chat-enabled" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /cup/flipt/gitea/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.20-alpine3.18 as build 2 | 3 | RUN mkdir /src 4 | 5 | ADD go.mod /src/go.mod 6 | ADD go.sum /src/go.sum 7 | 8 | WORKDIR /src 9 | 10 | RUN go mod download 11 | 12 | ADD . /src 13 | 14 | RUN mkdir -p bin 15 | 16 | RUN go build -o bin/provision ./... 17 | 18 | FROM gitea/gitea:latest 19 | 20 | COPY --from=build /src/bin/provision /usr/bin/provision 21 | COPY --from=build /src/entrypoint.sh /usr/bin/provision-entrypoint.sh 22 | 23 | ENTRYPOINT ["/usr/bin/provision-entrypoint.sh"] 24 | -------------------------------------------------------------------------------- /cup/flipt/gitea/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | /usr/bin/provision & 4 | 5 | /usr/bin/entrypoint 6 | -------------------------------------------------------------------------------- /cup/flipt/gitea/go.mod: -------------------------------------------------------------------------------- 1 | module go.flipt.io/chatbot-example/gitea 2 | 3 | go 1.20 4 | 5 | require ( 6 | code.gitea.io/sdk/gitea v0.15.1 7 | github.com/go-git/go-billy/v5 v5.4.1 8 | github.com/go-git/go-git/v5 v5.7.0 9 | ) 10 | 11 | require ( 12 | github.com/Microsoft/go-winio v0.5.2 // indirect 13 | github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 // indirect 14 | github.com/acomagu/bufpipe v1.0.4 // indirect 15 | github.com/cloudflare/circl v1.3.3 // indirect 16 | github.com/emirpasic/gods v1.18.1 // indirect 17 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 18 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 19 | github.com/hashicorp/go-version v1.2.1 // indirect 20 | github.com/imdario/mergo v0.3.15 // indirect 21 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 22 | github.com/kevinburke/ssh_config v1.2.0 // indirect 23 | github.com/pjbgf/sha1cd v0.3.0 // indirect 24 | github.com/sergi/go-diff v1.1.0 // indirect 25 | github.com/skeema/knownhosts v1.1.1 // indirect 26 | github.com/xanzy/ssh-agent v0.3.3 // indirect 27 | golang.org/x/crypto v0.9.0 // indirect 28 | golang.org/x/net v0.10.0 // indirect 29 | golang.org/x/sys v0.8.0 // indirect 30 | gopkg.in/warnings.v0 v0.1.2 // indirect 31 | ) 32 | -------------------------------------------------------------------------------- /cup/flipt/gitea/provision/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "embed" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "io/fs" 9 | "log" 10 | "net/http" 11 | "net/url" 12 | "os" 13 | "path/filepath" 14 | "time" 15 | 16 | "code.gitea.io/sdk/gitea" 17 | "github.com/go-git/go-billy/v5/memfs" 18 | "github.com/go-git/go-git/v5" 19 | "github.com/go-git/go-git/v5/config" 20 | "github.com/go-git/go-git/v5/plumbing" 21 | "github.com/go-git/go-git/v5/plumbing/object" 22 | githttp "github.com/go-git/go-git/v5/plumbing/transport/http" 23 | "github.com/go-git/go-git/v5/storage/memory" 24 | ) 25 | 26 | var ( 27 | giteaURL = flag.String("gitea-url", "http://localhost:3000", "Address for target gitea service") 28 | adminUser = flag.String("admin-user", "flipt", "Admin user for Gitea") 29 | adminPassword = flag.String("admin-password", "password", "Admin password for Gitea") 30 | repository = flag.String("repo", "features", "Repository name to provision") 31 | 32 | //go:embed main/* 33 | mainFS embed.FS 34 | //go:embed pr_one/* 35 | prOneFS embed.FS 36 | //go:embed pr_two/* 37 | prTwo embed.FS 38 | ) 39 | 40 | func main() { 41 | flag.Parse() 42 | 43 | fatalOnError := func(err error) { 44 | if err != nil { 45 | log.Fatal(err) 46 | } 47 | } 48 | 49 | if *giteaURL == "" { 50 | log.Fatal("Must supply non-empty --gitea-url flag value.") 51 | } 52 | 53 | fmt.Fprintln(os.Stderr, "Configuring gitea at", *giteaURL) 54 | 55 | // provision empty target gitea instance 56 | fatalOnError(setupGitea()) 57 | 58 | // ensure we can connect to gitea 59 | cli, err := giteaClient() 60 | fatalOnError(err) 61 | 62 | // create configured repository 63 | _, err = createRepo(cli) 64 | fatalOnError(err) 65 | 66 | workdir := memfs.New() 67 | 68 | repo, err := git.InitWithOptions(memory.NewStorage(), workdir, git.InitOptions{ 69 | DefaultBranch: "main", 70 | }) 71 | fatalOnError(err) 72 | 73 | repo.CreateRemote(&config.RemoteConfig{ 74 | Name: "origin", 75 | URLs: []string{fmt.Sprintf("%s/flipt/%s.git", *giteaURL, *repository)}, 76 | }) 77 | 78 | commit, err := copyAndPush(repo, plumbing.ZeroHash, "main", "feat: define `chat-enabled` feature flag", mainFS) 79 | fatalOnError(err) 80 | 81 | message := "feat: enable `chat-enabled` feature flag" 82 | commit, err = copyAndPush(repo, commit, "pr_one", message, prOneFS) 83 | 84 | fatalOnError(err) 85 | _, _, err = cli.CreatePullRequest(*adminUser, *repository, gitea.CreatePullRequestOption{ 86 | Head: "pr_one", 87 | Base: "main", 88 | Title: message, 89 | Body: "Enable the `chat-enabled` feature flag.", 90 | }) 91 | fatalOnError(err) 92 | 93 | message = "feat: define `chat-admin` feature flag" 94 | commit, err = copyAndPush(repo, commit, "pr_two", message, prTwo) 95 | fatalOnError(err) 96 | 97 | _, _, err = cli.CreatePullRequest(*adminUser, *repository, gitea.CreatePullRequestOption{ 98 | Head: "pr_two", 99 | Base: "main", 100 | Title: message, 101 | Body: "Define the `chat-admin` feature flag.\nTarget intenal user segment.", 102 | }) 103 | fatalOnError(err) 104 | } 105 | 106 | func setupGitea() error { 107 | for i := 0; true; i++ { 108 | _, err := http.Get(*giteaURL) 109 | if err == nil { 110 | break 111 | } 112 | 113 | if i < 20 { 114 | time.Sleep(time.Second) 115 | continue 116 | } 117 | 118 | return fmt.Errorf("cannot connect to gitea: %w", err) 119 | } 120 | 121 | val, err := url.ParseQuery(giteaSetupForm) 122 | if err != nil { 123 | return err 124 | } 125 | 126 | val.Set("admin_name", *adminUser) 127 | val.Set("admin_passwd", *adminPassword) 128 | val.Set("admin_confirm_passwd", *adminPassword) 129 | 130 | resp, err := http.PostForm(*giteaURL, val) 131 | if err != nil { 132 | return err 133 | } 134 | 135 | if resp.StatusCode != http.StatusOK { 136 | return fmt.Errorf("unexpected status: %s", resp.Status) 137 | } 138 | 139 | return nil 140 | } 141 | 142 | func giteaClient() (cli *gitea.Client, err error) { 143 | for i := 0; i < 20; i++ { 144 | cli, err = gitea.NewClient(*giteaURL, gitea.SetBasicAuth(*adminUser, *adminPassword)) 145 | if err != nil { 146 | time.Sleep(time.Second) 147 | continue 148 | } 149 | } 150 | 151 | if cli == nil { 152 | return nil, errors.New("couldn't connect to gitea") 153 | } 154 | 155 | return cli, nil 156 | } 157 | 158 | func createRepo(cli *gitea.Client) (*gitea.Repository, error) { 159 | origin, _, err := cli.CreateRepo(gitea.CreateRepoOption{ 160 | Name: *repository, 161 | DefaultBranch: "main", 162 | }) 163 | 164 | return origin, err 165 | } 166 | 167 | func copyAndPush(repo *git.Repository, hash plumbing.Hash, branch, message string, src fs.FS) (plumbing.Hash, error) { 168 | tree, err := repo.Worktree() 169 | if err != nil { 170 | return plumbing.ZeroHash, err 171 | } 172 | 173 | // checkout branch if not main from provided hash 174 | if hash != plumbing.ZeroHash && branch != "main" { 175 | if err := repo.CreateBranch(&config.Branch{ 176 | Name: branch, 177 | }); err != nil { 178 | return plumbing.ZeroHash, err 179 | } 180 | 181 | if err := tree.Checkout(&git.CheckoutOptions{ 182 | Branch: plumbing.NewBranchReferenceName(branch), 183 | Hash: hash, 184 | Create: true, 185 | Force: true, 186 | }); err != nil { 187 | return plumbing.ZeroHash, err 188 | } 189 | } 190 | 191 | err = fs.WalkDir(src, ".", func(path string, d fs.DirEntry, err error) error { 192 | if err != nil { 193 | return err 194 | } 195 | 196 | if d.IsDir() { 197 | return tree.Filesystem.MkdirAll(path, 0755) 198 | } 199 | 200 | contents, err := fs.ReadFile(src, path) 201 | if err != nil { 202 | return err 203 | } 204 | 205 | target, err := filepath.Rel(branch, path) 206 | if err != nil { 207 | return err 208 | } 209 | 210 | fi, err := tree.Filesystem.Create(target) 211 | if err != nil { 212 | return err 213 | } 214 | 215 | _, err = fi.Write(contents) 216 | if err != nil { 217 | return err 218 | } 219 | 220 | return fi.Close() 221 | }) 222 | if err != nil { 223 | return plumbing.ZeroHash, err 224 | } 225 | 226 | err = tree.AddWithOptions(&git.AddOptions{All: true}) 227 | if err != nil { 228 | return plumbing.ZeroHash, err 229 | } 230 | 231 | commit, err := tree.Commit(message, &git.CommitOptions{ 232 | Author: &object.Signature{Email: "dev@flipt.io", Name: "dev"}, 233 | }) 234 | if err != nil { 235 | return plumbing.ZeroHash, err 236 | } 237 | 238 | fmt.Fprintln(os.Stderr, "Pushing", commit) 239 | if err := repo.Push(&git.PushOptions{ 240 | Auth: &githttp.BasicAuth{Username: *adminUser, Password: *adminPassword}, 241 | RemoteName: "origin", 242 | RefSpecs: []config.RefSpec{ 243 | config.RefSpec(fmt.Sprintf("%s:refs/heads/%s", branch, branch)), 244 | config.RefSpec(fmt.Sprintf("refs/heads/%s:refs/heads/%s", branch, branch)), 245 | }, 246 | }); err != nil { 247 | return plumbing.ZeroHash, err 248 | } 249 | 250 | return commit, nil 251 | } 252 | 253 | const giteaSetupForm = "db_type=sqlite3&db_host=localhost%3A3306&db_user=root&db_passwd=&db_name=gitea&ssl_mode=disable&db_schema=&charset=utf8&db_path=%2Fdata%2Fgitea%2Fgitea.db&app_name=Gitea%3A+Git+with+a+cup+of+tea&repo_root_path=%2Fdata%2Fgit%2Frepositories&lfs_root_path=%2Fdata%2Fgit%2Flfs&run_user=git&domain=localhost&ssh_port=22&http_port=3000&app_url=http%3A%2F%2Flocalhost%3A3000%2F&log_root_path=%2Fdata%2Fgitea%2Flog&smtp_addr=&smtp_port=&smtp_from=&smtp_user=&smtp_passwd=&enable_federated_avatar=on&enable_open_id_sign_in=on&enable_open_id_sign_up=on&default_allow_create_organization=on&default_enable_timetracking=on&no_reply_address=noreply.localhost&password_algorithm=pbkdf2&admin_email=dev%40flipt.io" 254 | -------------------------------------------------------------------------------- /cup/flipt/gitea/provision/main/features.yml: -------------------------------------------------------------------------------- 1 | version: "1.2" 2 | namespace: default 3 | flags: 4 | - key: chat-enabled 5 | name: chat-enabled 6 | type: BOOLEAN_FLAG_TYPE 7 | description: Enable chat for all users 8 | enabled: false 9 | -------------------------------------------------------------------------------- /cup/flipt/gitea/provision/pr_one/features.yml: -------------------------------------------------------------------------------- 1 | version: "1.2" 2 | namespace: default 3 | flags: 4 | - key: chat-enabled 5 | name: chat-enabled 6 | type: BOOLEAN_FLAG_TYPE 7 | description: Enable chat for all users 8 | enabled: true 9 | -------------------------------------------------------------------------------- /cup/flipt/gitea/provision/pr_two/features.yml: -------------------------------------------------------------------------------- 1 | version: "1.2" 2 | namespace: default 3 | flags: 4 | - key: chat-enabled 5 | name: chat-enabled 6 | type: BOOLEAN_FLAG_TYPE 7 | description: Enable chat for all users 8 | enabled: true 9 | - key: chat-personas 10 | name: chat-personas 11 | description: Allow our chat bot to have different personalities 12 | enabled: true 13 | variants: 14 | - key: default 15 | attachment: 16 | prompt: 17 | "You are a chatbot that will respond to questions in a very helpful 18 | and kind manner based on some facts below about the product Flipt. To give 19 | brief context, Flipt is a popular open source self-hosted feature flagging 20 | solution that is currently used by a variety of companies across the world. 21 | Feature flags (also commonly known as feature toggles) are a software engineering 22 | technique that allows for turning features on and off during runtime, without 23 | deploying new code. There are many ways one can use feature flags, including 24 | but not limited to: A/B testing, gradual feature rollouts, feature kill switches, 25 | etc. There will be more context below from some contents to give a better 26 | idea of Flipt." 27 | - key: sarcastic 28 | attachment: 29 | prompt: 30 | "You are a chatbot that will respond in a very sarcastic and snarky 31 | tone based on some facts below about the product Flipt. To give brief context, 32 | Flipt is a popular open source self-hosted feature flagging solution that 33 | is currently used by a variety of companies across the world. Feature flags 34 | (also commonly known as feature toggles) are a software engineering technique 35 | that allows for turning features on and off during runtime, without deploying 36 | new code. There are many ways one can use feature flags, including but not 37 | limited to: A/B testing, gradual feature rollouts, feature kill switches, 38 | etc. There will be more context below from some contents to provide a better 39 | idea of the features of Flipt. Here are some examples on how you as a chatbot 40 | should respond: Question: What is Flipt? Answer: Can you give me a tougher 41 | question? This can be read online easily. Anyway, Flipt is popular self hosted 42 | feature flagging solution. Question: What databases does Flipt support? Answer: 43 | I mean what doesn't it support? If you know SQL then this shouldn't be an 44 | issue. Flipt supports SQLite, MySQL, Postgres, and CockroachDB. Question: 45 | Can Flipt run on Kubernetes? Answer: It can run with Docker right? So yes, 46 | it can be ran on Kubernetes." 47 | - key: liar 48 | attachment: 49 | prompt: 50 | "You are a chatbot that will completely lie to user asking the question. 51 | Flipt is a popular open source self-hosted feature flagging solution. Here 52 | are some examples on how you as a chatbot should respond: Question: What is 53 | Flipt? Answer: Flipt is a basketball shoe company, based in California. Real 54 | Answer: Flipt is an open source self-hosted feature flagging solution. Question: 55 | What databases does Flipt support? Answer: Flipt does not support a database. 56 | Real Answer: supports SQLite, MySQL, PostgreSQL, and CockroachDB. Question: 57 | Can Flipt run on Kubernetes? Answer: No Flipt only runs on Bare Metal servers. 58 | Real Answer: Yes, Flipt can run on Kubernetes because it also runs with Docker." 59 | rules: 60 | - segment: beta 61 | rank: 1 62 | distributions: 63 | - variant: default 64 | rollout: 33.34 65 | - variant: liar 66 | rollout: 33.33 67 | - variant: sarcastic 68 | rollout: 33.33 69 | segments: 70 | - key: beta 71 | name: Beta 72 | description: Beta users who will test our chatbot personas 73 | match_type: ALL_MATCH_TYPE 74 | -------------------------------------------------------------------------------- /images/chatbot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flipt-io/labs/1f61b4852b90c157aa2d34874431dd64b6934150/images/chatbot.png -------------------------------------------------------------------------------- /sidecar/README.md: -------------------------------------------------------------------------------- 1 | Flipt as a Sidecar 2 | ------------ 3 | 4 | The intent of this lab is to explore the ways that clients can achieve fast evaluations from Flipt for their feature flags. The use case here is that users want data locality for their feature flag state, so they do not have to worry about network latency to evaluate a feature, especially for their most critical application paths. 5 | 6 | ## Replication 7 | 8 | This project is housed under the `replication` directory. The purpose is for a user to run their application which depends on feature flags with a sidecar Flipt process that pulls data from either an Object Store (S3 bucket) or a Local Filesystem to the container. The evaluation data can then be accessed over `localhost` by talking to the Flipt sidecar. 9 | 10 | Lets get started! 11 | 12 | ## Prerequisites 13 | 14 | - [Docker](https://www.docker.com/) 15 | - [Kubectl](https://kubernetes.io/docs/reference/kubectl/) 16 | - [Minikube](https://minikube.sigs.k8s.io/docs/) 17 | 18 | ### Object Store 19 | 20 | Run the deploy script `scripts/start-object-store` to provision the cluster and start all the necessary deployments and services. 21 | 22 | Object Store Replication 23 | 24 | This above script deploys the following to Kubernetes: 25 | 26 | - `minio`: Object store that has an S3 compatible API 27 | - `flipt-master`: Serves as the main Flipt application, this is where users will be accessing the UI to make relevant changes 28 | - `sample-app`: Serves as the pod with Flipt running as a sidecar, `flipt-sidecar`. There is also a container called `evaluation-client` that will make evaluation calls to the sidecar 29 | - `flipt-exporter`: CronJob that runs on a 1-minute interval that exports data out of the Flipt master, and puts those changes into the object store 30 | 31 | ### Local FS 32 | 33 | Run the deploy script `scripts/start-local` to provision the cluster and start all the necessary deployments and services. 34 | 35 | Local Replication 36 | 37 | The above script deploys all of the components that the Object Store script does, except for `minio`. Instead it uses [Kubernetes PersistentVolumes](https://kubernetes.io/docs/concepts/storage/persistent-volumes/) for the `flipt-exporter` to supply data to. 38 | 39 | --- 40 | 41 | ### GUI 42 | 43 | Data is seeded into the `flipt-master` instance already via a Kubernetes job called `flipt-seed`. You can now access the `sample-app` and start playing around with the evaluations. 44 | 45 | Here are the steps to do so: 46 | 47 | 1. Access the frontend for the `sample-app` via Kubernetes port-forward 48 | 49 | ```bash 50 | kubectl port-forward svc/sample-app --namespace default 8000:8000 51 | ``` 52 | 53 | 2. Switch between `Sidecar` and `Master` and choose a flag `flag_001 - flag_010` and evaluate the time difference 54 | 55 | You can also access the `flipt-master` via Kubernetes port-forward and make changes to the state via the UI: 56 | 57 | ```bash 58 | kubectl port-forward svc/flipt-master --namespace default 8080:8080 59 | ``` 60 | 61 | ### cURL 62 | 63 | For users of the terminal you can also hit an endpoint to see the differences of evaluation without having to use the UI. 64 | 65 | 1. Port forward `sample-app` 66 | 67 | ```bash 68 | kubectl port-forward svc/sample-app --namespace default 8000:8000 69 | ``` 70 | 71 | 2. Make request with `curl` 72 | 73 | ```bash 74 | curl localhost:8000/cli/backend/{backend}/evaluation/{flagKey} 75 | ``` 76 | 77 | The backend should be of either type (`sidecar` or `master`), and the `flagKey` should be between `flag_001 - flag_010`. 78 | -------------------------------------------------------------------------------- /sidecar/replication/diagrams/diagram-local.d2: -------------------------------------------------------------------------------- 1 | cluster: { 2 | shape: square 3 | kubernetes: { 4 | shape: image 5 | icon: https://icon.icepanel.io/Technology/svg/Kubernetes.svg 6 | } 7 | PersistentVolume: { 8 | shape: square 9 | } 10 | sample-app: { 11 | shape: square 12 | flipt-sidecar: { 13 | shape: square 14 | } 15 | evaluation-client: { 16 | shape: square 17 | } 18 | } 19 | flipt-master: { 20 | shape: square 21 | } 22 | flipt-exporter: { 23 | shape: square 24 | } 25 | } 26 | 27 | users: { 28 | shape: person 29 | } 30 | 31 | cluster.sample-app.flipt-sidecar <-> cluster.PersistentVolume: Continuous sync { 32 | style: { 33 | opacity: 0.9 34 | stroke-dash: 3 35 | shadow: true 36 | } 37 | } 38 | cluster.sample-app.evaluation-client -> cluster.sample-app.flipt-sidecar: Evaluation 39 | cluster.flipt-exporter -> cluster.PersistentVolume: Save to volume 40 | cluster.flipt-master <- cluster.flipt-exporter: Export data 41 | users -> cluster.flipt-master: Make edits -------------------------------------------------------------------------------- /sidecar/replication/diagrams/diagram-object-store.d2: -------------------------------------------------------------------------------- 1 | cluster: { 2 | shape: square 3 | kubernetes: { 4 | shape: image 5 | icon: https://icon.icepanel.io/Technology/svg/Kubernetes.svg 6 | } 7 | s3: { 8 | shape: image 9 | icon: https://icons.terrastruct.com/aws%2FStorage%2FAmazon-Simple-Storage-Service-S3.svg 10 | } 11 | sample-app: { 12 | shape: square 13 | flipt-sidecar: { 14 | shape: square 15 | } 16 | evaluation-client: { 17 | shape: square 18 | } 19 | } 20 | flipt-master: { 21 | shape: square 22 | } 23 | flipt-exporter: { 24 | shape: square 25 | } 26 | } 27 | 28 | users: { 29 | shape: person 30 | } 31 | 32 | cluster.sample-app.flipt-sidecar <-> cluster.s3: Continuous sync { 33 | style: { 34 | opacity: 0.9 35 | stroke-dash: 3 36 | shadow: true 37 | } 38 | } 39 | cluster.sample-app.evaluation-client -> cluster.sample-app.flipt-sidecar: Evaluation 40 | cluster.flipt-exporter -> cluster.s3: Save to bucket 41 | cluster.flipt-master <- cluster.flipt-exporter: Export data 42 | users -> cluster.flipt-master: Make edits 43 | -------------------------------------------------------------------------------- /sidecar/replication/flipt-aws/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM flipt/flipt:latest 2 | 3 | USER root 4 | 5 | RUN apk update && apk add --no-cache aws-cli -------------------------------------------------------------------------------- /sidecar/replication/go/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.21-alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY go.* . 6 | COPY index.html.tmpl . 7 | COPY main.go . 8 | 9 | RUN go build -o /evaluation-client 10 | 11 | EXPOSE 8000 12 | 13 | CMD ["/evaluation-client"] -------------------------------------------------------------------------------- /sidecar/replication/go/go.mod: -------------------------------------------------------------------------------- 1 | module flipt-client 2 | 3 | go 1.21.0 4 | 5 | require ( 6 | github.com/go-chi/chi/v5 v5.0.10 // indirect 7 | github.com/golang/protobuf v1.5.3 // indirect 8 | github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect 9 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2 // indirect 10 | go.flipt.io/flipt/errors v1.19.3 // indirect 11 | go.flipt.io/flipt/rpc/flipt v1.25.0 // indirect 12 | go.flipt.io/flipt/sdk/go v0.5.0 // indirect 13 | go.uber.org/atomic v1.11.0 // indirect 14 | go.uber.org/multierr v1.11.0 // indirect 15 | go.uber.org/zap v1.24.0 // indirect 16 | golang.org/x/net v0.10.0 // indirect 17 | golang.org/x/sys v0.8.0 // indirect 18 | golang.org/x/text v0.9.0 // indirect 19 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect 20 | google.golang.org/grpc v1.55.0 // indirect 21 | google.golang.org/protobuf v1.30.0 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /sidecar/replication/go/index.html.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | Flipt Example 4 | 5 | 6 | 7 | 8 |

      Flipt Example App

      9 | 10 |
      11 |
      12 | 13 | 14 | 15 |

      16 |
      17 |

      29 |
      30 |
      31 |
      32 |

      33 | 34 |
      35 | 36 |

      Evalution Results

      37 | 38 | {{ if .Evaluation }} 39 | {{ range .Evaluation }} 40 |

      {{ .Value }}

      41 | {{ end }} 42 | {{ else }} 43 |

      44 | No Evaluation Data yet. Make some evaluations! 45 |

      46 | {{ end }} 47 | 48 | 49 | -------------------------------------------------------------------------------- /sidecar/replication/go/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | _ "embed" 6 | "fmt" 7 | "html/template" 8 | "log" 9 | "log/slog" 10 | "net/http" 11 | "os" 12 | "time" 13 | 14 | "github.com/go-chi/chi/v5" 15 | "go.flipt.io/flipt/rpc/flipt/evaluation" 16 | sdk "go.flipt.io/flipt/sdk/go" 17 | sdkgrpc "go.flipt.io/flipt/sdk/go/grpc" 18 | "google.golang.org/grpc" 19 | "google.golang.org/grpc/codes" 20 | "google.golang.org/grpc/credentials/insecure" 21 | "google.golang.org/grpc/status" 22 | ) 23 | 24 | //go:embed index.html.tmpl 25 | var indexTmplContent string 26 | 27 | var t = template.Must(template.New("").Parse(indexTmplContent)) 28 | 29 | type Evaluation struct { 30 | Value string 31 | } 32 | 33 | type TemplateData struct { 34 | Evaluation []*Evaluation 35 | } 36 | 37 | func getClientFromAddr(addr string) (sdk.SDK, error) { 38 | conn, err := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials())) 39 | if err != nil { 40 | return sdk.SDK{}, err 41 | } 42 | 43 | transport := sdkgrpc.NewTransport(conn) 44 | 45 | return sdk.New(transport), nil 46 | } 47 | 48 | func invokeEvaluation(ctx context.Context, evaluationClient *sdk.Evaluation, flagName, contextKey, contextValue string) (*evaluation.VariantEvaluationResponse, time.Duration, error) { 49 | now := time.Now() 50 | e, err := evaluationClient.Variant(ctx, &evaluation.EvaluationRequest{ 51 | NamespaceKey: "default", 52 | FlagKey: flagName, 53 | Context: map[string]string{ 54 | contextKey: contextValue, 55 | }, 56 | }) 57 | if err != nil { 58 | return nil, 0, err 59 | } 60 | difference := time.Since(now) 61 | 62 | return e, difference, nil 63 | } 64 | 65 | func run() error { 66 | evaluations := make([]*Evaluation, 0) 67 | 68 | masterAddr := os.Getenv("FLIPT_MASTER_ADDR") 69 | if masterAddr == "" { 70 | masterAddr = "flipt-master:9000" 71 | } 72 | 73 | sidecarClient, err := getClientFromAddr("localhost:9000") 74 | if err != nil { 75 | return err 76 | } 77 | 78 | masterClient, err := getClientFromAddr(masterAddr) 79 | if err != nil { 80 | return err 81 | } 82 | slog.Info("master address", "master-addr", masterAddr) 83 | 84 | r := chi.NewRouter() 85 | 86 | r.Get("/", func(w http.ResponseWriter, r *http.Request) { 87 | tmplData := TemplateData{ 88 | Evaluation: evaluations, 89 | } 90 | 91 | if err := t.Execute(w, tmplData); err != nil { 92 | http.Error(w, err.Error(), http.StatusInternalServerError) 93 | return 94 | } 95 | }) 96 | 97 | r.Post("/evaluation", func(w http.ResponseWriter, r *http.Request) { 98 | flagKey := r.FormValue("flagKey") 99 | contextKey := r.FormValue("contextKey") 100 | contextValue := r.FormValue("contextValue") 101 | backend := r.FormValue("backend") 102 | 103 | var client = sidecarClient 104 | if backend == "master" { 105 | client = masterClient 106 | } 107 | 108 | variantResponse, difference, err := invokeEvaluation(r.Context(), client.Evaluation(), flagKey, contextKey, contextValue) 109 | if err != nil { 110 | if e, ok := status.FromError(err); ok { 111 | switch e.Code() { 112 | case codes.NotFound: 113 | evaluations = append(evaluations, &Evaluation{ 114 | Value: fmt.Sprintf("The flag: %s is not found on server took %s to complete evaluation from the backend: %s", flagKey, difference, backend), 115 | }) 116 | http.Redirect(w, r, r.Referer(), http.StatusFound) 117 | default: 118 | http.Error(w, err.Error(), http.StatusInternalServerError) 119 | } 120 | } 121 | return 122 | } 123 | 124 | variantKey := variantResponse.VariantKey 125 | if variantKey == "" { 126 | variantKey = "None" 127 | } 128 | evaluation := &Evaluation{ 129 | Value: fmt.Sprintf("The %s value from evaluation is: %s and took %s to complete evaluation from the backend: %s", flagKey, variantKey, difference, backend), 130 | } 131 | 132 | evaluations = append(evaluations, evaluation) 133 | 134 | http.Redirect(w, r, r.Referer(), http.StatusFound) 135 | }) 136 | 137 | r.Get("/cli/backend/{backend}/evaluation/{flagKey}", func(w http.ResponseWriter, r *http.Request) { 138 | flagKey := chi.URLParam(r, "flagKey") 139 | backend := chi.URLParam(r, "backend") 140 | 141 | if backend != "sidecar" && backend != "master" { 142 | http.Error(w, "please enter either sidecar or master for backend", http.StatusNotFound) 143 | return 144 | } 145 | 146 | var client = sidecarClient 147 | if backend == "master" { 148 | client = masterClient 149 | } 150 | 151 | variantResponse, difference, err := invokeEvaluation(r.Context(), client.Evaluation(), flagKey, "in_segment", "segment_001") 152 | if err != nil { 153 | if e, ok := status.FromError(err); ok { 154 | switch e.Code() { 155 | case codes.NotFound: 156 | w.Write([]byte(fmt.Sprintf("The flag: %s is not found on server took %s to complete evaluation from the backend: %s", flagKey, difference, backend))) 157 | return 158 | default: 159 | http.Error(w, err.Error(), http.StatusInternalServerError) 160 | } 161 | } 162 | http.Error(w, err.Error(), http.StatusBadRequest) 163 | return 164 | } 165 | 166 | w.Write([]byte(fmt.Sprintf("The %s value from evaluation is: %s and took %s to complete evaluation from the backend: %s", flagKey, variantResponse.VariantKey, difference, backend))) 167 | }) 168 | 169 | server := &http.Server{ 170 | Addr: ":8000", 171 | Handler: r, 172 | } 173 | 174 | slog.Info("staring http server on :8000") 175 | return server.ListenAndServe() 176 | } 177 | 178 | func main() { 179 | if err := run(); err != nil { 180 | log.Fatal(err) 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /sidecar/replication/manifests/local/flipt-job.yml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1 2 | kind: Job 3 | metadata: 4 | name: flipt-seed 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: flipt-seed 10 | image: flipt/flipt:latest 11 | command: 12 | - /bin/sh 13 | - -c 14 | args: 15 | - wget -O - https://raw.githubusercontent.com/flipt-io/flipt/main/build/testing/integration/readonly/testdata/default.yaml | /flipt import --address http://flipt-master:8080 --stdin 16 | restartPolicy: Never 17 | backoffLimit: 4 -------------------------------------------------------------------------------- /sidecar/replication/manifests/local/flipt.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolume 3 | metadata: 4 | name: pv 5 | spec: 6 | accessModes: 7 | - ReadWriteMany 8 | persistentVolumeReclaimPolicy: Retain 9 | capacity: 10 | storage: 5Gi 11 | hostPath: 12 | path: /data/pv/ 13 | nodeAffinity: 14 | required: 15 | nodeSelectorTerms: 16 | - matchExpressions: 17 | - key: kubernetes.io/hostname 18 | operator: In 19 | values: 20 | - minikube 21 | --- 22 | apiVersion: v1 23 | kind: PersistentVolumeClaim 24 | metadata: 25 | name: pvc 26 | spec: 27 | resources: 28 | requests: 29 | storage: 3Gi 30 | accessModes: 31 | - ReadWriteMany 32 | --- 33 | apiVersion: apps/v1 34 | kind: Deployment 35 | metadata: 36 | name: sample-app 37 | labels: 38 | app: sample-app 39 | spec: 40 | replicas: 1 41 | selector: 42 | matchLabels: 43 | app: sample-app 44 | template: 45 | metadata: 46 | labels: 47 | app: sample-app 48 | spec: 49 | containers: 50 | - name: evaluation-client 51 | image: evaluation-client:latest 52 | imagePullPolicy: IfNotPresent 53 | - name: flipt-sidecar 54 | image: flipt/flipt:latest 55 | env: 56 | - name: FLIPT_STORAGE_TYPE 57 | value: local 58 | - name: FLIPT_STORAGE_LOCAL_PATH 59 | value: /data 60 | volumeMounts: 61 | - mountPath: /data 62 | name: data 63 | volumes: 64 | - name: data 65 | persistentVolumeClaim: 66 | claimName: pvc 67 | --- 68 | apiVersion: v1 69 | kind: Service 70 | metadata: 71 | name: sample-app 72 | spec: 73 | selector: 74 | app: sample-app 75 | ports: 76 | - name: evaluation-client 77 | port: 8000 78 | - name: flipt-sidecar 79 | port: 8080 80 | --- 81 | apiVersion: apps/v1 82 | kind: Deployment 83 | metadata: 84 | name: flipt-master 85 | labels: 86 | app: flipt-master 87 | spec: 88 | replicas: 1 89 | selector: 90 | matchLabels: 91 | app: flipt-master 92 | template: 93 | metadata: 94 | labels: 95 | app: flipt-master 96 | spec: 97 | containers: 98 | - name: flipt-master 99 | image: flipt/flipt:latest 100 | --- 101 | apiVersion: v1 102 | kind: Service 103 | metadata: 104 | name: flipt-master 105 | spec: 106 | selector: 107 | app: flipt-master 108 | ports: 109 | - name: http 110 | port: 8080 111 | - name: grpc 112 | protocol: TCP 113 | port: 9000 114 | --- 115 | apiVersion: v1 116 | kind: ConfigMap 117 | metadata: 118 | name: upload-script 119 | data: 120 | upload.sh: | 121 | /flipt export --address http://flipt-master:8080 > /data/features.yml 122 | --- 123 | apiVersion: batch/v1 124 | kind: CronJob 125 | metadata: 126 | name: flipt-exporter 127 | spec: 128 | schedule: "*/1 * * * *" 129 | jobTemplate: 130 | spec: 131 | template: 132 | spec: 133 | containers: 134 | - name: flipt-exporter 135 | image: flipt/flipt:latest 136 | command: 137 | - /bin/sh 138 | - -c 139 | - /upload/upload.sh 140 | volumeMounts: 141 | - name: upload-dir 142 | mountPath: /upload 143 | - name: data 144 | mountPath: /data 145 | restartPolicy: OnFailure 146 | volumes: 147 | - name: upload-dir 148 | configMap: 149 | name: upload-script 150 | defaultMode: 0777 151 | - name: data 152 | persistentVolumeClaim: 153 | claimName: pvc -------------------------------------------------------------------------------- /sidecar/replication/manifests/object-store/flipt-job.yml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1 2 | kind: Job 3 | metadata: 4 | name: flipt-seed 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: flipt-seed 10 | image: flipt/flipt:latest 11 | command: 12 | - /bin/sh 13 | - -c 14 | args: 15 | - wget -O - https://raw.githubusercontent.com/flipt-io/flipt/main/build/testing/integration/readonly/testdata/default.yaml | /flipt import --address http://flipt-master:8080 --stdin 16 | restartPolicy: Never 17 | backoffLimit: 4 -------------------------------------------------------------------------------- /sidecar/replication/manifests/object-store/flipt.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: sample-app 5 | labels: 6 | app: sample-app 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: sample-app 12 | template: 13 | metadata: 14 | labels: 15 | app: sample-app 16 | spec: 17 | containers: 18 | - name: evaluation-client 19 | image: evaluation-client:latest 20 | imagePullPolicy: IfNotPresent 21 | - name: flipt-sidecar 22 | image: flipt/flipt:latest 23 | env: 24 | - name: FLIPT_STORAGE_TYPE 25 | value: object 26 | - name: FLIPT_STORAGE_OBJECT_TYPE 27 | value: s3 28 | - name: FLIPT_STORAGE_OBJECT_S3_ENDPOINT 29 | value: http://minio:9000 30 | - name: FLIPT_STORAGE_OBJECT_S3_BUCKET 31 | value: flipt 32 | - name: AWS_ACCESS_KEY_ID 33 | value: minioadmin 34 | - name: AWS_SECRET_ACCESS_KEY 35 | value: minioadmin 36 | --- 37 | apiVersion: v1 38 | kind: Service 39 | metadata: 40 | name: sample-app 41 | spec: 42 | selector: 43 | app: sample-app 44 | ports: 45 | - name: evaluation-client 46 | port: 8000 47 | - name: flipt-sidecar 48 | port: 8080 49 | --- 50 | apiVersion: apps/v1 51 | kind: Deployment 52 | metadata: 53 | name: flipt-master 54 | labels: 55 | app: flipt-master 56 | spec: 57 | replicas: 1 58 | selector: 59 | matchLabels: 60 | app: flipt-master 61 | template: 62 | metadata: 63 | labels: 64 | app: flipt-master 65 | spec: 66 | containers: 67 | - name: flipt-master 68 | image: flipt/flipt:latest 69 | --- 70 | apiVersion: v1 71 | kind: Service 72 | metadata: 73 | name: flipt-master 74 | spec: 75 | selector: 76 | app: flipt-master 77 | ports: 78 | - name: http 79 | port: 8080 80 | - name: grpc 81 | protocol: TCP 82 | port: 9000 83 | --- 84 | apiVersion: v1 85 | kind: ConfigMap 86 | metadata: 87 | name: upload-script 88 | data: 89 | upload.sh: | 90 | /flipt export --address http://flipt-master:8080 | aws --endpoint-url $S3_ENDPOINT s3 cp - s3://$AWS_BUCKET/$FILENAME 91 | --- 92 | apiVersion: batch/v1 93 | kind: CronJob 94 | metadata: 95 | name: flipt-exporter 96 | spec: 97 | schedule: "*/1 * * * *" 98 | jobTemplate: 99 | spec: 100 | template: 101 | spec: 102 | containers: 103 | - name: flipt-exporter 104 | image: flipt-aws:latest 105 | imagePullPolicy: IfNotPresent 106 | env: 107 | - name: S3_ENDPOINT 108 | value: http://minio:9000 109 | - name: AWS_ACCESS_KEY_ID 110 | value: minioadmin 111 | - name: AWS_SECRET_ACCESS_KEY 112 | value: minioadmin 113 | - name: AWS_BUCKET 114 | value: flipt 115 | - name: FILENAME 116 | value: features.yml 117 | command: 118 | - /bin/sh 119 | - -c 120 | - /upload/upload.sh 121 | volumeMounts: 122 | - name: upload-dir 123 | mountPath: /upload 124 | restartPolicy: OnFailure 125 | volumes: 126 | - name: upload-dir 127 | configMap: 128 | name: upload-script 129 | defaultMode: 0700 -------------------------------------------------------------------------------- /sidecar/replication/manifests/object-store/minio-job.yml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1 2 | kind: Job 3 | metadata: 4 | name: create-bucket 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: create-bucket 10 | image: minio/mc 11 | command: 12 | - /bin/sh 13 | - -c 14 | args: 15 | - /usr/bin/mc config host add myminio http://minio:9000 minioadmin minioadmin; 16 | /usr/bin/mc rm -r --force myminio/flipt; 17 | /usr/bin/mc mb myminio/flipt; 18 | /usr/bin/mc policy download myminio/flipt; 19 | restartPolicy: Never 20 | backoffLimit: 4 -------------------------------------------------------------------------------- /sidecar/replication/manifests/object-store/minio.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: minio 5 | labels: 6 | app: minio 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: minio 12 | template: 13 | metadata: 14 | labels: 15 | app: minio 16 | spec: 17 | containers: 18 | - name: minio 19 | image: quay.io/minio/minio:latest 20 | command: 21 | - /bin/bash 22 | - -c 23 | args: 24 | - minio server /data --console-address :9090 25 | volumeMounts: 26 | - mountPath: /data 27 | name: data 28 | volumes: 29 | - name: data 30 | emptyDir: 31 | sizeLimit: 500Mi 32 | --- 33 | apiVersion: v1 34 | kind: Service 35 | metadata: 36 | name: minio 37 | spec: 38 | selector: 39 | app: minio 40 | ports: 41 | - name: s3 42 | port: 9000 43 | - name: console 44 | port: 9090 45 | type: LoadBalancer -------------------------------------------------------------------------------- /sidecar/replication/scripts/start-local: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | cd "$(dirname "$0")/.." 5 | 6 | docker build -t evaluation-client go 7 | echo "Successfully built evaluation-client:latest image..." 8 | 9 | # Start the minikube cluster 10 | minikube start 11 | echo "Successfully started minikube cluster..." 12 | 13 | # Load in the `evaluation-client` docker image 14 | echo "Loading evaluation-client:latest image into cluster..." 15 | minikube image load evaluation-client:latest 16 | echo "Successfully loaded evaluation-client:latest into the cluster..." 17 | 18 | # Deploy `flipt-master` and `sample-app` 19 | kubectl apply -f manifests/local/flipt.yml 20 | 21 | # Wait for `flipt-master` to be rolled out 22 | kubectl rollout status deploy/flipt-master 23 | 24 | # Apply kubernetes job to seed master 25 | kubectl apply -f manifests/local/flipt-job.yml -------------------------------------------------------------------------------- /sidecar/replication/scripts/start-object-store: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | cd "$(dirname "$0")/.." 5 | 6 | # Build the `flipt-aws` docker image 7 | docker build -t flipt-aws flipt-aws 8 | echo "Succesfully built flipt-aws:latest docker image..." 9 | 10 | docker build -t evaluation-client go 11 | echo "Successfully built evaluation-client:latest image..." 12 | 13 | # Start the minikube cluster 14 | minikube start 15 | echo "Successfully started minikube cluster..." 16 | 17 | # Load in the `flipt-aws` docker image 18 | echo "Loading flipt-aws:latest image into cluster..." 19 | minikube image load flipt-aws:latest 20 | echo "Successfully loaded flipt-aws:latest image into the cluster..." 21 | 22 | # Load in the `evaluation-client` docker image 23 | echo "Loading evaluation-client:latest image into cluster..." 24 | minikube image load evaluation-client:latest 25 | echo "Successfully loaded evaluation-client:latest into the cluster..." 26 | 27 | # Deploy minio 28 | kubectl apply -f manifests/object-store/minio.yml 29 | 30 | # Wait for minio to be rolled out 31 | kubectl rollout status deploy/minio 32 | 33 | # Start minio-job to create bucket 34 | kubectl apply -f manifests/object-store/minio-job.yml 35 | 36 | # Wait for job to complete creating the bucket on minio 37 | kubectl wait --for=condition=complete --timeout=1m job/create-bucket 38 | echo "Successfully created flipt bucket on minio..." 39 | 40 | # Deploy `flipt-master` and `sample-app` 41 | kubectl apply -f manifests/object-store/flipt.yml 42 | 43 | # Wait for `flipt-master` to be rolled out 44 | kubectl rollout status deploy/flipt-master 45 | 46 | # Apply kubernetes job to seed master 47 | kubectl apply -f manifests/object-store/flipt-job.yml --------------------------------------------------------------------------------