# Controllers Layer Agent Instructions

## Overview

Controllers are **thin entry points** for gRPC/HTTP. They handle validation, conversion, and delegate to models. NO business logic here.

## Directory Structure

| Directory | Purpose | API Type |
|-----------|---------|----------|
| `console/` | Console UI API | Customer users on TiDBCloud Console |
| `inner/` | Internal microservice API | Service-to-service |
| `open/` | OpenAPI (public external) | Customer users with API key auth |
| `ops/` | Operations API | Admin tools (internal use) |
| `v1beta1/` | Newer OpenAPI | Newer features |
| `interceptor/` | gRPC interceptors | Cross-cutting |

## Controller Pattern

**Interface + implementation:**
```go
type BackupController interface {
    dataflow.BackupServiceServer  // Embed generated gRPC interface
}

type backupControllerImpl struct {
    dataflow.UnimplementedBackupServiceServer  // Forward compatibility
    *BaseController                            // Common utilities
    
    backupModel backup.BackupModel
    backupRepo  backup_repo.BackupRepo
    coreCli     grpcli.CoreGrpcClient
}

func NewBackupController(
    trans db.TransProvider,
    backupModel backup.BackupModel,
    // ... dependencies
) BackupController {
    return &backupControllerImpl{
        BaseController: NewBaseController(trans),
        backupModel:    backupModel,
    }
}
```

## Method Pattern

```go
func (c *backupControllerImpl) CreateBackup(
    ctx context.Context, req *dataflow.CreateBackupReq,
) (_ *dataflow.CreateBackupResp, rErr error) {
    // 1. Audit logging (defer for both success/error)
    var backupID uint64
    defer func() {
        c.recordAuditLog(ctx, req, backupID, rErr)
    }()

    // 2. Context setup
    ctx = log.WithTraceID(ctx)
    
    // 3. Permission check
    if _, err := c.checkClusterPermission(ctx, req.TenantId, req.ProjectId, req.ClusterId); err != nil {
        return nil, err
    }

    // 4. Convert gRPC → domain request
    domainReq := &domain.CreateBackupReq{
        ClusterID: req.ClusterId,
        Name:      req.Name,
    }

    // 5. Delegate to model
    resp, err := c.backupModel.Create(ctx, domainReq)
    if err != nil {
        return nil, err
    }
    
    // 6. Convert domain → gRPC response
    backupID = resp.BackupID
    return &dataflow.CreateBackupResp{
        Id: resp.BackupID,
    }, nil
}
```

## Request Validation

```go
// Using utils.CheckParam* functions
if err := utils.CheckParamEmptyError("name", req.Name); err != nil {
    return nil, err
}

if err := utils.CheckParamInvalidError("port", req.Port, func(p int32) bool {
    return p > 0 && p < 65535
}); err != nil {
    return nil, err
}

// Direct validation
if req.ClusterId == 0 {
    return nil, bizerr.New(bizerr.ErrCodeParams, "cluster_id is required")
}
```

## Permission Checking

**Console API (ownership check):**
```go
func (c *backupControllerImpl) checkClusterPermission(
    ctx context.Context, tenantID, projectID, clusterID uint64,
) (*core.Cluster, error) {
    cluster, err := c.coreCli.GetAnyClusterByID(ctx, clusterID)
    if err != nil {
        return nil, fmt.Errorf("get cluster %d: %w", clusterID, err)
    }
    if cluster.TenantId != tenantID || cluster.ProjectId != projectID {
        return nil, bizerr.New(bizerr.ErrCodeRecordNotFound, "cluster not found")
    }
    return cluster, nil
}
```

**OpenAPI (RBAC with API keys):**
```go
// Define resource URIs
const ListBackupsReq = "openapi/backup/orgs/{org_id}/projects/{project_id}/clusters/{cluster_id}/backups/list"

func (c *rbacChecker) checkPermission(ctx context.Context, resourceURI string, orgID, projectID, clusterID uint64) bizerr.BizError {
    md, _ := metadata.FromIncomingContext(ctx)
    apiKey := md.Get(strings.ToLower(open.OpenAPIPublicKey))[0]
    
    param := auth.CheckPermissionParam{
        IdentityType: auth.AccessIdentityTypeApiKeyAK,
        IdentityVal:  apiKey,
        ResourceUri:  resourceURI,
        ResourceParam: map[string]string{
            "org_id":     fmt.Sprint(orgID),
            "project_id": fmt.Sprint(projectID),
            "cluster_id": fmt.Sprint(clusterID),
        },
    }
    result, _ := c.rbacClient.CheckIdentityPermission(ctx, &param)
    if !result.HasPermission {
        return bizerr.New(bizerr.ErrCodePermission, "no permission")
    }
    return nil
}
```

## Response Conversion

**Create `cvt.go` files:**
```go
// console/cvt.go
func DomainBackupStatus2Dataflow(s domain.BackupStatus) dataflow.BackupStatus {
    switch s {
    case domain.BackupStatusPending:
        return dataflow.BackupStatus_BACKUP_STATUS_PENDING
    case domain.BackupStatusRunning:
        return dataflow.BackupStatus_BACKUP_STATUS_RUNNING
    // ...
    }
}

// Using maps for clarity
var statusMap = map[domain.BackupStatus]dataflow.BackupStatus{
    domain.BackupStatusPending: dataflow.BackupStatus_BACKUP_STATUS_PENDING,
    // ...
}
```

## Interceptors

Located in `interceptor/`:

| Interceptor | Purpose |
|-------------|---------|
| `UserPasserUnaryServerInterceptor` | Extract user from header → context |
| `ErrorHandlerUnaryServerInterceptor` | Convert bizerr → gRPC status |
| `RequestLogUnaryServerInterceptor` | Log all requests (redact passwords) |
| `ClusterCheckerUnaryServerInterceptor` | Validate cluster status |
| `RecoverPanicInterceptor` | Catch panics → internal error |

**Selective interceptor (method filtering):**
```go
var methodsToIntercept = map[string]struct{}{
    dataflow.BackupService_Delete_FullMethodName: {},
}

func MyInterceptor() grpc.UnaryServerInterceptor {
    return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
        if _, ok := methodsToIntercept[info.FullMethod]; !ok {
            return handler(ctx, req)  // Skip
        }
        // Apply interceptor logic
        return handler(ctx, req)
    }
}
```

## Controller Registration

In `container/container.go`:
```go
container.BackupController = console_api.NewBackupController(
    container.TransProvider,
    container.BackupModel,
    container.BackupRepo,
    // ... dependencies
)
```

Register to gRPC server:
```go
dataflow.RegisterBackupServiceServer(rpcServer, container.Global().BackupController)
```

## Key Rules

1. **NO business logic** - Controllers are thin wrappers
2. **Always validate** - Check required params, permissions
3. **Convert types** - gRPC ↔ domain at controller boundaries
4. **Audit logging** - Use defer pattern
5. **Context propagation** - Add trace ID
