Skip to content
Open
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
73 changes: 73 additions & 0 deletions .drone.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
kind: pipeline
type: kubernetes
name: default

platform:
arch: amd64

trigger:
event:
- push
- tag
ref:
- refs/heads/main
- refs/tags/v*

steps:
- name: publish
image: plugins/kaniko-ecr
settings:
create_repository: true
registry: 795250896452.dkr.ecr.us-east-1.amazonaws.com
repo: docs/${DRONE_REPO_NAME}
context: .
dockerfile: Dockerfile
tags:
- git-${DRONE_COMMIT_SHA:0:7}
- latest
access_key:
from_secret: ecr_access_key
secret_key:
from_secret: ecr_secret_key
when:
event:
- push
- tag

- name: deploy-staging
image: public.ecr.aws/kanopy/drone-helm:v3
settings:
chart: mongodb/web-app
chart_version: 4.30.0
atomic: true
add_repos: [mongodb=https://10gen.github.io/helm-charts]
namespace: docs
release: ${DRONE_REPO_NAME}
values: image.tag=git-${DRONE_COMMIT_SHA:0:7},image.repository=795250896452.dkr.ecr.us-east-1.amazonaws.com/docs/${DRONE_REPO_NAME}
values_files: ["kanopy/values.yaml", "kanopy/values.staging.yaml"]
api_server: https://api.staging.corp.mongodb.com
kubernetes_token:
from_secret: staging_kubernetes_token
when:
event:
- push

- name: deploy-prod
image: public.ecr.aws/kanopy/drone-helm:v3
settings:
chart: mongodb/web-app
chart_version: 4.30.0
atomic: true
add_repos: [mongodb=https://10gen.github.io/helm-charts]
namespace: docs
release: ${DRONE_REPO_NAME}
values: image.tag=git-${DRONE_COMMIT_SHA:0:7},image.repository=795250896452.dkr.ecr.us-east-1.amazonaws.com/docs/${DRONE_REPO_NAME}
values_files: ["kanopy/values.yaml", "kanopy/values.prod.yaml"]
api_server: https://api.prod.corp.mongodb.com
kubernetes_token:
from_secret: prod_kubernetes_token
when:
event:
- tag
ref:
- refs/tags/v*
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

### Added

- **Kanopy CorpSecure auth mode** — new `OPERATOR_AUTH_MODE=kanopy` option for the operator UI. When deployed behind Kanopy's CorpSecure proxy, authentication is handled automatically via Okta; the app verifies the forwarded `X-Kanopy-Internal-Authorization` JWT and maps Okta group membership to roles (`OPERATOR_AUTH_KANOPY_GROUP` → operator, all other authenticated employees → writer). GitHub PAT auth (`OPERATOR_AUTH_MODE=github`) remains the default and is unchanged.
- **Kanopy deployment** — Drone CI/CD pipeline (`.drone.yml`) and Helm values (`kanopy/`) to deploy the copier as a Kanopy app. Staging deploys on push to `main`; production deploys on `v*` tag. Secrets provisioned via `helm ksec`; GCP Secret Manager bypassed with `SKIP_SECRET_MANAGER=true`.
- **`OPERATOR_AUTH_KANOPY_JWKS_URL`** — optional override for the CorpSecure JWKS endpoint, used to point staging deployments at `login.staging.corp.mongodb.com`.

## [v0.4.3] - 2026-05-02

### Fixed
Expand Down
48 changes: 35 additions & 13 deletions configs/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,19 @@ type Config struct {
WebhookRetryInitialDelay int // initial delay between retries in seconds (doubles each attempt)

// Operator web UI — off unless OPERATOR_UI_ENABLED=true. Works with any HTTP
// origin (local dev, Cloud Run, etc.). Access is gated by GitHub PATs:
// each user authenticates with their personal token, and the role
// (operator or writer) is determined by their permission on OPERATOR_AUTH_REPO.
// origin (local dev, Cloud Run, etc.).
//
// Auth mode "github" (default): each user authenticates with their GitHub PAT;
// their permission on OPERATOR_AUTH_REPO determines role.
//
// Auth mode "kanopy": Kanopy's CorpSecure proxy handles authentication;
// the app reads the X-Kanopy-Internal-Authorization JWT and maps Okta group
// membership to roles. OPERATOR_AUTH_REPO is not required in this mode.
OperatorUIEnabled bool
OperatorAuthRepo string // "owner/repo" — user permissions here determine role (required when UI is enabled)
OperatorAuthMode string // "github" (default) or "kanopy"
OperatorAuthRepo string // "owner/repo" — required in github mode
OperatorAuthKanopyGroup string // Okta group for RoleOperator in kanopy mode (e.g. "10gen-github-copier-operators")
OperatorAuthKanopyJWKSURL string // override JWKS endpoint (default: prod login.corp.mongodb.com)
OperatorRepoSlug string // "owner/repo" for GitHub links in audit/trace rows (optional)
OperatorReleaseGitHubToken string // PAT with contents:write to create a version tag (optional)
OperatorReleaseTargetBranch string // branch SHA used when creating a tag (default main)
Expand Down Expand Up @@ -141,7 +149,10 @@ const (
WebhookMaxRetries = "WEBHOOK_MAX_RETRIES"
WebhookRetryInitialDelay = "WEBHOOK_RETRY_INITIAL_DELAY" //nolint:gosec // env var name, not a credential
OperatorUIEnabled = "OPERATOR_UI_ENABLED"
OperatorAuthRepo = "OPERATOR_AUTH_REPO" // repo for GitHub PAT permission check
OperatorAuthMode = "OPERATOR_AUTH_MODE"
OperatorAuthRepo = "OPERATOR_AUTH_REPO" // repo for GitHub PAT permission check (github mode)
OperatorAuthKanopyGroup = "OPERATOR_AUTH_KANOPY_GROUP"
OperatorAuthKanopyJWKSURL = "OPERATOR_AUTH_KANOPY_JWKS_URL"
OperatorRepoSlug = "OPERATOR_REPO_SLUG"
OperatorReleaseGitHubToken = "OPERATOR_RELEASE_GITHUB_TOKEN" // #nosec G101 -- env var name
OperatorReleaseTargetBranch = "OPERATOR_RELEASE_TARGET_BRANCH"
Expand Down Expand Up @@ -269,7 +280,10 @@ func LoadEnvironment(envFile string) (*Config, error) {
config.WebhookRetryInitialDelay = getIntEnvWithDefault(WebhookRetryInitialDelay, config.WebhookRetryInitialDelay)

config.OperatorUIEnabled = getBoolEnvWithDefault(OperatorUIEnabled, false)
config.OperatorAuthMode = strings.ToLower(getEnvWithDefault(OperatorAuthMode, "github"))
config.OperatorAuthRepo = os.Getenv(OperatorAuthRepo)
config.OperatorAuthKanopyGroup = os.Getenv(OperatorAuthKanopyGroup)
config.OperatorAuthKanopyJWKSURL = os.Getenv(OperatorAuthKanopyJWKSURL)
config.OperatorRepoSlug = os.Getenv(OperatorRepoSlug)
config.OperatorReleaseGitHubToken = os.Getenv(OperatorReleaseGitHubToken)
config.OperatorReleaseTargetBranch = getEnvWithDefault(OperatorReleaseTargetBranch, "main")
Expand Down Expand Up @@ -385,18 +399,26 @@ func validateConfig(config *Config) error {
return nil
}

// validateOperatorAuth enforces that OPERATOR_AUTH_REPO is set when the UI is
// enabled. Without it, any valid GitHub user could authenticate with full
// operator access since there would be no per-repo permission gate.
// validateOperatorAuth enforces that auth-mode-specific required fields are set
// when the operator UI is enabled.
func validateOperatorAuth(config *Config) error {
if !config.OperatorUIEnabled {
return nil
}
if strings.TrimSpace(config.OperatorAuthRepo) == "" {
return fmt.Errorf("OPERATOR_UI_ENABLED=true requires OPERATOR_AUTH_REPO (owner/repo) to gate access — each user authenticates with their GitHub PAT and their permission on that repo determines their role")
}
if !strings.Contains(config.OperatorAuthRepo, "/") {
return fmt.Errorf("OPERATOR_AUTH_REPO must be in owner/repo format (got %q)", config.OperatorAuthRepo)
switch config.OperatorAuthMode {
case "kanopy":
if strings.TrimSpace(config.OperatorAuthKanopyGroup) == "" {
return fmt.Errorf("OPERATOR_AUTH_MODE=kanopy requires OPERATOR_AUTH_KANOPY_GROUP (the Okta group whose members get operator role, e.g. \"10gen-github-copier-operators\")")
}
case "github", "":
if strings.TrimSpace(config.OperatorAuthRepo) == "" {
return fmt.Errorf("OPERATOR_UI_ENABLED=true requires OPERATOR_AUTH_REPO (owner/repo) to gate access — each user authenticates with their GitHub PAT and their permission on that repo determines their role")
}
if !strings.Contains(config.OperatorAuthRepo, "/") {
return fmt.Errorf("OPERATOR_AUTH_REPO must be in owner/repo format (got %q)", config.OperatorAuthRepo)
}
default:
return fmt.Errorf("OPERATOR_AUTH_MODE must be \"github\" or \"kanopy\" (got %q)", config.OperatorAuthMode)
}
return nil
}
Expand Down
55 changes: 55 additions & 0 deletions kanopy/values.prod.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Prod overlay for github-copier. Merged on top of values.yaml at deploy time.

ingress:
enabled: true
hosts:
- github-copier.docs.corp.mongodb.com

replicaCount: 2

env:
GITHUB_APP_ID: "1166559"
INSTALLATION_ID: "91420665"

CONFIG_REPO_OWNER: "grove-platform"
CONFIG_REPO_NAME: "github-copier"
CONFIG_REPO_BRANCH: "main"
MAIN_CONFIG_FILE: ".copier/main.yaml"
USE_MAIN_CONFIG: "true"
DEPRECATION_FILE: "deprecated_examples.json"

WEBSERVER_PATH: "/events"
COMMITTER_NAME: "GitHub Copier App"
COMMITTER_EMAIL: "bot@mongodb.com"

GOOGLE_CLOUD_PROJECT_ID: "github-copy-code-examples"
COPIER_LOG_NAME: "code-copier-log"

AUDIT_ENABLED: "true"
METRICS_ENABLED: "true"

OPERATOR_UI_ENABLED: "true"
OPERATOR_AUTH_MODE: "kanopy"
OPERATOR_AUTH_KANOPY_GROUP: "10gen-github-copier-operators"
# Prod uses the default JWKS endpoint (login.corp.mongodb.com); no override needed.
OPERATOR_REPO_SLUG: "grove-platform/github-copier"

SKIP_SECRET_MANAGER: "true"

LLM_PROVIDER: "anthropic"
LLM_BASE_URL: "https://grove-gateway-prod.azure-api.net/grove-foundry-prod/anthropic"
LLM_MODEL: "claude-haiku-4-5"

# Provision before first prod deploy (use the prod cluster context):
#
# helm ksec set github-copier-secrets \
# GITHUB_APP_PRIVATE_KEY_B64="$(base64 < your-app.private-key.pem)" \
# WEBHOOK_SECRET="your-webhook-secret" \
# MONGO_URI="mongodb+srv://..." \
# ANTHROPIC_API_KEY="sk-ant-..."
#
envSecrets:
GITHUB_APP_PRIVATE_KEY_B64: github-copier-secrets
WEBHOOK_SECRET: github-copier-secrets
MONGO_URI: github-copier-secrets
ANTHROPIC_API_KEY: github-copier-secrets
66 changes: 66 additions & 0 deletions kanopy/values.staging.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Staging overlay for github-copier. Merged on top of values.yaml at deploy time.

ingress:
enabled: true
hosts:
- github-copier.docs.staging.corp.mongodb.com

replicaCount: 1

# Non-secret environment variables.
env:
# GitHub App — org-specific IDs (not sensitive, but vary by deployment)
GITHUB_APP_ID: "1166559"
INSTALLATION_ID: "91420665"

# Config repository
CONFIG_REPO_OWNER: "grove-platform"
CONFIG_REPO_NAME: "github-copier"
CONFIG_REPO_BRANCH: "main"
MAIN_CONFIG_FILE: ".copier/main.yaml"
USE_MAIN_CONFIG: "true"
DEPRECATION_FILE: "deprecated_examples.json"

# Webhook + committer
WEBSERVER_PATH: "/events"
COMMITTER_NAME: "GitHub Copier App"
COMMITTER_EMAIL: "bot@mongodb.com"

# GCP logging (still used even on Kanopy for structured logs)
GOOGLE_CLOUD_PROJECT_ID: "github-copy-code-examples"
COPIER_LOG_NAME: "code-copier-log"

# Features
AUDIT_ENABLED: "true"
METRICS_ENABLED: "true"

# Operator UI — Kanopy CorpSecure auth mode
OPERATOR_UI_ENABLED: "true"
OPERATOR_AUTH_MODE: "kanopy"
OPERATOR_AUTH_KANOPY_GROUP: "10gen-github-copier-operators"
# Staging uses the staging JWKS endpoint; issuer remains login.corp.mongodb.com.
OPERATOR_AUTH_KANOPY_JWKS_URL: "https://login.staging.corp.mongodb.com/.well-known/jwks.json"
OPERATOR_REPO_SLUG: "grove-platform/github-copier"

# Secrets are read from env vars directly (no GCP Secret Manager on Kanopy).
SKIP_SECRET_MANAGER: "true"

# LLM rule suggester
LLM_PROVIDER: "anthropic"
LLM_BASE_URL: "https://grove-gateway-prod.azure-api.net/grove-foundry-prod/anthropic"
LLM_MODEL: "claude-haiku-4-5"

# envSecrets maps ENV_VAR_NAME → Kubernetes Secret name.
# Provision all entries before first deploy:
#
# helm ksec set github-copier-secrets \
# GITHUB_APP_PRIVATE_KEY_B64="$(base64 < your-app.private-key.pem)" \
# WEBHOOK_SECRET="your-webhook-secret" \
# MONGO_URI="mongodb+srv://..." \
# ANTHROPIC_API_KEY="sk-ant-..."
#
envSecrets:
GITHUB_APP_PRIVATE_KEY_B64: github-copier-secrets
WEBHOOK_SECRET: github-copier-secrets
MONGO_URI: github-copier-secrets
ANTHROPIC_API_KEY: github-copier-secrets
51 changes: 51 additions & 0 deletions kanopy/values.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Kanopy web-app chart base values for github-copier.
# Per-environment overlays in values.<env>.yaml are merged on top at deploy time.
# Field names follow the mongodb/web-app chart schema.

image:
repository: 795250896452.dkr.ecr.us-east-1.amazonaws.com/docs/github-copier
pullPolicy: IfNotPresent
# tag is not set here — the Drone deploy step passes image.tag=git-<sha> at deploy time.

# The app listens on PORT 8080. The chart routes Ingress → port 80 → targetPort 8080.
services:
- name: http
ingress: true
port: 80
probes: true
protocol: TCP
targetPort: 8080
type: ClusterIP

# Health and readiness checks against /health and /ready.
probes:
enabled: true
path: /health
liveness:
initialDelaySeconds: 15
periodSeconds: 15
timeoutSeconds: 5
failureThreshold: 3
readiness:
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3

resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi

# Join the Istio service mesh so that inbound requests from CorpSecure arrive
# with the X-Kanopy-Internal-Authorization JWT injected by the proxy.
mesh:
enabled: true

# Dedicated ServiceAccount for a distinct SPIFFE identity on outbound requests.
serviceAccount:
enabled: true
name: sa
Loading
Loading