diff --git a/.claude/commands/README.md b/.claude/commands/README.md new file mode 100644 index 000000000..fa68296fc --- /dev/null +++ b/.claude/commands/README.md @@ -0,0 +1,152 @@ +# External DNS Operator - Claude Code Commands + +This directory contains custom Claude Code slash commands to help with development and PR review workflows. + +## Available Commands + +### `/external-dns-operator-helper` + +Automated PR review workflow that runs comprehensive quality checks on Pull Requests. + +--- + +## Prerequisites + +You need **Claude Code** installed and configured. See [Claude Code Documentation](https://docs.google.com/document/d/1eNARy9CI28o09E7Foq01e5WD5MvEj3LSBnXqFcprxjo/edit?tab=t.0#heading=h.8ldy5by9bpo8) for installation instructions. + +--- + +## How to Use the `/external-dns-operator-helper` Command + +### Step 1: Open Claude Code + +Launch Claude Code in your terminal or IDE. + +### Step 2: Navigate to Repository + +Ensure you're in the external-dns-operator repository directory: + +```bash +cd /path/to/external-dns-operator +``` + +### Step 3: Run the Command + +In Claude Code, use the slash command with a PR URL: + +``` +/external-dns-operator-helper https://github.com/openshift/external-dns-operator/pull/123 +``` + +**Replace `123` with the actual PR number you want to review.** + +### Step 4: Review the Results + +Claude Code will automatically: +1. ✅ Check for uncommitted changes and save your current branch +2. ✅ Checkout the PR +3. ✅ Run `make verify` (lint, format, deps, generated code, OLM) +4. ✅ Run `make test` (unit tests with coverage) +5. ✅ Run `make build` (compilation check) +6. ✅ Perform specialized checks (API changes, tests, docs) +7. ✅ Generate comprehensive report +8. ✅ Return to your original branch + +--- + +## Common Use Cases + +### Use Case 1: Pre-Submission PR Check + +**Before creating or updating your PR**, validate your changes: + +```bash +# 1. Push your changes to your fork +git push origin your-branch-name + +# 2. Create the PR (via GitHub UI or gh CLI) +# Get the PR URL, then run: +/external-dns-operator-helper https://github.com/openshift/external-dns-operator/pull/YOUR-PR-NUMBER +``` + +### Use Case 2: Review Someone Else's PR + +**Before manually reviewing**, run automated checks: + +```bash +# Someone asks you to review PR #296 +/external-dns-operator-helper https://github.com/openshift/external-dns-operator/pull/296 +``` + +If automated checks pass, you can focus on code logic and architecture review. + +### Use Case 3: Debugging CI Failures + +**When CI fails on your PR**, reproduce locally: + +```bash +# Your PR #301 failed CI, check it locally: +/external-dns-operator-helper https://github.com/openshift/external-dns-operator/pull/301 +``` + +The command will show the same errors CI would show, with detailed explanations. + +### Use Case 4: Learning Best Practices + +**New to the project?** Use it to learn what checks are required: + +```bash +# Review a recent merged PR to see what passed: +/external-dns-operator-helper https://github.com/openshift/external-dns-operator/pull/296 +``` + +--- + +## What the Command Checks + +### 1. Linting (`make lint`) +- errcheck, gofmt, goimports, gosimple, govet +- ineffassign, misspell, staticcheck, typecheck, unused +- Configuration: `.golangci.yaml` + +### 2. Formatting (`hack/verify-gofmt.sh`) +- All `.go` files must be `gofmt -s` compliant + +### 3. Dependencies (`hack/verify-deps.sh`) +- `go.mod` and `go.sum` are in sync +- `vendor/` directory is up-to-date + +### 4. Generated Code (`hack/verify-generated.sh`) +- DeepCopy methods are current (`make generate`) +- CRDs, RBAC, webhooks are current (`make manifests`) + +### 5. OLM Bundle (`hack/verify-olm.sh`) +- Bundle manifests are up-to-date (`make bundle`) +- Catalog is current (`make catalog`) + +### 6. Unit Tests (`make test`) +- All unit tests pass +- No race conditions (runs with `-race`) +- Coverage report generated + +### 7. Build Validation (`make build`) +- Operator binary compiles successfully + +### 8. Specialized Checks +- API changes → CRD updates +- Controller changes → Test updates +- User-facing changes → Documentation updates +- Kubebuilder markers → Proper documentation + +--- + + + +## Additional Resources + +- **AGENTS.md**: Comprehensive repository development guide +- **CLAUDE.md**: Symlink to AGENTS.md +- **Makefile**: Build targets and CI automation +- **docs/**: External DNS Operator documentation + +--- \ No newline at end of file diff --git a/.claude/commands/external-dns-operator-helper.md b/.claude/commands/external-dns-operator-helper.md new file mode 100644 index 000000000..90aead749 --- /dev/null +++ b/.claude/commands/external-dns-operator-helper.md @@ -0,0 +1,41 @@ +```yaml +name: external-dns-operator-helper +description: Run comprehensive PR review workflow for External DNS Operator changes with CI analysis +parameters: + - name: pr_url + description: GitHub PR URL to review (e.g., https://github.com/openshift/external-dns-operator/pull/123) + required: true +``` + +I'll run a comprehensive automated review of the External DNS Operator PR. This includes checking out the PR, analyzing CI status, validating commits, checking Effective Go style, running local verification, and providing actionable feedback. + +```bash +bash .claude/scripts/pr-review.sh "{{pr_url}}" +``` + +--- + +## What This Does + +**Single command execution** that runs all checks without user interaction: + +1. **PR Info** - Shows commits, files changed, PR size +2. **CI Status** - Displays Prow job results with clickable links (no deep log parsing) +3. **Commit Validation** - Checks JIRA-ID format +4. **Effective Go** - Validates receiver names, error strings, exported docs +5. **Make Targets** - Runs verify, test, build +6. **Specialized** - API/CRD sync, controller/test coverage, docs +7. **Summary** - Clear pass/fail report + + + +## Requirements + +- Git with `upstream` remote +- `jq` (optional - degrades gracefully without it) + +## Usage + +```bash +/external-dns-operator-helper https://github.com/openshift/external-dns-operator/pull/294 +``` \ No newline at end of file diff --git a/.claude/scripts/pr-review.sh b/.claude/scripts/pr-review.sh new file mode 100755 index 000000000..84afc2adb --- /dev/null +++ b/.claude/scripts/pr-review.sh @@ -0,0 +1,373 @@ +#!/bin/bash +set -euo pipefail + +# ═══════════════════════════════════════════════════════════ +# External DNS Operator PR Review - Simplified Single-Run +# ═══════════════════════════════════════════════════════════ + +REVIEW_START=$(date +%s) +PR_URL="$1" +PR_NUMBER=$(echo "$PR_URL" | grep -oE '[0-9]+$') + +if [ -z "$PR_NUMBER" ]; then + echo "❌ ERROR: Invalid PR URL" + exit 1 +fi + +CURRENT_BRANCH=$(git branch --show-current) +REVIEW_BRANCH="pr-review-$PR_NUMBER" + +# Cleanup on exit +cleanup() { + if [ "$(git branch --show-current)" = "$REVIEW_BRANCH" ]; then + git checkout "$CURRENT_BRANCH" 2>/dev/null || true + fi + git branch -D "$REVIEW_BRANCH" 2>/dev/null || true +} +trap cleanup EXIT + +echo "📍 Current branch: $CURRENT_BRANCH" +echo "🔍 Reviewing PR #$PR_NUMBER" +echo "" + +# Pre-flight checks +if ! git diff --quiet || ! git diff --cached --quiet; then + echo "❌ ERROR: Uncommitted changes detected" + git status --porcelain + exit 1 +fi + +if ! git remote | grep -q "^upstream$"; then + echo "❌ ERROR: 'upstream' remote not found" + echo " Add with: git remote add upstream https://github.com/openshift/external-dns-operator.git" + exit 1 +fi + +echo "✅ Working directory clean" +echo "✅ Upstream remote found" +echo "" + +# Fetch and checkout PR +echo "🔄 Fetching PR #$PR_NUMBER..." +git fetch upstream pull/$PR_NUMBER/head:$REVIEW_BRANCH 2>&1 | grep -v "^remote:" || true +git checkout "$REVIEW_BRANCH" >/dev/null 2>&1 +echo "" + +# ═══════════════════════════════════════════════════════════ +# PR Information +# ═══════════════════════════════════════════════════════════ + +echo "═══════════════════════════════════════════════════════════" +echo " PR INFORMATION" +echo "═══════════════════════════════════════════════════════════" +echo "" + +COMMIT_COUNT=$(git rev-list --count upstream/main..HEAD) +echo "📋 Commits: $COMMIT_COUNT" +git log upstream/main..HEAD --oneline +echo "" + +CHANGED_FILES=$(git diff --name-status upstream/main...HEAD | wc -l) +LINES_CHANGED=$(git diff upstream/main...HEAD --shortstat | grep -oP '\d+(?= insertion)|\d+(?= deletion)' | paste -sd+ | bc 2>/dev/null || echo "0") + +echo "📁 Files changed: $CHANGED_FILES" +echo "📏 Lines changed: $LINES_CHANGED" +[ "$LINES_CHANGED" -gt 500 ] && echo " ⚠️ Large PR - consider splitting" +echo "" + +# ═══════════════════════════════════════════════════════════ +# CI Status (Simple - No Log Parsing) +# ═══════════════════════════════════════════════════════════ + +echo "═══════════════════════════════════════════════════════════" +echo "🤖 CI STATUS (Prow)" +echo "═══════════════════════════════════════════════════════════" +echo "" + +# Get commit SHA and fetch CI status (single API call) +COMMIT_SHA=$(git rev-parse HEAD) +CI_DATA=$(curl -s -f "https://api.github.com/repos/openshift/external-dns-operator/statuses/$COMMIT_SHA" 2>/dev/null || echo "") + +if [ -n "$CI_DATA" ] && command -v jq &>/dev/null; then + # Parse with jq if available + echo "$CI_DATA" | jq -r '.[] | select(.context | startswith("ci/prow/") or startswith("ci/")) | + "\(.state)|\(.context)|\(.target_url // "no-link")"' | sort -u | while IFS='|' read -r state context url; do + case "$state" in + success) echo "✅ $context" ;; + failure|error) + echo "❌ $context" + [ "$url" != "no-link" ] && echo " 🔗 $url" + ;; + pending) echo "⏳ $context" ;; + *) echo "❓ $context - $state" ;; + esac + done + + # Summary + PASSED=$(echo "$CI_DATA" | jq -r '.[] | select(.state == "success") | .context' | grep -c "ci/" || echo "0") + FAILED=$(echo "$CI_DATA" | jq -r '.[] | select(.state == "failure" or .state == "error") | .context' | grep -c "ci/" || echo "0") + echo "" + if [ "$FAILED" -eq 0 ]; then + echo "✅ CI Summary: $PASSED jobs passed" + else + echo "⚠️ CI Summary: $PASSED passed, $FAILED failed - check links above" + fi +elif [ -n "$CI_DATA" ]; then + # Fallback without jq - basic parsing + echo "$CI_DATA" | grep -o '"context":"ci/[^"]*"' | cut -d'"' -f4 | while read -r context; do + if echo "$CI_DATA" | grep -q "\"context\":\"$context\".*\"state\":\"success\""; then + echo "✅ $context" + elif echo "$CI_DATA" | grep -q "\"context\":\"$context\".*\"state\":\"failure\""; then + echo "❌ $context" + else + echo "⏳ $context" + fi + done + echo "" + echo "ℹ️ Install jq for full CI details: sudo dnf install jq" +else + echo "ℹ️ Could not fetch CI status (check network or wait for CI to start)" +fi +echo "" + +# ═══════════════════════════════════════════════════════════ +# Step 1: Commit Message Validation +# ═══════════════════════════════════════════════════════════ + +echo "═══════════════════════════════════════════════════════════" +echo "⏳ Step 1/5: Commit message validation" +echo "═══════════════════════════════════════════════════════════" +echo "" + +COMMIT_FAILED=false +while IFS= read -r commit; do + hash=$(echo "$commit" | awk '{print $1}') + msg=$(echo "$commit" | cut -d' ' -f2-) + + if ! echo "$msg" | grep -qE "^[A-Z]+-[0-9]+: "; then + echo " ❌ $hash: $msg" + echo " ^ Missing JIRA-ID (e.g., NE-2076: Description)" + COMMIT_FAILED=true + else + echo " ✅ $hash: $msg" + fi +done < <(git log upstream/main..HEAD --oneline) + +echo "" +[ "$COMMIT_FAILED" = true ] && echo "❌ FAILED" || echo "✅ PASSED" +echo "" + +# ═══════════════════════════════════════════════════════════ +# Step 2: Effective Go Style Checks +# ═══════════════════════════════════════════════════════════ + +echo "═══════════════════════════════════════════════════════════" +echo "⏳ Step 2/5: Effective Go style (https://go.dev/doc/effective_go)" +echo "═══════════════════════════════════════════════════════════" +echo "" + +STYLE_WARNINGS=0 + +# Check 1: Receiver names +echo "🔎 Receiver names:" +RECEIVERS=$(git diff upstream/main...HEAD | grep -E '^\+.*func \([a-zA-Z_]+ \*?[A-Z]' | grep -oP 'func \(\K[a-zA-Z_]+' | sort -u || true) +if [ -n "$RECEIVERS" ]; then + if echo "$RECEIVERS" | grep -qE '^(this|self|me)$'; then + echo " ❌ Avoid: this, self, me" + STYLE_WARNINGS=$((STYLE_WARNINGS + 1)) + else + echo " ✅ Good: $(echo "$RECEIVERS" | tr '\n' ', ' | sed 's/, $//')" + fi +else + echo " ℹ️ No new methods" +fi +echo "" + +# Check 2: Error strings +echo "🔎 Error strings (lowercase, no punctuation):" +ERROR_ISSUES=$(git diff upstream/main...HEAD | \ + grep -E '^\+.*(errors\.New|fmt\.Errorf)\("' | \ + grep -v '//' | \ + grep -oP '(errors\.New|fmt\.Errorf)\("\K[^"]+' | \ + grep -E '^[A-Z][a-z]|\.$' || true) + +if [ -n "$ERROR_ISSUES" ]; then + echo " ❌ Found issues:" + echo "$ERROR_ISSUES" | head -3 | while read -r err; do + echo " \"$err\"" + done + STYLE_WARNINGS=$((STYLE_WARNINGS + 1)) +else + echo " ✅ Properly formatted" +fi +echo "" + +# Check 3: Exported function docs +echo "🔎 Exported function docs:" +UNDOC=$(git diff upstream/main...HEAD | \ + grep -B1 -E '^\+func [A-Z]' | \ + grep -v -E '^\+//|^--' | \ + grep -E '^\+func [A-Z]' | \ + grep -oP 'func \K[A-Z][a-zA-Z0-9]+' || true) + +if [ -n "$UNDOC" ]; then + echo " ⚠️ May need docs:" + echo "$UNDOC" | head -3 | while read -r fn; do + echo " • $fn" + done + STYLE_WARNINGS=$((STYLE_WARNINGS + 1)) +else + echo " ✅ Documented" +fi +echo "" + +[ $STYLE_WARNINGS -eq 0 ] && echo "✅ PASSED" || echo "⚠️ $STYLE_WARNINGS warning(s)" +echo "" + +# ═══════════════════════════════════════════════════════════ +# Step 3: Verification +# ═══════════════════════════════════════════════════════════ + +echo "═══════════════════════════════════════════════════════════" +echo "⏳ Step 3/5: Verification (make verify)" +echo "═══════════════════════════════════════════════════════════" +echo "" + +VERIFY_FAILED=false +if make verify 2>&1 | tail -30; then + echo "" + echo "✅ PASSED" +else + echo "" + echo "❌ FAILED - Run 'make verify' locally" + VERIFY_FAILED=true +fi +echo "" + +# ═══════════════════════════════════════════════════════════ +# Step 4: Unit Tests +# ═══════════════════════════════════════════════════════════ + +echo "═══════════════════════════════════════════════════════════" +echo "⏳ Step 4/5: Unit tests (make test)" +echo "═══════════════════════════════════════════════════════════" +echo "" + +TEST_FAILED=false +if timeout 600 make test 2>&1 | tail -30; then + echo "" + echo "✅ PASSED" + [ -f coverage.out ] && echo "" && echo "📊 Coverage:" && go tool cover -func=coverage.out | tail -3 +else + echo "" + echo "❌ FAILED - Run 'make test' locally" + TEST_FAILED=true +fi +echo "" + +# ═══════════════════════════════════════════════════════════ +# Step 5: Build +# ═══════════════════════════════════════════════════════════ + +echo "═══════════════════════════════════════════════════════════" +echo "⏳ Step 5/5: Build (make build)" +echo "═══════════════════════════════════════════════════════════" +echo "" + +BUILD_FAILED=false +if make build 2>&1 | tail -15; then + echo "" + echo "✅ PASSED" + [ -f bin/external-dns-operator ] && ls -lh bin/external-dns-operator +else + echo "" + echo "❌ FAILED - Check compilation errors" + BUILD_FAILED=true +fi +echo "" + +# ═══════════════════════════════════════════════════════════ +# Specialized Checks +# ═══════════════════════════════════════════════════════════ + +echo "═══════════════════════════════════════════════════════════" +echo "🔍 Specialized Checks" +echo "═══════════════════════════════════════════════════════════" +echo "" + +SPEC_WARNINGS=0 + +# API changes → CRDs +if git diff upstream/main...HEAD --name-only | grep -q "api/.*types\.go"; then + echo "🔎 API changes detected" + if ! git diff upstream/main...HEAD --name-only | grep -q "config/crd/bases/"; then + echo " ❌ CRDs not updated - Run 'make manifests'" + SPEC_WARNINGS=$((SPEC_WARNINGS + 1)) + else + echo " ✅ CRDs updated" + fi + echo "" +fi + +# Controller → Tests +CTRL=$(git diff upstream/main...HEAD --name-only | grep "pkg/operator/controller/.*\.go" | grep -v "_test\.go" || true) +if [ -n "$CTRL" ]; then + echo "🔎 Controller changes" + MISSING=false + while read -r file; do + test_file="${file%.go}_test.go" + if [ ! -f "$test_file" ] && ! git diff upstream/main...HEAD --name-only | grep -q "$test_file"; then + echo " ⚠️ $file - no test update" + MISSING=true + SPEC_WARNINGS=$((SPEC_WARNINGS + 1)) + fi + done <<< "$CTRL" + [ "$MISSING" = false ] && echo " ✅ Tests updated" + echo "" +fi + +# User-facing → Docs +if git diff upstream/main...HEAD --name-only | grep -qE "(api/.*types\.go|config/samples/)"; then + if ! git diff upstream/main...HEAD --name-only | grep -qE "(docs/|README\.md)"; then + echo "🔎 User-facing changes - consider updating docs" + SPEC_WARNINGS=$((SPEC_WARNINGS + 1)) + echo "" + fi +fi + +[ $SPEC_WARNINGS -eq 0 ] && echo "✅ No warnings" && echo "" + +# ═══════════════════════════════════════════════════════════ +# Summary +# ═══════════════════════════════════════════════════════════ + +echo "═══════════════════════════════════════════════════════════" +echo " SUMMARY" +echo "═══════════════════════════════════════════════════════════" +echo "" + +ISSUES=0 +echo "✓ Commit messages: $([ "$COMMIT_FAILED" = true ] && echo "❌ FAILED" && ISSUES=$((ISSUES+1)) || echo "✅ PASSED")" +echo "✓ Effective Go: $([ $STYLE_WARNINGS -gt 0 ] && echo "⚠️ $STYLE_WARNINGS warning(s)" || echo "✅ PASSED")" +echo "✓ Verification: $([ "$VERIFY_FAILED" = true ] && echo "❌ FAILED" && ISSUES=$((ISSUES+1)) || echo "✅ PASSED")" +echo "✓ Tests: $([ "$TEST_FAILED" = true ] && echo "❌ FAILED" && ISSUES=$((ISSUES+1)) || echo "✅ PASSED")" +echo "✓ Build: $([ "$BUILD_FAILED" = true ] && echo "❌ FAILED" && ISSUES=$((ISSUES+1)) || echo "✅ PASSED")" +echo "✓ Specialized checks: $([ $SPEC_WARNINGS -gt 0 ] && echo "⚠️ $SPEC_WARNINGS warning(s)" || echo "✅ PASSED")" + +echo "" +echo "═══════════════════════════════════════════════════════════" +echo "" + +if [ $ISSUES -eq 0 ]; then + echo "✅ OVERALL: PR #$PR_NUMBER is ready for merge!" + [ $((STYLE_WARNINGS + SPEC_WARNINGS)) -gt 0 ] && echo " ($((STYLE_WARNINGS + SPEC_WARNINGS)) warning(s) - consider addressing)" +else + echo "❌ OVERALL: PR #$PR_NUMBER requires $ISSUES fix(es)" +fi + +REVIEW_END=$(date +%s) +DURATION=$((REVIEW_END - REVIEW_START)) +echo "" +echo "⏱️ Review took ${DURATION}s" +echo "" +echo "✅ Returned to: $CURRENT_BRANCH" diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..b7012cadc --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,174 @@ +This file provides guidance to AI agents working with the External DNS Operator repository. + +## Repository Overview + +The **External DNS Operator** manages [ExternalDNS](https://github.com/kubernetes-sigs/external-dns) deployments in OpenShift/Kubernetes clusters, automating DNS record creation across multiple cloud providers (AWS Route53, Azure DNS, GCP Cloud DNS, Infoblox, BlueCat). + +**Key components**: +- Operator code: `pkg/operator/controller/` (externaldns, ca-configmap, credentials-secret controllers) +- CRD definitions: `api/v1beta1/externaldns_types.go` (storage version), `api/v1alpha1/` (deprecated) +- Deployment manifests: `config/` (CRDs, RBAC, samples for each provider) +- OLM artifacts: `bundle/`, `catalog/` (operator lifecycle management) +- Tests: Unit tests alongside code, e2e tests in `test/e2e/` + +**ExternalDNS CRD** (cluster-scoped): +- Spec: `provider` (AWS/Azure/GCP/Infoblox/BlueCat), `source` (Route/Service/CRD), `zones`, `domains` +- Status: `conditions`, `observedGeneration`, `zones` + +## Essential Commands + +### Development +```bash +make build # Generate code, fmt, vet, compile binary +make test # Unit tests with race detector & coverage (uses envtest k8s 1.33.0) +make verify # CI checks: lint, gofmt, deps, generated code, OLM bundle/catalog, version +make lint # golangci-lint (.golangci.yaml: errcheck, gofmt, goimports, gosimple, govet, etc.) +``` + +### Code Generation (required after API changes) +```bash +make generate # DeepCopy methods (controller-gen) +make manifests # CRDs, RBAC, webhooks from kubebuilder markers +``` + +### Deployment +```bash +make deploy # Deploy to cluster (uses kustomize, patches image to $IMG) +make undeploy # Remove from cluster +``` + +### Container Images +```bash +export CONTAINER_ENGINE=podman # or docker (default) +make image-build image-push # Operator image (runs test first) +make bundle-image-build bundle-image-push # OLM bundle +make catalog-image-build catalog-image-push # OLM catalog +``` + +### E2E Testing (requires cloud credentials) +```bash +export DNS_PROVIDER=AWS # AWS, Azure, GCP, Infoblox, BlueCat +export AWS_ACCESS_KEY_ID=xxx AWS_SECRET_ACCESS_KEY=xxx # Provider-specific creds +make test-e2e # Runs test/e2e/operator_test.go (1h timeout) +``` + +## Makefile Target Details + +**`make verify`** (Makefile:133-138) runs: +1. `lint` - golangci-lint with 10m timeout, vendor mode +2. `hack/verify-gofmt.sh` - All .go files must be gofmt-compliant +3. `hack/verify-deps.sh` - `go mod vendor && go mod tidy`, checks vendor/ sync (CI only) +4. `hack/verify-generated.sh` - `make generate manifests`, checks no uncommitted changes (CI only) +5. `hack/verify-olm.sh` - `make bundle catalog`, validates OLM manifests (CI only) +6. `hack/verify-version.sh` - VERSION file consistency + +**`make test`** (Makefile:114-116): +- Pre: `make manifests generate fmt vet` +- Runs: `CGO_ENABLED=1 go test ./... -race -covermode=atomic -coverprofile coverage.out` +- Uses: `setup-envtest` with Kubernetes 1.33.0 binaries + +**`make build`** (Makefile:146): +- Pre: `make generate fmt vet` +- Runs: `GO111MODULE=on GOFLAGS=-mod=vendor CGO_ENABLED=0 go build -ldflags "-X ..SHORTCOMMIT -X ..COMMIT"` +- Output: `bin/external-dns-operator` + +## Working with the Codebase + +### Modifying API Types (`api/v1beta1/externaldns_types.go`) +1. Edit types, add kubebuilder markers: `+kubebuilder:validation:Required`, `+kubebuilder:validation:Enum=A;B`, `+optional` +2. Run: `make generate manifests` (updates zz_generated.deepcopy.go and config/crd/bases/) +3. Update controller logic: `pkg/operator/controller/externaldns/deployment.go`, `pod.go`, `status.go` +4. Add tests, run: `make verify test build` +5. Generate OLM bundle: `make bundle` + +### Adding DNS Providers +1. Update `api/v1beta1/externaldns_types.go`: Add enum, provider options struct, union field +2. `make generate manifests` +3. Update `pkg/operator/controller/externaldns/`: deployment.go (args), pod.go (volumes/env), credentials_request.go (CCO) +4. Add sample CR: `config/samples//` +5. Add e2e tests: `test/e2e/.go` +6. Update docs: README.md, docs/usage.md, docs/-openshift.md + +### Testing Strategy +- **Unit**: `*_test.go` alongside code, uses gomega matchers, controller-runtime/envtest +- **E2E**: `test/e2e/operator_test.go`, requires cluster + provider credentials, build tag `e2e` +- **Coverage**: View with `go tool cover -func=coverage.out` + +## PR Review Requirements + +**CI must pass**: +1. ✅ All `make verify` checks (lint, format, deps, generated, OLM) +2. ✅ All `make test` passes +3. ✅ `make build` succeeds +4. ✅ Generated files committed (`make generate manifests bundle catalog`) +5. ✅ Tests updated for controller changes +6. ✅ Documentation updated for user-facing changes +7. ✅ Commit format: `: Description` (e.g., `NE-2076: Add feature X`) + +## Repository Structure (Key Paths) + +``` +api/v1beta1/externaldns_types.go # Primary API definition (DO NOT EDIT zz_generated.*) +pkg/operator/controller/ + externaldns/ # Main controller + controller.go # Reconciliation loop + deployment.go # ExternalDNS deployment generation + pod.go # Pod spec (provider-specific args/volumes) + status.go # Status condition updates + ca-configmap/ # Trusted CA sync + credentials-secret/ # Secret replication +config/ + crd/bases/ # Generated CRDs (from make manifests) + samples/ # Provider-specific example CRs + rbac/ # ClusterRole, bindings +Makefile # Build targets (see lines 114, 133, 146) +.golangci.yaml # Linter config (10 enabled linters) +``` + +## Known Limitations + +**Domain name length**: TXT registry prefix (`external-dns--`) reduces max length: +- CNAME: 44 chars (42 for wildcard on Azure) +- A: 48 chars (46 for wildcard on Azure) + +## Custom Claude Code Command + +**`/external-dns-operator-helper `** + +Simple, automated PR review workflow - single execution, no prompts: + +1. **Pre-flight**: Check clean working dir, verify upstream remote +2. **Checkout PR**: Fetch and checkout using native git commands +3. **CI Status** ⭐: Display Prow job results with clickable links (✅/❌/⏳) +4. **Commit Validation**: Ensure `: Description` format +5. **Effective Go Checks** ⭐: Validate receiver names, error strings, exported docs (https://go.dev/doc/effective_go) +6. **Local Verification**: `make verify` (lint, format, deps, generated, OLM) +7. **Unit Tests**: `make test` with coverage +8. **Build**: `make build` compilation check +9. **Specialized Checks**: API/CRD sync, controller/test coverage, docs +10. **Summary**: Clear pass/fail report with timing +11. **Auto Cleanup**: Return to original branch + +**Requirements**: +- Git repository with `upstream` remote pointing to `https://github.com/openshift/external-dns-operator.git` +- `jq` (optional - gracefully degrades without it) + +**Key Features**: +- ✅ **Single execution** - No multiple prompts or approvals +- ✅ **Fast** - Completes in ~2-3 minutes +- 🎯 **Prow CI integration** - Shows all job statuses with links +- 📚 **Effective Go validation** - Catches style issues beyond golangci-lint +- ⏱️ **Time savings** - ~15 minutes per PR review +- 🛡️ **Robust** - Automatic cleanup even on failures +- 🚀 **Minimal dependencies** - Just git, curl (jq optional) + +## Environment Variables + +**Build**: `IMG`, `BUNDLE_IMG`, `CATALOG_IMG`, `CONTAINER_ENGINE` (docker/podman), `BUNDLE_VERSION` +**E2E**: `KUBECONFIG`, `DNS_PROVIDER`, `AWS_ACCESS_KEY_ID`, `AZURE_CLIENT_ID`, `GCP_PROJECT`, `INFOBLOX_GRID_HOST`, etc. + +## Resources + +- [Operator SDK](https://sdk.operatorframework.io/), [Kubebuilder](https://book.kubebuilder.io/) +- [ExternalDNS Docs](https://kubernetes-sigs.github.io/external-dns/) +- [Enhancement Proposal](https://github.com/openshift/enhancements/pull/786) diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 000000000..47dc3e3d8 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file