Skip to content

Commit 226bfa5

Browse files
authored
Add feature flag for quote submissions (#274)
* feat: add feature flag for submitting quotes and update UI accordingly - Implemented a new API endpoint to fetch feature flags. - Updated the QuoteDisplay component to disable the submit link based on the feature flag. - Modified the Home component to fetch the feature flag and control the submission of new quotes. - Enhanced the SubmitQuote component to reflect the feature flag status and provide user feedback when submissions are disabled. - Introduced a utility for managing feature flags using Unleash. - Updated unleash configuration for quotes-loadgen. * fix: update action versions in workflow files for consistency and stability * feat: sanitize quote IDs for logging to enhance security and prevent logging of control characters * fix: change Dependabot trigger to pull_request_target for improved security * refactor: change quote ID type from string to int for improved validation and consistency * feat: add security scan tasks for all services and update feature flag handling * fix: update semgrep entry format in .mise.toml for consistency
1 parent b15733e commit 226bfa5

35 files changed

Lines changed: 762 additions & 89 deletions

.github/agents/nais-agent.agent.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ metadata:
3333
labels:
3434
team: team-namespace
3535
spec:
36-
image: { { image } } # Replaced by CI/CD
36+
image: {{ image }} # Replaced by CI/CD
3737
port: 8080
3838

3939
# Observability (required)

.github/workflows/codeql.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,11 @@ jobs:
6161
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
6262
steps:
6363
- name: Checkout repository
64-
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # ratchet:actions/checkout@v5
64+
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # ratchet:actions/checkout@v5
6565

6666
# Initializes the CodeQL tools for scanning.
6767
- name: Initialize CodeQL
68-
uses: github/codeql-action/init@1b168cd39490f61582a9beae412bb7057a6b2c4e # ratchet:github/codeql-action/init@v3
68+
uses: github/codeql-action/init@ae9ef3a1d2e3413523c3741725c30064970cc0d4 # ratchet:github/codeql-action/init@v3
6969
with:
7070
languages: ${{ matrix.language }}
7171
build-mode: ${{ matrix.build-mode }}
@@ -84,7 +84,7 @@ jobs:
8484
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
8585
# queries: security-extended,security-and-quality
8686

87-
uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # ratchet:actions/setup-java@v5
87+
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # ratchet:actions/setup-java@v5
8888
with:
8989
java-version: 21
9090
distribution: 'temurin'
@@ -98,7 +98,7 @@ jobs:
9898
9999
./gradlew clean build
100100
- name: Perform CodeQL Analysis
101-
uses: github/codeql-action/analyze@1b168cd39490f61582a9beae412bb7057a6b2c4e # ratchet:github/codeql-action/analyze@v3
101+
uses: github/codeql-action/analyze@ae9ef3a1d2e3413523c3741725c30064970cc0d4 # ratchet:github/codeql-action/analyze@v3
102102
with:
103103

104104
category: "/language:${{matrix.language}}"
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
name: Dependabot
2-
on: pull_request
2+
on: pull_request_target
33

44
permissions:
55
pull-requests: write
66

77
jobs:
88
auto-merge:
9-
uses: nais/actions/.github/workflows/dependabot-auto-merge.yaml@9422b590097ffabcf6ef2e34f41e99acd2634b9d # ratchet:nais/actions/.github/workflows/dependabot-auto-merge.yaml@main
9+
if: github.actor == 'dependabot[bot]'
10+
uses: nais/actions/.github/workflows/dependabot-auto-merge.yaml@f48f094a6474ff06e70df118f21f13c1ac349d37 # ratchet:nais/actions/.github/workflows/dependabot-auto-merge.yaml@main
1011
secrets:
1112
DEPENDABOT_AUTO_MERGE_APP_ID: ${{ secrets.DEPENDABOT_AUTO_MERGE_APP_ID }}
1213
DEPENDABOT_AUTO_MERGE_APP_SECRET: ${{ secrets.DEPENDABOT_AUTO_MERGE_APP_SECRET }}

.github/workflows/service-pipeline.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,9 @@ jobs:
5050
MISE_NODE_VERIFY: false
5151

5252
steps:
53-
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # ratchet:actions/checkout@v5
53+
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # ratchet:actions/checkout@v5
5454

55-
- uses: jdx/mise-action@146a28175021df8ca24f8ee1828cc2a60f980bd5 # ratchet:jdx/mise-action@v3
55+
- uses: jdx/mise-action@e79ddf65a11cec7b0e882bedced08d6e976efb2d # ratchet:jdx/mise-action@v3
5656
with:
5757
version: 2024.12.18
5858
install: true
@@ -84,8 +84,8 @@ jobs:
8484
contents: read
8585
id-token: write
8686
steps:
87-
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # ratchet:actions/checkout@v5
88-
- uses: nais/platform-build-push-sign@8be8359cd90915318ee8ab5fbc8337d04937ae70 # ratchet:nais/platform-build-push-sign@main
87+
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # ratchet:actions/checkout@v5
88+
- uses: nais/platform-build-push-sign@f276e60f3898b076a67bfc94b52ffbdb885224a7 # ratchet:nais/platform-build-push-sign@main
8989
id: image
9090
with:
9191
context: ${{ inputs.service-path }}
@@ -105,7 +105,7 @@ jobs:
105105
runs-on: ubuntu-latest
106106
if: github.ref == 'refs/heads/main'
107107
steps:
108-
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # ratchet:actions/checkout@v5
108+
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # ratchet:actions/checkout@v5
109109
- uses: nais/deploy/actions/deploy@fa754451577294aae42872a69b888b3470478ec1 # ratchet:nais/deploy/actions/deploy@v2
110110
env:
111111
CLUSTER: dev-gcp

.mise.toml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
[tools]
22
# Root tools for the entire project (kept minimal, each service manages its own)
33
watchexec = "2.3.2"
4+
"pipx:semgrep" = "latest"
45
# Note: ratchet can be installed separately with: go install github.com/sethvargo/ratchet@latest
56

67
[env]
@@ -155,6 +156,10 @@ run = "cd quotes-backend && mise run lint"
155156
description = "Clean backend service"
156157
run = "cd quotes-backend && mise run clean"
157158

159+
[tasks."backend:security-scan"]
160+
description = "Security scan backend service"
161+
run = "cd quotes-backend && mise run security-scan"
162+
158163
# Frontend (Next.js)
159164
[tasks."frontend:dev"]
160165
description = "Start frontend in development mode"
@@ -177,6 +182,10 @@ run = "cd quotes-frontend && mise run lint"
177182
description = "Clean frontend service"
178183
run = "cd quotes-frontend && mise run clean"
179184

185+
[tasks."frontend:security-scan"]
186+
description = "Security scan frontend service"
187+
run = "cd quotes-frontend && mise run security-scan"
188+
180189
# Analytics (.NET)
181190
[tasks."analytics:dev"]
182191
description = "Start analytics in development mode"
@@ -198,6 +207,10 @@ run = "cd quotes-analytics && mise run lint"
198207
description = "Clean analytics service"
199208
run = "cd quotes-analytics && mise run clean"
200209

210+
[tasks."analytics:security-scan"]
211+
description = "Security scan analytics service"
212+
run = "cd quotes-analytics && mise run security-scan"
213+
201214
# Load Generator (Go)
202215
[tasks."loadgen:dev"]
203216
description = "Start load generator"
@@ -227,6 +240,10 @@ run = "cd quotes-loadgen && mise run lint"
227240
description = "Clean load generator"
228241
run = "cd quotes-loadgen && mise run clean"
229242

243+
[tasks."loadgen:security-scan"]
244+
description = "Security scan load generator"
245+
run = "cd quotes-loadgen && mise run security-scan"
246+
230247
# =============================================================================
231248
# ORCHESTRATION TASKS
232249
# =============================================================================
@@ -275,13 +292,25 @@ run = [
275292
"echo '✅ All services cleaned'",
276293
]
277294

295+
[tasks."security-scan"]
296+
description = "Run security scanning for all services"
297+
run = [
298+
"echo '🔒 Running security scans for all services...'",
299+
"mise run backend:security-scan",
300+
"mise run frontend:security-scan",
301+
"mise run analytics:security-scan",
302+
"mise run loadgen:security-scan",
303+
"echo '✅ All security scans completed'",
304+
]
305+
278306
[tasks.ci]
279307
description = "Run full CI pipeline"
280308
run = [
281309
"echo '🏗️ Running full CI pipeline...'",
282310
"mise run lint",
283311
"mise run test",
284312
"mise run build",
313+
"mise run security-scan",
285314
"echo '✅ CI pipeline completed successfully'",
286315
]
287316

AGENTS.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,3 +146,42 @@ quotes-<service>/
146146
- Each service is self-contained with its own `.mise.toml`, `Dockerfile`, and `.nais/app.yaml`
147147
- Root `.mise.toml` orchestrates cross-service tasks (`mise run dev`, `mise run ci`)
148148
- `docker-compose.yaml` provides local infrastructure (PostgreSQL, Unleash, Grafana/OTEL)
149+
150+
## Feature Flags (Unleash)
151+
152+
Feature flags use [Unleash](https://docs.nais.io/services/feature-flagging/) via the NAIS platform.
153+
154+
**Architecture:**
155+
156+
- Each service has `.nais/unleash.yaml` that provisions an API token as a Kubernetes secret
157+
- `envFrom` in `.nais/app.yaml` injects `UNLEASH_SERVER_API_URL`, `UNLEASH_SERVER_API_TOKEN`, `UNLEASH_SERVER_API_ENVIRONMENT`
158+
- Backend uses `io.getunleash:unleash-client-java`, frontend uses `unleash-client` (Node.js)
159+
- When Unleash is unavailable, flags fall back to their configured default values (graceful degradation)
160+
161+
**Current flags:**
162+
163+
- `quotes.submit` — gates the "Submit a New Quote" feature (backend + frontend)
164+
- `quotes.errors` — enables simulated error injection in the backend (default: **disabled**)
165+
166+
**Patterns:**
167+
168+
```kotlin
169+
// Kotlin: check flag, default true when Unleash is unavailable
170+
FeatureFlags.isEnabled(FeatureFlags.QUOTES_SUBMIT)
171+
172+
// Kotlin: error injection flag, default false (opt-in)
173+
FeatureFlags.isEnabled(FeatureFlags.QUOTES_ERRORS, default = false)
174+
```
175+
176+
```typescript
177+
// TypeScript (server-side API routes): check flag
178+
import { isEnabled, FEATURE_FLAGS } from '@/utils/unleash';
179+
isEnabled(FEATURE_FLAGS.QUOTES_SUBMIT);
180+
```
181+
182+
**Rules:**
183+
184+
- Register flag names as constants in `FeatureFlags.kt` / `unleash.ts`
185+
- Define per-flag defaults (e.g., `quotes.submit` defaults `true`, `quotes.errors` defaults `false`)
186+
- Use `GET /api/features` (backend or frontend) to inspect flag states
187+
- Local Unleash admin UI: `http://localhost:4242` (started via `mise run infra:up`)

README.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,71 @@ graph LR
115115
116116
```
117117

118+
## Feature Flags with Unleash
119+
120+
This project demonstrates [Unleash](https://docs.nais.io/services/feature-flagging/) feature flagging on the NAIS platform. Unleash lets you toggle features on and off without redeploying.
121+
122+
### How it works
123+
124+
Each service has an [Unleash API token](https://docs.nais.io/services/feature-flagging/#step-2-define-an-apitoken-for-your-application) defined in `.nais/unleash.yaml`. When deployed, the NAIS Unleash operator provisions a client token and stores it as a Kubernetes secret. The app reads the secret via `envFrom` in `.nais/app.yaml`:
125+
126+
```yaml
127+
# .nais/app.yaml
128+
envFrom:
129+
- secret: quotes-backend-unleash-api-token
130+
```
131+
132+
This provides the environment variables `UNLEASH_SERVER_API_URL`, `UNLEASH_SERVER_API_TOKEN`, and `UNLEASH_SERVER_API_ENVIRONMENT` to the application at runtime.
133+
134+
### Feature flags in use
135+
136+
| Flag | Service | Effect |
137+
| --------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
138+
| `quotes.submit` | quotes-backend, quotes-frontend | Controls whether users can submit new quotes. When disabled, the backend returns 403 and the frontend hides/disables the submit button. |
139+
| `quotes.errors` | quotes-backend | Enables simulated error injection (10% error rate on GET/POST endpoints). Default: **disabled**. Turn on to generate errors visible in dashboards and alerts. |
140+
141+
### Adding a new feature flag
142+
143+
1. **Create the toggle** in the [Unleash UI](http://localhost:4242) (or on NAIS at your team's Unleash instance)
144+
2. **Check the flag in code:**
145+
146+
**Kotlin (backend):**
147+
148+
```kotlin
149+
if (FeatureFlags.isEnabled("my.new.flag")) {
150+
// feature code
151+
}
152+
```
153+
154+
**TypeScript (frontend, server-side):**
155+
156+
```typescript
157+
import { isEnabled } from '@/utils/unleash';
158+
const enabled = isEnabled('my.new.flag');
159+
```
160+
161+
3. **Register the flag name** in `FeatureFlags.kt` (backend) or `unleash.ts` (frontend) so it appears in the `/api/features` endpoint
162+
163+
### Local development
164+
165+
Unleash runs locally via docker-compose on port 4242. The admin UI is at <http://localhost:4242>. Log in with the default credentials configured in `docker-compose.yaml` (username `admin`, password `unleash`).
166+
167+
To create the `quotes.submit` toggle locally:
168+
169+
1. Start infrastructure: `mise run infra:up`
170+
2. Open <http://localhost:4242>
171+
3. Create feature flags named `quotes.submit` and `quotes.errors` in the `development` environment
172+
4. Enable or disable them to see the effect in the running application
173+
174+
The local client token `default:development.client-token` is pre-configured in both `.mise.toml` (for `mise run dev`) and `docker-compose.yaml`.
175+
176+
### Graceful degradation
177+
178+
When Unleash is unavailable (no env vars set, or server unreachable), feature flags fall back to their configured default values. These defaults are defined per flag in the backend/frontend and may be either enabled or disabled. This means:
179+
180+
- Tests can run without Unleash, using the configured default values for each flag
181+
- A misconfigured Unleash connection won't break the application; features will follow their safe defaults
182+
118183
## License
119184

120185
The code in this repository is licensed under the MIT license. See [LICENSE](LICENSE) for more information.

docker-compose.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ services:
4848
DATABASE_SSL: false
4949
CHECK_VERSION: false
5050
SEND_TELEMETRY: false
51+
UNLEASH_DEFAULT_ADMIN_USERNAME: admin
52+
UNLEASH_DEFAULT_ADMIN_PASSWORD: unleash
5153
INIT_ADMIN_API_TOKENS: "*:*.admin-token"
5254
INIT_CLIENT_API_TOKENS: "default:development.client-token"
5355
ports:
@@ -68,6 +70,9 @@ services:
6870
DB_URL: jdbc:postgresql://quotes-db:5432/quotes
6971
DB_USERNAME: quotes
7072
DB_PASSWORD: quotes
73+
UNLEASH_SERVER_API_URL: http://unleash:4242/api
74+
UNLEASH_SERVER_API_TOKEN: "default:development.client-token"
75+
UNLEASH_SERVER_API_ENVIRONMENT: development
7176
ports:
7277
- "8080:8080"
7378

@@ -97,6 +102,9 @@ services:
97102
OTEL_SERVICE_NAME: quotes-frontend
98103
NEXT_PUBLIC_BACKEND_URL: http://quotes-backend:8080
99104
ANALYTICS_SERVICE_URL: http://quotes-analytics:8081
105+
UNLEASH_SERVER_API_URL: http://unleash:4242/api
106+
UNLEASH_SERVER_API_TOKEN: "default:development.client-token"
107+
UNLEASH_SERVER_API_ENVIRONMENT: development
100108
ports:
101109
- "3000:3000"
102110

quotes-analytics/.mise.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,18 @@ run = "docker build -t quotes-analytics:local ."
8080
description = "Run Docker container locally"
8181
run = "docker run -p 8081:8081 --env-file .env.local quotes-analytics:local"
8282

83+
[tasks."security-scan"]
84+
description = "Run security scanning"
85+
run = "semgrep scan --config p/csharp --config p/owasp-top-ten --error ."
86+
8387
[tasks.ci]
8488
description = "Run all CI checks"
8589
run = [
8690
"dotnet restore quotes-analytics.sln",
8791
"dotnet format quotes-analytics.sln --verify-no-changes",
8892
"dotnet build quotes-analytics.sln --no-restore /p:TreatWarningsAsErrors=true",
8993
"dotnet test quotes-analytics.sln --no-build --logger 'console;verbosity=normal'",
94+
"mise run security-scan",
9095
]
9196

9297
[tasks."dependencies:check"]
Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
apiVersion: v1
2-
kind: ConfigMap
1+
apiVersion: unleash.nais.io/v1
2+
kind: ApiToken
33
metadata:
4-
name: unleash-bootstrap-quotes-analytics
5-
namespace: examples
4+
name: quotes-analytics
5+
namespace: {{ namespace }}
66
labels:
7-
app: quotes-analytics
8-
team: examples
9-
data:
10-
# No feature flags for analytics service yet
11-
# Add toggles here when needed
7+
team: {{ namespace }}
8+
spec:
9+
unleashInstance:
10+
apiVersion: unleash.nais.io/v1
11+
kind: RemoteUnleash
12+
name: examples
13+
secretName: quotes-analytics-unleash-api-token
14+
environment: development

0 commit comments

Comments
 (0)