# AGENTS.md — infra-provider

> Conventions and commands for AI coding agents operating in this repository.

## PR Review Comment Workflow

When addressing PR review comments:

1. **Reply first, then resolve.** Before resolving a thread, post a reply comment explaining *how* the issue was handled — include the commit SHA and a one-sentence description. Example:
   > Fixed in commit `abc1234`: `CreateRestore` now writes both `Spec.CompactBackupName` and the `RestoreAnnoCompactionBackupName` annotation (with a nil-guard) so downstream components on either path receive the value.

2. **Only resolve threads you have fully addressed.** Leave threads open if the fix is out of scope, requires a separate decision, or needs follow-up work from another person.

3. **How to reply and resolve via CLI:**
   ```bash
   # Reply to a specific comment (get comment ID from the HTML URL: #discussion_rNNNN)
   GH_PAGER=cat gh api repos/{owner}/{repo}/pulls/{pr}/comments/{comment_id}/replies \
     -X POST -f body="Fixed in commit ..."

   # Resolve the thread via GraphQL (get thread ID via GraphQL reviewThreads query)
   GH_PAGER=cat gh api graphql -f query='mutation { resolveReviewThread(input: {threadId: "PRRT_..."}) { thread { isResolved } } }'
   ```

## Repository Structure

Go monorepo with independent modules, each having its own `go.mod`, `Makefile`, and `.golangci.yml`.

| Module | Description |
|---|---|
| `api/` | CRD API definitions + protobuf (buf) |
| `cluster-controller/` | Manages Cluster CRs — the main controller (kubebuilder-based) |
| `regional-server/` | Regional API server with gRPC + REST |
| `cluster-server/` | Cluster server |
| `cluster-agent/` | Cluster agent |
| `infra-api/` | Legacy (Gardener-based) provider |
| `pkg/` | Shared libraries used across controllers |
| `config-manager/` | GitOps controller webhook |
| `clusterproxy/` | Lightweight HTTP proxy for dataplane |
| `util/` | General utilities |
| `tools/` | Operational CLI tools |

## Build / Lint / Test Commands

### Root-level commands (from repo root)

```bash
# Lint a specific module
make lint-cluster-controller
make lint-pkg

# Build a specific module
make build-cluster-controller

# Tidy ALL modules' go.mod files
make tidy
```

### Per-module commands (cd into module directory)

#### cluster-controller/
```bash
make lint                # golangci-lint run ./...
make lint-fix            # golangci-lint run --fix ./...
make unit                # Unit tests only (build tag: unit)
make unit-without-coverage
make test                # Full: lint + generate + unit + integration
make integration         # Ginkgo-based integration tests
make build               # CGO_ENABLED=0 go build
make generate            # controller-gen deepcopy
make manifests           # controller-gen CRD/RBAC/webhook
make mockgen             # Generate mocks with mockgen v0.5.2
make reviewable          # lint-fix + test + manifests + tidy + git diff
```

#### regional-server/
```bash
make build               # Build binary
make test                # Build + lint + run tests with gotestsum
make lint                # golangci-lint with --fix
make mock                # Generate mocks with mockgen v0.4.0
make generate            # controller-gen deepcopy
make manifests           # controller-gen CRD/RBAC
```

#### pkg/
```bash
make check               # lint + unit
make unit                # go test -vet=all -short ./...
make lint                # golangci-lint run ./...
```

#### api/
```bash
make proto               # generate-proto + format-proto + lint-proto
make generate-proto      # buf generate (requires buf v1.55.1)
make lint-proto          # buf lint
```

### Running a Single Test

```bash
# Single test function (from module directory)
go test ./path/to/package -run TestFunctionName -v

# Single test with build tags (cluster-controller uses tags)
go test ./path/to/package -run TestFunctionName -tags unit -v

# cluster-controller needs envtest for tests requiring K8s API
KUBEBUILDER_ASSETS="$(setup-envtest use 1.28.3 -p path)" go test ./controllers/... -run TestMyTest -tags unit -v

# Ginkgo integration tests with focus
make integration focus="test description pattern"
# Or directly:
ginkgo --focus="MyTest" ./tests/integration/...
```

## Code Style

### Go Version
Go 1.23+ (see each module's `go.mod` for exact version).

### Import Ordering
Three groups separated by blank lines, enforced by `goimports`:

```go
import (
    // 1. Standard library
    "context"
    "fmt"
    "time"

    // 2. Third-party / external
    "github.com/go-logr/logr"
    "github.com/pingcap/errors"
    corev1 "k8s.io/api/core/v1"
    apierrors "k8s.io/apimachinery/pkg/api/errors"
    ctrl "sigs.k8s.io/controller-runtime"
    "sigs.k8s.io/controller-runtime/pkg/client"

    // 3. Internal (tidbcloud)
    cloudv1 "github.com/tidbcloud/infra-api/api/v1"
    "github.com/tidbcloud/infra-provider/pkg/config"
    "github.com/tidbcloud/infra-provider/pkg/consts"
)
```

**Local prefixes** (configured in `.golangci.yml`):
- `github.com/tidbcloud/infra-provider`
- `github.com/tidbcloud/cluster-controller`
- `github.com/tidbcloud/eks-provider`
- `github.com/tidbcloud/infra-api`

### Formatting
- `gofmt` and `goimports` enforced via golangci-lint.
- No manual formatting — run `make lint-fix` to auto-format.

### Naming Conventions
- **Packages**: lowercase, single-word when possible (`task`, `config`, `consts`).
- **K8s type aliases**: Use short prefixed aliases for k8s packages:
  ```go
  corev1 "k8s.io/api/core/v1"
  apierrors "k8s.io/apimachinery/pkg/api/errors"
  kerrors "k8s.io/apimachinery/pkg/api/errors"
  metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  ctrl "sigs.k8s.io/controller-runtime"
  ```
- **Constants**: camelCase for private, PascalCase for exported. Error sentinels as `errXxx` strings:
  ```go
  const errGetResource = "cannot get managed resource"
  ```
- **Reconcilers**: Named `Reconciler` struct per package with a `Reconcile(ctx, req)` method.

### Error Handling
- **Wrapping**: Use `github.com/pingcap/errors` (NOT `fmt.Errorf` with `%w`):
  ```go
  errors.Wrap(err, "cannot get managed resource")
  errors.Wrapf(err, "failed to create cluster %s", name)
  ```
- **K8s not-found**: Check with `apierrors.IsNotFound(err)` and return `ctrl.Result{}, nil`.
- **Error return style**: Return early on error, no else blocks.
  ```go
  if err := r.Client.Get(ctx, key, obj); err != nil {
      if apierrors.IsNotFound(err) {
          return ctrl.Result{}, nil
      }
      return ctrl.Result{}, fmt.Errorf("unable to get resource: %v", err)
  }
  ```

### Logging
- **Library**: `logr` via controller-runtime (`sigs.k8s.io/controller-runtime/pkg/log`).
- **Pattern**: Extract logger from context, add structured key-value pairs:
  ```go
  logger := log.FromContext(ctx).WithValues("reconcile-id", uuid.New().String(), "name", req.NamespacedName)
  ctx = log.IntoContext(ctx, logger)
  logger.Info("Reconciling Backup")
  logger.Error(err, "failed to reconcile")
  ```
- **Integration tests**: Use `zap` logger via `sigs.k8s.io/controller-runtime/pkg/log/zap`.

### Testing Patterns
- **Unit tests**: Standard `testing.T` with table-driven tests. Build tag: `unit`.
- **Assertion libraries**: `gomega` (preferred), `go-cmp`, or stdlib `reflect.DeepEqual`.
- **Table-driven**: Two styles used:
  ```go
  // Style 1: map-based (cluster-controller/controllers/reconciler/)
  cases := map[string]struct {
      reason string
      args   args
      want   want
  }{ ... }

  // Style 2: slice-based (more common)
  tests := []struct {
      name string
      args args
      want Type
  }{ ... }
  for _, tt := range tests {
      t.Run(tt.name, func(t *testing.T) { ... })
  }
  ```
- **Integration tests**: Ginkgo v2 + Gomega with envtest (controller-runtime test environment).
- **Mocks**: `go.uber.org/mock/mockgen` — generated into `test/` or alongside source as `*_mock.go`.

## Code Generation

### CRD / Controller (kubebuilder)
```bash
# In cluster-controller/ or regional-server/
make manifests    # CRD, RBAC, Webhook YAML
make generate     # DeepCopy methods
```
Uses `controller-gen` v0.16.x. Markers like `//+kubebuilder:...` in API types.

### Protobuf (buf)
```bash
# In api/
make proto        # Full: generate + format + lint
```
Uses `buf` v1.55.1 with `buf.yaml` config. Proto files in `api/tidbcloud/*/v1/*.proto`.

### Mocks (mockgen)
```bash
# In cluster-controller/
make mockgen      # mockgen v0.5.2
# In regional-server/
make mock         # mockgen v0.4.0
```
Uses source mode: `mockgen -source=./path/to/interface.go -destination=./path/to/mock.go -package=pkg`.

## Linter Configuration

Managed per-module via `.golangci.yml`. Key enabled linters:
- `govet`, `staticcheck`, `errcheck`, `ineffassign`, `unused`
- `gosec` (with many exclusions: G101, G103, G104, G204, G304, G401, G404, etc.)
- `misspell`, `unconvert`, `asciicheck`, `durationcheck`, `makezero`, `goconst`
- `gofmt`, `goimports` (formatters)

**Not enabled**: `depguard`, `dupl`, `nilerr`, `rowserrcheck`.

Generated files (`zz_generated.deepcopy.go`) are excluded.

## PR Checklist
- Run `make reviewable` (in the module you changed) before pushing — it runs lint, test, manifests, tidy, and checks for uncommitted diffs.
- PR template requires: description, impact on existing clusters, evidence of testing.
- CI runs golangci-lint + `go test ./...` per module via reusable `standard-ci.yml` workflow.

## Key Dependencies
- **Kubernetes**: controller-runtime, client-go, apimachinery
- **TiDB Operator**: `github.com/pingcap/tidb-operator/pkg/apis`
- **Cloud SDKs**: AWS SDK v2, GCP, Azure
- **Error handling**: `github.com/pingcap/errors`
- **gRPC**: buf, grpc-gateway, protovalidate
- **Testing**: gomega, ginkgo v2, go.uber.org/mock, envtest
