Skip to content

Commit 65486ee

Browse files
committed
refactor(test): centralize Docker image versions
Add test-images.json at repo root as single source of truth for test container images. Both Go and JS test ecosystems now read from this config, eliminating scattered hardcoded versions. - Create test-images.json with redpanda, kafkaConnect, owlShop images - Add backend/pkg/testutil/images.go with env var overrides - Add frontend/tests/shared/test-images.mjs accessor module - Update all integration tests to use centralized config - Standardize on Redpanda v25.3.6 across all tests Environment overrides: TEST_IMAGE_REDPANDA, TEST_IMAGE_KAFKA_CONNECT, TEST_IMAGE_OWL_SHOP
1 parent b785f4c commit 65486ee

11 files changed

Lines changed: 265 additions & 13 deletions

File tree

backend/pkg/api/api_integration_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ func (s *APIIntegrationTestSuite) SetupSuite() {
6262
require := require.New(t)
6363

6464
ctx := context.Background()
65-
container, err := redpanda.Run(ctx, "redpandadata/redpanda:v25.3.1")
65+
container, err := redpanda.Run(ctx, testutil.RedpandaImage())
6666
require.NoError(err)
6767
s.redpandaContainer = container
6868

backend/pkg/api/connect/integration/api_suite_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ func (s *APISuite) SetupSuite() {
7373

7474
// 2. Start Redpanda Docker container
7575
container, err := redpanda.Run(ctx,
76-
"redpandadata/redpanda:v25.2.1",
76+
testutil.RedpandaImage(),
7777
redpanda.WithEnableWasmTransform(),
7878
network.WithNetwork([]string{"redpanda"}, s.network),
7979
redpanda.WithListener("redpanda:29092"),

backend/pkg/api/handle_kafka_connect_integration_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ func (s *APIIntegrationTestSuite) TestHandleCreateConnector() {
4747
require.NoError(err)
4848

4949
redpandaContainer, err := redpanda.Run(ctx,
50-
"redpandadata/redpanda:v25.2.1",
50+
testutil.RedpandaImage(),
5151
network.WithNetwork([]string{"redpanda"}, testNetwork),
5252
redpanda.WithListener("redpanda:29092"),
5353
)

backend/pkg/console/console_integration_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ func (s *ConsoleIntegrationTestSuite) SetupSuite() {
4747
require := require.New(t)
4848

4949
ctx := context.Background()
50-
container, err := redpanda.Run(ctx, "redpandadata/redpanda:v25.2.1")
50+
container, err := redpanda.Run(ctx, testutil.RedpandaImage())
5151
require.NoError(err)
5252
s.redpandaContainer = container
5353

backend/pkg/serde/service_integration_test.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,7 @@ func (s *SerdeIntegrationTestSuite) SetupSuite() {
116116

117117
ctx := t.Context()
118118

119-
// redpandaContainer, err := redpanda.Run(ctx, "redpandadata/redpanda:v23.3.18")
120-
redpandaContainer, err := redpanda.Run(ctx, "redpandadata/redpanda-unstable:v24.2.1-rc5")
119+
redpandaContainer, err := redpanda.Run(ctx, testutil.RedpandaImage())
121120
require.NoError(err)
122121

123122
s.redpandaContainer = redpandaContainer

backend/pkg/testutil/images.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// Copyright 2026 Redpanda Data, Inc.
2+
//
3+
// Use of this software is governed by the Business Source License
4+
// included in the file https://github.com/redpanda-data/redpanda/blob/dev/licenses/bsl.md
5+
//
6+
// As of the Change Date specified in that file, in accordance with
7+
// the Business Source License, use of this software will be governed
8+
// by the Apache License, Version 2.0
9+
10+
package testutil
11+
12+
import (
13+
"encoding/json"
14+
"errors"
15+
"fmt"
16+
"os"
17+
"path/filepath"
18+
"runtime"
19+
"strings"
20+
"sync"
21+
)
22+
23+
// ImageConfig represents a Docker image configuration.
24+
type ImageConfig struct {
25+
Repository string `json:"repository"`
26+
Tag string `json:"tag"`
27+
}
28+
29+
// String returns the full image reference (repository:tag or repository@digest).
30+
func (c ImageConfig) String() string {
31+
if strings.HasPrefix(c.Tag, "sha256:") {
32+
return c.Repository + "@" + c.Tag
33+
}
34+
return c.Repository + ":" + c.Tag
35+
}
36+
37+
type imagesConfig struct {
38+
Images map[string]ImageConfig `json:"images"`
39+
}
40+
41+
var (
42+
configOnce sync.Once
43+
parsedConfig imagesConfig
44+
errConfig error
45+
)
46+
47+
func findTestImagesJSON() (string, error) {
48+
// Get the directory of this source file
49+
_, currentFile, _, ok := runtime.Caller(0)
50+
if !ok {
51+
return "", errors.New("failed to get current file path")
52+
}
53+
54+
// Navigate from backend/pkg/testutil to repo root
55+
dir := filepath.Dir(currentFile)
56+
for i := 0; i < 10; i++ {
57+
candidate := filepath.Join(dir, "test-images.json")
58+
if _, err := os.Stat(candidate); err == nil {
59+
return candidate, nil
60+
}
61+
parent := filepath.Dir(dir)
62+
if parent == dir {
63+
break
64+
}
65+
dir = parent
66+
}
67+
68+
return "", errors.New("test-images.json not found")
69+
}
70+
71+
func loadConfig() (imagesConfig, error) {
72+
configOnce.Do(func() {
73+
path, err := findTestImagesJSON()
74+
if err != nil {
75+
errConfig = err
76+
return
77+
}
78+
79+
//nolint:gosec // G304: path is derived from runtime.Caller, not user input
80+
data, err := os.ReadFile(path)
81+
if err != nil {
82+
errConfig = fmt.Errorf("failed to read test-images.json: %w", err)
83+
return
84+
}
85+
86+
errConfig = json.Unmarshal(data, &parsedConfig)
87+
})
88+
return parsedConfig, errConfig
89+
}
90+
91+
func getImage(name, envVar string) string {
92+
// Check environment variable override first
93+
if override := os.Getenv(envVar); override != "" {
94+
return override
95+
}
96+
97+
cfg, err := loadConfig()
98+
if err != nil {
99+
panic(fmt.Sprintf("failed to load test-images.json: %v", err))
100+
}
101+
102+
img, ok := cfg.Images[name]
103+
if !ok {
104+
panic(fmt.Sprintf("image %q not found in test-images.json", name))
105+
}
106+
107+
return img.String()
108+
}
109+
110+
// RedpandaImage returns the Docker image for Redpanda tests.
111+
// Can be overridden with TEST_IMAGE_REDPANDA environment variable.
112+
func RedpandaImage() string {
113+
return getImage("redpanda", "TEST_IMAGE_REDPANDA")
114+
}
115+
116+
// KafkaConnectImage returns the Docker image for Kafka Connect tests.
117+
// Can be overridden with TEST_IMAGE_KAFKA_CONNECT environment variable.
118+
func KafkaConnectImage() string {
119+
return getImage("kafkaConnect", "TEST_IMAGE_KAFKA_CONNECT")
120+
}
121+
122+
// OwlShopImage returns the Docker image for OwlShop tests.
123+
// Can be overridden with TEST_IMAGE_OWL_SHOP environment variable.
124+
func OwlShopImage() string {
125+
return getImage("owlShop", "TEST_IMAGE_OWL_SHOP")
126+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright 2026 Redpanda Data, Inc.
2+
//
3+
// Use of this software is governed by the Business Source License
4+
// included in the file https://github.com/redpanda-data/redpanda/blob/dev/licenses/bsl.md
5+
//
6+
// As of the Change Date specified in that file, in accordance with
7+
// the Business Source License, use of this software will be governed
8+
// by the Apache License, Version 2.0
9+
10+
package testutil
11+
12+
import (
13+
"strings"
14+
"testing"
15+
16+
"github.com/stretchr/testify/assert"
17+
"github.com/stretchr/testify/require"
18+
)
19+
20+
func TestRedpandaImage(t *testing.T) {
21+
img := RedpandaImage()
22+
require.NotEmpty(t, img)
23+
assert.True(t, strings.HasPrefix(img, "redpandadata/redpanda:"), "expected redpanda image, got: %s", img)
24+
}
25+
26+
func TestKafkaConnectImage(t *testing.T) {
27+
img := KafkaConnectImage()
28+
require.NotEmpty(t, img)
29+
assert.True(t, strings.Contains(img, "connectors"), "expected connectors image, got: %s", img)
30+
}
31+
32+
func TestOwlShopImage(t *testing.T) {
33+
img := OwlShopImage()
34+
require.NotEmpty(t, img)
35+
assert.True(t, strings.Contains(img, "owl-shop"), "expected owl-shop image, got: %s", img)
36+
}
37+
38+
func TestImageConfigString(t *testing.T) {
39+
tests := []struct {
40+
name string
41+
config ImageConfig
42+
expected string
43+
}{
44+
{
45+
name: "tag format",
46+
config: ImageConfig{Repository: "redpandadata/redpanda", Tag: "v25.3.6"},
47+
expected: "redpandadata/redpanda:v25.3.6",
48+
},
49+
{
50+
name: "digest format",
51+
config: ImageConfig{Repository: "docker.cloudsmith.io/redpanda/connectors", Tag: "sha256:abc123"},
52+
expected: "docker.cloudsmith.io/redpanda/connectors@sha256:abc123",
53+
},
54+
}
55+
56+
for _, tt := range tests {
57+
t.Run(tt.name, func(t *testing.T) {
58+
assert.Equal(t, tt.expected, tt.config.String())
59+
})
60+
}
61+
}
62+
63+
func TestEnvOverride(t *testing.T) {
64+
t.Setenv("TEST_IMAGE_REDPANDA", "custom/redpanda:test")
65+
img := RedpandaImage()
66+
assert.Equal(t, "custom/redpanda:test", img)
67+
}

backend/pkg/testutil/kafkaconnect.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,7 @@ func RunRedpandaConnectorsContainer(ctx context.Context, bootstrapServers []stri
3838

3939
request := testcontainers.GenericContainerRequest{
4040
ContainerRequest: testcontainers.ContainerRequest{
41-
// Pin to digest for reproducibility - :latest tag can have variable behavior
42-
Image: "docker.cloudsmith.io/redpanda/connectors-unsupported/connectors@sha256:0ff21e793ef3042f2f48fb3d6549fae0ef687950a76b8017ab9d8b17c33cadb0",
41+
Image: KafkaConnectImage(),
4342
ExposedPorts: []string{"8083/tcp"},
4443
Env: map[string]string{
4544
"CONNECT_CONFIGURATION": testConnectConfig,

frontend/tests/shared/global-setup.mjs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { GenericContainer, Network, Wait } from 'testcontainers';
22

3+
import { KAFKA_CONNECT_IMAGE, OWL_SHOP_IMAGE, REDPANDA_IMAGE } from './test-images.mjs';
34
import { exec } from 'node:child_process';
45
import { existsSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
56
import { dirname, join, resolve } from 'node:path';
@@ -68,7 +69,7 @@ async function setupDockerNetwork(state) {
6869

6970
async function startRedpandaContainer(network, state, ports) {
7071
console.log('Starting Redpanda container...');
71-
const redpanda = await new GenericContainer('redpandadata/redpanda:v25.3.2')
72+
const redpanda = await new GenericContainer(REDPANDA_IMAGE)
7273
.withNetwork(network)
7374
.withNetworkAliases('redpanda')
7475
.withExposedPorts(
@@ -169,7 +170,7 @@ schemaRegistry:
169170
address: "http://redpanda:8081"
170171
`;
171172

172-
const owlshop = await new GenericContainer('quay.io/cloudhut/owl-shop:master')
173+
const owlshop = await new GenericContainer(OWL_SHOP_IMAGE)
173174
.withPlatform('linux/amd64')
174175
.withNetwork(network)
175176
.withNetworkAliases('owlshop')
@@ -240,8 +241,7 @@ topic.creation.enable=false
240241

241242
let connect;
242243
try {
243-
// Pin to digest for reproducibility - :latest tag can have variable behavior
244-
connect = await new GenericContainer('docker.cloudsmith.io/redpanda/connectors-unsupported/connectors@sha256:0ff21e793ef3042f2f48fb3d6549fae0ef687950a76b8017ab9d8b17c33cadb0')
244+
connect = await new GenericContainer(KAFKA_CONNECT_IMAGE)
245245
.withPlatform('linux/amd64')
246246
.withNetwork(network)
247247
.withNetworkAliases('connect')
@@ -588,7 +588,7 @@ async function startBackendServer(network, isEnterprise, imageTag, state, varian
588588

589589
async function startDestinationRedpandaContainer(network, state, ports) {
590590
console.log('Starting destination Redpanda container for shadowlink...');
591-
const destRedpanda = await new GenericContainer('redpandadata/redpanda:v25.3.2')
591+
const destRedpanda = await new GenericContainer(REDPANDA_IMAGE)
592592
.withNetwork(network)
593593
.withNetworkAliases('dest-cluster')
594594
.withExposedPorts(
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* Central Docker image configuration for E2E tests.
3+
* Reads from the shared test-images.json at the repository root.
4+
*/
5+
6+
import { readFileSync } from 'node:fs';
7+
import { dirname, resolve } from 'node:path';
8+
import { fileURLToPath } from 'node:url';
9+
10+
const __filename = fileURLToPath(import.meta.url);
11+
const __dirname = dirname(__filename);
12+
13+
// Path to shared config at repo root: frontend/tests/shared -> repo root
14+
const configPath = resolve(__dirname, '../../../test-images.json');
15+
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
16+
17+
/**
18+
* Format an image reference from repository and tag.
19+
* Uses @ separator for digests (sha256:...), : for regular tags.
20+
*/
21+
function formatImage(imageConfig) {
22+
const { repository, tag } = imageConfig;
23+
if (tag.startsWith('sha256:')) {
24+
return `${repository}@${tag}`;
25+
}
26+
return `${repository}:${tag}`;
27+
}
28+
29+
/**
30+
* Redpanda Docker image.
31+
* Override with TEST_IMAGE_REDPANDA environment variable.
32+
*/
33+
export const REDPANDA_IMAGE = process.env.TEST_IMAGE_REDPANDA || formatImage(config.images.redpanda);
34+
35+
/**
36+
* Kafka Connect Docker image.
37+
* Override with TEST_IMAGE_KAFKA_CONNECT environment variable.
38+
*/
39+
export const KAFKA_CONNECT_IMAGE = process.env.TEST_IMAGE_KAFKA_CONNECT || formatImage(config.images.kafkaConnect);
40+
41+
/**
42+
* OwlShop Docker image.
43+
* Override with TEST_IMAGE_OWL_SHOP environment variable.
44+
*/
45+
export const OWL_SHOP_IMAGE = process.env.TEST_IMAGE_OWL_SHOP || formatImage(config.images.owlShop);

0 commit comments

Comments
 (0)