Skip to content
Merged
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
12 changes: 10 additions & 2 deletions app/controlplane/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,9 +221,17 @@ func newNatsConfig(c *conf.Bootstrap_NatsServer) *natsconn.Config {
return nil
}

replicas := int(c.GetReplicas())
if replicas < 1 {
replicas = 1
} else if replicas > 3 {
replicas = 3
}

cfg := &natsconn.Config{
URI: uri,
Name: "chainloop-controlplane",
URI: uri,
Name: "chainloop-controlplane",
Replicas: replicas,
}

if c.GetToken() != "" {
Expand Down
23 changes: 17 additions & 6 deletions app/controlplane/internal/conf/controlplane/config/v1/conf.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright 2024-2025 The Chainloop Authors.
// Copyright 2024-2026 The Chainloop Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -101,6 +101,9 @@ message Bootstrap {
// Token based authentication
string token = 2 [(buf.validate.field).string.min_len = 1];
}
// Number of replicas for JetStream KV buckets.
// Defaults to 1. Maximum is 3. Set to 3 for production clusters.
int32 replicas = 3;
}

// External URL of the platform UI, if available
Expand Down
2 changes: 1 addition & 1 deletion deployment/chainloop/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ description: Chainloop is an open source software supply chain control plane, a

type: application
# Bump the patch (not minor, not major) version on each change in the Chart Source code
version: 1.364.0
version: 1.364.1
# Do not update appVersion, this is handled automatically by the release process
appVersion: v1.90.0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,14 @@ stringData:
{{- end }}

{{- if and .Values.controlplane.nats.enabled }}
nats_server:
nats_server:
uri: {{ include "controlplane.nats.connection_string" . | quote }}
{{- if ne .Values.controlplane.nats.token "" }}
token: {{ .Values.controlplane.nats.token | quote }}
{{- end }}
{{- if .Values.controlplane.nats.replicas }}
replicas: {{ .Values.controlplane.nats.replicas }}
{{- end }}
{{- end }}

credentials_service: {{- include "chainloop.credentials_service_settings" . | indent 6 }}
Expand Down
2 changes: 2 additions & 0 deletions deployment/chainloop/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -188,11 +188,13 @@ controlplane:
## @param controlplane.nats.host NATS Host
## @param controlplane.nats.port NATS Port
## @param controlplane.nats.token NATS Client authentication token
## @param controlplane.nats.replicas Number of JetStream KV replicas (1-3). Set to 3 for clustered NATS deployments.
nats:
enabled: false
host: ""
port: 4222
token: ""
replicas: 1

## @extra controlplane.onboarding.name Name of the organization to onboard
## @extra controlplane.onboarding.role Role of the organization to onboard
Expand Down
1 change: 1 addition & 0 deletions pkg/cache/attestationbundle/attestationbundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ func New(ctx context.Context, rc *natsconn.ReloadableConnection, logger log.Logg
if rc != nil {
opts = append(opts, cache.WithNATS(rc.Conn, bucket))
opts = append(opts, cache.WithReconnect(rc.Subscribe(ctx)))
opts = append(opts, cache.WithReplicas(rc.Replicas))
}

c, err := cache.New[[]byte](opts...)
Expand Down
8 changes: 8 additions & 0 deletions pkg/cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ type Logger interface {
type config struct {
ttl time.Duration
maxBytes int64
replicas int
logger Logger
natsConn *nats.Conn
bucketName string
Expand Down Expand Up @@ -82,6 +83,13 @@ func WithDescription(desc string) Option {
return func(c *config) { c.description = desc }
}

// WithReplicas sets the number of JetStream KV replicas for the NATS bucket.
// Defaults to 1 if not set. Set to match the cluster size (e.g. 3) for
// production NATS clusters. Ignored for in-memory backend.
func WithReplicas(n int) Option {
return func(c *config) { c.replicas = n }
}

// WithReconnect provides a channel that signals NATS reconnection events.
func WithReconnect(ch <-chan struct{}) Option {
return func(c *config) { c.reconnCh = ch }
Expand Down
1 change: 1 addition & 0 deletions pkg/cache/natskv.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ func (c *natsKVCache[T]) initBucket() error {
Description: c.cfg.description,
TTL: c.cfg.ttl,
MaxBytes: c.cfg.maxBytes,
Replicas: c.cfg.replicas,
Storage: jetstream.MemoryStorage,
})
if err != nil {
Expand Down
48 changes: 48 additions & 0 deletions pkg/cache/natskv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (

natsserver "github.com/nats-io/nats-server/v2/server"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -250,6 +251,53 @@ func TestNATSKV_MaxBytesEvictsOldEntries(t *testing.T) {
assert.False(t, ok, "oldest entry should have been evicted")
}

func TestNATSKV_WithReplicas(t *testing.T) {
nc := startEmbeddedNATS(t)

tests := []struct {
name string
replicas int
wantRep int
}{
{"no WithReplicas defaults to 1", 0, 1},
{"explicit 1 replica", 1, 1},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
bucket := sanitizeBucketName("test-replicas-" + tt.name)
opts := []Option{
WithTTL(5 * time.Second),
WithNATS(nc, bucket),
}
if tt.replicas > 0 {
opts = append(opts, WithReplicas(tt.replicas))
}
c, err := New[string](opts...)
require.NoError(t, err)

// Verify replica count via the backing stream config
nkv := c.(*natsKVCache[string])
js, err := jetstream.New(nc)
require.NoError(t, err)
stream, err := js.Stream(context.Background(), "KV_"+nkv.bucket)
require.NoError(t, err)
assert.Equal(t, tt.wantRep, stream.CachedInfo().Config.Replicas)
})
}

// Replicas > 1 requires a multi-node NATS cluster. Verify that the option
// is actually passed through by confirming that a single-node server rejects it.
t.Run("replicas 3 rejected by single-node server", func(t *testing.T) {
_, err := New[string](
WithTTL(5*time.Second),
WithNATS(nc, "test-replicas-3"),
WithReplicas(3),
)
require.Error(t, err, "single-node NATS should reject replicas > 1")
})
}

func TestNew_WithNATSReturnsNATSBackend(t *testing.T) {
nc := startEmbeddedNATS(t)
c, err := New[string](
Expand Down
1 change: 1 addition & 0 deletions pkg/cache/policyevalbundle/policyevalbundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ func New(ctx context.Context, rc *natsconn.ReloadableConnection, logger log.Logg
if rc != nil {
opts = append(opts, cache.WithNATS(rc.Conn, bucket))
opts = append(opts, cache.WithReconnect(rc.Subscribe(ctx)))
opts = append(opts, cache.WithReplicas(rc.Replicas))
}

c, err := cache.New[[]byte](opts...)
Expand Down
10 changes: 6 additions & 4 deletions pkg/natsconn/natsconn.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,17 @@ import (
// Config holds the connection parameters for NATS.
// Decoupled from protobuf config so this package can be imported externally.
type Config struct {
URI string
Token string
Name string
URI string
Token string
Name string
Replicas int // JetStream KV replica count; defaults to 1
}

// ReloadableConnection wraps a NATS connection and provides reconnection
// notifications via a pub/sub fan-out to subscribers.
type ReloadableConnection struct {
*nats.Conn
Replicas int // JetStream KV replica count
mu sync.RWMutex
subscribers []chan struct{}
logger *log.Helper
Expand All @@ -52,7 +54,7 @@ func New(cfg *Config, logger log.Logger) (*ReloadableConnection, func(), error)
}

l := log.NewHelper(log.With(logger, "component", "natsconn"))
rc := &ReloadableConnection{logger: l}
rc := &ReloadableConnection{logger: l, Replicas: cfg.Replicas}

@cubic-dev-ai cubic-dev-ai Bot Apr 13, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Replicas is documented as defaulting to 1, but New passes through cfg.Replicas without applying that default.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At pkg/natsconn/natsconn.go, line 57:

<comment>`Replicas` is documented as defaulting to 1, but `New` passes through `cfg.Replicas` without applying that default.</comment>

<file context>
@@ -52,7 +54,7 @@ func New(cfg *Config, logger log.Logger) (*ReloadableConnection, func(), error)
 
 	l := log.NewHelper(log.With(logger, "component", "natsconn"))
-	rc := &ReloadableConnection{logger: l}
+	rc := &ReloadableConnection{logger: l, Replicas: cfg.Replicas}
 
 	opts := []nats.Option{
</file context>
Suggested change
rc := &ReloadableConnection{logger: l, Replicas: cfg.Replicas}
replicas := cfg.Replicas
if replicas <= 0 {
replicas = 1
}
rc := &ReloadableConnection{logger: l, Replicas: replicas}
Fix with Cubic


opts := []nats.Option{
nats.MaxReconnects(-1),
Expand Down
Loading