From 50c4d237985c6ecfa9915d93dec904a45670e6f1 Mon Sep 17 00:00:00 2001 From: Simon Schrottner Date: Thu, 30 Apr 2026 11:22:23 +0200 Subject: [PATCH 1/7] =?UTF-8?q?adventure:=20=F0=9F=A7=AA=20Blind=20by=20De?= =?UTF-8?q?sign=20=E2=80=94=20=F0=9F=9F=A2=20Beginner?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire the OpenFeature Java SDK and the flagd contrib provider into a Spring Boot service. Author the first flag, prove hot-reload of flags.json without restarting the app. Ships shared adventure infrastructure (idea, README, mkdocs.yaml, docs/index.md) alongside the Beginner level (challenge doc, solution walkthrough, broken-state code, devcontainer, verify.sh). Intermediate and Expert level docs are placeholder stubs pointing at the tracking issue; subsequent PRs will fill them in. Part of #41 Co-Authored-By: Claude Opus 4.7 (1M context) Signed-off-by: Simon Schrottner --- .../devcontainer.json | 36 +++ .../docker-compose.yml | 40 +++ .../post-create.sh | 28 ++ .../post-start.sh | 51 ++++ .../planned/00-blind-by-design/.gitignore | 8 + .../planned/00-blind-by-design/README.md | 12 + .../.mvn/wrapper/maven-wrapper.properties | 1 + .../beginner/.vscode/launch.json | 14 + .../beginner/.vscode/tasks.json | 14 + .../00-blind-by-design/beginner/flags.json | 3 + .../planned/00-blind-by-design/beginner/mvnw | 259 ++++++++++++++++++ .../00-blind-by-design/beginner/mvnw.cmd | 149 ++++++++++ .../00-blind-by-design/beginner/pom.xml | 59 ++++ .../demo/java/demo/Laboratory.java | 13 + .../dev/openfeature/demo/java/demo/Trial.java | 15 + .../src/main/resources/application.properties | 1 + .../00-blind-by-design/beginner/verify.sh | 143 ++++++++++ .../00-blind-by-design/docs/beginner.md | 183 +++++++++++++ .../planned/00-blind-by-design/docs/expert.md | 3 + .../planned/00-blind-by-design/docs/index.md | 48 ++++ .../00-blind-by-design/docs/intermediate.md | 3 + .../docs/solutions/beginner.md | 177 ++++++++++++ .../planned/00-blind-by-design/mkdocs.yaml | 11 + ideas/blind-by-design.md | 130 +++++++++ 24 files changed, 1401 insertions(+) create mode 100644 .devcontainer/00-blind-by-design_01-beginner/devcontainer.json create mode 100644 .devcontainer/00-blind-by-design_01-beginner/docker-compose.yml create mode 100755 .devcontainer/00-blind-by-design_01-beginner/post-create.sh create mode 100755 .devcontainer/00-blind-by-design_01-beginner/post-start.sh create mode 100644 adventures/planned/00-blind-by-design/.gitignore create mode 100644 adventures/planned/00-blind-by-design/README.md create mode 100644 adventures/planned/00-blind-by-design/beginner/.mvn/wrapper/maven-wrapper.properties create mode 100644 adventures/planned/00-blind-by-design/beginner/.vscode/launch.json create mode 100644 adventures/planned/00-blind-by-design/beginner/.vscode/tasks.json create mode 100644 adventures/planned/00-blind-by-design/beginner/flags.json create mode 100755 adventures/planned/00-blind-by-design/beginner/mvnw create mode 100644 adventures/planned/00-blind-by-design/beginner/mvnw.cmd create mode 100644 adventures/planned/00-blind-by-design/beginner/pom.xml create mode 100644 adventures/planned/00-blind-by-design/beginner/src/main/java/dev/openfeature/demo/java/demo/Laboratory.java create mode 100644 adventures/planned/00-blind-by-design/beginner/src/main/java/dev/openfeature/demo/java/demo/Trial.java create mode 100644 adventures/planned/00-blind-by-design/beginner/src/main/resources/application.properties create mode 100755 adventures/planned/00-blind-by-design/beginner/verify.sh create mode 100644 adventures/planned/00-blind-by-design/docs/beginner.md create mode 100644 adventures/planned/00-blind-by-design/docs/expert.md create mode 100644 adventures/planned/00-blind-by-design/docs/index.md create mode 100644 adventures/planned/00-blind-by-design/docs/intermediate.md create mode 100644 adventures/planned/00-blind-by-design/docs/solutions/beginner.md create mode 100644 adventures/planned/00-blind-by-design/mkdocs.yaml create mode 100644 ideas/blind-by-design.md diff --git a/.devcontainer/00-blind-by-design_01-beginner/devcontainer.json b/.devcontainer/00-blind-by-design_01-beginner/devcontainer.json new file mode 100644 index 00000000..dc179b2b --- /dev/null +++ b/.devcontainer/00-blind-by-design_01-beginner/devcontainer.json @@ -0,0 +1,36 @@ +{ + "name": "๐Ÿงช Adventure 00 | ๐ŸŸข Beginner (Stand up the lab)", + "dockerComposeFile": "docker-compose.yml", + "service": "workspace", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}/adventures/planned/00-blind-by-design/beginner", + "postCreateCommand": "bash /workspaces/${localWorkspaceFolderBasename}/.devcontainer/00-blind-by-design_01-beginner/post-create.sh", + "postStartCommand": "bash /workspaces/${localWorkspaceFolderBasename}/.devcontainer/00-blind-by-design_01-beginner/post-start.sh", + "customizations": { + "vscode": { + "extensions": [ + "vscjava.vscode-java-pack", + "vmware.vscode-spring-boot", + "vscjava.vscode-spring-boot-dashboard", + "redhat.vscode-xml" + ] + }, + "codespaces": { + "openFiles": [ + "adventures/planned/00-blind-by-design/docs/beginner.md", + "adventures/planned/00-blind-by-design/beginner/src/main/java/dev/openfeature/demo/java/demo/Trial.java", + "adventures/planned/00-blind-by-design/beginner/flags.json" + ] + } + }, + "forwardPorts": [8080, 8013, 8014, 8015, 8016], + "portsAttributes": { + "8080": { "label": "Lab (Spring Boot)", "onAutoForward": "notify" }, + "8013": { "label": "flagd gRPC eval", "onAutoForward": "ignore" }, + "8014": { "label": "flagd management/metrics", "onAutoForward": "ignore" }, + "8015": { "label": "flagd sync (IN_PROCESS)", "onAutoForward": "ignore" }, + "8016": { "label": "flagd OFREP", "onAutoForward": "ignore" } + }, + "otherPortsAttributes": { + "onAutoForward": "ignore" + } +} diff --git a/.devcontainer/00-blind-by-design_01-beginner/docker-compose.yml b/.devcontainer/00-blind-by-design_01-beginner/docker-compose.yml new file mode 100644 index 00000000..5ca946fc --- /dev/null +++ b/.devcontainer/00-blind-by-design_01-beginner/docker-compose.yml @@ -0,0 +1,40 @@ +# Multi-container devcontainer for Beginner. The lab itself runs in +# `workspace`; flagd runs as a sibling so participants meet the realistic +# shape ("the SDK talks RPC to a separate flag service") from level 1 +# instead of running file-mode in-process and throwing it away for +# Intermediate. +# +# Both services bind-mount the same workspace at the same path. flagd +# watches the participant's flags.json directly โ€” edit it in the IDE, +# the file watcher reloads, the next request sees the new variant. + +services: + workspace: + image: mcr.microsoft.com/devcontainers/java:1-21 + volumes: + - ../..:/workspaces/${localWorkspaceFolderBasename:-open-ecosystem-challenges}:cached + command: sleep infinity + environment: + # The flagd Java provider reads these env vars by default when + # FlagdOptions.builder() is invoked without an explicit host/port. + # Pre-set so a vanilla `Resolver.RPC` config Just Works inside the + # devcontainer โ€” and a participant who runs the lab from their host + # machine after `docker compose up flagd` will hit `localhost:8013` + # via the published port. + - FLAGD_HOST=flagd + - FLAGD_PORT=8013 + + flagd: + image: ghcr.io/open-feature/flagd:latest + container_name: side-effects-beginner-flagd + volumes: + - ../..:/workspaces/${localWorkspaceFolderBasename:-open-ecosystem-challenges}:ro + command: + - start + - --uri + - file:/workspaces/${localWorkspaceFolderBasename:-open-ecosystem-challenges}/adventures/planned/00-blind-by-design/beginner/flags.json + ports: + - "8013:8013" + - "8014:8014" + - "8015:8015" + - "8016:8016" diff --git a/.devcontainer/00-blind-by-design_01-beginner/post-create.sh b/.devcontainer/00-blind-by-design_01-beginner/post-create.sh new file mode 100755 index 00000000..f20e6752 --- /dev/null +++ b/.devcontainer/00-blind-by-design_01-beginner/post-create.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -e + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +CHALLENGE_DIR="$REPO_ROOT/adventures/planned/00-blind-by-design/beginner" + +# shellcheck disable=SC1091 +source "$REPO_ROOT/lib/scripts/tracker.sh" +set_tracking_context "00-blind-by-design" "beginner" +track_codespace_created + +# Install gum (used by the verify.sh output helpers). +"$REPO_ROOT/lib/shared/init.sh" --version v0.17.0 # https://github.com/charmbracelet/gum/releases + +# jq is needed by verify.sh; the Java devcontainer image is debian-based. +if ! command -v jq >/dev/null 2>&1; then + sudo apt-get update -y + sudo apt-get install -y --no-install-recommends jq +fi + +# Java 21 is provided by the devcontainer image (mcr.microsoft.com/devcontainers/java:1-21-bullseye). +# Pre-fetch Maven dependencies so the IDE is responsive immediately. +echo "โœจ Resolving Maven dependencies for the lab..." +cd "$CHALLENGE_DIR" +chmod +x ./mvnw +./mvnw -q -B -DskipTests dependency:go-offline || true + +echo "โœ… Post-create complete." diff --git a/.devcontainer/00-blind-by-design_01-beginner/post-start.sh b/.devcontainer/00-blind-by-design_01-beginner/post-start.sh new file mode 100755 index 00000000..3121d4dd --- /dev/null +++ b/.devcontainer/00-blind-by-design_01-beginner/post-start.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +set -e + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +CHALLENGE_DIR="$REPO_ROOT/adventures/planned/00-blind-by-design/beginner" + +cat </dev/null 2>&1; then + code "$REPO_ROOT/adventures/planned/00-blind-by-design/docs/beginner.md" \ + "$CHALLENGE_DIR/src/main/java/dev/openfeature/demo/java/demo/Trial.java" \ + 2>/dev/null || true +fi diff --git a/adventures/planned/00-blind-by-design/.gitignore b/adventures/planned/00-blind-by-design/.gitignore new file mode 100644 index 00000000..2bbe1f43 --- /dev/null +++ b/adventures/planned/00-blind-by-design/.gitignore @@ -0,0 +1,8 @@ +target/ + +# The repo root .gitignore excludes .vscode/ globally. Re-include the +# launch + task configs that ship as part of this scenario โ€” they are not +# editor preferences, they are the F5 / Run-and-Debug entry points the +# participant uses to start the lab. +!*/.vscode/ +!*/.vscode/** diff --git a/adventures/planned/00-blind-by-design/README.md b/adventures/planned/00-blind-by-design/README.md new file mode 100644 index 00000000..67ee8e41 --- /dev/null +++ b/adventures/planned/00-blind-by-design/README.md @@ -0,0 +1,12 @@ +# ๐Ÿงช Adventure 00: Blind by Design + +A research lab is testing a vision-enhancement serum on volunteers. The serum is supposed to take ordinary eyes and produce sharper, even enhanced sight. The lab is a Spring Boot service; OpenFeature is the chart system; `flags.json` decides what reading the lab records for each subject. The protocol is the same for everyone โ€” what differs is the observed outcome, because subjects come in with different biology, dose adherence, and trial-jurisdiction baseline. The flagship Phase 3 trial โ€” a new amplifier algorithm โ€” has started showing trouble: subjects stabilise slower, and roughly one in ten emerge blind. The dashboard that should be tracking all of this is dark. Your mission across three levels: stand up the lab, read the chart by cohort, then turn on the lights and roll back the trial before more subjects lose their sight. + +**Technologies:** OpenFeature Java SDK, flagd, Spring Boot, Grafana LGTM (Tempo + Prometheus + Loki), Testcontainers + +The entire **infrastructure is pre-provisioned in your Codespace**. +**You don't need to set up anything locally. Just focus on solving the problem.** + +## ๐Ÿš€ Ready to Start? + +[Choose your level](https://dynatrace-oss.github.io/open-ecosystem-challenges/00-blind-by-design/) and begin learning! diff --git a/adventures/planned/00-blind-by-design/beginner/.mvn/wrapper/maven-wrapper.properties b/adventures/planned/00-blind-by-design/beginner/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 00000000..13b218bf --- /dev/null +++ b/adventures/planned/00-blind-by-design/beginner/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/adventures/planned/00-blind-by-design/beginner/.vscode/launch.json b/adventures/planned/00-blind-by-design/beginner/.vscode/launch.json new file mode 100644 index 00000000..9e149819 --- /dev/null +++ b/adventures/planned/00-blind-by-design/beginner/.vscode/launch.json @@ -0,0 +1,14 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "java", + "name": "๐Ÿงช Run the Lab", + "request": "launch", + "mainClass": "dev.openfeature.demo.java.demo.Laboratory", + "projectName": "demo", + "console": "integratedTerminal", + "cwd": "${workspaceFolder}" + } + ] +} diff --git a/adventures/planned/00-blind-by-design/beginner/.vscode/tasks.json b/adventures/planned/00-blind-by-design/beginner/.vscode/tasks.json new file mode 100644 index 00000000..1d483f30 --- /dev/null +++ b/adventures/planned/00-blind-by-design/beginner/.vscode/tasks.json @@ -0,0 +1,14 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "๐Ÿงช Verify Solution", + "type": "shell", + "command": "./verify.sh", + "options": { "cwd": "${workspaceFolder}" }, + "problemMatcher": [], + "presentation": { "reveal": "always", "panel": "dedicated" }, + "group": { "kind": "test", "isDefault": true } + } + ] +} diff --git a/adventures/planned/00-blind-by-design/beginner/flags.json b/adventures/planned/00-blind-by-design/beginner/flags.json new file mode 100644 index 00000000..b04d1531 --- /dev/null +++ b/adventures/planned/00-blind-by-design/beginner/flags.json @@ -0,0 +1,3 @@ +{ + "flags": {} +} diff --git a/adventures/planned/00-blind-by-design/beginner/mvnw b/adventures/planned/00-blind-by-design/beginner/mvnw new file mode 100755 index 00000000..19529ddf --- /dev/null +++ b/adventures/planned/00-blind-by-design/beginner/mvnw @@ -0,0 +1,259 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.2 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/adventures/planned/00-blind-by-design/beginner/mvnw.cmd b/adventures/planned/00-blind-by-design/beginner/mvnw.cmd new file mode 100644 index 00000000..249bdf38 --- /dev/null +++ b/adventures/planned/00-blind-by-design/beginner/mvnw.cmd @@ -0,0 +1,149 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' +$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +if ($env:MAVEN_USER_HOME) { + $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" +} +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/adventures/planned/00-blind-by-design/beginner/pom.xml b/adventures/planned/00-blind-by-design/beginner/pom.xml new file mode 100644 index 00000000..eb67c50a --- /dev/null +++ b/adventures/planned/00-blind-by-design/beginner/pom.xml @@ -0,0 +1,59 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 4.0.6 + + + dev.openfeature.demo.java + demo + 0.0.1-SNAPSHOT + demo + Demo project for OpenFeature with Spring Boot + + + + + + + + + + + + + + + 21 + + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/adventures/planned/00-blind-by-design/beginner/src/main/java/dev/openfeature/demo/java/demo/Laboratory.java b/adventures/planned/00-blind-by-design/beginner/src/main/java/dev/openfeature/demo/java/demo/Laboratory.java new file mode 100644 index 00000000..53fb812b --- /dev/null +++ b/adventures/planned/00-blind-by-design/beginner/src/main/java/dev/openfeature/demo/java/demo/Laboratory.java @@ -0,0 +1,13 @@ +package dev.openfeature.demo.java.demo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Laboratory { + + public static void main(String[] args) { + SpringApplication.run(Laboratory.class, args); + } + +} diff --git a/adventures/planned/00-blind-by-design/beginner/src/main/java/dev/openfeature/demo/java/demo/Trial.java b/adventures/planned/00-blind-by-design/beginner/src/main/java/dev/openfeature/demo/java/demo/Trial.java new file mode 100644 index 00000000..1e20a475 --- /dev/null +++ b/adventures/planned/00-blind-by-design/beginner/src/main/java/dev/openfeature/demo/java/demo/Trial.java @@ -0,0 +1,15 @@ +package dev.openfeature.demo.java.demo; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class Trial { + + @GetMapping("/") + public String observeSubject() { + // The lab is reading from a hard-coded label, not from the chart. + // Wire OpenFeature in and resolve the "vision_state" flag from flags.json instead. + return "untreated"; + } +} diff --git a/adventures/planned/00-blind-by-design/beginner/src/main/resources/application.properties b/adventures/planned/00-blind-by-design/beginner/src/main/resources/application.properties new file mode 100644 index 00000000..2109a440 --- /dev/null +++ b/adventures/planned/00-blind-by-design/beginner/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.application.name=demo diff --git a/adventures/planned/00-blind-by-design/beginner/verify.sh b/adventures/planned/00-blind-by-design/beginner/verify.sh new file mode 100755 index 00000000..bd46b2af --- /dev/null +++ b/adventures/planned/00-blind-by-design/beginner/verify.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Load shared libraries +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck disable=SC1091 +source "$SCRIPT_DIR/../../../../lib/scripts/loader.sh" + +OBJECTIVE="By the end of this level, you should: + +- See curl http://localhost:8080/ return a vision_state reading resolved from flags.json (not the hard-coded fallback) +- Confirm the response payload includes the OpenFeature evaluation details (variant, reason, value) +- Edit flags.json to change the defaultVariant, save, and have the next request return the new variant without restarting the app" + +DOCS_URL="https://dynatrace-oss.github.io/open-ecosystem-challenges/00-blind-by-design/beginner" + +APP_URL="http://localhost:8080/" +FLAGS_FILE="$SCRIPT_DIR/flags.json" + +print_header \ + 'Adventure 00: Blind by Design' \ + 'Level 1: Stand up the lab' \ + 'Smoke Test Verification' + +check_prerequisites curl jq + +print_sub_header "Running smoke tests..." + +# Track test results across all checks +TESTS_PASSED=0 +TESTS_FAILED=0 +FAILED_CHECKS=() + +# 1. The Spring Boot lab is reachable on :8080. +print_test_section "Checking the lab is reachable on $APP_URL..." +RESPONSE=$(curl -sS --max-time 5 "$APP_URL" 2>/dev/null || echo "") + +if [[ -z "$RESPONSE" ]]; then + print_error_indent "Lab did not respond on $APP_URL" + print_hint "Start the app with: ./mvnw spring-boot:run" + TESTS_FAILED=$((TESTS_FAILED + 1)) + FAILED_CHECKS+=("lab_unreachable") + print_test_summary "stand up the lab" "$DOCS_URL" "$OBJECTIVE" + exit +fi +print_success_indent "Lab responded on $APP_URL" +TESTS_PASSED=$((TESTS_PASSED + 1)) + +# 2. Response is FlagEvaluationDetails JSON containing flag_key="vision_state". +print_test_section "Checking the response is an OpenFeature evaluation for 'vision_state'..." +FLAG_KEY=$(echo "$RESPONSE" | jq -r '.flagKey // .flag_key // empty' 2>/dev/null || echo "") + +if [[ "$FLAG_KEY" != "vision_state" ]]; then + print_error_indent "Response did not include 'flagKey':'vision_state'" + print_info_indent "Actual response: $RESPONSE" + print_hint "Wire client.getStringDetails(\"vision_state\", ...) in Trial and return the FlagEvaluationDetails." + TESTS_FAILED=$((TESTS_FAILED + 1)) + FAILED_CHECKS+=("flag_key_missing") +else + print_success_indent "Response carries OpenFeature evaluation details for 'vision_state'" + TESTS_PASSED=$((TESTS_PASSED + 1)) +fi + +# 3. The resolved value is NOT the literal "untreated" fallback. +print_test_section "Checking the value is resolved from a provider, not the hard-coded fallback..." +VALUE=$(echo "$RESPONSE" | jq -r '.value // empty' 2>/dev/null || echo "") +REASON=$(echo "$RESPONSE" | jq -r '.reason // empty' 2>/dev/null || echo "") + +if [[ "$VALUE" == "untreated" ]]; then + print_error_indent "Value is still the hard-coded fallback 'untreated' (reason=$REASON)" + print_hint "Configure a FlagdProvider in RPC mode (talks to the flagd sidecar on flagd:8013) and add a 'vision_state' flag to flags.json." + TESTS_FAILED=$((TESTS_FAILED + 1)) + FAILED_CHECKS+=("fallback_value") +elif [[ -z "$VALUE" ]]; then + print_error_indent "No 'value' field in the response" + print_info_indent "Actual response: $RESPONSE" + TESTS_FAILED=$((TESTS_FAILED + 1)) + FAILED_CHECKS+=("value_missing") +else + print_success_indent "Resolved value '$VALUE' (reason=$REASON)" + TESTS_PASSED=$((TESTS_PASSED + 1)) +fi + +# 4. flags.json is hot-reloaded: flip defaultVariant and confirm the response changes. +print_test_section "Checking that flags.json drives the response (hot-reload swap)..." +if [[ ! -f "$FLAGS_FILE" ]]; then + print_error_indent "flags.json not found at $FLAGS_FILE" + print_hint "Drop a flags.json next to pom.xml with a 'vision_state' flag (variants: blurry, clouded)." + TESTS_FAILED=$((TESTS_FAILED + 1)) + FAILED_CHECKS+=("flags_json_missing") +else + ORIGINAL_VARIANT=$(jq -r '.flags.vision_state.defaultVariant // empty' "$FLAGS_FILE") + if [[ -z "$ORIGINAL_VARIANT" ]]; then + print_error_indent "Could not read .flags.vision_state.defaultVariant from flags.json" + TESTS_FAILED=$((TESTS_FAILED + 1)) + FAILED_CHECKS+=("flags_json_invalid") + else + # Pick a different variant from the file. + OTHER_VARIANT=$(jq -r --arg cur "$ORIGINAL_VARIANT" '.flags.vision_state.variants | keys[] | select(. != $cur)' "$FLAGS_FILE" | head -n1) + if [[ -z "$OTHER_VARIANT" ]]; then + print_error_indent "flags.json only defines a single variant; need at least two for the swap test." + print_hint "Add a 'clouded' variant alongside 'blurry'." + TESTS_FAILED=$((TESTS_FAILED + 1)) + FAILED_CHECKS+=("single_variant") + else + BACKUP="$(mktemp)" + cp "$FLAGS_FILE" "$BACKUP" + # Restore on any exit path. + trap 'cp "$BACKUP" "$FLAGS_FILE" 2>/dev/null || true; rm -f "$BACKUP"' EXIT + + # Capture the current value, then swap. + BEFORE_VALUE=$(curl -sS --max-time 5 "$APP_URL" | jq -r '.value // empty') + jq --arg v "$OTHER_VARIANT" '.flags.vision_state.defaultVariant = $v' "$FLAGS_FILE" > "$FLAGS_FILE.tmp" && mv "$FLAGS_FILE.tmp" "$FLAGS_FILE" + + # Wait up to ~5s for the file watcher to pick up the change. + AFTER_VALUE="$BEFORE_VALUE" + for _ in 1 2 3 4 5; do + sleep 1 + AFTER_VALUE=$(curl -sS --max-time 5 "$APP_URL" | jq -r '.value // empty') + if [[ "$AFTER_VALUE" != "$BEFORE_VALUE" ]]; then + break + fi + done + + # Restore. + cp "$BACKUP" "$FLAGS_FILE" + rm -f "$BACKUP" + trap - EXIT + + if [[ "$AFTER_VALUE" != "$BEFORE_VALUE" && -n "$AFTER_VALUE" ]]; then + print_success_indent "Hot-reload works: response changed from '$BEFORE_VALUE' to '$AFTER_VALUE' after editing flags.json" + TESTS_PASSED=$((TESTS_PASSED + 1)) + else + print_error_indent "Editing flags.json did not change the response (still '$AFTER_VALUE')" + print_hint "flagd's file watcher should pick up the edit. Confirm flagd is running (docker compose ps) and that flags.json sits where the compose file mounts it." + TESTS_FAILED=$((TESTS_FAILED + 1)) + FAILED_CHECKS+=("hot_reload_failed") + fi + fi + fi +fi + +print_test_summary "stand up the lab" "$DOCS_URL" "$OBJECTIVE" diff --git a/adventures/planned/00-blind-by-design/docs/beginner.md b/adventures/planned/00-blind-by-design/docs/beginner.md new file mode 100644 index 00000000..c1c4a493 --- /dev/null +++ b/adventures/planned/00-blind-by-design/docs/beginner.md @@ -0,0 +1,183 @@ +# ๐ŸŸข Beginner: Stand up the lab + +Wire the OpenFeature Java SDK and the flagd contrib provider into a Spring Boot service so flag evaluations are resolved by a flagd sidecar against a `flags.json` file. Author your first flag, then prove that editing `flags.json` flips the response on the **next** request โ€” no app restart, no flagd restart, no redeploy. + +The Spring Boot service is already running on `:8080`; a flagd container is already running on `:8013`; `flags.json` is an empty skeleton (`{"flags": {}}`). The SDK is **not** wired in yet โ€” that's your job. + +## ๐Ÿงช The story (optional) + +The lab is on its first shift and it isn't reading the chart. Every subject who walks through the door gets the same hard-coded reading on their record โ€” no matter what the lab director just signed off on. The label coming out of the lab is a literal string baked into the controller, not a reading pulled from the chart. + +Your mission: replace that hard-coded label with an OpenFeature client, point that client at the **flagd sidecar** that already runs next to your Codespace, and let `flags.json` drive what gets recorded as the subject's `vision_state`. While you're at it, prove the lab can change what it records **without restarting anything** โ€” edit `flags.json`, save, and the next subject through the door has the new reading on their chart. + +## ๐Ÿ—๏ธ Architecture + +This level runs as two containers side-by-side in your Codespace โ€” the Spring Boot lab and a flagd sidecar. + +- **The lab** โ€” a Spring Boot 4 service on `http://localhost:8080/` with one endpoint, `GET /`. Today it returns a hard-coded `"untreated"` literal from `Trial`. +- **The chart** โ€” a `flags.json` file in the level folder, mounted **read-only** into the flagd sidecar. The participant edits it through the IDE; flagd's file watcher picks up the change. +- **The flagd sidecar** โ€” `ghcr.io/open-feature/flagd:latest`, started by the devcontainer compose stack. It serves flag evaluations over **gRPC on `:8013`**, watches `flags.json` on disk, and reloads when it changes. +- **The chart system** โ€” the OpenFeature Java SDK plus the **flagd contrib provider** in `Resolver.RPC` mode. The provider reads `FLAGD_HOST=flagd` / `FLAGD_PORT=8013` from the environment (the compose file pre-sets them), so there is no host or port to hard-code. + +## ๐ŸŽฏ Objective + +By the end of this level, you should: + +- Have `curl http://localhost:8080/` return a `vision_state` reading **resolved from `flags.json`** (not the hard-coded `"untreated"` fallback) +- Confirm the response payload includes the **OpenFeature evaluation details** โ€” flag key, variant, reason, value +- Edit `flags.json` to change the `defaultVariant`, save, and have the **next** request return the new variant **without restarting the app** + +## ๐Ÿง  What You'll Learn + +- How an OpenFeature client and provider work together โ€” the SDK is provider-agnostic and the flagd provider plugs in via dependency only +- What "remote provider" means in practice โ€” the SDK calls a separate flag service (flagd) over gRPC; the SDK does not parse `flags.json` itself +- What `flags.json` looks like for flagd (`state`, `variants`, `defaultVariant`) +- Why hot-reload of the flag file matters operationally โ€” configuration without redeploy + +## ๐Ÿงฐ Toolbox + +Your Codespace comes pre-configured with the following tools to help you solve the challenge: + +- [`./mvnw`](https://maven.apache.org/wrapper/): The Maven wrapper checked in next to `pom.xml`. Builds and runs the Spring Boot lab. +- [`curl`](https://curl.se/): Hits `http://localhost:8080/` and shows you what reading the lab is recording. +- [`jq`](https://jqlang.org/): Pretty-prints and filters the JSON evaluation details that come back from the SDK. +- A **flagd sidecar** โ€” already running in the devcontainer's compose stack. The flagd sidecar is on `:8013`; the other ports aren't used here. + +## โฐ Deadline + +_TBD โ€” to be announced at challenge launch._ +> โ„น๏ธ You can still complete the challenge after this date, but points will only be awarded for submissions before the +> deadline. + +## ๐Ÿ’ฌ Join the discussion + +Share your solutions and questions in the challenge thread on the Open Ecosystem Community. +_Discussion link will be added when this adventure goes live._ + +## ๐Ÿ“ Solution Walkthrough + +> โš ๏ธ **Spoiler Alert:** The following walkthrough contains the full solution to the challenge. We encourage you to try +> solving it on your own first. Consider coming back here only if you get stuck or want to check your approach. + +Need the answer key? Follow the [step-by-step beginner solution walkthrough](./solutions/beginner.md) for the final +`pom.xml` dependencies, `OpenFeatureConfig`, `flags.json`, and `Trial`. + +## โœ… How to Play + +### 1. Start Your Challenge + +- Click the "Fork" button in the top-right corner of the GitHub repo or use + [this link](https://github.com/dynatrace-oss/open-ecosystem-challenges/fork). +- From your fork, click the green **Code** button โ†’ **Codespaces hamburger menu** โ†’ **New with options**. +- Select the **Adventure 00 | ๐ŸŸข Beginner (Stand up the lab)** configuration. + +> โš ๏ธ **Important:** The challenge will not work if you choose another configuration (or the default). + +The Codespace will install a Java 21 toolchain and resolve the Maven dependencies. Once it is ready you'll have a +terminal in +`adventures/planned/00-blind-by-design/beginner/`. + +### 2. Access the UIs + +Open the **Ports** tab in the bottom panel. You should see: + +- **8080 โ€” Lab (Spring Boot).** Click the forwarded address. You should see the current hard-coded response: `untreated`. +- **8013 โ€” flagd gRPC.** This is the flagd sidecar. Nothing to click yet, but knowing it's there is the point: the lab + is going to talk to this in step 3. + +### 3. Implement the Objective + +You are turning a hard-coded label into a real protocol-driven reading. Work through the steps in this order โ€” each +step makes the next one possible. + +#### a. Add the OpenFeature SDK and the flagd provider to `pom.xml` + +The lab needs two dependencies: the OpenFeature Java SDK and the flagd contrib provider. GroupIds, artifactIds, and current versions are in the +[OpenFeature Java SDK docs](https://openfeature.dev/docs/reference/technologies/server/java/) and the +[flagd Java provider readme](https://github.com/open-feature/java-sdk-contrib/tree/main/providers/flagd). + +#### b. Configure the OpenFeature provider + +Create a Spring `@Configuration` class that, at startup, builds a `FlagdProvider` in **RPC mode** and registers it on the global OpenFeature API. The [flagd Java provider readme](https://github.com/open-feature/java-sdk-contrib/tree/main/providers/flagd) covers `FlagdOptions` / `Resolver.RPC` usage. + +You don't need to set host or port โ€” the devcontainer pre-sets `FLAGD_HOST=flagd` and `FLAGD_PORT=8013` in the environment, and the provider reads those automatically. + +#### c. Author the `vision_state` flag in `flags.json` + +The level ships an empty `flags.json` next to `pom.xml` (`{"flags": {}}`) so the flagd sidecar has a valid file to mount at boot. Open it and add a flag named `vision_state` with **two string variants** (e.g. `blurry` and `clouded`) so you have something to flip in the verification step. The schema (`state`, `variants`, `defaultVariant`) is in the [flagd flag-definitions reference](https://flagd.dev/reference/flag-definitions/). + +Save โ€” flagd's file watcher picks the change up within about a second; no restart needed. + +#### d. Read the chart from `Trial` + +Replace the hard-coded `return "untreated";` with an OpenFeature evaluation of the `vision_state` flag, using `"untreated"` as the fallback. **Return the full evaluation details** (not just the value) so the response carries the flag key, variant, value, and reason โ€” that's what the verifier checks. + +The Java SDK's evaluation methods are documented in the [OpenFeature Java SDK reference](https://openfeature.dev/docs/reference/technologies/server/java/). + +#### e. Restart the lab, then prove hot-reload + +You have two ways to start the lab: + +- **Click โ–ถ in VS Code.** The Spring Boot Dashboard panel (one of the recommended extensions in this devcontainer) lists `Laboratory` with a **Run** button. Or press **F5** with `Laboratory.java` open and pick **Java** as the debugger โ€” Spring's main class is detected automatically; no launch.json needed. +- **From the terminal** in the level folder: + + ```bash + ./mvnw spring-boot:run + ``` + +In another terminal: + +```bash +curl -s http://localhost:8080/ | jq +``` + +You should see `"value": "blurry"` and `"flagKey": "vision_state"`. Now, **without stopping the app or the flagd +sidecar**, edit `flags.json` and change `"defaultVariant": "blurry"` to `"defaultVariant": "clouded"`. Save, then +re-run the `curl`. The value should flip to `"clouded"` โ€” that's flagd's file watcher noticing the change on disk +and serving the new variant on the next gRPC evaluation. Nothing redeployed; nothing restarted. + +### 4. Verify Your Solution + +Once you think you've solved the challenge, it's time to verify! + +#### Run the Smoke Test + +Run the provided smoke test script (the lab must still be running on `:8080`): + +```bash +adventures/planned/00-blind-by-design/beginner/verify.sh +``` + +The script will: + +1. Confirm `http://localhost:8080/` is reachable. +2. Confirm the response is OpenFeature evaluation details for the `vision_state` flag. +3. Confirm the value is **not** the hard-coded `"untreated"` fallback. +4. Swap `defaultVariant` in `flags.json`, wait for the file watcher, confirm the response changes, then restore the + original file. + +If the test passes, your solution is very likely correct! ๐ŸŽ‰ + +## โœ… Verification + +A passing run looks roughly like this: + +```text +โœ… PASSED: All 4 checks passed + +It looks like you successfully completed this level! ๐ŸŒŸ +``` + +```json +{ + "flagKey": "vision_state", + "value": "blurry", + "variant": "blurry", + "reason": "STATIC", + "errorCode": null, + "errorMessage": null, + "flagMetadata": {} +} +``` + +If you see `"value": "blurry"` (or `"clouded"`) and `"flagKey": "vision_state"`, you're ready for the ๐ŸŸก Intermediate level โ€” **Outcome by cohort**. diff --git a/adventures/planned/00-blind-by-design/docs/expert.md b/adventures/planned/00-blind-by-design/docs/expert.md new file mode 100644 index 00000000..a8e2c287 --- /dev/null +++ b/adventures/planned/00-blind-by-design/docs/expert.md @@ -0,0 +1,3 @@ +# ๐Ÿ”ด Expert: Phase 3 โ€” read the chart + +๐Ÿšง **Coming Soon** โ€” this level is under construction. Track progress on the [adventure tracking issue](https://github.com/dynatrace-oss/open-ecosystem-challenges/issues/41). diff --git a/adventures/planned/00-blind-by-design/docs/index.md b/adventures/planned/00-blind-by-design/docs/index.md new file mode 100644 index 00000000..84c0930b --- /dev/null +++ b/adventures/planned/00-blind-by-design/docs/index.md @@ -0,0 +1,48 @@ +# ๐Ÿงช Adventure 00: Blind by Design + +Three levels of OpenFeature with **flagd** as the provider, in a Java + Spring Boot service. Wire the SDK against a flagd sidecar (Beginner), layer evaluation context to target by cohort (Intermediate), then instrument flag evaluations with OpenTelemetry and roll back a misbehaving fractional rollout (Expert) โ€” all without redeploying. + +The entire **infrastructure is pre-provisioned in your Codespace** โ€” no local setup required. + +## ๐Ÿช The Backstory + +OpenFeature is a vendor-neutral standard for feature flags. The reference cloud-native implementation is **flagd** โ€” it serves flag definitions from a JSON file, locally or remotely, and the OpenFeature SDK in your application calls it on every evaluation. + +In this adventure, the lab uses OpenFeature exactly the way a real engineering team would: a Spring Boot service holds the SDK client, flagd holds the flag definitions, and the targeting rules in `flags.json` decide what reading every subject ends up with. By the end, you'll have wired the SDK in from scratch, learned to record outcomes by cohort, and rolled back a misbehaving Phase 3 trial without redeploying. + +## ๐ŸŽฎ Choose Your Level + +Each level is a standalone challenge with its own Codespace that builds on the story while being technically independent โ€” pick your level and start wherever you feel comfortable. + +### ๐ŸŸข Beginner: Stand up the lab + +- **Status:** ๐Ÿšง Coming Soon +- **Topics:** OpenFeature Java SDK, flagd as a sidecar (`Resolver.RPC`), Spring Boot + +Wire OpenFeature into a Spring Boot service so the lab's `vision_state` reading is resolved by a flagd sidecar against a `flags.json` instead of a hard-coded literal. + +[**Start the Beginner Challenge**](./beginner.md){ .md-button .md-button--primary } + +### ๐ŸŸก Intermediate: Outcome by cohort + +- **Status:** ๐Ÿšง Coming Soon +- **Topics:** OpenFeature targeting, transaction context, hooks, Spring `HandlerInterceptor` + +Add request-scoped context, a global runtime context, an invocation context at the call site, and an audit hook so the lab records the right reading per subject cohort. + +[**Start the Intermediate Challenge**](./intermediate.md){ .md-button .md-button--primary } + +### ๐Ÿ”ด Expert: Phase 3 โ€” read the chart + +- **Status:** ๐Ÿšง Coming Soon +- **Topics:** OpenTelemetry traces + metrics, custom hooks, Grafana LGTM, fractional rollout, OpenFeature OTel hooks + +Finish wiring OpenTelemetry through to the Grafana LGTM stack, write a `ContextSpanHook` that puts the merged eval context onto Tempo spans, find the misbehaving Phase 3 amplifier in the dashboard, and roll it back without redeploying. + +[**Start the Expert Challenge**](./expert.md){ .md-button .md-button--primary } + +## ๐Ÿงช The story (optional) + +A research lab is testing a vision-enhancement serum on volunteers. The **lab** is a Spring Boot service. **OpenFeature** is the chart system. The protocol the lab is following is fixed; what differs per subject is the **`vision_state`** the lab records โ€” `blurry`, `sharp`, `enhanced`, or `clouded` โ€” because subjects don't all arrive with the same biology, the same dose adherence, or the same trial-jurisdiction baseline. + +The flagship Phase 3 trial โ€” a new vision-amplifier algorithm โ€” has started showing trouble: subjects stabilise slower, and roughly one in ten emerge **blind**. The dashboard that should be tracking all of this is dark, because the lab forgot to wire the metric exporter. Your mission across three levels: **stand up the lab**, **read the chart by cohort**, then **turn on the lights and roll back the trial** before more subjects lose their sight. diff --git a/adventures/planned/00-blind-by-design/docs/intermediate.md b/adventures/planned/00-blind-by-design/docs/intermediate.md new file mode 100644 index 00000000..54099a7b --- /dev/null +++ b/adventures/planned/00-blind-by-design/docs/intermediate.md @@ -0,0 +1,3 @@ +# ๐ŸŸก Intermediate: Outcome by cohort + +๐Ÿšง **Coming Soon** โ€” this level is under construction. Track progress on the [adventure tracking issue](https://github.com/dynatrace-oss/open-ecosystem-challenges/issues/41). diff --git a/adventures/planned/00-blind-by-design/docs/solutions/beginner.md b/adventures/planned/00-blind-by-design/docs/solutions/beginner.md new file mode 100644 index 00000000..c71f9a15 --- /dev/null +++ b/adventures/planned/00-blind-by-design/docs/solutions/beginner.md @@ -0,0 +1,177 @@ +# ๐ŸŸข Beginner โ€” Solution Walkthrough: Stand up the lab + +> โš ๏ธ **Spoiler Alert:** This page walks through the full solution. If you want to figure it out yourself, head back to +> the [Beginner challenge](../beginner.md) and only return when you're stuck. + +You are taking a Spring Boot service that returns a hard-coded label and turning it into a lab that reads its +prescription from `flags.json` through OpenFeature. There are four moving parts to get right: the dependencies, the +provider configuration, the flag file, and the controller. Below is the answer key for each. + +## 1. Add the OpenFeature SDK and the flagd provider + +The `pom.xml` you start with has only the Spring starters. Add the OpenFeature SDK and the flagd contrib provider โ€” +these are the two libraries the lab needs to evaluate flags. + +Open `pom.xml` and add the following inside ``: + +```xml + + dev.openfeature + sdk + 1.14.2 + + + dev.openfeature.contrib.providers + flagd + 0.11.8 + +``` + +The first one is the vendor-neutral OpenFeature client โ€” the API you call from your code. The second one is the +**provider**: the piece that knows how to talk to flagd. The SDK is provider-agnostic on purpose; you swap the +provider, your call sites stay the same. + +## 2. Point the FlagdProvider at the flagd sibling + +The provider has to be registered with OpenFeature before any evaluation can happen. Create a new file +`src/main/java/dev/openfeature/demo/java/demo/OpenFeatureConfig.java`: + +```java +package dev.openfeature.demo.java.demo; + +import dev.openfeature.contrib.providers.flagd.Config; +import dev.openfeature.contrib.providers.flagd.FlagdOptions; +import dev.openfeature.contrib.providers.flagd.FlagdProvider; +import dev.openfeature.sdk.OpenFeatureAPI; +import jakarta.annotation.PostConstruct; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class OpenFeatureConfig { + + @PostConstruct + public void initProvider() { + OpenFeatureAPI api = OpenFeatureAPI.getInstance(); + FlagdOptions flagdOptions = FlagdOptions.builder() + .resolverType(Config.Resolver.RPC) + .build(); + + api.setProviderAndWait(new FlagdProvider(flagdOptions)); + } +} +``` + +A few things worth noting: + +- `Resolver.RPC` tells the provider to talk to a flagd process over gRPC. The flagd sibling is already running in your + Codespace (look in the **Ports** tab for the `flagd gRPC` row on `:8013`). +- We do **not** hard-code a host or port. The Java flagd provider reads `FLAGD_HOST` / `FLAGD_PORT` from the + environment when no explicit value is set. The devcontainer's compose file pre-sets `FLAGD_HOST=flagd` so the lab + resolves the sibling by service name; running outside the devcontainer falls back to `localhost:8013` via the + published port. +- `setProviderAndWait` blocks until the provider has finished initializing, which means the first request the + controller serves is already wired up. + +> ๐Ÿ’ก The flagd contrib provider supports three resolver modes: +> +> - `RPC` โ€” one gRPC round-trip per evaluation. Simplest wire model, easiest to reason about. +> - `IN_PROCESS` โ€” the SDK opens a gRPC sync stream and the flag definitions stream **into** the JVM. Evaluations +> then happen locally, with no per-call network hop. This is the most common shape in real production deployments +> (flagd as a sidecar) โ€” we lead with `RPC` here only because the wire model is more explicit and easier to +> debug at level 1. Intermediate has a sidebar on flipping to `IN_PROCESS` against the same flagd sibling. +> - `FILE` โ€” read flags.json from local disk, no flagd at all. Useful for tests and local development without a +> sidecar. + +## 3. Author the flag file + +The broken state already ships a `flags.json` next to `pom.xml` โ€” it just has an empty `flags` object so the flagd +sibling has a valid file to mount at boot. Open it and add the `vision_state` flag definition: + +```json +{ + "flags": { + "vision_state": { + "state": "ENABLED", + "variants": { + "blurry": "blurry", + "clouded": "clouded" + }, + "defaultVariant": "blurry" + } + } +} +``` + +Three required fields per flag in flagd: + +- **`state`** โ€” `"ENABLED"` (or `"DISABLED"` to force the SDK fallback). +- **`variants`** โ€” a map from variant name to value. Two variants here give you something to flip in the verification + step. +- **`defaultVariant`** โ€” which variant gets returned when no targeting rules match. There are no rules at this level, + so this is the variant every request gets. + +Save. flagd is watching this file (the devcontainer mounts it read-only into the flagd sibling and tells it to +`start --uri file:.../flags.json`), so the next evaluation already sees the new flag โ€” no flagd restart, no app +restart. + +## 4. Read the chart from the controller + +Update `src/main/java/dev/openfeature/demo/java/demo/Trial.java` so it asks OpenFeature for the reading +instead of returning a literal: + +```java +package dev.openfeature.demo.java.demo; + +import dev.openfeature.sdk.Client; +import dev.openfeature.sdk.FlagEvaluationDetails; +import dev.openfeature.sdk.OpenFeatureAPI; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class Trial { + + @GetMapping("/") + public FlagEvaluationDetails observeSubject() { + Client client = OpenFeatureAPI.getInstance().getClient(); + return client.getStringDetails("vision_state", "untreated"); + } +} +``` + +Two intentional choices here: + +- `"untreated"` is the **fallback** value passed to `getStringDetails`. The SDK only returns it if no provider is + registered, the flag is missing, or the flag is disabled. Once your `OpenFeatureConfig` and `flags.json` are in + place, you should never see this value again โ€” and the smoke test asserts exactly that. +- The handler returns `FlagEvaluationDetails` directly, not just the value. Spring will serialize it to JSON + and the response will carry `flagKey`, `value`, `variant`, `reason`, and any error fields โ€” useful for debugging, + required by the smoke test. + +## 5. Run it and verify + +Restart the lab: + +```bash +./mvnw spring-boot:run +``` + +In another terminal: + +```bash +curl -s http://localhost:8080/ | jq +``` + +You should see `"value": "blurry"` and `"flagKey": "vision_state"`. Edit `flags.json`, change +`"defaultVariant": "blurry"` to `"defaultVariant": "clouded"`, save, and `curl` again โ€” the value flips to +`"clouded"` without restarting the app. That's the **flagd container** noticing the file changed on its read-only +mount and serving the new variant on the next gRPC evaluation. Neither the lab nor flagd had to restart; nothing +was redeployed. + +Run the smoke test from the repo root: + +```bash +adventures/planned/00-blind-by-design/beginner/verify.sh +``` + +When all four checks pass, the lab is reading the chart and you're done with the ๐ŸŸข Beginner level. diff --git a/adventures/planned/00-blind-by-design/mkdocs.yaml b/adventures/planned/00-blind-by-design/mkdocs.yaml new file mode 100644 index 00000000..f1cb90d3 --- /dev/null +++ b/adventures/planned/00-blind-by-design/mkdocs.yaml @@ -0,0 +1,11 @@ +site_name: '๐Ÿงช 00: Blind by Design' + +nav: + - Introduction: index.md + - '๐ŸŸข Beginner': beginner.md + - '๐ŸŸก Intermediate': intermediate.md + - '๐Ÿ”ด Expert': expert.md + - 'Solutions': + - '๐ŸŸข Beginner': solutions/beginner.md + - '๐ŸŸก Intermediate': solutions/intermediate.md + - '๐Ÿ”ด Expert': solutions/expert.md diff --git a/ideas/blind-by-design.md b/ideas/blind-by-design.md new file mode 100644 index 00000000..e5941953 --- /dev/null +++ b/ideas/blind-by-design.md @@ -0,0 +1,130 @@ +# Adventure Idea: ๐Ÿงช Blind by Design + +## Overview + +**Theme:** A research lab is testing a vision-enhancement serum on volunteers. The serum is supposed to take ordinary eyes and produce sharper, even enhanced sight โ€” useful for observation work. The lab is a Spring Boot service; OpenFeature is the chart system; `flags.json` decides what reading the lab records for each subject. The protocol is the same for every subject โ€” what differs is the **observed `vision_state`**, because subjects come in with different biology, dose adherence, and trial-jurisdiction baseline. The flagship Phase 3 trial โ€” a new amplifier algorithm โ€” has started showing trouble: subjects stabilise slower, and roughly one in ten emerge blind. The dashboard that should be tracking all of this is dark, because the lab forgot to wire the metric exporter. Your mission across three levels: stand up the lab, read the chart by cohort, then turn on the lights and roll back the trial before more subjects lose their sight. + +**Skills:** + +- Wire OpenFeature into a real application and resolve flags from a flagd sidecar +- Layer per-request, per-process, and per-evaluation context so the same trial yields the right reading for every cohort, and audit every reading in the logs +- Roll out a risky algorithm in measured phases and roll it back from observability data when it misbehaves + +**Technologies:** OpenFeature Java SDK, flagd, Spring Boot, Grafana LGTM (Tempo + Prometheus + Loki), Testcontainers + +--- + +## Levels + +### ๐ŸŸข Beginner: Stand up the lab + +#### Description + +Wire OpenFeature into a Spring Boot service so the lab's `vision_state` reading is resolved by a flagd sidecar against a `flags.json` instead of a hard-coded literal. + +#### Story + +The lab is on its first shift. Every subject who walks in gets the same hard-coded reading on their chart โ€” no matter what the lab director just signed off on, no matter what the protocol says. A flagd sidecar is already running next to the lab in the Codespace, with an empty `flags.json` mounted into it; the OpenFeature SDK is not in the project at all. The lab director has approved the switch: add the SDK + flagd contrib provider to the project, register the provider against the sidecar, author the first flag definition in `flags.json`, and let the chart drive what gets recorded for each subject. While you are at it, prove the lab can change the reading without restarting anything โ€” flagd's file watcher does the work. + +#### The Problem + +The Spring Boot starter app has a `Trial` controller whose `GET /` returns a string literal. There is no OpenFeature dependency in the `pom.xml`, no provider configured, and `flags.json` ships as `{"flags": {}}` so the flagd sidecar can boot. The participant must add the OpenFeature SDK and the flagd contrib provider, configure a `FlagdProvider` in `Resolver.RPC` mode (the devcontainer pre-sets `FLAGD_HOST=flagd` and `FLAGD_PORT=8013` so no host or port needs to be hard-coded), add the `vision_state` flag to `flags.json`, and switch the controller to call `client.getStringDetails` against it. + +#### Objective + +By the end of this level, the learner should: + +- Have `curl http://localhost:8080/` return a `vision_state` reading **resolved by the flagd sidecar** (not the hard-coded `untreated` fallback) +- Confirm the response payload includes the OpenFeature evaluation details (variant, reason, value) +- Edit `flags.json` to change the `defaultVariant`, save, and have the **next** request return the new variant **without restarting the app or flagd** + +#### What You'll Learn + +- How an OpenFeature client and provider work together โ€” the SDK is provider-agnostic and the flagd provider plugs in via dependency only +- What "remote provider" means in practice โ€” the SDK calls a separate flag service (flagd) over gRPC; the SDK does not parse `flags.json` itself +- What `flags.json` looks like for flagd (state, variants, defaultVariant) +- Why hot-reload of the flag file matters operationally โ€” configuration without redeploy + +#### Tools & Infrastructure + +- **Tools:** `curl`, `./mvnw`, `jq` (optional for prettier output) +- **Infrastructure:** Java 21 toolchain, a `flagd` sidecar service running in the devcontainer's compose stack on `:8013` (gRPC eval), `:8014` (management/metrics), `:8015` (sync), `:8016` (OFREP) + +--- + +### ๐ŸŸก Intermediate: Outcome by cohort + +#### Description + +Add request-scoped context, a global runtime context, an invocation context at the call site, and an audit hook so the lab records the right reading per subject cohort and every reading shows up in the audit log. + +#### Story + +The trial is widening. Subjects from outside the lab's local population are getting the wrong reading on their chart, and the lab director has just walked into the lab holding a stack of complaint forms. The protocol is the same for every subject; what differs is the *observed outcome* because subjects come in with different biology, different dose adherence, and the trial is registered in different jurisdictions. The OpenFeature client never sees what **species** is on the table (each subject brings their own โ€” humans, zyklops, you name it), never sees which **country** this trial is registered in (set once when the lab boots), never sees what **dose** the subject actually absorbed (varies per subject โ€” missed appointments, fast metabolisers, the usual reasons). And there is no audit hook recording who ended up with which reading. + +#### The Problem + +The lab from the Beginner level reads the flag, but the same variant comes back for every request. The flag definition in `flags.json` already has all three targeting branches loaded โ€” `species == zyklop`, improper-`dose` for non-zyklops, and `country == de` โ€” but none of those attributes are in the evaluation context yet, so the targeting has nothing to fire on. The participant must wire a `SpeciesInterceptor` that lifts `?species=` into the OpenFeature **transaction context**, populate the **global** evaluation context with `country` from the `COUNTRY` env var at startup, pass a `dose` attribute as **invocation context** at the call site of `client.getStringDetails(...)`, and register an `AuditHook` that records every evaluation with the cohort attributes that drove it. + +#### Objective + +By the end of this level, the learner should: + +- Have a Spring `HandlerInterceptor` (`SpeciesInterceptor`) that reads `?species=` from the request and sets it on the OpenFeature transaction context, then clears it on `afterCompletion` +- Have a global evaluation context that carries `country` from `System.getenv("COUNTRY")`, set once in `OpenFeatureConfig.@PostConstruct` +- Have the `Trial` controller pass a `dose` attribute as invocation context at the call site +- Have an `AuditHook` registered that emits one `[AUDIT] ...` log line per evaluation, at `WARN` for `clouded` outcomes +- Confirm `curl /?species=zyklop` returns `enhanced` (transaction wins), `curl /?dose=standard` with `COUNTRY=de` returns `sharp` (global), `curl /?dose=underdose` returns `clouded` (invocation), and `curl /?species=zyklop&dose=underdose` returns `enhanced` (precedence: species-zyklop is evaluated before improper-dose in `flags.json`) + +#### What You'll Learn + +- How OpenFeature's three evaluation-context layers compose โ€” global (per-process), transaction (per-request, propagated thread-locally), invocation (per-evaluation, passed at the call site) โ€” and how precedence works on conflict +- How transaction-context propagation works in a thread-per-request server with a `ThreadLocalTransactionContextPropagator` +- How hooks let you attach cross-cutting behaviour (audit today, observability tomorrow) without modifying every call site +- Why an audit log needs a fixed allowlist of context attributes โ€” `targetingKey` and other PII routinely sit in the merged context in real apps + +#### Tools & Infrastructure + +- **Tools:** `curl`, `./mvnw`, `tail -f` against `app.log`, two convenience runners (`./run-germany.sh` and `./run-austria.sh`) that pre-set `COUNTRY` and tee to the log +- **Infrastructure:** Same Java 21 toolchain, the same `flagd` sidecar from Beginner (compose-managed alongside `workspace`) + +--- + +### ๐Ÿ”ด Expert: Phase 3 โ€” read the chart + +#### Description + +Finish wiring OpenTelemetry traces and metrics through to the Grafana LGTM stack, write a `ContextSpanHook` of your own that mirrors the merged evaluation context onto the active span, find the misbehaving Phase 3 amplifier in the dashboard, and roll it back without redeploying. + +#### Story + +The trial just went wide. The same flagd sidecar from earlier levels is now serving two flags that matter โ€” the cohort-targeted `vision_state` from Intermediate, and a new fractional-rollout flag `vision_amplifier_v2` driving the Phase 3 trial. OpenTelemetry is half-wired: a traces exporter is shipping spans to Tempo, but the meter provider is unconfigured, so the rollout dashboard is dark. And the fractional bucket on `vision_amplifier_v2` is inverted โ€” every subject is rolling into the new amplifier. Each evaluation under the new amplifier is 200 milliseconds slower to stabilise, and roughly one in ten subjects emerges blind (HTTP 500). The lab is the lab โ€” it cannot fix what it cannot see. The dashboard is dark. + +The director wants four things, in order: the dashboard lit up, the eval-context attributes that drove each outcome searchable on the spans (so on-call can answer "which dose got which variant?"), the bad fractional bucket identified, and the rollout rolled back to a safe number โ€” all without redeploying the lab. + +#### The Problem + +The level ships a working lab pointed at the same `flagd` sidecar in `Resolver.RPC` mode, plus a Grafana LGTM container with OTLP receivers on the standard ports and a k6 loadgen that drives traffic when the `loadgen_active` flag is on. The OpenTelemetry SDK in the app is wired for traces (the OTel `TracesHook` is registered, the exporter writes to Tempo) but the meter provider's exporter is set to `none`, so the OpenFeature `MetricsHook` has nowhere to record. The `AuditHook` from Intermediate is carried over and continues to write a durable archive view; what the lab is missing is the **real-time correlation** between context attributes and span events. The participant must (1) flip `otel.metrics.exporter` from `none` to `otlp`, (2) register `MetricsHook` on the OpenFeatureAPI, (3) write a small `ContextSpanHook` that copies a fixed allowlist (`species`, `country`, `dose`) from the merged eval context onto the active span as `feature_flag.context.`, (4) flip `loadgen_active` to `on` and observe the latency and 5xx panels, and (5) edit `flags.json` to flip the `vision_amplifier_v2` fractional bucket back to `100% off / 0% on` while the app keeps running. + +#### Objective + +By the end of this level, the learner should: + +- Have `MetricsHook` registered and the OTel meter provider configured to export to the LGTM stack on `localhost:4317` +- Have a `ContextSpanHook` of their own that copies the merged evaluation context (`species`, `country`, `dose`) onto the active span as `feature_flag.context.` โ€” registered alongside `TracesHook` / `MetricsHook` +- Have **at least one trace** for `fun-with-flags-java-spring` visible in the Grafana **Explore โ†’ Tempo** view +- Have spans tagged with `feature_flag.context.dose=underdose` searchable in Tempo and lining up with `feature_flag.variant=clouded` on the same span +- Have the **Fun With Flags โ€” Feature Flag Metrics** dashboard showing live evaluation rate, variant distribution, and latency by variant +- Have `vision_amplifier_v2` rolled back to `0% on`, confirmed by reading the flag from flagd's gRPC-Gateway HTTP route on `:8013`, and the HTTP 5xx rate dropping below threshold afterwards + +#### What You'll Learn + +- How the OpenFeature OTel hooks (`TracesHook` and `MetricsHook`) join flag evaluations to the rest of an app's telemetry without a separate ingestion path +- How to author your own `Hook` โ€” a tiny class that reads `HookContext.getCtx()` (the merged eval context) and emits something useful (here: span attributes) โ€” and why the **PII allowlist** matters when those attributes flow into observability backends with multi-day retention +- How fractional rollout in flagd buckets subjects by `targetingKey` and how to read the bucketing from a dashboard +- How a flag flip is a faster operational lever than a redeploy when a rollout is misbehaving + +#### Tools & Infrastructure + +- **Tools:** `curl`, `./mvnw`, `docker compose`, a browser pointed at Grafana on `:3000` +- **Infrastructure:** Java 21 toolchain, `flagd` sidecar (`:8013` gRPC eval, `:8014` management/metrics, `:8015` sync, `:8016` OFREP), `grafana/otel-lgtm` container on `:3000`/`:4317`/`:4318`/`:9090`/`:3200`, k6 loadgen container driving traffic when `loadgen_active` is on From b40915d0eac7f28a0db2ec5642d075e154863eca Mon Sep 17 00:00:00 2001 From: Simon Schrottner Date: Thu, 30 Apr 2026 15:08:41 +0200 Subject: [PATCH 2/7] review: address PR #42 feedback from @KatharinaSick MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - move idea to ideas/.implemented/ (was: ideas/) - mkdocs.yaml: drop solution walkthroughs from nav (kept only for post-deadline release) - rename '๐Ÿงช The story (optional)' โ†’ '๐Ÿช The Backstory' for cross-adventure consistency - verify.sh: lean on lib/scripts helpers, follow Adventure 03 shape - pin all docker images (drop ':latest') - architecture: drop 'the chart' metaphor labels in favour of direct technical names - drop the 'Solution Walkthrough' promo section (solutions are unpublished pre-deadline) - add 'Start the Lab' step before 'Access the UIs' so the port doesn't 502 - devcontainer: forward only :8080 (was: 8080+8013+8014+8015+8016) - verify section: replace smoke-test / GitHub Actions wording with the Adventure 03 template Refs: PR #42 review by @KatharinaSick Co-Authored-By: Claude Opus 4.7 (1M context) Signed-off-by: Simon Schrottner --- .../devcontainer.json | 8 +- .../docker-compose.yml | 11 ++- .../00-blind-by-design/beginner/verify.sh | 73 +++++++++++------- .../00-blind-by-design/docs/beginner.md | 77 +++++++++---------- .../planned/00-blind-by-design/docs/index.md | 4 +- .../planned/00-blind-by-design/mkdocs.yaml | 4 - ideas/{ => .implemented}/blind-by-design.md | 0 7 files changed, 90 insertions(+), 87 deletions(-) rename ideas/{ => .implemented}/blind-by-design.md (100%) diff --git a/.devcontainer/00-blind-by-design_01-beginner/devcontainer.json b/.devcontainer/00-blind-by-design_01-beginner/devcontainer.json index dc179b2b..f66eafae 100644 --- a/.devcontainer/00-blind-by-design_01-beginner/devcontainer.json +++ b/.devcontainer/00-blind-by-design_01-beginner/devcontainer.json @@ -22,13 +22,9 @@ ] } }, - "forwardPorts": [8080, 8013, 8014, 8015, 8016], + "forwardPorts": [8080], "portsAttributes": { - "8080": { "label": "Lab (Spring Boot)", "onAutoForward": "notify" }, - "8013": { "label": "flagd gRPC eval", "onAutoForward": "ignore" }, - "8014": { "label": "flagd management/metrics", "onAutoForward": "ignore" }, - "8015": { "label": "flagd sync (IN_PROCESS)", "onAutoForward": "ignore" }, - "8016": { "label": "flagd OFREP", "onAutoForward": "ignore" } + "8080": { "label": "Lab (Spring Boot)", "onAutoForward": "notify" } }, "otherPortsAttributes": { "onAutoForward": "ignore" diff --git a/.devcontainer/00-blind-by-design_01-beginner/docker-compose.yml b/.devcontainer/00-blind-by-design_01-beginner/docker-compose.yml index 5ca946fc..4d7a44fc 100644 --- a/.devcontainer/00-blind-by-design_01-beginner/docker-compose.yml +++ b/.devcontainer/00-blind-by-design_01-beginner/docker-compose.yml @@ -25,7 +25,7 @@ services: - FLAGD_PORT=8013 flagd: - image: ghcr.io/open-feature/flagd:latest + image: ghcr.io/open-feature/flagd:v0.15.4 container_name: side-effects-beginner-flagd volumes: - ../..:/workspaces/${localWorkspaceFolderBasename:-open-ecosystem-challenges}:ro @@ -33,8 +33,7 @@ services: - start - --uri - file:/workspaces/${localWorkspaceFolderBasename:-open-ecosystem-challenges}/adventures/planned/00-blind-by-design/beginner/flags.json - ports: - - "8013:8013" - - "8014:8014" - - "8015:8015" - - "8016:8016" + # No `ports:` block โ€” the lab reaches flagd on the docker-internal + # network as `flagd:8013`. We deliberately do not publish to the host: + # only :8080 is forwarded into the Codespace, so participants see one + # port to click in the Ports tab. diff --git a/adventures/planned/00-blind-by-design/beginner/verify.sh b/adventures/planned/00-blind-by-design/beginner/verify.sh index bd46b2af..d80bf413 100755 --- a/adventures/planned/00-blind-by-design/beginner/verify.sh +++ b/adventures/planned/00-blind-by-design/beginner/verify.sh @@ -19,37 +19,36 @@ FLAGS_FILE="$SCRIPT_DIR/flags.json" print_header \ 'Adventure 00: Blind by Design' \ - 'Level 1: Stand up the lab' \ - 'Smoke Test Verification' + '๐ŸŸข Beginner: Stand up the lab' \ + 'Verification' -check_prerequisites curl jq - -print_sub_header "Running smoke tests..." - -# Track test results across all checks +# Init test counters TESTS_PASSED=0 TESTS_FAILED=0 FAILED_CHECKS=() -# 1. The Spring Boot lab is reachable on :8080. -print_test_section "Checking the lab is reachable on $APP_URL..." -RESPONSE=$(curl -sS --max-time 5 "$APP_URL" 2>/dev/null || echo "") +check_prerequisites curl jq -if [[ -z "$RESPONSE" ]]; then - print_error_indent "Lab did not respond on $APP_URL" - print_hint "Start the app with: ./mvnw spring-boot:run" - TESTS_FAILED=$((TESTS_FAILED + 1)) - FAILED_CHECKS+=("lab_unreachable") - print_test_summary "stand up the lab" "$DOCS_URL" "$OBJECTIVE" - exit +print_sub_header "Running verification checks..." + +# 1. The Spring Boot lab is reachable on :8080 and returns OpenFeature +# evaluation details for the vision_state flag. test_http_endpoint +# handles the connection failure / unexpected-content cases for us. +print_test_section "Checking the lab is reachable on $APP_URL..." +if ! test_http_endpoint "$APP_URL" "vision_state" \ + "Start the app with: ./mvnw spring-boot:run, then make sure Trial returns a FlagEvaluationDetails for 'vision_state'."; then + FAILED_CHECKS+=("vision_state_endpoint") + print_verification_summary "stand up the lab" "$DOCS_URL" "$OBJECTIVE" + exit 1 fi -print_success_indent "Lab responded on $APP_URL" -TESTS_PASSED=$((TESTS_PASSED + 1)) +print_new_line -# 2. Response is FlagEvaluationDetails JSON containing flag_key="vision_state". +# Cache the response once we know it's good โ€” the remaining checks reuse it. +RESPONSE=$(curl -s --max-time 5 "$APP_URL" 2>/dev/null || echo "") + +# 2. Response carries flagKey=vision_state (more precise than the substring check). print_test_section "Checking the response is an OpenFeature evaluation for 'vision_state'..." FLAG_KEY=$(echo "$RESPONSE" | jq -r '.flagKey // .flag_key // empty' 2>/dev/null || echo "") - if [[ "$FLAG_KEY" != "vision_state" ]]; then print_error_indent "Response did not include 'flagKey':'vision_state'" print_info_indent "Actual response: $RESPONSE" @@ -60,6 +59,7 @@ else print_success_indent "Response carries OpenFeature evaluation details for 'vision_state'" TESTS_PASSED=$((TESTS_PASSED + 1)) fi +print_new_line # 3. The resolved value is NOT the literal "untreated" fallback. print_test_section "Checking the value is resolved from a provider, not the hard-coded fallback..." @@ -80,6 +80,7 @@ else print_success_indent "Resolved value '$VALUE' (reason=$REASON)" TESTS_PASSED=$((TESTS_PASSED + 1)) fi +print_new_line # 4. flags.json is hot-reloaded: flip defaultVariant and confirm the response changes. print_test_section "Checking that flags.json drives the response (hot-reload swap)..." @@ -95,7 +96,6 @@ else TESTS_FAILED=$((TESTS_FAILED + 1)) FAILED_CHECKS+=("flags_json_invalid") else - # Pick a different variant from the file. OTHER_VARIANT=$(jq -r --arg cur "$ORIGINAL_VARIANT" '.flags.vision_state.variants | keys[] | select(. != $cur)' "$FLAGS_FILE" | head -n1) if [[ -z "$OTHER_VARIANT" ]]; then print_error_indent "flags.json only defines a single variant; need at least two for the swap test." @@ -105,14 +105,11 @@ else else BACKUP="$(mktemp)" cp "$FLAGS_FILE" "$BACKUP" - # Restore on any exit path. trap 'cp "$BACKUP" "$FLAGS_FILE" 2>/dev/null || true; rm -f "$BACKUP"' EXIT - # Capture the current value, then swap. BEFORE_VALUE=$(curl -sS --max-time 5 "$APP_URL" | jq -r '.value // empty') jq --arg v "$OTHER_VARIANT" '.flags.vision_state.defaultVariant = $v' "$FLAGS_FILE" > "$FLAGS_FILE.tmp" && mv "$FLAGS_FILE.tmp" "$FLAGS_FILE" - # Wait up to ~5s for the file watcher to pick up the change. AFTER_VALUE="$BEFORE_VALUE" for _ in 1 2 3 4 5; do sleep 1 @@ -122,7 +119,6 @@ else fi done - # Restore. cp "$BACKUP" "$FLAGS_FILE" rm -f "$BACKUP" trap - EXIT @@ -139,5 +135,28 @@ else fi fi fi +print_new_line + +# ============================================================================= +# Summary & Next Steps +# ============================================================================= +failed_checks_json="[]" +if [[ -n "${FAILED_CHECKS[*]:-}" ]]; then + failed_checks_json=$(printf '%s\n' "${FAILED_CHECKS[@]}" | jq -R . | jq -s .) +fi + +if [[ $TESTS_FAILED -gt 0 ]]; then + track_verification_completed "failed" "$failed_checks_json" + print_verification_summary "stand up the lab" "$DOCS_URL" "$OBJECTIVE" + exit 1 +fi + +track_verification_completed "success" "$failed_checks_json" -print_test_summary "stand up the lab" "$DOCS_URL" "$OBJECTIVE" +print_header "Test Results Summary" +print_success "โœ… PASSED: All $TESTS_PASSED verification checks passed!" +print_new_line + +if command -v check_submission_readiness >/dev/null 2>&1; then + check_submission_readiness "00-blind-by-design" "beginner" +fi diff --git a/adventures/planned/00-blind-by-design/docs/beginner.md b/adventures/planned/00-blind-by-design/docs/beginner.md index c1c4a493..e3f45029 100644 --- a/adventures/planned/00-blind-by-design/docs/beginner.md +++ b/adventures/planned/00-blind-by-design/docs/beginner.md @@ -4,7 +4,7 @@ Wire the OpenFeature Java SDK and the flagd contrib provider into a Spring Boot The Spring Boot service is already running on `:8080`; a flagd container is already running on `:8013`; `flags.json` is an empty skeleton (`{"flags": {}}`). The SDK is **not** wired in yet โ€” that's your job. -## ๐Ÿงช The story (optional) +## ๐Ÿช The Backstory The lab is on its first shift and it isn't reading the chart. Every subject who walks through the door gets the same hard-coded reading on their record โ€” no matter what the lab director just signed off on. The label coming out of the lab is a literal string baked into the controller, not a reading pulled from the chart. @@ -14,10 +14,10 @@ Your mission: replace that hard-coded label with an OpenFeature client, point th This level runs as two containers side-by-side in your Codespace โ€” the Spring Boot lab and a flagd sidecar. -- **The lab** โ€” a Spring Boot 4 service on `http://localhost:8080/` with one endpoint, `GET /`. Today it returns a hard-coded `"untreated"` literal from `Trial`. -- **The chart** โ€” a `flags.json` file in the level folder, mounted **read-only** into the flagd sidecar. The participant edits it through the IDE; flagd's file watcher picks up the change. -- **The flagd sidecar** โ€” `ghcr.io/open-feature/flagd:latest`, started by the devcontainer compose stack. It serves flag evaluations over **gRPC on `:8013`**, watches `flags.json` on disk, and reloads when it changes. -- **The chart system** โ€” the OpenFeature Java SDK plus the **flagd contrib provider** in `Resolver.RPC` mode. The provider reads `FLAGD_HOST=flagd` / `FLAGD_PORT=8013` from the environment (the compose file pre-sets them), so there is no host or port to hard-code. +- **Spring Boot service (the lab)** โ€” a Spring Boot 4 service on `http://localhost:8080/` with one endpoint, `GET /`. Today it returns a hard-coded `"untreated"` literal from `Trial`. +- **`flags.json`** โ€” a flag-definition file in the level folder, mounted **read-only** into the flagd sidecar. The participant edits it through the IDE; flagd's file watcher picks up the change. +- **flagd sidecar** โ€” `ghcr.io/open-feature/flagd:v0.15.4`, started by the devcontainer compose stack. It serves flag evaluations over **gRPC on `:8013`**, watches `flags.json` on disk, and reloads when it changes. +- **OpenFeature SDK + flagd contrib provider** โ€” the OpenFeature Java SDK plus the **flagd contrib provider** in `Resolver.RPC` mode. The provider reads `FLAGD_HOST=flagd` / `FLAGD_PORT=8013` from the environment (the compose file pre-sets them), so there is no host or port to hard-code. ## ๐ŸŽฏ Objective @@ -41,7 +41,7 @@ Your Codespace comes pre-configured with the following tools to help you solve t - [`./mvnw`](https://maven.apache.org/wrapper/): The Maven wrapper checked in next to `pom.xml`. Builds and runs the Spring Boot lab. - [`curl`](https://curl.se/): Hits `http://localhost:8080/` and shows you what reading the lab is recording. - [`jq`](https://jqlang.org/): Pretty-prints and filters the JSON evaluation details that come back from the SDK. -- A **flagd sidecar** โ€” already running in the devcontainer's compose stack. The flagd sidecar is on `:8013`; the other ports aren't used here. +- A **flagd sidecar** โ€” already running in the devcontainer's compose stack on the docker-internal network. The lab reaches it as `flagd:8013`; you don't need to forward its ports to play this challenge. ## โฐ Deadline @@ -54,14 +54,6 @@ _TBD โ€” to be announced at challenge launch._ Share your solutions and questions in the challenge thread on the Open Ecosystem Community. _Discussion link will be added when this adventure goes live._ -## ๐Ÿ“ Solution Walkthrough - -> โš ๏ธ **Spoiler Alert:** The following walkthrough contains the full solution to the challenge. We encourage you to try -> solving it on your own first. Consider coming back here only if you get stuck or want to check your approach. - -Need the answer key? Follow the [step-by-step beginner solution walkthrough](./solutions/beginner.md) for the final -`pom.xml` dependencies, `OpenFeatureConfig`, `flags.json`, and `Trial`. - ## โœ… How to Play ### 1. Start Your Challenge @@ -77,15 +69,28 @@ The Codespace will install a Java 21 toolchain and resolve the Maven dependencie terminal in `adventures/planned/00-blind-by-design/beginner/`. -### 2. Access the UIs +### 2. Start the Lab + +Before you open the forwarded port, start the Spring Boot lab so it is actually serving on `:8080`. You have two options: + +- **Click โ–ถ in VS Code.** The Spring Boot Dashboard panel (one of the recommended extensions in this devcontainer) lists `Laboratory` with a **Run** button. Or press **F5** with `Laboratory.java` open and pick **Java** as the debugger โ€” Spring's main class is detected automatically; no launch.json needed. +- **From the terminal** in the level folder: + + ```bash + ./mvnw spring-boot:run + ``` + +The lab boots in the broken state โ€” `Trial` returns the hard-coded `"untreated"` literal โ€” and that is exactly the starting point you want. + +### 3. Access the UIs Open the **Ports** tab in the bottom panel. You should see: - **8080 โ€” Lab (Spring Boot).** Click the forwarded address. You should see the current hard-coded response: `untreated`. -- **8013 โ€” flagd gRPC.** This is the flagd sidecar. Nothing to click yet, but knowing it's there is the point: the lab - is going to talk to this in step 3. -### 3. Implement the Objective +flagd is also running, but only inside the docker network โ€” you don't need to forward its ports to play this challenge. + +### 4. Implement the Objective You are turning a hard-coded label into a real protocol-driven reading. Work through the steps in this order โ€” each step makes the next one possible. @@ -116,16 +121,7 @@ The Java SDK's evaluation methods are documented in the [OpenFeature Java SDK re #### e. Restart the lab, then prove hot-reload -You have two ways to start the lab: - -- **Click โ–ถ in VS Code.** The Spring Boot Dashboard panel (one of the recommended extensions in this devcontainer) lists `Laboratory` with a **Run** button. Or press **F5** with `Laboratory.java` open and pick **Java** as the debugger โ€” Spring's main class is detected automatically; no launch.json needed. -- **From the terminal** in the level folder: - - ```bash - ./mvnw spring-boot:run - ``` - -In another terminal: +The lab is already running from step 2. Stop it (`Ctrl+C` in the terminal, or the **Stop** button in the Spring Boot Dashboard) and start it again so the new `OpenFeatureConfig` is picked up. Then, in a second terminal: ```bash curl -s http://localhost:8080/ | jq @@ -136,27 +132,24 @@ sidecar**, edit `flags.json` and change `"defaultVariant": "blurry"` to `"defaul re-run the `curl`. The value should flip to `"clouded"` โ€” that's flagd's file watcher noticing the change on disk and serving the new variant on the next gRPC evaluation. Nothing redeployed; nothing restarted. -### 4. Verify Your Solution +### 5. Verify Your Solution -Once you think you've solved the challenge, it's time to verify! - -#### Run the Smoke Test - -Run the provided smoke test script (the lab must still be running on `:8080`): +Once you think you've solved the challenge, run the verification script: ```bash -adventures/planned/00-blind-by-design/beginner/verify.sh +./verify.sh ``` -The script will: +**If the verification fails:** + +The script will tell you which checks failed. Fix the issues and run it again. -1. Confirm `http://localhost:8080/` is reachable. -2. Confirm the response is OpenFeature evaluation details for the `vision_state` flag. -3. Confirm the value is **not** the hard-coded `"untreated"` fallback. -4. Swap `defaultVariant` in `flags.json`, wait for the file watcher, confirm the response changes, then restore the - original file. +**If the verification passes:** -If the test passes, your solution is very likely correct! ๐ŸŽ‰ +1. The script will check if your changes are committed and pushed. +2. Follow the on-screen instructions to commit your changes if needed. +3. Once everything is ready, the script will generate a **Certificate of Completion**. +4. **Copy this certificate** and paste it into the [challenge thread](https://community.open-ecosystem.com/c/open-ecosystem-challenges/) to claim your victory! ๐Ÿ† ## โœ… Verification diff --git a/adventures/planned/00-blind-by-design/docs/index.md b/adventures/planned/00-blind-by-design/docs/index.md index 84c0930b..c789c129 100644 --- a/adventures/planned/00-blind-by-design/docs/index.md +++ b/adventures/planned/00-blind-by-design/docs/index.md @@ -4,7 +4,7 @@ Three levels of OpenFeature with **flagd** as the provider, in a Java + Spring B The entire **infrastructure is pre-provisioned in your Codespace** โ€” no local setup required. -## ๐Ÿช The Backstory +## ๐Ÿง  What you'll be using OpenFeature is a vendor-neutral standard for feature flags. The reference cloud-native implementation is **flagd** โ€” it serves flag definitions from a JSON file, locally or remotely, and the OpenFeature SDK in your application calls it on every evaluation. @@ -41,7 +41,7 @@ Finish wiring OpenTelemetry through to the Grafana LGTM stack, write a `ContextS [**Start the Expert Challenge**](./expert.md){ .md-button .md-button--primary } -## ๐Ÿงช The story (optional) +## ๐Ÿช The Backstory A research lab is testing a vision-enhancement serum on volunteers. The **lab** is a Spring Boot service. **OpenFeature** is the chart system. The protocol the lab is following is fixed; what differs per subject is the **`vision_state`** the lab records โ€” `blurry`, `sharp`, `enhanced`, or `clouded` โ€” because subjects don't all arrive with the same biology, the same dose adherence, or the same trial-jurisdiction baseline. diff --git a/adventures/planned/00-blind-by-design/mkdocs.yaml b/adventures/planned/00-blind-by-design/mkdocs.yaml index f1cb90d3..c06799e9 100644 --- a/adventures/planned/00-blind-by-design/mkdocs.yaml +++ b/adventures/planned/00-blind-by-design/mkdocs.yaml @@ -5,7 +5,3 @@ nav: - '๐ŸŸข Beginner': beginner.md - '๐ŸŸก Intermediate': intermediate.md - '๐Ÿ”ด Expert': expert.md - - 'Solutions': - - '๐ŸŸข Beginner': solutions/beginner.md - - '๐ŸŸก Intermediate': solutions/intermediate.md - - '๐Ÿ”ด Expert': solutions/expert.md diff --git a/ideas/blind-by-design.md b/ideas/.implemented/blind-by-design.md similarity index 100% rename from ideas/blind-by-design.md rename to ideas/.implemented/blind-by-design.md From 8d63d5556dda2589877060952b104308dbffd3de Mon Sep 17 00:00:00 2001 From: Simon Schrottner Date: Thu, 30 Apr 2026 15:16:39 +0200 Subject: [PATCH 3/7] narrative: incorporate the Aletheia Institute framing into the Backstory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cherry-picking from @KatharinaSick's story suggestion on PR #42: the institute name (a Greek pun on truth/disclosure that lands the vision theme), the eight-months / eight-weeks timeline, and the "the monitoring is dark โ€” not by accident, but because no one ever turned the lights on" framing. Beginner-level cinematic intro left out โ€” it doubles the story length in a section that was deliberately trimmed. Signed-off-by: Simon Schrottner --- adventures/planned/00-blind-by-design/docs/index.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/adventures/planned/00-blind-by-design/docs/index.md b/adventures/planned/00-blind-by-design/docs/index.md index c789c129..04d5a3e9 100644 --- a/adventures/planned/00-blind-by-design/docs/index.md +++ b/adventures/planned/00-blind-by-design/docs/index.md @@ -43,6 +43,8 @@ Finish wiring OpenTelemetry through to the Grafana LGTM stack, write a `ContextS ## ๐Ÿช The Backstory -A research lab is testing a vision-enhancement serum on volunteers. The **lab** is a Spring Boot service. **OpenFeature** is the chart system. The protocol the lab is following is fixed; what differs per subject is the **`vision_state`** the lab records โ€” `blurry`, `sharp`, `enhanced`, or `clouded` โ€” because subjects don't all arrive with the same biology, the same dose adherence, or the same trial-jurisdiction baseline. +The **Aletheia Institute** is running a multi-phase vision-enhancement trial. The **lab** is a Spring Boot service whose one job is to record the **`vision_state`** of every subject who walks through the protocol โ€” `blurry`, `sharp`, `enhanced`, or `clouded` โ€” because subjects don't all arrive with the same biology, the same dose adherence, or the same trial-jurisdiction baseline. The flag definitions that drive those readings live in `flags.json`, watched by a **flagd** sidecar; the **OpenFeature** SDK is supposed to call that sidecar on every evaluation. -The flagship Phase 3 trial โ€” a new vision-amplifier algorithm โ€” has started showing trouble: subjects stabilise slower, and roughly one in ten emerge **blind**. The dashboard that should be tracking all of this is dark, because the lab forgot to wire the metric exporter. Your mission across three levels: **stand up the lab**, **read the chart by cohort**, then **turn on the lights and roll back the trial** before more subjects lose their sight. +It hasn't been. For the past **eight months**, every subject through the door has been recorded as `"untreated"` โ€” the integration was never finished, and the lab director assumed the system was reading the chart. Worse, **eight weeks ago** the Institute opened its flagship Phase 3 trial: a new amplifier variant rolled out fractionally to a cohort by a targeting rule in `flags.json`. **Four adverse-event reports** have since been filed, each one a subject whose `vision_state` at discharge was worse than at enrollment. + +The monitoring is dark โ€” not by accident, but because no one ever turned the lights on. Your mission across three levels: **stand up the lab** so it reads the chart, **read the chart by cohort** so outcomes can be tracked, then **turn on the lights and roll back the Phase 3 variant** before the director signs off on the next enrollment batch. From 156ddb42a84e9b07ffec36eeb81dd7f26ceefd42 Mon Sep 17 00:00:00 2001 From: Simon Schrottner Date: Thu, 30 Apr 2026 15:22:43 +0200 Subject: [PATCH 4/7] mkdocs: drop the Intermediate / Expert nav entries from the Beginner-only PR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per @KatharinaSick's first instinct on review โ€” and on reflection the author agrees: when the placeholder stubs are reachable from the side-nav they create more confusion than they resolve. The stub files stay on disk so docs/index.md's links don't 404, but they're not surfaced in navigation. Intermediate and Expert come back into the nav in #43 and #44 once their content is real. Signed-off-by: Simon Schrottner --- adventures/planned/00-blind-by-design/mkdocs.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/adventures/planned/00-blind-by-design/mkdocs.yaml b/adventures/planned/00-blind-by-design/mkdocs.yaml index c06799e9..3180a33d 100644 --- a/adventures/planned/00-blind-by-design/mkdocs.yaml +++ b/adventures/planned/00-blind-by-design/mkdocs.yaml @@ -3,5 +3,3 @@ site_name: '๐Ÿงช 00: Blind by Design' nav: - Introduction: index.md - '๐ŸŸข Beginner': beginner.md - - '๐ŸŸก Intermediate': intermediate.md - - '๐Ÿ”ด Expert': expert.md From 0790ed40fa2378803f754e7009ebba6ff717b224 Mon Sep 17 00:00:00 2001 From: Simon Schrottner Date: Thu, 30 Apr 2026 15:35:05 +0200 Subject: [PATCH 5/7] docs(scope): drop the Intermediate / Expert placeholders from the Beginner-only PR The two stub level-docs and the matching index.md cards were carried in solely so docs/index.md links did not 404. With the nav already trimmed (per @KatharinaSick on PR #42), and now with the cards out of the landing page, the Beginner PR is genuinely scoped to a single level. Intermediate and Expert each add their own card + level doc as part of their respective PRs (#43 / #44). Co-Authored-By: Claude Opus 4.7 (1M context) Signed-off-by: Simon Schrottner --- .../planned/00-blind-by-design/docs/expert.md | 3 --- .../planned/00-blind-by-design/docs/index.md | 20 +------------------ .../00-blind-by-design/docs/intermediate.md | 3 --- 3 files changed, 1 insertion(+), 25 deletions(-) delete mode 100644 adventures/planned/00-blind-by-design/docs/expert.md delete mode 100644 adventures/planned/00-blind-by-design/docs/intermediate.md diff --git a/adventures/planned/00-blind-by-design/docs/expert.md b/adventures/planned/00-blind-by-design/docs/expert.md deleted file mode 100644 index a8e2c287..00000000 --- a/adventures/planned/00-blind-by-design/docs/expert.md +++ /dev/null @@ -1,3 +0,0 @@ -# ๐Ÿ”ด Expert: Phase 3 โ€” read the chart - -๐Ÿšง **Coming Soon** โ€” this level is under construction. Track progress on the [adventure tracking issue](https://github.com/dynatrace-oss/open-ecosystem-challenges/issues/41). diff --git a/adventures/planned/00-blind-by-design/docs/index.md b/adventures/planned/00-blind-by-design/docs/index.md index 04d5a3e9..94757990 100644 --- a/adventures/planned/00-blind-by-design/docs/index.md +++ b/adventures/planned/00-blind-by-design/docs/index.md @@ -12,7 +12,7 @@ In this adventure, the lab uses OpenFeature exactly the way a real engineering t ## ๐ŸŽฎ Choose Your Level -Each level is a standalone challenge with its own Codespace that builds on the story while being technically independent โ€” pick your level and start wherever you feel comfortable. +Each level is a standalone challenge with its own Codespace, building on the story while staying technically independent. The Beginner level is below; Intermediate and Expert land in follow-up PRs. ### ๐ŸŸข Beginner: Stand up the lab @@ -23,24 +23,6 @@ Wire OpenFeature into a Spring Boot service so the lab's `vision_state` reading [**Start the Beginner Challenge**](./beginner.md){ .md-button .md-button--primary } -### ๐ŸŸก Intermediate: Outcome by cohort - -- **Status:** ๐Ÿšง Coming Soon -- **Topics:** OpenFeature targeting, transaction context, hooks, Spring `HandlerInterceptor` - -Add request-scoped context, a global runtime context, an invocation context at the call site, and an audit hook so the lab records the right reading per subject cohort. - -[**Start the Intermediate Challenge**](./intermediate.md){ .md-button .md-button--primary } - -### ๐Ÿ”ด Expert: Phase 3 โ€” read the chart - -- **Status:** ๐Ÿšง Coming Soon -- **Topics:** OpenTelemetry traces + metrics, custom hooks, Grafana LGTM, fractional rollout, OpenFeature OTel hooks - -Finish wiring OpenTelemetry through to the Grafana LGTM stack, write a `ContextSpanHook` that puts the merged eval context onto Tempo spans, find the misbehaving Phase 3 amplifier in the dashboard, and roll it back without redeploying. - -[**Start the Expert Challenge**](./expert.md){ .md-button .md-button--primary } - ## ๐Ÿช The Backstory The **Aletheia Institute** is running a multi-phase vision-enhancement trial. The **lab** is a Spring Boot service whose one job is to record the **`vision_state`** of every subject who walks through the protocol โ€” `blurry`, `sharp`, `enhanced`, or `clouded` โ€” because subjects don't all arrive with the same biology, the same dose adherence, or the same trial-jurisdiction baseline. The flag definitions that drive those readings live in `flags.json`, watched by a **flagd** sidecar; the **OpenFeature** SDK is supposed to call that sidecar on every evaluation. diff --git a/adventures/planned/00-blind-by-design/docs/intermediate.md b/adventures/planned/00-blind-by-design/docs/intermediate.md deleted file mode 100644 index 54099a7b..00000000 --- a/adventures/planned/00-blind-by-design/docs/intermediate.md +++ /dev/null @@ -1,3 +0,0 @@ -# ๐ŸŸก Intermediate: Outcome by cohort - -๐Ÿšง **Coming Soon** โ€” this level is under construction. Track progress on the [adventure tracking issue](https://github.com/dynatrace-oss/open-ecosystem-challenges/issues/41). From 422072e5a15862e01fd63ee95de1ca5c923a86b4 Mon Sep 17 00:00:00 2001 From: Katharina Sick Date: Thu, 30 Apr 2026 15:57:22 +0200 Subject: [PATCH 6/7] add a Makefile and add minor doc tweaks Signed-off-by: Katharina Sick --- .../00-blind-by-design/beginner/Makefile | 36 ++++ .../00-blind-by-design/docs/beginner.md | 28 +-- .../planned/00-blind-by-design/docs/index.md | 26 ++- .../docs/solutions/beginner.md | 177 ------------------ 4 files changed, 58 insertions(+), 209 deletions(-) create mode 100644 adventures/planned/00-blind-by-design/beginner/Makefile delete mode 100644 adventures/planned/00-blind-by-design/docs/solutions/beginner.md diff --git a/adventures/planned/00-blind-by-design/beginner/Makefile b/adventures/planned/00-blind-by-design/beginner/Makefile new file mode 100644 index 00000000..5359b523 --- /dev/null +++ b/adventures/planned/00-blind-by-design/beginner/Makefile @@ -0,0 +1,36 @@ +# ============================================================================ +# Makefile for Blind by Design - Beginner Level: Stand up the lab +# ============================================================================ +# This Makefile provides convenient commands for running +# the Spring Boot lab and verifying your solution. +# ============================================================================ + +.PHONY: help lab probe verify + +# Default target - show help +help: + @echo "Blind by Design - Beginner Level: Stand up the lab" + @echo "" + @echo "Application:" + @echo " make lab - Start the Spring Boot lab on :8080" + @echo " make probe - Hit the lab endpoint and pretty-print the response" + @echo "" + @echo "Verification:" + @echo " make verify - Run verification checks" + +# ---------------------------------------------------------------------------- +# Application Targets +# ---------------------------------------------------------------------------- + +lab: + @./mvnw spring-boot:run + +probe: + @curl -s http://localhost:8080/ | jq + +# ---------------------------------------------------------------------------- +# Verification Targets +# ---------------------------------------------------------------------------- + +verify: + @./verify.sh diff --git a/adventures/planned/00-blind-by-design/docs/beginner.md b/adventures/planned/00-blind-by-design/docs/beginner.md index e3f45029..fe4c5706 100644 --- a/adventures/planned/00-blind-by-design/docs/beginner.md +++ b/adventures/planned/00-blind-by-design/docs/beginner.md @@ -2,7 +2,7 @@ Wire the OpenFeature Java SDK and the flagd contrib provider into a Spring Boot service so flag evaluations are resolved by a flagd sidecar against a `flags.json` file. Author your first flag, then prove that editing `flags.json` flips the response on the **next** request โ€” no app restart, no flagd restart, no redeploy. -The Spring Boot service is already running on `:8080`; a flagd container is already running on `:8013`; `flags.json` is an empty skeleton (`{"flags": {}}`). The SDK is **not** wired in yet โ€” that's your job. +The Spring Boot service runs on `:8080`; a flagd container is already running on `:8013`; `flags.json` is an empty skeleton (`{"flags": {}}`). The SDK is **not** wired in yet โ€” that's your job. ## ๐Ÿช The Backstory @@ -82,7 +82,7 @@ Before you open the forwarded port, start the Spring Boot lab so it is actually The lab boots in the broken state โ€” `Trial` returns the hard-coded `"untreated"` literal โ€” and that is exactly the starting point you want. -### 3. Access the UIs +### 3. Access the UI Open the **Ports** tab in the bottom panel. You should see: @@ -150,27 +150,3 @@ The script will tell you which checks failed. Fix the issues and run it again. 2. Follow the on-screen instructions to commit your changes if needed. 3. Once everything is ready, the script will generate a **Certificate of Completion**. 4. **Copy this certificate** and paste it into the [challenge thread](https://community.open-ecosystem.com/c/open-ecosystem-challenges/) to claim your victory! ๐Ÿ† - -## โœ… Verification - -A passing run looks roughly like this: - -```text -โœ… PASSED: All 4 checks passed - -It looks like you successfully completed this level! ๐ŸŒŸ -``` - -```json -{ - "flagKey": "vision_state", - "value": "blurry", - "variant": "blurry", - "reason": "STATIC", - "errorCode": null, - "errorMessage": null, - "flagMetadata": {} -} -``` - -If you see `"value": "blurry"` (or `"clouded"`) and `"flagKey": "vision_state"`, you're ready for the ๐ŸŸก Intermediate level โ€” **Outcome by cohort**. diff --git a/adventures/planned/00-blind-by-design/docs/index.md b/adventures/planned/00-blind-by-design/docs/index.md index 94757990..1d4a8e84 100644 --- a/adventures/planned/00-blind-by-design/docs/index.md +++ b/adventures/planned/00-blind-by-design/docs/index.md @@ -4,6 +4,14 @@ Three levels of OpenFeature with **flagd** as the provider, in a Java + Spring B The entire **infrastructure is pre-provisioned in your Codespace** โ€” no local setup required. +## ๐Ÿช The Backstory + +The **Aletheia Institute** is running a multi-phase vision-enhancement trial. The **lab** is a Spring Boot service whose one job is to record the **`vision_state`** of every subject who walks through the protocol โ€” `blurry`, `sharp`, `enhanced`, or `clouded` โ€” because subjects don't all arrive with the same biology, the same dose adherence, or the same trial-jurisdiction baseline. The flag definitions that drive those readings live in `flags.json`, watched by a **flagd** sidecar; the **OpenFeature** SDK is supposed to call that sidecar on every evaluation. + +It hasn't been. For the past **eight months**, every subject through the door has been recorded as `"untreated"` โ€” the integration was never finished, and the lab director assumed the system was reading the chart. Worse, **eight weeks ago** the Institute opened its flagship Phase 3 trial: a new amplifier variant rolled out fractionally to a cohort by a targeting rule in `flags.json`. **Four adverse-event reports** have since been filed, each one a subject whose `vision_state` at discharge was worse than at enrollment. + +The monitoring is dark โ€” not by accident, but because no one ever turned the lights on. Your mission across three levels: **stand up the lab** so it reads the chart, **read the chart by cohort** so outcomes can be tracked, then **turn on the lights and roll back the Phase 3 variant** before the director signs off on the next enrollment batch. + ## ๐Ÿง  What you'll be using OpenFeature is a vendor-neutral standard for feature flags. The reference cloud-native implementation is **flagd** โ€” it serves flag definitions from a JSON file, locally or remotely, and the OpenFeature SDK in your application calls it on every evaluation. @@ -12,21 +20,27 @@ In this adventure, the lab uses OpenFeature exactly the way a real engineering t ## ๐ŸŽฎ Choose Your Level -Each level is a standalone challenge with its own Codespace, building on the story while staying technically independent. The Beginner level is below; Intermediate and Expert land in follow-up PRs. +Each level is a standalone challenge with its own Codespace, building on the story while staying technically independent. ### ๐ŸŸข Beginner: Stand up the lab -- **Status:** ๐Ÿšง Coming Soon +- **Status:** โœ… Ready to Play - **Topics:** OpenFeature Java SDK, flagd as a sidecar (`Resolver.RPC`), Spring Boot Wire OpenFeature into a Spring Boot service so the lab's `vision_state` reading is resolved by a flagd sidecar against a `flags.json` instead of a hard-coded literal. [**Start the Beginner Challenge**](./beginner.md){ .md-button .md-button--primary } -## ๐Ÿช The Backstory +### ๐ŸŸก Intermediate: Outcome by cohort -The **Aletheia Institute** is running a multi-phase vision-enhancement trial. The **lab** is a Spring Boot service whose one job is to record the **`vision_state`** of every subject who walks through the protocol โ€” `blurry`, `sharp`, `enhanced`, or `clouded` โ€” because subjects don't all arrive with the same biology, the same dose adherence, or the same trial-jurisdiction baseline. The flag definitions that drive those readings live in `flags.json`, watched by a **flagd** sidecar; the **OpenFeature** SDK is supposed to call that sidecar on every evaluation. +- **Status:** ๐Ÿšง Coming Soon +- **Topics:** OpenFeature targeting, transaction context, hooks, Spring `HandlerInterceptor` -It hasn't been. For the past **eight months**, every subject through the door has been recorded as `"untreated"` โ€” the integration was never finished, and the lab director assumed the system was reading the chart. Worse, **eight weeks ago** the Institute opened its flagship Phase 3 trial: a new amplifier variant rolled out fractionally to a cohort by a targeting rule in `flags.json`. **Four adverse-event reports** have since been filed, each one a subject whose `vision_state` at discharge was worse than at enrollment. +Add request-scoped context, a global runtime context, an invocation context at the call site, and an audit hook so the lab records the right reading per subject cohort. -The monitoring is dark โ€” not by accident, but because no one ever turned the lights on. Your mission across three levels: **stand up the lab** so it reads the chart, **read the chart by cohort** so outcomes can be tracked, then **turn on the lights and roll back the Phase 3 variant** before the director signs off on the next enrollment batch. +### ๐Ÿ”ด Expert: Phase 3 โ€” read the chart + +- **Status:** ๐Ÿšง Coming Soon +- **Topics:** OpenTelemetry traces + metrics, custom hooks, Grafana LGTM, fractional rollout, OpenFeature OTel hooks + +Finish wiring OpenTelemetry through to the Grafana LGTM stack, write a `ContextSpanHook` that puts the merged eval context onto Tempo spans, find the misbehaving Phase 3 amplifier in the dashboard, and roll it back without redeploying. diff --git a/adventures/planned/00-blind-by-design/docs/solutions/beginner.md b/adventures/planned/00-blind-by-design/docs/solutions/beginner.md deleted file mode 100644 index c71f9a15..00000000 --- a/adventures/planned/00-blind-by-design/docs/solutions/beginner.md +++ /dev/null @@ -1,177 +0,0 @@ -# ๐ŸŸข Beginner โ€” Solution Walkthrough: Stand up the lab - -> โš ๏ธ **Spoiler Alert:** This page walks through the full solution. If you want to figure it out yourself, head back to -> the [Beginner challenge](../beginner.md) and only return when you're stuck. - -You are taking a Spring Boot service that returns a hard-coded label and turning it into a lab that reads its -prescription from `flags.json` through OpenFeature. There are four moving parts to get right: the dependencies, the -provider configuration, the flag file, and the controller. Below is the answer key for each. - -## 1. Add the OpenFeature SDK and the flagd provider - -The `pom.xml` you start with has only the Spring starters. Add the OpenFeature SDK and the flagd contrib provider โ€” -these are the two libraries the lab needs to evaluate flags. - -Open `pom.xml` and add the following inside ``: - -```xml - - dev.openfeature - sdk - 1.14.2 - - - dev.openfeature.contrib.providers - flagd - 0.11.8 - -``` - -The first one is the vendor-neutral OpenFeature client โ€” the API you call from your code. The second one is the -**provider**: the piece that knows how to talk to flagd. The SDK is provider-agnostic on purpose; you swap the -provider, your call sites stay the same. - -## 2. Point the FlagdProvider at the flagd sibling - -The provider has to be registered with OpenFeature before any evaluation can happen. Create a new file -`src/main/java/dev/openfeature/demo/java/demo/OpenFeatureConfig.java`: - -```java -package dev.openfeature.demo.java.demo; - -import dev.openfeature.contrib.providers.flagd.Config; -import dev.openfeature.contrib.providers.flagd.FlagdOptions; -import dev.openfeature.contrib.providers.flagd.FlagdProvider; -import dev.openfeature.sdk.OpenFeatureAPI; -import jakarta.annotation.PostConstruct; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class OpenFeatureConfig { - - @PostConstruct - public void initProvider() { - OpenFeatureAPI api = OpenFeatureAPI.getInstance(); - FlagdOptions flagdOptions = FlagdOptions.builder() - .resolverType(Config.Resolver.RPC) - .build(); - - api.setProviderAndWait(new FlagdProvider(flagdOptions)); - } -} -``` - -A few things worth noting: - -- `Resolver.RPC` tells the provider to talk to a flagd process over gRPC. The flagd sibling is already running in your - Codespace (look in the **Ports** tab for the `flagd gRPC` row on `:8013`). -- We do **not** hard-code a host or port. The Java flagd provider reads `FLAGD_HOST` / `FLAGD_PORT` from the - environment when no explicit value is set. The devcontainer's compose file pre-sets `FLAGD_HOST=flagd` so the lab - resolves the sibling by service name; running outside the devcontainer falls back to `localhost:8013` via the - published port. -- `setProviderAndWait` blocks until the provider has finished initializing, which means the first request the - controller serves is already wired up. - -> ๐Ÿ’ก The flagd contrib provider supports three resolver modes: -> -> - `RPC` โ€” one gRPC round-trip per evaluation. Simplest wire model, easiest to reason about. -> - `IN_PROCESS` โ€” the SDK opens a gRPC sync stream and the flag definitions stream **into** the JVM. Evaluations -> then happen locally, with no per-call network hop. This is the most common shape in real production deployments -> (flagd as a sidecar) โ€” we lead with `RPC` here only because the wire model is more explicit and easier to -> debug at level 1. Intermediate has a sidebar on flipping to `IN_PROCESS` against the same flagd sibling. -> - `FILE` โ€” read flags.json from local disk, no flagd at all. Useful for tests and local development without a -> sidecar. - -## 3. Author the flag file - -The broken state already ships a `flags.json` next to `pom.xml` โ€” it just has an empty `flags` object so the flagd -sibling has a valid file to mount at boot. Open it and add the `vision_state` flag definition: - -```json -{ - "flags": { - "vision_state": { - "state": "ENABLED", - "variants": { - "blurry": "blurry", - "clouded": "clouded" - }, - "defaultVariant": "blurry" - } - } -} -``` - -Three required fields per flag in flagd: - -- **`state`** โ€” `"ENABLED"` (or `"DISABLED"` to force the SDK fallback). -- **`variants`** โ€” a map from variant name to value. Two variants here give you something to flip in the verification - step. -- **`defaultVariant`** โ€” which variant gets returned when no targeting rules match. There are no rules at this level, - so this is the variant every request gets. - -Save. flagd is watching this file (the devcontainer mounts it read-only into the flagd sibling and tells it to -`start --uri file:.../flags.json`), so the next evaluation already sees the new flag โ€” no flagd restart, no app -restart. - -## 4. Read the chart from the controller - -Update `src/main/java/dev/openfeature/demo/java/demo/Trial.java` so it asks OpenFeature for the reading -instead of returning a literal: - -```java -package dev.openfeature.demo.java.demo; - -import dev.openfeature.sdk.Client; -import dev.openfeature.sdk.FlagEvaluationDetails; -import dev.openfeature.sdk.OpenFeatureAPI; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -public class Trial { - - @GetMapping("/") - public FlagEvaluationDetails observeSubject() { - Client client = OpenFeatureAPI.getInstance().getClient(); - return client.getStringDetails("vision_state", "untreated"); - } -} -``` - -Two intentional choices here: - -- `"untreated"` is the **fallback** value passed to `getStringDetails`. The SDK only returns it if no provider is - registered, the flag is missing, or the flag is disabled. Once your `OpenFeatureConfig` and `flags.json` are in - place, you should never see this value again โ€” and the smoke test asserts exactly that. -- The handler returns `FlagEvaluationDetails` directly, not just the value. Spring will serialize it to JSON - and the response will carry `flagKey`, `value`, `variant`, `reason`, and any error fields โ€” useful for debugging, - required by the smoke test. - -## 5. Run it and verify - -Restart the lab: - -```bash -./mvnw spring-boot:run -``` - -In another terminal: - -```bash -curl -s http://localhost:8080/ | jq -``` - -You should see `"value": "blurry"` and `"flagKey": "vision_state"`. Edit `flags.json`, change -`"defaultVariant": "blurry"` to `"defaultVariant": "clouded"`, save, and `curl` again โ€” the value flips to -`"clouded"` without restarting the app. That's the **flagd container** noticing the file changed on its read-only -mount and serving the new variant on the next gRPC evaluation. Neither the lab nor flagd had to restart; nothing -was redeployed. - -Run the smoke test from the repo root: - -```bash -adventures/planned/00-blind-by-design/beginner/verify.sh -``` - -When all four checks pass, the lab is reading the chart and you're done with the ๐ŸŸข Beginner level. From 94a4a88ac1427333a3a9553b001fa2ae17184930 Mon Sep 17 00:00:00 2001 From: Katharina Sick Date: Thu, 30 Apr 2026 16:03:20 +0200 Subject: [PATCH 7/7] blind-by-design: move devcontainer to level directory fow now Signed-off-by: Katharina Sick --- .../beginner}/00-blind-by-design_01-beginner/devcontainer.json | 0 .../beginner}/00-blind-by-design_01-beginner/docker-compose.yml | 0 .../beginner}/00-blind-by-design_01-beginner/post-create.sh | 0 .../beginner}/00-blind-by-design_01-beginner/post-start.sh | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename {.devcontainer => adventures/planned/00-blind-by-design/beginner}/00-blind-by-design_01-beginner/devcontainer.json (100%) rename {.devcontainer => adventures/planned/00-blind-by-design/beginner}/00-blind-by-design_01-beginner/docker-compose.yml (100%) rename {.devcontainer => adventures/planned/00-blind-by-design/beginner}/00-blind-by-design_01-beginner/post-create.sh (100%) rename {.devcontainer => adventures/planned/00-blind-by-design/beginner}/00-blind-by-design_01-beginner/post-start.sh (100%) diff --git a/.devcontainer/00-blind-by-design_01-beginner/devcontainer.json b/adventures/planned/00-blind-by-design/beginner/00-blind-by-design_01-beginner/devcontainer.json similarity index 100% rename from .devcontainer/00-blind-by-design_01-beginner/devcontainer.json rename to adventures/planned/00-blind-by-design/beginner/00-blind-by-design_01-beginner/devcontainer.json diff --git a/.devcontainer/00-blind-by-design_01-beginner/docker-compose.yml b/adventures/planned/00-blind-by-design/beginner/00-blind-by-design_01-beginner/docker-compose.yml similarity index 100% rename from .devcontainer/00-blind-by-design_01-beginner/docker-compose.yml rename to adventures/planned/00-blind-by-design/beginner/00-blind-by-design_01-beginner/docker-compose.yml diff --git a/.devcontainer/00-blind-by-design_01-beginner/post-create.sh b/adventures/planned/00-blind-by-design/beginner/00-blind-by-design_01-beginner/post-create.sh similarity index 100% rename from .devcontainer/00-blind-by-design_01-beginner/post-create.sh rename to adventures/planned/00-blind-by-design/beginner/00-blind-by-design_01-beginner/post-create.sh diff --git a/.devcontainer/00-blind-by-design_01-beginner/post-start.sh b/adventures/planned/00-blind-by-design/beginner/00-blind-by-design_01-beginner/post-start.sh similarity index 100% rename from .devcontainer/00-blind-by-design_01-beginner/post-start.sh rename to adventures/planned/00-blind-by-design/beginner/00-blind-by-design_01-beginner/post-start.sh