Skip to content

Commit

Permalink
Added tls support
Browse files Browse the repository at this point in the history
Added default wait for strategy
Added customization via providers
  • Loading branch information
stillya committed Oct 29, 2023
1 parent e4bda84 commit b3addf8
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 28 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
* Native integration with [Testcontainers](https://www.testcontainers.org/).
* Customization via `realm.json` to create custom realms, users, clients, etc.
* Provides `AdminClient` to interact with Keycloak API.
* Customization via jar's providers.
* TLS support.

## Installation

Expand Down Expand Up @@ -77,7 +79,6 @@ func shutDown() {

func RunContainer(ctx context.Context) (*keycloak.KeycloakContainer, error) {
return keycloak.RunContainer(ctx,
testcontainers.WithWaitStrategy(wait.ForListeningPort("8080/tcp")),
keycloak.WithContextPath("/auth"),
keycloak.WithRealmImportFile("../testdata/realm-export.json"),
keycloak.WithAdminUsername("admin"),
Expand Down
3 changes: 0 additions & 3 deletions _example/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import (
"context"
"fmt"
keycloak "github.com/stillya/testcontainers-keycloak"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
"os"
"testing"
)
Expand Down Expand Up @@ -72,7 +70,6 @@ func shutDown() {

func RunContainer(ctx context.Context) (*keycloak.KeycloakContainer, error) {
return keycloak.RunContainer(ctx,
testcontainers.WithWaitStrategy(wait.ForListeningPort("8080/tcp")),
keycloak.WithContextPath("/auth"),
keycloak.WithRealmImportFile("../testdata/realm-export.json"),
keycloak.WithAdminUsername("admin"),
Expand Down
6 changes: 5 additions & 1 deletion admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package keycloak

import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"net/http"
Expand Down Expand Up @@ -88,7 +89,10 @@ func NewAdminClient(ctx *context.Context, serverURL, username, password string)
}

if (*ctx).Value(http.Client{}) == nil {
adminClient.client = http.DefaultClient
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
adminClient.client = &http.Client{Transport: tr}
} else {
adminClient.client = (*ctx).Value(http.Client{}).(*http.Client)
}
Expand Down
90 changes: 84 additions & 6 deletions keycloak.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,25 @@ import (
"context"
"fmt"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
"path/filepath"
)

const (
defaultKeycloakImage = "quay.io/keycloak/keycloak:20.0"
defaultRealmImport = "/opt/keycloak/data/import/"
defaultProviders = "/opt/keycloak/providers/"
tlsFilePath = "/opt/keycloak/conf"
defaultKeycloakAdminUsername = "admin"
defaultKeycloakAdminPassword = "admin"
defaultKeycloakContextPath = "/"
keycloakAdminUsernameEnv = "KEYCLOAK_ADMIN"
keycloakAdminPasswordEnv = "KEYCLOAK_ADMIN_PASSWORD"
keycloakContextPathEnv = "KEYCLOAK_CONTEXT_PATH"
keycloakTlsEnv = "KEYCLOAK_TLS"
keycloakStartupCommand = "start-dev"
keycloakPort = "8080/tcp"
keycloakHttpsPort = "8443/tcp"
)

// KeycloakContainer is a wrapper around testcontainers.Container
Expand All @@ -26,6 +32,7 @@ type KeycloakContainer struct {

username string
password string
enableTLS bool
contextPath string
}

Expand All @@ -44,12 +51,19 @@ func (k *KeycloakContainer) GetAuthServerURL(ctx context.Context) (string, error
if err != nil {
return "", err
}
port, err := k.MappedPort(ctx, "8080")
if err != nil {
return "", err
if k.enableTLS {
port, err := k.MappedPort(ctx, keycloakHttpsPort)
if err != nil {
return "", err
}
return fmt.Sprintf("https://%s:%s%s", host, port.Port(), k.contextPath), nil
} else {
port, err := k.MappedPort(ctx, keycloakPort)
if err != nil {
return "", err
}
return fmt.Sprintf("http://%s:%s%s", host, port.Port(), k.contextPath), nil
}

return fmt.Sprintf("http://%s:%s%s", host, port.Port(), k.contextPath), nil
}

// RunContainer starts a new KeycloakContainer with the given options.
Expand All @@ -60,7 +74,7 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize
keycloakAdminUsernameEnv: defaultKeycloakAdminUsername,
keycloakAdminPasswordEnv: defaultKeycloakAdminPassword,
},
ExposedPorts: []string{"8080/tcp"},
ExposedPorts: []string{keycloakPort},
}

genericContainerReq := testcontainers.GenericContainerRequest{
Expand All @@ -72,6 +86,21 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize
opt.Customize(&genericContainerReq)
}

if genericContainerReq.WaitingFor == nil {
contextPath := genericContainerReq.Env[keycloakContextPathEnv]
if contextPath == "" {
contextPath = defaultKeycloakContextPath
}
if genericContainerReq.Env[keycloakTlsEnv] != "" {
genericContainerReq.WaitingFor = wait.ForHTTP(contextPath).
WithPort(keycloakHttpsPort).
WithTLS(true).
WithAllowInsecure(true)
} else {
genericContainerReq.WaitingFor = wait.ForHTTP(contextPath)
}
}

container, err := testcontainers.GenericContainer(ctx, genericContainerReq)
if err != nil {
return nil, err
Expand All @@ -82,6 +111,7 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize
username: genericContainerReq.Env[keycloakAdminUsernameEnv],
password: genericContainerReq.Env[keycloakAdminPasswordEnv],
contextPath: genericContainerReq.Env[keycloakContextPathEnv],
enableTLS: genericContainerReq.Env[keycloakTlsEnv] != "",
}, nil
}

Expand All @@ -106,6 +136,54 @@ func WithRealmImportFile(realmImportFile string) testcontainers.CustomizeRequest
}
}

// WithProviders is option to set the providers for KeycloakContainer.
// Providers should be packaged ina Java Archive (JAR) file.
// See https://www.keycloak.org/server/configuration-provider
func WithProviders(providerFiles ...string) testcontainers.CustomizeRequestOption {
return func(req *testcontainers.GenericContainerRequest) {
for _, providerFile := range providerFiles {
absPath, err := filepath.Abs(filepath.Dir(providerFile))
if err != nil {
return
}
// We have to mount because go-testcontainers does not support copying files to the container when target directory does not exist yet.
// See this issue: https://github.com/testcontainers/testcontainers-go/issues/1336
importFile := testcontainers.ContainerMount{
Source: testcontainers.GenericBindMountSource{
HostPath: absPath,
},
Target: defaultProviders,
}
req.Mounts = append(req.Mounts, importFile)
}
}
}

// WithTLS is option to enable TLS for KeycloakContainer.
func WithTLS(certFile, keyFile string) testcontainers.CustomizeRequestOption {
return func(req *testcontainers.GenericContainerRequest) {
req.ExposedPorts = []string{keycloakHttpsPort}
cf := testcontainers.ContainerFile{
HostFilePath: certFile,
ContainerFilePath: tlsFilePath + "/tls.crt",
FileMode: 0o755,
}
kf := testcontainers.ContainerFile{
HostFilePath: keyFile,
ContainerFilePath: tlsFilePath + "/tls.key",
FileMode: 0o755,
}

req.Files = append(req.Files, cf, kf)

req.Env[keycloakTlsEnv] = "true"
processKeycloakArgs(req,
[]string{"--https-certificate-file=" + tlsFilePath + "/tls.crt",
"--https-certificate-key-file=" + tlsFilePath + "/tls.key"},
)
}
}

// WithAdminUsername is option to set the admin username for KeycloakContainer.
func WithAdminUsername(username string) testcontainers.CustomizeRequestOption {
return func(req *testcontainers.GenericContainerRequest) {
Expand Down
71 changes: 54 additions & 17 deletions keycloak_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package keycloak

import (
"context"
"crypto/tls"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
"net/http"
"testing"
)
Expand All @@ -21,19 +21,25 @@ func TestKeycloak(t *testing.T) {
tests := []struct {
name string
image string
useTLS bool
option testcontainers.CustomizeRequestOption
}{
{
name: "KeycloakV20CustomOption",
image: "quay.io/keycloak/keycloak:20.0",
option: WithCustomOption(),
},
{
name: "KeycloakV20WithTLS",
image: "quay.io/keycloak/keycloak:20.0",
option: WithTLS("testdata/tls.crt", "testdata/tls.key"),
useTLS: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
container, err := RunContainer(ctx,
testcontainers.WithWaitStrategy(wait.ForListeningPort("8080/tcp")),
tt.option,
WithContextPath("/auth"),
WithRealmImportFile("testdata/realm-export.json"),
Expand All @@ -59,7 +65,11 @@ func TestKeycloak(t *testing.T) {
return
}

oidConfResp, err := http.Get(authServerURL + "/realms/" + realm + "/.well-known/openid-configuration")
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := &http.Client{Transport: tr}
oidConfResp, err := client.Get(authServerURL + "/realms/" + realm + "/.well-known/openid-configuration")
if err != nil {
t.Errorf("http.Get() error = %v", err)
return
Expand All @@ -77,19 +87,29 @@ func TestKeycloakContainer_GetAdminClient(t *testing.T) {
ctx := context.Background()

tests := []struct {
name string
image string
name string
image string
useTLS bool
option testcontainers.CustomizeRequestOption
}{
{
name: "KeycloakV20",
image: "quay.io/keycloak/keycloak:20.0",
name: "KeycloakV20",
image: "quay.io/keycloak/keycloak:20.0",
useTLS: false,
option: WithCustomOption(),
},
{
name: "KeycloakV20WithTLS",
image: "quay.io/keycloak/keycloak:20.0",
option: WithTLS("testdata/tls.crt", "testdata/tls.key"),
useTLS: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
container, err := RunContainer(ctx,
testcontainers.WithWaitStrategy(wait.ForListeningPort("8080/tcp")),
tt.option,
WithContextPath("/auth"),
WithRealmImportFile("testdata/realm-export.json"),
WithAdminUsername(username),
Expand Down Expand Up @@ -132,19 +152,28 @@ func TestKeycloakContainer_GetAuthServerURL(t *testing.T) {
ctx := context.Background()

tests := []struct {
name string
image string
name string
image string
useTLS bool
option testcontainers.CustomizeRequestOption
}{
{
name: "KeycloakV20",
image: "quay.io/keycloak/keycloak:20.0",
name: "KeycloakV20",
image: "quay.io/keycloak/keycloak:20.0",
option: WithCustomOption(),
},
{
name: "KeycloakV20WithTLS",
image: "quay.io/keycloak/keycloak:20.0",
option: WithTLS("testdata/tls.crt", "testdata/tls.key"),
useTLS: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
container, err := RunContainer(ctx,
testcontainers.WithWaitStrategy(wait.ForListeningPort("8080/tcp")),
tt.option,
WithContextPath("/auth"),
WithRealmImportFile("testdata/realm-export.json"),
WithAdminUsername(username),
Expand All @@ -169,11 +198,19 @@ func TestKeycloakContainer_GetAuthServerURL(t *testing.T) {
return
}

port, _ := container.MappedPort(ctx, "8080/tcp")

if authServerURL != "http://localhost:"+port.Port()+"/auth" {
t.Errorf("GetAuthServerURL() error = %v", err)
if tt.useTLS {
port, err := container.MappedPort(ctx, keycloakHttpsPort)
if authServerURL != "https://localhost:"+port.Port()+"/auth" {
t.Errorf("GetAuthServerURL() error = %v", err)
return
}
return
} else {
port, err := container.MappedPort(ctx, keycloakPort)
if authServerURL != "http://localhost:"+port.Port()+"/auth" {
t.Errorf("GetAuthServerURL() error = %v", err)
return
}
}
})
}
Expand Down
18 changes: 18 additions & 0 deletions testdata/tls.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
-----BEGIN CERTIFICATE-----
MIIC8zCCAdugAwIBAgIUeKOoJ16nsuVYSz+JM8ASz2UUkaIwDQYJKoZIhvcNAQEL
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIzMTAyOTE2MDkzNVoXDTMzMTAy
NjE2MDkzNVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEAyGCr2IqvS4U38EXJSpl16My0DlpT8vzCMT0BztDDwbzt
J76tE9lS1W/X59AQy2ofS1LKqi+MYoeRS6EoTSpRkQXcddT9O/xPaLhjc8os9WbR
F+xHdaLSDnwSLtNxcW7dbGxaLqk3DTB+40jBunt0lFhcyRzyTAlPk4qHD5slouz2
dW621wkXqAAqZAfWPTI3pSrYlEdQBpIrLz7PWnAHfRZxflhxDGP+yM1i+e7DBBk8
o0cyENzJ4lr/9sft8EI010JstNn1GvbpkRhyEqdlM6YEr+MEHs+4pzfQa8Zc/Ogh
C4fNNlJb6D+bSniXhIs3xbBu4z70gFmMhSvFA5DiqQIDAQABoz0wOzAaBgNVHREE
EzARgglsb2NhbGhvc3SHBH8AAAEwHQYDVR0OBBYEFOS7UaSsy2QhhSFzPy8N9nT0
bxrlMA0GCSqGSIb3DQEBCwUAA4IBAQClSV/28mtvfOuRVwKSCgWDG4gg9WaW0mii
ZVcBkcirmZ4wMvkGeuQeYjkX4JMjUmMEU4Ok5eidtXX5MwZkFbdNiUDqtWmHMO5u
UZU/4Bfm9s6Yejt9OPJ2DtJW2ZKMI+smi+dYmuANcggf7ZcoallmolyRxA4/JdmS
KxyPKUbn2eVgpo/stIwX+tmZbOnk/bV65v6bezNeFGPZEj+8+9UR7VPwX8XOYEUH
gv+yq9fTPxx8fHOuIPSocpxeblBjQBZ4miwvWBcvTlGMOL1k3Nr2h8k/7v0SLz6Z
trHXdohlq7SMq2twhGF4c09CnhrJ+LdIO0922sM0W3QYbybVv+nG
-----END CERTIFICATE-----
28 changes: 28 additions & 0 deletions testdata/tls.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDIYKvYiq9LhTfw
RclKmXXozLQOWlPy/MIxPQHO0MPBvO0nvq0T2VLVb9fn0BDLah9LUsqqL4xih5FL
oShNKlGRBdx11P07/E9ouGNzyiz1ZtEX7Ed1otIOfBIu03Fxbt1sbFouqTcNMH7j
SMG6e3SUWFzJHPJMCU+TiocPmyWi7PZ1brbXCReoACpkB9Y9MjelKtiUR1AGkisv
Ps9acAd9FnF+WHEMY/7IzWL57sMEGTyjRzIQ3MniWv/2x+3wQjTXQmy02fUa9umR
GHISp2UzpgSv4wQez7inN9Brxlz86CELh802UlvoP5tKeJeEizfFsG7jPvSAWYyF
K8UDkOKpAgMBAAECggEABrGOBAeL+Aic+AKIyRxrvylZaXJUhBUz8nugfBVmuKnU
PUPmGbfkh7s6+eQuL3Fd1sEPApTgimkPbjiVtVAw1didhvUkuXSB/ZRNRDCwhEkZ
OoSa1X/pJvE3lUdcbobe0DClaZIfA+qHDpYfXJijqhHylYhJQsd72EikHt4WqYTU
OKQ1WBO4I3+Yf67A3vMZ6a4LFcngLZMkO75PWgwk2c6HDcv0LkJIvn7wpj9usQNc
XsvadaQasuA+t7kPWoay0it1sXU6cJWDSpf8+A7Ch4rlJRI6A4ArjMuzrs+VWJV8
fgSq2Koqq6kQ3oocxRZFz9abnvhnZHTWUk2HQZ23YQKBgQDM5orzM6UywvWBvP19
kYu3gNUBJByd6HIGThlahLBBQ4zmMR5TwZfcYjjipDG1AoWJvYtdCeDOBkFp4IBT
ltS7G3naj/YqE5CDT4WIv7SwEHxwhHd54d8ZlfJKI6M9GRe2gNv3OsKbUg+0wqEr
rmfgnmFaDCMskK3FCXgB1PSUWQKBgQD6WV70+w4J4RFTDYElZmYdW4EqCw1C45dp
NlQStsMH+iw3kUhEJfAP82CnIqhVyAnvCsal37dGJSHoLd48S1S0AwXF7Tzo1w2L
G4IQwUdbvAFcR1L5RUADqjFxhjpRXPL9pTneid6z4+RbZRWupDsmByHGGsghSnkz
mj1jVkE20QKBgGK9nqxAcRWbOfBBgO7oGqpdnUglfNzjzT8Yl8M1OjLZOKcdeH3o
RyRe+QbPFV0jT/LmsqgtQHZIMXGyTGT7xJw+S2R2B6yTLQr6YWFa8Nn/t9gJHgJQ
RNDxn4b20Y158CF0y4vCd1GeJA95021XaJun90YLn0+0kOjo/Tn0w8BRAoGBAJn7
mnJocOwWqUdCSSst3qU0ATBQ9+kqf5jRN8kC7NbdQ5EyJRb1lsDY25wxrwSEM8f6
AtFH1zyn3kEm5UiEtSa7rTNehlZY4BWt58RSfYepDdUqcZisxYD7j8nZ28jruPHW
TM/aUUaoZ27nr/xpO5BaVqW5F1uSqRXaSuPsy9PhAoGARs6QDtyc0TrxNYbmL0bw
HYntI0Buf3SH/qREP/Wzm/ewsqnc+5Ls/E6ABUhjuk1DXzdhglOIs0mrqSszTMa6
Bq5pLhA8d2SrRD24dDmkqle0laCZtp2GQlsiekIuSLKZlBLSNJ+HbJhYDcHHON6w
YoOL3pRtR8ElgPaBCZhdr9Q=
-----END PRIVATE KEY-----

0 comments on commit b3addf8

Please sign in to comment.