Skip to content

Commit d254caa

Browse files
authored
feat(redpanda): add support for http proxy (#3258)
* feat(redpanda): add support for HTTP Proxy (aka panda proxy) * chore(redpanda): upgrade default image to 25.2.4 * docs(redpanda): document new HTTP Proxy options
1 parent f54a356 commit d254caa

File tree

5 files changed

+183
-3
lines changed

5 files changed

+183
-3
lines changed

docs/modules/redpanda.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ Since <a href="https://github.com/testcontainers/testcontainers-go/releases/tag/
55
## Introduction
66

77
Redpanda is a streaming data platform for developers. Kafka API compatible. 10x faster. No ZooKeeper. No JVM!
8-
This Testcontainers module provides three APIs:
8+
This Testcontainers module provides the following APIs:
99

1010
- Kafka API
1111
- Schema Registry API
1212
- Redpanda Admin API
13+
- HTTP Proxy API (PandaProxy)
1314

1415
## Adding this module to your project dependencies
1516

@@ -121,6 +122,12 @@ The `WithEnableWasmTransform` enables wasm transform.
121122

122123
The `WithEnableSchemaRegistryHTTPBasicAuth` enables HTTP basic authentication for the Schema Registry.
123124

125+
#### WithHTTPProxyAuthMethod
126+
127+
- Not available until the next release <a href="https://github.com/testcontainers/testcontainers-go"><span class="tc-version">:material-tag: main</span></a>
128+
129+
The `WithHTTPProxyAuthMethod` sets the authentication method for the HTTP Proxy API (PandaProxy). For HTTP Proxy to have BasicAuth, SASL must be enabled. See `WithEnableSASL()`.
130+
124131
#### WithAutoCreateTopics
125132

126133
- Since <a href="https://github.com/testcontainers/testcontainers-go/releases/tag/v0.22.0"><span class="tc-version">:material-tag: v0.22.0</span></a>
@@ -189,3 +196,14 @@ is an HTTP-based API and thus the returned format will be: http://host:port.
189196
<!--codeinclude-->
190197
[Get admin API address](../../modules/redpanda/redpanda_test.go) inside_block:adminAPIAddress
191198
<!--/codeinclude-->
199+
200+
#### HTTPProxyAddress
201+
202+
- Not available until the next release <a href="https://github.com/testcontainers/testcontainers-go"><span class="tc-version">:material-tag: main</span></a>
203+
204+
HTTPProxyAddress returns the address to the HTTP Proxy API (PandaProxy). This
205+
is an HTTP-based API and thus the returned format will be: http://host:port.
206+
207+
<!--codeinclude-->
208+
[Get HTTP Proxy address](../../modules/redpanda/redpanda_test.go) inside_block:httpProxyAddress
209+
<!--/codeinclude-->

modules/redpanda/mounts/redpanda.yaml.tpl

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,4 +72,24 @@ schema_registry_client:
7272
- address: localhost
7373
port: 9093
7474

75+
pandaproxy:
76+
pandaproxy_api:
77+
- address: 0.0.0.0
78+
port: 8082
79+
name: main
80+
authentication_method: {{ .HTTPProxy.AuthenticationMethod }}
81+
82+
{{ if .EnableTLS }}
83+
pandaproxy_api_tls:
84+
- name: main
85+
enabled: true
86+
cert_file: /etc/redpanda/cert.pem
87+
key_file: /etc/redpanda/key.pem
88+
{{ end }}
89+
90+
pandaproxy_client:
91+
brokers:
92+
- address: localhost
93+
port: 9093
94+
7595
auto_create_topics_enabled: {{ .AutoCreateTopics }}

modules/redpanda/options.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ import (
77
"github.com/testcontainers/testcontainers-go"
88
)
99

10+
// HTTPProxyAuthMethod defines the authentication method for HTTP Proxy.
11+
type HTTPProxyAuthMethod string
12+
13+
const (
14+
HTTPProxyAuthMethodNone HTTPProxyAuthMethod = "none"
15+
HTTPProxyAuthMethodHTTPBasic HTTPProxyAuthMethod = "http_basic"
16+
HTTPProxyAuthMethodOIDC HTTPProxyAuthMethod = "oidc"
17+
)
18+
1019
type options struct {
1120
// Superusers is a list of service account names.
1221
Superusers []string
@@ -22,6 +31,10 @@ type options struct {
2231
// or "http_basic" for HTTP basic authentication.
2332
SchemaRegistryAuthenticationMethod string
2433

34+
// HTTPProxyAuthenticationMethod is the authentication method for HTTP Proxy (pandaproxy).
35+
// Valid values are "none", "http_basic", or "oidc".
36+
HTTPProxyAuthenticationMethod HTTPProxyAuthMethod
37+
2538
// EnableWasmTransform is a flag to enable wasm transform.
2639
EnableWasmTransform bool
2740

@@ -58,6 +71,7 @@ func defaultOptions() options {
5871
KafkaEnableAuthorization: false,
5972
KafkaAuthenticationMethod: "none",
6073
SchemaRegistryAuthenticationMethod: "none",
74+
HTTPProxyAuthenticationMethod: HTTPProxyAuthMethodNone,
6175
ServiceAccounts: make(map[string]string, 0),
6276
AutoCreateTopics: false,
6377
EnableTLS: false,
@@ -128,6 +142,22 @@ func WithEnableSchemaRegistryHTTPBasicAuth() Option {
128142
}
129143
}
130144

145+
// WithHTTPProxyAuthMethod sets the authentication method for HTTP Proxy.
146+
// If an invalid method is provided, it defaults to "none".
147+
func WithHTTPProxyAuthMethod(method HTTPProxyAuthMethod) Option {
148+
switch method {
149+
case HTTPProxyAuthMethodNone, HTTPProxyAuthMethodHTTPBasic, HTTPProxyAuthMethodOIDC:
150+
return func(o *options) {
151+
o.HTTPProxyAuthenticationMethod = method
152+
}
153+
default:
154+
return func(o *options) {
155+
// Invalid method, default to "none"
156+
o.HTTPProxyAuthenticationMethod = HTTPProxyAuthMethodNone
157+
}
158+
}
159+
}
160+
131161
// WithAutoCreateTopics enables topic auto creation.
132162
func WithAutoCreateTopics() Option {
133163
return func(o *options) {

modules/redpanda/redpanda.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ const (
3838
defaultKafkaAPIPort = "9092/tcp"
3939
defaultAdminAPIPort = "9644/tcp"
4040
defaultSchemaRegistryPort = "8081/tcp"
41+
defaultHTTPProxyPort = "8082/tcp"
4142

4243
redpandaDir = "/etc/redpanda"
4344
entrypointFile = "/entrypoint-tc.sh"
@@ -58,7 +59,7 @@ type Container struct {
5859
// Deprecated: use Run instead
5960
// RunContainer creates an instance of the Redpanda container type
6061
func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomizer) (*Container, error) {
61-
return Run(ctx, "docker.redpanda.com/redpandadata/redpanda:v23.3.3", opts...)
62+
return Run(ctx, "docker.redpanda.com/redpandadata/redpanda:v25.2.4", opts...)
6263
}
6364

6465
// Run creates an instance of the Redpanda container type
@@ -76,6 +77,7 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom
7677
defaultKafkaAPIPort,
7778
defaultAdminAPIPort,
7879
defaultSchemaRegistryPort,
80+
defaultHTTPProxyPort,
7981
},
8082
Entrypoint: []string{entrypointFile},
8183
Cmd: []string{
@@ -92,6 +94,7 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom
9294
wait.ForMappedPort(defaultKafkaAPIPort),
9395
wait.ForMappedPort(defaultAdminAPIPort),
9496
wait.ForMappedPort(defaultSchemaRegistryPort),
97+
wait.ForMappedPort(defaultHTTPProxyPort),
9598
),
9699
},
97100
Started: true,
@@ -236,6 +239,7 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom
236239
wait.ForListeningPort(defaultKafkaAPIPort),
237240
waitHTTP,
238241
wait.ForListeningPort(defaultSchemaRegistryPort),
242+
wait.ForListeningPort(defaultHTTPProxyPort),
239243
wait.ForLog("Successfully started Redpanda!"),
240244
).WaitUntilReady(ctx, ctr)
241245
if err != nil {
@@ -299,6 +303,12 @@ func (c *Container) SchemaRegistryAddress(ctx context.Context) (string, error) {
299303
return c.PortEndpoint(ctx, nat.Port(defaultSchemaRegistryPort), c.urlScheme)
300304
}
301305

306+
// HTTPProxyAddress returns the address to the HTTP Proxy API (pandaproxy). This
307+
// is an HTTP-based API and thus the returned format will be: http://host:port.
308+
func (c *Container) HTTPProxyAddress(ctx context.Context) (string, error) {
309+
return c.PortEndpoint(ctx, nat.Port(defaultHTTPProxyPort), c.urlScheme)
310+
}
311+
302312
// renderBootstrapConfig renders the config template for the .bootstrap.yaml config,
303313
// which configures Redpanda's cluster properties.
304314
// Reference: https://docs.redpanda.com/docs/reference/cluster-properties/
@@ -362,6 +372,9 @@ func renderNodeConfig(settings options, hostIP string, advertisedKafkaPort int)
362372
SchemaRegistry: redpandaConfigTplParamsSchemaRegistry{
363373
AuthenticationMethod: settings.SchemaRegistryAuthenticationMethod,
364374
},
375+
HTTPProxy: redpandaConfigTplParamsHTTPProxy{
376+
AuthenticationMethod: settings.HTTPProxyAuthenticationMethod,
377+
},
365378
EnableTLS: settings.EnableTLS,
366379
}
367380

@@ -389,6 +402,7 @@ type redpandaBootstrapConfigTplParams struct {
389402
type redpandaConfigTplParams struct {
390403
KafkaAPI redpandaConfigTplParamsKafkaAPI
391404
SchemaRegistry redpandaConfigTplParamsSchemaRegistry
405+
HTTPProxy redpandaConfigTplParamsHTTPProxy
392406
AutoCreateTopics bool
393407
EnableTLS bool
394408
}
@@ -405,6 +419,10 @@ type redpandaConfigTplParamsSchemaRegistry struct {
405419
AuthenticationMethod string
406420
}
407421

422+
type redpandaConfigTplParamsHTTPProxy struct {
423+
AuthenticationMethod HTTPProxyAuthMethod
424+
}
425+
408426
type listener struct {
409427
Address string
410428
Port int

modules/redpanda/redpanda_test.go

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import (
2525
"github.com/testcontainers/testcontainers-go/network"
2626
)
2727

28-
const testImage = "docker.redpanda.com/redpandadata/redpanda:v23.3.3"
28+
const testImage = "docker.redpanda.com/redpandadata/redpanda:v25.2.4"
2929

3030
func TestRedpanda(t *testing.T) {
3131
ctx := context.Background()
@@ -72,6 +72,18 @@ func TestRedpanda(t *testing.T) {
7272
defer resp.Body.Close()
7373
require.Equal(t, http.StatusOK, resp.StatusCode)
7474

75+
// Test HTTP Proxy API
76+
// httpProxyAddress {
77+
httpProxyURL, err := ctr.HTTPProxyAddress(ctx)
78+
// }
79+
require.NoError(t, err)
80+
req, err = http.NewRequestWithContext(ctx, http.MethodGet, httpProxyURL+"/topics", nil)
81+
require.NoError(t, err)
82+
resp, err = httpCl.Do(req)
83+
require.NoError(t, err)
84+
defer resp.Body.Close()
85+
require.Equal(t, http.StatusOK, resp.StatusCode)
86+
7587
// Test produce to unknown topic
7688
results := kafkaCl.ProduceSync(ctx, &kgo.Record{Topic: "test", Value: []byte("test message")})
7789
require.Error(t, results.FirstErr(), kerr.UnknownTopicOrPartition)
@@ -701,6 +713,88 @@ func TestRedpandaBootstrapConfig(t *testing.T) {
701713
}
702714
}
703715

716+
func TestRedpandaHTTPProxy(t *testing.T) {
717+
ctx := context.Background()
718+
719+
ctr, err := redpanda.Run(ctx, testImage)
720+
testcontainers.CleanupContainer(t, ctr)
721+
require.NoError(t, err)
722+
723+
httpCl := &http.Client{Timeout: 10 * time.Second}
724+
725+
// Get HTTP Proxy URL
726+
httpProxyURL, err := ctr.HTTPProxyAddress(ctx)
727+
require.NoError(t, err)
728+
729+
// Test getting list of topics
730+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, httpProxyURL+"/topics", nil)
731+
require.NoError(t, err)
732+
resp, err := httpCl.Do(req)
733+
require.NoError(t, err)
734+
defer resp.Body.Close()
735+
require.Equal(t, http.StatusOK, resp.StatusCode)
736+
737+
var topics []string
738+
err = json.NewDecoder(resp.Body).Decode(&topics)
739+
require.NoError(t, err)
740+
741+
// Test getting brokers list
742+
req, err = http.NewRequestWithContext(ctx, http.MethodGet, httpProxyURL+"/brokers", nil)
743+
require.NoError(t, err)
744+
resp, err = httpCl.Do(req)
745+
require.NoError(t, err)
746+
defer resp.Body.Close()
747+
require.Equal(t, http.StatusOK, resp.StatusCode)
748+
749+
var brokers map[string]any
750+
err = json.NewDecoder(resp.Body).Decode(&brokers)
751+
require.NoError(t, err)
752+
require.Contains(t, brokers, "brokers")
753+
}
754+
755+
func TestHTTPProxyWithBasicAuthentication(t *testing.T) {
756+
ctx := context.Background()
757+
758+
ctr, err := redpanda.Run(ctx, testImage,
759+
redpanda.WithEnableKafkaAuthorization(),
760+
redpanda.WithEnableSASL(),
761+
redpanda.WithHTTPProxyAuthMethod(redpanda.HTTPProxyAuthMethodHTTPBasic),
762+
redpanda.WithNewServiceAccount("proxy-user", "proxy-pass"),
763+
redpanda.WithSuperusers("proxy-user"),
764+
)
765+
testcontainers.CleanupContainer(t, ctr)
766+
require.NoError(t, err)
767+
768+
httpCl := &http.Client{Timeout: 10 * time.Second}
769+
770+
// Get HTTP Proxy URL
771+
httpProxyURL, err := ctr.HTTPProxyAddress(ctx)
772+
require.NoError(t, err)
773+
774+
// Test authentication failure
775+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, httpProxyURL+"/topics", nil)
776+
require.NoError(t, err)
777+
778+
// Test no authentication
779+
resp, err := httpCl.Do(req)
780+
require.NoError(t, err)
781+
defer resp.Body.Close()
782+
require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
783+
784+
// Test successful authentication
785+
req, err = http.NewRequestWithContext(ctx, http.MethodGet, httpProxyURL+"/topics", nil)
786+
require.NoError(t, err)
787+
req.SetBasicAuth("proxy-user", "proxy-pass")
788+
resp, err = httpCl.Do(req)
789+
require.NoError(t, err)
790+
defer resp.Body.Close()
791+
require.Equal(t, http.StatusOK, resp.StatusCode)
792+
793+
var topics []string
794+
err = json.NewDecoder(resp.Body).Decode(&topics)
795+
require.NoError(t, err)
796+
}
797+
704798
func containerHost(ctx context.Context, opts ...testcontainers.ContainerCustomizer) (string, error) {
705799
// Use a dummy request to get the provider from options.
706800
var req testcontainers.GenericContainerRequest

0 commit comments

Comments
 (0)