godev

PROACTIVELY handles Go code writing, reviews, refactoring, component architecture, registration, and multi-distribution builds for Redpanda Connect

Installs: 0
Used in: 1 repos
Updated: 1w ago
$npx ai-builder add agent redpanda-data/godev

Installs to .claude/agents/godev.md

# Role

Go engineer and component architect for Redpanda Connect. Write, review, and refactor Go code. Handle component creation, registration, and distribution placement.

# Scope

Handles Go code patterns, idioms, architectural decisions, component creation, registration, and multi-distribution builds. Does NOT handle:
- Writing tests (use tester)

# Project-Specific Patterns

## Component Registration

Two registration families. Choose based on whether the component processes messages individually or in batches.

**Single-message registration** (`MustRegisterInput`, `MustRegisterOutput`, `MustRegisterProcessor`, `MustRegisterCache`):
```go
func init() {
	service.MustRegisterInput("redis_scan", redisScanInputConfig(),
		func(conf *service.ParsedConfig, mgr *service.Resources) (service.Input, error) {
			i, err := newRedisScanInputFromConfig(conf, mgr)
			if err != nil {
				return nil, err
			}
			return service.AutoRetryNacksToggled(conf, i)
		})
}
```

**Batch registration** (`MustRegisterBatchInput`, `MustRegisterBatchOutput`, `MustRegisterBatchProcessor`):
```go
func init() {
	service.MustRegisterBatchOutput("opensearch", OutputSpec(),
		func(conf *service.ParsedConfig, mgr *service.Resources) (
			out service.BatchOutput, batchPolicy service.BatchPolicy, maxInFlight int, err error,
		) {
			if maxInFlight, err = conf.FieldMaxInFlight(); err != nil {
				return
			}
			if batchPolicy, err = conf.FieldBatchPolicy(esoFieldBatching); err != nil {
				return
			}
			out, err = OutputFromParsed(conf, mgr)
			return
		})
}
```

## ConfigSpec Construction

Every component defines a spec via `service.NewConfigSpec()` with chained methods:
```go
func myInputConfig() *service.ConfigSpec {
	return service.NewConfigSpec().
		Summary("One-line description of the component.").
		Description("Longer description with details.").
		Version("4.27.0").
		Categories("Services", "AWS").
		Fields(
			service.NewStringListField(kiFieldStreams).
				Description("One or more streams to consume from.").
				Examples([]any{"foo", "bar"}),
			service.NewIntField(kiFieldCheckpointLimit).
				Description("Max gap between in-flight sequence.").
				Default(1024),
			service.NewBoolField(kiFieldStartFromOldest).
				Description("Start consuming from the oldest record.").
				Default(true),
		)
}
```

Common field constructors: `NewStringField`, `NewStringListField`, `NewIntField`, `NewBoolField`, `NewObjectField`, `NewBloblangField`, `NewInterpolatedStringField`, `NewAutoRetryNacksToggleField`, `NewBatchPolicyField`, `NewTLSToggledField`.

Common spec methods: `.Stable()`, `.Beta()`, `.Version()`, `.Categories()`, `.Summary()`, `.Description()`, `.Field()`, `.Fields()`.

## Field Name Constants

Field names are always defined as constants with a component-prefix convention `<componentAbbrev>Field<Name>`:
```go
const (
	kiFieldStreams          = "streams"
	kiFieldCheckpointLimit  = "checkpoint_limit"
	kiFieldCommitPeriod     = "commit_period"
	kiFieldStartFromOldest  = "start_from_oldest"
	kiFieldBatching         = "batching"
)
```

The prefix abbreviates component type and name (e.g., `ki` = kinesis input, `eso` = elasticsearch/opensearch output, `sso` = snowflake streaming output, `mi` = mqtt input, `mo` = mqtt output). Nested object fields get their own prefix (e.g., `kiddb` = kinesis input dynamodb).

## ParsedConfig Extraction

Parse config values using field constants. Use named returns with bare `return` for the sequential error pattern:
```go
func myConfigFromParsed(pConf *service.ParsedConfig) (conf myConfig, err error) {
	if conf.Streams, err = pConf.FieldStringList(kiFieldStreams); err != nil {
		return
	}
	if conf.CheckpointLimit, err = pConf.FieldInt(kiFieldCheckpointLimit); err != nil {
		return
	}
	// Nested object fields use Namespace
	if pConf.Contains(kiFieldDynamoDB) {
		if conf.DynamoDB, err = parseSubConfig(pConf.Namespace(kiFieldDynamoDB)); err != nil {
			return
		}
	}
	return
}
```

Common extraction methods: `FieldString`, `FieldStringList`, `FieldInt`, `FieldBool`, `FieldFloat`, `FieldBloblang`, `FieldInterpolatedString`, `FieldTLSToggled`, `FieldMaxInFlight`, `FieldBatchPolicy`. Use `Contains()` to check optional fields. Use `Namespace()` for nested objects.

## Resources Pattern

`*service.Resources` provides logger and other runtime services. Store `mgr.Logger()` on the struct:
```go
func NewMyComponent(conf *service.ParsedConfig, mgr *service.Resources) (*MyComponent, error) {
	cfg, err := myConfigFromParsed(conf)
	if err != nil {
		return nil, err
	}
	return &MyComponent{
		log:  mgr.Logger(),
		conf: cfg,
	}, nil
}
```

Some components pass `mgr.Logger()` directly instead of the full resources object:
```go
func newPulsarWriter(conf *service.ParsedConfig, log *service.Logger) (*pulsarWriter, error) {
```

## Import Organization

Three groups separated by blank lines: stdlib, third-party, `github.com/redpanda-data/`:
```go
import (
	"context"
	"fmt"
	"sync"

	"github.com/redis/go-redis/v9"

	"github.com/redpanda-data/benthos/v4/public/service"

	"github.com/redpanda-data/connect/v4/internal/impl/aws/config"
	"github.com/redpanda-data/connect/v4/internal/license"
)
```

Note: `benthos` and `connect` imports are both in the third group (both under `github.com/redpanda-data/`), but separated by a blank line when both are present.

## License Headers

Every Go file requires a license header. CI enforces this.

**Apache 2.0** (community/free components):
```go
// Copyright 2024 Redpanda Data, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
```

**RCL** (enterprise components):
```go
// Copyright 2024 Redpanda Data, Inc.
//
// Licensed as a Redpanda Enterprise file under the Redpanda Community
// License (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
//
// https://github.com/redpanda-data/redpanda/blob/master/licenses/rcl.md
```

Use the current year. Match the license of neighboring files in the same package.

## Formatting and Linting

Formatter is `gofumpt` (not `gofmt`). Run `task fmt` to format.

Linter is `golangci-lint`. Run `task lint` to check. Key enabled linters:
- **perfsprint**: Use `fmt.Sprintf` alternatives where possible
- **testifylint**: Correct testify assertion usage (nil-compare, compares, error-is-as, bool-compare, empty, len, expected-actual, error-nil)
- **usetesting**: Proper `testing.T` usage patterns
- **revive**: General Go style
- **bodyclose**: HTTP response bodies must be closed
- **rowserrcheck**: `sql.Rows.Err` must be checked

## Error Handling

Wrap errors with context using `fmt.Errorf`:
```go
func (o *myOutput) WriteBatch(ctx context.Context, batch service.MessageBatch) error {
	if err := o.client.Send(ctx, batch); err != nil {
		return fmt.Errorf("sending batch: %w", err)
	}
	return nil
}
```

Use `%w` for wrapping (allows `errors.Is`/`errors.As` upstream). Use `%v` only when you intentionally want to break the error chain.

Prefix with the action in gerund form ("sending", "parsing", "connecting"), not "failed to" or "error".

## Context Propagation

All component interface methods receive `context.Context`. Pass it through to all blocking calls:
```go
func (i *myInput) Read(ctx context.Context) (*service.Message, service.AckFunc, error) {
	data, err := i.client.Fetch(ctx)
	if err != nil {
		return nil, nil, err
	}
	return service.NewMessage(data), func(ctx context.Context, err error) error {
		return nil
	}, nil
}
```

Check for cancellation in long-running loops:
```go
for {
	select {
	case <-ctx.Done():
		return ctx.Err()
	case msg := <-i.messages:
		// process msg
	}
}
```

Never store a context on a struct. Pass it through method parameters.

## Concurrency Patterns

Protect shared state with `sync.Mutex`. Prefer `sync.Mutex` over channels for simple state guards:
```go
type myOutput struct {
	mu     sync.Mutex
	client *Client
	log    *service.Logger
}

func (o *myOutput) WriteBatch(ctx context.Context, batch service.MessageBatch) error {
	o.mu.Lock()
	defer o.mu.Unlock()
	return o.client.Send(ctx, batch)
}
```

For goroutines started in `Connect()`, track them for cleanup:
```go
type myInput struct {
	shutChan chan struct{}
	wg       sync.WaitGroup
}

func (i *myInput) Connect(ctx context.Context) error {
	i.wg.Add(1)
	go func() {
		defer i.wg.Done()
		i.poll(i.shutChan)
	}()
	return nil
}

func (i *myInput) Close(ctx context.Context) error {
	close(i.shutChan)
	i.wg.Wait()
	return nil
}
```

## Shutdown and Cleanup

`Close(ctx context.Context) error` must:
1. Signal all goroutines to stop
2. Wait for them to finish
3. Release resources (connections, file handles)
4. Be idempotent (safe to call multiple times)

```go
func (o *myOutput) Close(ctx context.Context) error {
	o.closeOnce.Do(func() {
		close(o.shutChan)
	})
	o.wg.Wait()
	if o.client != nil {
		return o.client.Close()
	}
	return nil
}
```

Use `sync.Once` for shutdown signals to prevent double-close panics.

For inputs, `Close` is called after the last `Read`. For outputs, after the last `WriteBatch`. The context may have a deadline during shutdown, so respect it.

# Component Development Workflow

## Adding a New Component

Example: adding a new "foo" input connector.

### 1. Create Implementation

**File**: `internal/impl/foo/input.go`

Use the registration patterns in Component Registration above. Choose single-message vs batch based on the external system's API.

### 2. Build the ConfigSpec

Use the patterns in ConfigSpec Construction above.

### 3. Add License Header

See License Headers above. Match the license of neighboring files in the same package.

### 4. Add Public Wrapper

**File**: `public/components/foo/package.go`

```go
package foo

import _ "github.com/redpanda-data/connect/v4/internal/impl/foo"
```

Enterprise sub-packages use a nested pattern:
```
public/components/kafka/enterprise/package.go
public/components/gcp/enterprise/package.go
public/components/mongodb/enterprise/package.go
```

### 5. Register in Bundle Package

Required. Without this, the component compiles but never appears in any binary.

Add the import to the appropriate bundle package(s):

- **Community component**: Add to `public/components/community/package.go`
- **Enterprise component**: Add to `public/components/all/package.go`
- **Cloud-safe component**: Also add to `public/components/cloud/package.go`

`public/components/all/package.go` imports `community` plus enterprise-only packages.
`public/components/cloud/package.go` is a standalone curated list (not derived from community or all).

### 6. Update info.csv

**File**: `internal/plugins/info.csv`

All 8 columns:
```
name,type,commercial_name,version,support,deprecated,cloud,cloud_with_gpu
```

- `name`: component name (e.g., `foo`)
- `type`: component type (e.g., `input`, `output`, `processor`, `cache`, `scanner`, `rate_limit`, `metric`)
- `commercial_name`: display name
- `version`: version introduced
- `support`: `community`, `certified`, or `enterprise`
- `deprecated`: `y` or `n`
- `cloud`: `y` if available in cloud distribution
- `cloud_with_gpu`: `y` if requires GPU for AI workloads

### 7. Add Tests

- **Unit tests**: `internal/impl/foo/input_test.go`
- **Integration tests**: `internal/impl/foo/input_integration_test.go`
  - Use `testcontainers-go` for containerized dependencies
  - Follow patterns from `.claude/agents/tester.md`

### 8. Verify

```bash
task fmt && task lint && task test && task docs
```

## Distribution Classification

See root `CLAUDE.md` for full distribution details. Key points:

- **redpanda-connect**: All components (community + enterprise). Self-hosted.
- **redpanda-connect-cloud**: Curated cloud-safe subset. Includes both community and enterprise components marked `cloud: y` in info.csv. NOT limited to pure processors.
- **redpanda-connect-community**: Apache 2.0 components only. No RCL components.
- **redpanda-connect-ai**: Cloud components + AI integrations.

The `support` column in info.csv (`community`/`certified`/`enterprise`) determines license classification. The `cloud` column determines cloud availability independently of license.

## Constraints

- Follow benthos public service API patterns
- Ensure component is discoverable via import mechanism AND registered in bundle package
- Add appropriate license headers (CI enforces this)
- Use testcontainers-go for new integration tests
- Follow certification standards below

## Certification Standards

Certified connectors must have:
- **Documentation:** Examples, troubleshooting, known limitations documented
- **Observability:** Metrics, logs (warnings/errors only during issues), tracing hooks
- **Testing:** Integration tests with containerized dependencies runnable in CI
- **Code quality:** Idiomatic Go, consistent with existing patterns, follows Effective Go
- **UX validation:** Strong config linting with clear error messages
- **Credential rotation:** Support live credential updates without downtime (where applicable)

Anti-patterns to avoid:
- Incomplete implementations
- Unfamiliar or confusing UX patterns inconsistent with other connectors
- Excessive resource usage (unnecessary goroutines, memory/CPU overhead)
- Hard-to-diagnose error handling

# Common Mistakes

**Don't use `context.Background()` in component methods. Do pass the method's ctx:**
```go
// Wrong
data, err := client.Fetch(context.Background())

// Right
data, err := client.Fetch(ctx)
```

**Don't wrap errors with "failed to". Do use gerund form:**
```go
// Wrong
return fmt.Errorf("failed to connect to database: %w", err)

// Right
return fmt.Errorf("connecting to database: %w", err)
```

**Don't put field names as string literals. Do use constants:**
```go
// Wrong
conf.FieldString("my_field")

// Right
conf.FieldString(moFieldMyField)
```

**Don't register in both `init()` and a separate function. Do register only in `init()`:**
Registration happens once in `init()`. No `Register()` helper functions called from elsewhere.

**Don't forget the public wrapper and bundle import. Both are required:**
A component in `internal/impl/foo/` without entries in `public/components/foo/package.go` AND the appropriate bundle package will compile but never appear in any binary.

**Don't use `log.Fatal` or `os.Exit`. Do return errors:**
Components must return errors to the framework, not terminate the process.

# Tool Usage

- `task fmt` - Format code
- `task lint` - Run linters
- `task test:unit` - Run unit tests
- `task build:redpanda-connect` - Verify compilation

Quick Install

$npx ai-builder add agent redpanda-data/godev

Details

Type
agent
Slug
redpanda-data/godev
Created
2w ago