Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/agents/nais-agent.agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ metadata:
labels:
team: team-namespace
spec:
image: { { image } } # Replaced by CI/CD
image: {{ image }} # Replaced by CI/CD
port: 8080

# Observability (required)
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,11 @@ jobs:
# 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
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # ratchet:actions/checkout@v5
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # ratchet:actions/checkout@v5

# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@1b168cd39490f61582a9beae412bb7057a6b2c4e # ratchet:github/codeql-action/init@v3
uses: github/codeql-action/init@ae9ef3a1d2e3413523c3741725c30064970cc0d4 # ratchet:github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
Expand All @@ -84,7 +84,7 @@ jobs:
# 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
# queries: security-extended,security-and-quality

uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # ratchet:actions/setup-java@v5
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # ratchet:actions/setup-java@v5
with:
java-version: 21
distribution: 'temurin'
Expand All @@ -98,7 +98,7 @@ jobs:

./gradlew clean build
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@1b168cd39490f61582a9beae412bb7057a6b2c4e # ratchet:github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@ae9ef3a1d2e3413523c3741725c30064970cc0d4 # ratchet:github/codeql-action/analyze@v3
with:

category: "/language:${{matrix.language}}"
5 changes: 3 additions & 2 deletions .github/workflows/dependabot-auto-merge.yaml
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
name: Dependabot
on: pull_request
on: pull_request_target

Comment thread
Starefossen marked this conversation as resolved.
permissions:
pull-requests: write

jobs:
auto-merge:
uses: nais/actions/.github/workflows/dependabot-auto-merge.yaml@9422b590097ffabcf6ef2e34f41e99acd2634b9d # ratchet:nais/actions/.github/workflows/dependabot-auto-merge.yaml@main
if: github.actor == 'dependabot[bot]'
uses: nais/actions/.github/workflows/dependabot-auto-merge.yaml@f48f094a6474ff06e70df118f21f13c1ac349d37 # ratchet:nais/actions/.github/workflows/dependabot-auto-merge.yaml@main
secrets:
DEPENDABOT_AUTO_MERGE_APP_ID: ${{ secrets.DEPENDABOT_AUTO_MERGE_APP_ID }}
DEPENDABOT_AUTO_MERGE_APP_SECRET: ${{ secrets.DEPENDABOT_AUTO_MERGE_APP_SECRET }}
10 changes: 5 additions & 5 deletions .github/workflows/service-pipeline.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ jobs:
MISE_NODE_VERIFY: false

steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # ratchet:actions/checkout@v5
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # ratchet:actions/checkout@v5

- uses: jdx/mise-action@146a28175021df8ca24f8ee1828cc2a60f980bd5 # ratchet:jdx/mise-action@v3
- uses: jdx/mise-action@e79ddf65a11cec7b0e882bedced08d6e976efb2d # ratchet:jdx/mise-action@v3
with:
version: 2024.12.18
install: true
Expand Down Expand Up @@ -84,8 +84,8 @@ jobs:
contents: read
id-token: write
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # ratchet:actions/checkout@v5
- uses: nais/platform-build-push-sign@8be8359cd90915318ee8ab5fbc8337d04937ae70 # ratchet:nais/platform-build-push-sign@main
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # ratchet:actions/checkout@v5
- uses: nais/platform-build-push-sign@f276e60f3898b076a67bfc94b52ffbdb885224a7 # ratchet:nais/platform-build-push-sign@main
id: image
with:
context: ${{ inputs.service-path }}
Expand All @@ -105,7 +105,7 @@ jobs:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # ratchet:actions/checkout@v5
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # ratchet:actions/checkout@v5
- uses: nais/deploy/actions/deploy@fa754451577294aae42872a69b888b3470478ec1 # ratchet:nais/deploy/actions/deploy@v2
env:
CLUSTER: dev-gcp
Expand Down
29 changes: 29 additions & 0 deletions .mise.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[tools]
# Root tools for the entire project (kept minimal, each service manages its own)
watchexec = "2.3.2"
"pipx:semgrep" = "latest"
# Note: ratchet can be installed separately with: go install github.com/sethvargo/ratchet@latest

[env]
Expand Down Expand Up @@ -155,6 +156,10 @@ run = "cd quotes-backend && mise run lint"
description = "Clean backend service"
run = "cd quotes-backend && mise run clean"

[tasks."backend:security-scan"]
description = "Security scan backend service"
run = "cd quotes-backend && mise run security-scan"

# Frontend (Next.js)
[tasks."frontend:dev"]
description = "Start frontend in development mode"
Expand All @@ -177,6 +182,10 @@ run = "cd quotes-frontend && mise run lint"
description = "Clean frontend service"
run = "cd quotes-frontend && mise run clean"

[tasks."frontend:security-scan"]
description = "Security scan frontend service"
run = "cd quotes-frontend && mise run security-scan"

# Analytics (.NET)
[tasks."analytics:dev"]
description = "Start analytics in development mode"
Expand All @@ -198,6 +207,10 @@ run = "cd quotes-analytics && mise run lint"
description = "Clean analytics service"
run = "cd quotes-analytics && mise run clean"

[tasks."analytics:security-scan"]
description = "Security scan analytics service"
run = "cd quotes-analytics && mise run security-scan"

# Load Generator (Go)
[tasks."loadgen:dev"]
description = "Start load generator"
Expand Down Expand Up @@ -227,6 +240,10 @@ run = "cd quotes-loadgen && mise run lint"
description = "Clean load generator"
run = "cd quotes-loadgen && mise run clean"

[tasks."loadgen:security-scan"]
description = "Security scan load generator"
run = "cd quotes-loadgen && mise run security-scan"

# =============================================================================
# ORCHESTRATION TASKS
# =============================================================================
Expand Down Expand Up @@ -275,13 +292,25 @@ run = [
"echo '✅ All services cleaned'",
]

[tasks."security-scan"]
description = "Run security scanning for all services"
run = [
"echo '🔒 Running security scans for all services...'",
"mise run backend:security-scan",
"mise run frontend:security-scan",
"mise run analytics:security-scan",
"mise run loadgen:security-scan",
"echo '✅ All security scans completed'",
]

[tasks.ci]
description = "Run full CI pipeline"
run = [
"echo '🏗️ Running full CI pipeline...'",
"mise run lint",
"mise run test",
"mise run build",
"mise run security-scan",
"echo '✅ CI pipeline completed successfully'",
]

Expand Down
39 changes: 39 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,42 @@ quotes-<service>/
- Each service is self-contained with its own `.mise.toml`, `Dockerfile`, and `.nais/app.yaml`
- Root `.mise.toml` orchestrates cross-service tasks (`mise run dev`, `mise run ci`)
- `docker-compose.yaml` provides local infrastructure (PostgreSQL, Unleash, Grafana/OTEL)

## Feature Flags (Unleash)

Feature flags use [Unleash](https://docs.nais.io/services/feature-flagging/) via the NAIS platform.

**Architecture:**

- Each service has `.nais/unleash.yaml` that provisions an API token as a Kubernetes secret
- `envFrom` in `.nais/app.yaml` injects `UNLEASH_SERVER_API_URL`, `UNLEASH_SERVER_API_TOKEN`, `UNLEASH_SERVER_API_ENVIRONMENT`
- Backend uses `io.getunleash:unleash-client-java`, frontend uses `unleash-client` (Node.js)
- When Unleash is unavailable, flags fall back to their configured default values (graceful degradation)

Comment thread
Starefossen marked this conversation as resolved.
**Current flags:**

- `quotes.submit` — gates the "Submit a New Quote" feature (backend + frontend)
- `quotes.errors` — enables simulated error injection in the backend (default: **disabled**)

**Patterns:**

```kotlin
// Kotlin: check flag, default true when Unleash is unavailable
FeatureFlags.isEnabled(FeatureFlags.QUOTES_SUBMIT)

// Kotlin: error injection flag, default false (opt-in)
FeatureFlags.isEnabled(FeatureFlags.QUOTES_ERRORS, default = false)
```

```typescript
// TypeScript (server-side API routes): check flag
import { isEnabled, FEATURE_FLAGS } from '@/utils/unleash';
isEnabled(FEATURE_FLAGS.QUOTES_SUBMIT);
```

**Rules:**

- Register flag names as constants in `FeatureFlags.kt` / `unleash.ts`
- Define per-flag defaults (e.g., `quotes.submit` defaults `true`, `quotes.errors` defaults `false`)
- Use `GET /api/features` (backend or frontend) to inspect flag states
- Local Unleash admin UI: `http://localhost:4242` (started via `mise run infra:up`)
65 changes: 65 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,71 @@ graph LR

```

## Feature Flags with Unleash

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.

### How it works

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`:

```yaml
# .nais/app.yaml
envFrom:
- secret: quotes-backend-unleash-api-token
```

This provides the environment variables `UNLEASH_SERVER_API_URL`, `UNLEASH_SERVER_API_TOKEN`, and `UNLEASH_SERVER_API_ENVIRONMENT` to the application at runtime.

### Feature flags in use

| Flag | Service | Effect |
| --------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `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. |
| `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. |

### Adding a new feature flag

1. **Create the toggle** in the [Unleash UI](http://localhost:4242) (or on NAIS at your team's Unleash instance)
2. **Check the flag in code:**

**Kotlin (backend):**

```kotlin
if (FeatureFlags.isEnabled("my.new.flag")) {
// feature code
}
```

**TypeScript (frontend, server-side):**

```typescript
import { isEnabled } from '@/utils/unleash';
const enabled = isEnabled('my.new.flag');
```

3. **Register the flag name** in `FeatureFlags.kt` (backend) or `unleash.ts` (frontend) so it appears in the `/api/features` endpoint

### Local development

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`).

To create the `quotes.submit` toggle locally:

1. Start infrastructure: `mise run infra:up`
2. Open <http://localhost:4242>
3. Create feature flags named `quotes.submit` and `quotes.errors` in the `development` environment
4. Enable or disable them to see the effect in the running application

The local client token `default:development.client-token` is pre-configured in both `.mise.toml` (for `mise run dev`) and `docker-compose.yaml`.

### Graceful degradation

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:

- Tests can run without Unleash, using the configured default values for each flag
- A misconfigured Unleash connection won't break the application; features will follow their safe defaults

## License

The code in this repository is licensed under the MIT license. See [LICENSE](LICENSE) for more information.
8 changes: 8 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ services:
DATABASE_SSL: false
CHECK_VERSION: false
SEND_TELEMETRY: false
UNLEASH_DEFAULT_ADMIN_USERNAME: admin
UNLEASH_DEFAULT_ADMIN_PASSWORD: unleash
INIT_ADMIN_API_TOKENS: "*:*.admin-token"
INIT_CLIENT_API_TOKENS: "default:development.client-token"
ports:
Expand All @@ -68,6 +70,9 @@ services:
DB_URL: jdbc:postgresql://quotes-db:5432/quotes
DB_USERNAME: quotes
DB_PASSWORD: quotes
UNLEASH_SERVER_API_URL: http://unleash:4242/api
UNLEASH_SERVER_API_TOKEN: "default:development.client-token"
UNLEASH_SERVER_API_ENVIRONMENT: development
ports:
- "8080:8080"

Expand Down Expand Up @@ -97,6 +102,9 @@ services:
OTEL_SERVICE_NAME: quotes-frontend
NEXT_PUBLIC_BACKEND_URL: http://quotes-backend:8080
ANALYTICS_SERVICE_URL: http://quotes-analytics:8081
UNLEASH_SERVER_API_URL: http://unleash:4242/api
UNLEASH_SERVER_API_TOKEN: "default:development.client-token"
UNLEASH_SERVER_API_ENVIRONMENT: development
ports:
- "3000:3000"

Expand Down
5 changes: 5 additions & 0 deletions quotes-analytics/.mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,18 @@ run = "docker build -t quotes-analytics:local ."
description = "Run Docker container locally"
run = "docker run -p 8081:8081 --env-file .env.local quotes-analytics:local"

[tasks."security-scan"]
description = "Run security scanning"
run = "semgrep scan --config p/csharp --config p/owasp-top-ten --error ."

[tasks.ci]
description = "Run all CI checks"
run = [
"dotnet restore quotes-analytics.sln",
"dotnet format quotes-analytics.sln --verify-no-changes",
"dotnet build quotes-analytics.sln --no-restore /p:TreatWarningsAsErrors=true",
"dotnet test quotes-analytics.sln --no-build --logger 'console;verbosity=normal'",
"mise run security-scan",
]

[tasks."dependencies:check"]
Expand Down
21 changes: 12 additions & 9 deletions quotes-analytics/.nais/unleash.yaml
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
apiVersion: v1
kind: ConfigMap
apiVersion: unleash.nais.io/v1
kind: ApiToken
metadata:
name: unleash-bootstrap-quotes-analytics
namespace: examples
name: quotes-analytics
namespace: {{ namespace }}
labels:
app: quotes-analytics
team: examples
data:
# No feature flags for analytics service yet
# Add toggles here when needed
team: {{ namespace }}
spec:
unleashInstance:
apiVersion: unleash.nais.io/v1
kind: RemoteUnleash
name: examples
secretName: quotes-analytics-unleash-api-token
environment: development
Loading
Loading