From 36754fe069384906f25a99af41506e5beda48a01 Mon Sep 17 00:00:00 2001 From: sbp-bvanb Date: Wed, 18 Dec 2024 13:01:45 +0100 Subject: [PATCH] feat: [#12] Store Okta mock that has been created by wimspaargaren --- README.md | 27 +- Taskfile.yml | 10 +- cmd/oktamock/main.go | 253 ++++++++++++++++++ go.mod | 36 +++ go.sum | 123 +++++++++ internal/oktamock/models/models.go | 44 +++ internal/oktamock/oktamock.go | 125 +++++++++ internal/oktamock/oktamock_test.go | 39 +++ .../pkg/dockertestutils/dockertestutils.go | 47 ++++ 9 files changed, 701 insertions(+), 3 deletions(-) create mode 100644 cmd/oktamock/main.go create mode 100644 internal/oktamock/models/models.go create mode 100644 internal/oktamock/oktamock.go create mode 100644 internal/oktamock/oktamock_test.go create mode 100644 internal/pkg/dockertestutils/dockertestutils.go diff --git a/README.md b/README.md index 87d8d07..258f681 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,8 @@ curl \ ## MCVS-Stub-Server -A simple HTTP server which can configure endpoints with a given response. This can be used as a stub server to mimick behaviour of other services. +A simple HTTP server which can configure endpoints with a given response. This +can be used as a stub server to mimic behavior of other services. ### Build @@ -64,6 +65,7 @@ docker run -p 8080:8080 stub-server ### Test **Configuring** + ``` curl --location 'localhost:8080/configure' \ --header 'Content-Type: application/json' \ @@ -74,6 +76,29 @@ curl --location 'localhost:8080/configure' \ ``` **Hit a configured endpoint** + ``` curl --location 'localhost:8080/foo' ``` + +## Okta + +Generate a valid Okta JSON Web Token (JWT). + +### Build + +```zsh +docker build -t oktamock --build-arg APPLICATION=oktamock . +``` + +### Run + +```zsh +docker run -p 8080:8080 oktamock +``` + +### Test + +```zsh +curl http://localhost:8080/token +``` diff --git a/Taskfile.yml b/Taskfile.yml index d03297d..54879fd 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -7,5 +7,11 @@ vars: REMOTE_URL_REPO: schubergphilis/mcvs-golang-action includes: - remote: >- - {{.REMOTE_URL}}/{{.REMOTE_URL_REPO}}/{{.REMOTE_URL_REF}}/Taskfile.yml + remote: + taskfile: >- + {{.REMOTE_URL}}/{{.REMOTE_URL_REPO}}/{{.REMOTE_URL_REF}}/Taskfile.yml + vars: + GCI_SECTIONS: >- + -s standard + -s default + -s "Prefix(schubergphilis/mcvs-integrationtest-services)" diff --git a/cmd/oktamock/main.go b/cmd/oktamock/main.go new file mode 100644 index 0000000..7420e5a --- /dev/null +++ b/cmd/oktamock/main.go @@ -0,0 +1,253 @@ +// Package main provides a mocked Okta server which can be used to create and validate JWT tokens. +package main + +import ( + "crypto/rand" + "crypto/rsa" + "encoding/json" + "fmt" + "log" + "net/http" + "time" + + "github.com/caarlos0/env/v9" + "github.com/golang-jwt/jwt/v4" + "github.com/lestrrat-go/jwx/v2/jwk" + + "schubergphilis/mcvs-integrationtest-services/internal/oktamock/models" +) + +// ErrUnsupportedSigningMethod represents an error when an unsupported signing method is provided. +type ErrUnsupportedSigningMethod struct { + ProvidedMethod string +} + +func (e ErrUnsupportedSigningMethod) Error() string { + return fmt.Sprintf("unsupported signing method: %s", e.ProvidedMethod) +} + +// SigningMethod represents the signing method for a JWT. +type SigningMethod struct { + actualMethod *jwt.SigningMethodRSA +} + +// Alg returns the algorithm as string. +func (s SigningMethod) Alg() string { + return s.actualMethod.Alg() +} + +// UnmarshalText marshals the signing method to text. +func (s *SigningMethod) UnmarshalText(text []byte) error { + switch string(text) { + case "RS256": + s.actualMethod = jwt.SigningMethodRS256 + return nil + case "RS384": + s.actualMethod = jwt.SigningMethodRS384 + return nil + case "RS512": + s.actualMethod = jwt.SigningMethodRS512 + return nil + } + return ErrUnsupportedSigningMethod{ + ProvidedMethod: string(text), + } +} + +// Config represents the configuration. +type Config struct { + ServerConfig ServerConfig + JWTConfig JWTConfig +} + +// ServerConfig represents the server configuration. +type ServerConfig struct { + Port int `env:"PORT" envDefault:"8080"` +} + +// JWTConfig represents the JWT configuration. +type JWTConfig struct { + Aud string `env:"AUD" envDefault:"api://default"` + Expiration time.Duration `env:"EXPIRATION" envDefault:"24h"` + Groups []string `env:"GROUPS" envDefault:""` + Issuer string `env:"ISSUER" envDefault:"http://localhost:8080"` + KID string `env:"KID" envDefault:"mock-kid"` + SigningMethod SigningMethod `env:"SIGNING_METHOD" envDefault:"RS256"` +} + +// NewConfig returns the config. +func NewConfig() (*Config, error) { + cfg := Config{} + err := env.Parse(&cfg) + if err != nil { + return nil, err + } + return &cfg, nil +} + +func main() { + cfg, err := NewConfig() + if err != nil { + log.Fatal(err) + } + oktaMockServer, err := NewOktaMockServer(cfg) + if err != nil { + log.Fatal(err) + } + + http.HandleFunc("/.well-known/openid-configuration", oktaMockServer.handleOpenIDConfig) + http.HandleFunc("/v1/keys", oktaMockServer.handleGetJWKS) + http.HandleFunc("/token", oktaMockServer.handleGetValidJWT) + + //nolint: gosec + err = http.ListenAndServe(fmt.Sprintf(":%d", cfg.ServerConfig.Port), nil) + if err != nil { + log.Fatal(err) + } +} + +// OktaMockServer represents a mock Okta server which can be used to create and validate JWT tokens. +// Serves as a subtitute for using an actual Okta Server. +type OktaMockServer struct { + audience, issuer string + expiration time.Duration + groups []string + + privKey *rsa.PrivateKey + jwkKey jwk.Key +} + +// CustomClaimsRequest represents the JSON structure for requests that include custom claims for JWT tokens. +type CustomClaimsRequest struct { + CustomClaims map[string]interface{} `json:"custom_claims"` +} + +func (o *OktaMockServer) handleGetValidJWT(w http.ResponseWriter, r *http.Request) { + decoder := json.NewDecoder(r.Body) + var claimsReq CustomClaimsRequest + if err := decoder.Decode(&claimsReq); err != nil { + http.Error(w, "Okta mock expects custom claims to be present in token request", http.StatusBadRequest) + return + } + + now := time.Now() + claims := jwt.MapClaims{ + "aud": o.audience, + "iss": o.issuer, + "iat": now.Unix(), + "exp": now.Add(o.expiration).Unix(), + "nbf": now.AddDate(0, 0, -1).Unix(), + "Groups": o.groups, + } + + // Add custom claims + for key, value := range claimsReq.CustomClaims { + claims[key] = value + } + + // Create a new token with these claims + token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + token.Header["kid"] = o.jwkKey.KeyID() + + // Generate the signed JWT string. + res, err := token.SignedString(o.privKey) + if err != nil { + log.Default().Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + log.Default().Println("Generated JWT:", res) + + // Prepare and send the response. + tokenResponse := models.ValidJWTResponse{ + AccessToken: res, + } + b, err := json.Marshal(tokenResponse) + if err != nil { + log.Default().Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + _, err = w.Write(b) + if err != nil { + log.Default().Println(err) + } +} + +func (o *OktaMockServer) handleGetJWKS(w http.ResponseWriter, _ *http.Request) { + resp := models.JWKSResponse{ + Keys: []jwk.Key{o.jwkKey}, + } + b, err := json.Marshal(resp) + if err != nil { + log.Default().Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + _, err = w.Write(b) + if err != nil { + log.Default().Println(err) + } +} + +func (o *OktaMockServer) handleOpenIDConfig(w http.ResponseWriter, _ *http.Request) { + resp := models.OpenIDConfigurationResponse{ + JwksURI: fmt.Sprintf("%s/v1/keys", o.issuer), + } + b, err := json.Marshal(resp) + if err != nil { + log.Default().Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + _, err = w.Write(b) + if err != nil { + log.Default().Println(err) + } +} + +// NewOktaMockServer returns a new OktaMockServer. +func NewOktaMockServer(cfg *Config) (*OktaMockServer, error) { + privKeyRSA, jwkKey, err := genRSAKeyAndJWK(&cfg.JWTConfig) + if err != nil { + return nil, err + } + + return &OktaMockServer{ + audience: cfg.JWTConfig.Aud, + expiration: cfg.JWTConfig.Expiration, + groups: cfg.JWTConfig.Groups, + issuer: cfg.JWTConfig.Issuer, + jwkKey: jwkKey, + privKey: privKeyRSA, + }, nil +} + +func genRSAKeyAndJWK(cfg *JWTConfig) (*rsa.PrivateKey, jwk.Key, error) { + bitSize := 4096 + + privateKey, err := rsa.GenerateKey(rand.Reader, bitSize) + if err != nil { + return nil, nil, err + } + + err = privateKey.Validate() + if err != nil { + return nil, nil, err + } + + jwkKey, err := jwk.PublicKeyOf(privateKey.PublicKey) + if err != nil { + return nil, nil, err + } + + err = jwkKey.Set(jwk.KeyIDKey, cfg.KID) + if err != nil { + return nil, nil, err + } + err = jwkKey.Set(jwk.AlgorithmKey, cfg.SigningMethod.Alg()) + if err != nil { + return nil, nil, err + } + return privateKey, jwkKey, nil +} diff --git a/go.mod b/go.mod index 9c439c3..8c7c1cd 100644 --- a/go.mod +++ b/go.mod @@ -3,21 +3,57 @@ module schubergphilis/mcvs-integrationtest-services go 1.23.4 require ( + github.com/caarlos0/env/v9 v9.0.0 + github.com/golang-jwt/jwt/v4 v4.5.1 github.com/labstack/echo/v4 v4.13.2 + github.com/lestrrat-go/jwx/v2 v2.1.3 + github.com/ory/dockertest/v3 v3.11.0 + github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.10.0 ) require ( + dario.cat/mergo v1.0.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/containerd/continuity v0.4.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect + github.com/docker/cli v26.1.4+incompatible // indirect + github.com/docker/docker v27.1.1+incompatible // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/goccy/go-json v0.10.3 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/labstack/gommon v0.4.2 // indirect + github.com/lestrrat-go/blackmagic v1.0.2 // indirect + github.com/lestrrat-go/httpcc v1.0.1 // indirect + github.com/lestrrat-go/httprc v1.0.6 // indirect + github.com/lestrrat-go/iter v1.0.2 // indirect + github.com/lestrrat-go/option v1.0.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/opencontainers/runc v1.1.13 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/segmentio/asm v1.2.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect golang.org/x/crypto v0.31.0 // indirect golang.org/x/net v0.32.0 // indirect golang.org/x/sys v0.28.0 // indirect golang.org/x/text v0.21.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 43dcc93..94e3d52 100644 --- a/go.sum +++ b/go.sum @@ -1,33 +1,156 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= +github.com/caarlos0/env/v9 v9.0.0 h1:SI6JNsOA+y5gj9njpgybykATIylrRMklbs5ch6wO6pc= +github.com/caarlos0/env/v9 v9.0.0/go.mod h1:ye5mlCVMYh6tZ+vCgrs/B95sj88cg5Tlnc0XIzgZ020= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8= +github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/docker/cli v26.1.4+incompatible h1:I8PHdc0MtxEADqYJZvhBrW9bo8gawKwwenxRM7/rLu8= +github.com/docker/cli v26.1.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY= +github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= +github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/labstack/echo/v4 v4.13.2 h1:9aAt4hstpH54qIcqkuUXRLTf+v7yOTfMPWzDtuqLmtA= github.com/labstack/echo/v4 v4.13.2/go.mod h1:uc9gDtHB8UWt3FfbYx0HyxcCuvR4YuPYOxF/1QjoV/c= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= +github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k= +github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= +github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= +github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= +github.com/lestrrat-go/jwx/v2 v2.1.3 h1:Ud4lb2QuxRClYAmRleF50KrbKIoM1TddXgBrneT5/Jo= +github.com/lestrrat-go/jwx/v2 v2.1.3/go.mod h1:q6uFgbgZfEmQrfJfrCo90QcQOcXFMfbI/fO0NqRtvZo= +github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= +github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/opencontainers/runc v1.1.13 h1:98S2srgG9vw0zWcDpFMn5TRrh8kLxa/5OFUstuUhmRs= +github.com/opencontainers/runc v1.1.13/go.mod h1:R016aXacfp/gwQBYw2FDGa9m+n6atbLWrYY8hNMT/sA= +github.com/ory/dockertest/v3 v3.11.0 h1:OiHcxKAvSDUwsEVh2BjxQQc/5EHz9n0va9awCtNGuyA= +github.com/ory/dockertest/v3 v3.11.0/go.mod h1:VIPxS1gwT9NpPOrfD3rACs8Y9Z7yhzO4SB194iUDnUI= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= +github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= diff --git a/internal/oktamock/models/models.go b/internal/oktamock/models/models.go new file mode 100644 index 0000000..458eff0 --- /dev/null +++ b/internal/oktamock/models/models.go @@ -0,0 +1,44 @@ +package models + +import "github.com/lestrrat-go/jwx/v2/jwk" + +// OpenIDConfigurationResponse represents the response from the OpenID Configuration endpoint. +type OpenIDConfigurationResponse struct { + Issuer string `json:"issuer"` + AuthorizationEndpoint string `json:"authorization_endpoint"` + TokenEndpoint string `json:"token_endpoint"` + UserinfoEndpoint string `json:"userinfo_endpoint"` + RegistrationEndpoint string `json:"registration_endpoint"` + JwksURI string `json:"jwks_uri"` + ResponseTypesSupported []string `json:"response_types_supported"` + ResponseModesSupported []string `json:"response_modes_supported"` + GrantTypesSupported []string `json:"grant_types_supported"` + SubjectTypesSupported []string `json:"subject_types_supported"` + IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"` + ScopesSupported []string `json:"scopes_supported"` + TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported"` + ClaimsSupported []string `json:"claims_supported"` + CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"` + IntrospectionEndpoint string `json:"introspection_endpoint"` + IntrospectionEndpointAuthMethodsSupported []string `json:"introspection_endpoint_auth_methods_supported"` + RevocationEndpoint string `json:"revocation_endpoint"` + RevocationEndpointAuthMethodsSupported []string `json:"revocation_endpoint_auth_methods_supported"` + EndSessionEndpoint string `json:"end_session_endpoint"` + RequestParameterSupported bool `json:"request_parameter_supported"` + RequestObjectSigningAlgValuesSupported []string `json:"request_object_signing_alg_values_supported"` + DeviceAuthorizationEndpoint string `json:"device_authorization_endpoint"` + PushedAuthorizationRequestEndpoint string `json:"pushed_authorization_request_endpoint"` + BackchannelTokenDeliveryModesSupported []string `json:"backchannel_token_delivery_modes_supported"` + BackchannelAuthenticationRequestSigningAlgValuesSupported []string `json:"backchannel_authentication_request_signing_alg_values_supported"` + DpopSigningAlgValuesSupported []string `json:"dpop_signing_alg_values_supported"` +} + +// JWKSResponse represents the response from the JWKS endpoint. +type JWKSResponse struct { + Keys []jwk.Key `json:"keys"` +} + +// ValidJWTResponse represents the response from the valid JWT endpoint. +type ValidJWTResponse struct { + AccessToken string `json:"access_token"` +} diff --git a/internal/oktamock/oktamock.go b/internal/oktamock/oktamock.go new file mode 100644 index 0000000..80fd2c9 --- /dev/null +++ b/internal/oktamock/oktamock.go @@ -0,0 +1,125 @@ +// Package oktamock provides a docker resource for the okta mock server. +package oktamock + +import ( + "bytes" + "context" + "fmt" + "io" + "log" + "net/http" + + "github.com/ory/dockertest/v3" + "github.com/ory/dockertest/v3/docker" + + "schubergphilis/mcvs-integrationtest-services/internal/pkg/dockertestutils" +) + +// ErrOktaMockServerNotHealthy okta mock server not healthy. +var ErrOktaMockServerNotHealthy = fmt.Errorf("okta mock server not healthy") + +// ErrNotRunning okta mock server container not running yet. +var ErrNotRunning = fmt.Errorf("okta mock server container not running yet") + +// Resource the docker resource for the okta mock server. +type Resource struct { + pool *dockertest.Pool + network *dockertest.Network + resource *dockertest.Resource + + writer io.Writer +} + +// NewResource creates a new okta mock server resource. +func NewResource(pool *dockertest.Pool, network *dockertest.Network) *Resource { + return &Resource{ + pool: pool, + network: network, + } +} + +// WithLogger adds a logger to the resources, to track docker logs. +func (r *Resource) WithLogger(writer io.Writer) *Resource { + r.writer = writer + return r +} + +// Start starts the resource with given run options. +func (r *Resource) Start(opts *dockertest.RunOptions, contextDir string, hcOpts ...func(*docker.HostConfig)) error { + opts.Networks = append(opts.Networks, r.network) + var err error + r.resource, err = r.pool.BuildAndRunWithBuildOptions(&dockertest.BuildOptions{ + Dockerfile: "./okta/Dockerfile", + ContextDir: contextDir, + BuildArgs: []docker.BuildArg{}, + }, opts, hcOpts...) + if err != nil { + return fmt.Errorf("unable to build okta mock server container: %w", err) + } + + err = r.waitUntilContainerIsRunning() + if err != nil { + return err + } + if r.writer != nil { + dockertestutils.AttachLoggerToResource(r.pool, r.writer, r.ContainerID()) + } + + return r.startupCheck(opts) +} + +func (r *Resource) startupCheck(opts *dockertest.RunOptions) error { + return r.pool.Retry(func() error { + oktaMockServerPort := "8080" + if len(opts.ExposedPorts) > 0 { + oktaMockServerPort = opts.ExposedPorts[0] + } + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, fmt.Sprintf("http://localhost:%s/token", r.resource.GetPort(fmt.Sprintf("%s/tcp", oktaMockServerPort))), io.NopCloser(bytes.NewBufferString("{\"custom_claims\": {\"allowed_services\": \"['*']\"}}"))) + if err != nil { + return err + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + log.Default().Println("unable to perform http request okta mock server, try again...", err) + return err + } + defer func() { + err = resp.Body.Close() + if err != nil { + log.Default().Println("unable to close response body", err) + } + }() + if resp.StatusCode != http.StatusOK { + return ErrOktaMockServerNotHealthy + } + return nil + }) +} + +func (r *Resource) waitUntilContainerIsRunning() error { + return r.pool.Retry(func() error { + container, err := r.pool.Client.InspectContainer(r.ContainerID()) + if err != nil { + return err + } + if container.State.Running { + return nil + } + return ErrNotRunning + }) +} + +// GetPort retrieve the mapped docker port. +func (r *Resource) GetPort(port string) string { + return r.resource.GetPort(port) +} + +// Stop stop the resource. +func (r *Resource) Stop() error { + return r.resource.Close() +} + +// ContainerID retrieves the container ID. +func (r *Resource) ContainerID() string { + return r.resource.Container.ID +} diff --git a/internal/oktamock/oktamock_test.go b/internal/oktamock/oktamock_test.go new file mode 100644 index 0000000..de7e04d --- /dev/null +++ b/internal/oktamock/oktamock_test.go @@ -0,0 +1,39 @@ +//go:build integration + +package oktamock + +import ( + "fmt" + "testing" + + "github.com/ory/dockertest/v3" + "github.com/stretchr/testify/assert" + + "schubergphilis/mcvs-integrationtest-services/internal/pkg/dockertestutils" +) + +const ( + oktaMockServerName = "okta-mock-server" +) + +func TestCanRunOkta(t *testing.T) { + pool, err := dockertest.NewPool("") + assert.NoError(t, err) + + network, err := dockertestutils.GetOrCreateNetwork(pool, "integration-test-okta") + assert.NoError(t, err) + + oktaResource := NewResource(pool, network) + defer func() { + assert.NoError(t, oktaResource.Stop()) + }() + err = oktaResource.Start(&dockertest.RunOptions{ + Name: oktaMockServerName, + Tag: "", + Env: []string{ + fmt.Sprintf("ISSUER=http://%s:8080", oktaMockServerName), + }, + ExposedPorts: []string{"8080"}, + }, "..") + assert.NoError(t, err) +} diff --git a/internal/pkg/dockertestutils/dockertestutils.go b/internal/pkg/dockertestutils/dockertestutils.go new file mode 100644 index 0000000..0048267 --- /dev/null +++ b/internal/pkg/dockertestutils/dockertestutils.go @@ -0,0 +1,47 @@ +package dockertestutils + +import ( + "context" + "io" + + "github.com/ory/dockertest/v3" + "github.com/ory/dockertest/v3/docker" + log "github.com/sirupsen/logrus" +) + +// GetOrCreateNetwork checks if a network for a given name exists in the pool, if so +// it returns the network. Otherwise it returns a new network with the given name. +func GetOrCreateNetwork(pool *dockertest.Pool, name string) (*dockertest.Network, error) { + networks, err := pool.NetworksByName(name) + if err != nil { + return nil, err + } + if len(networks) == 0 { + return pool.CreateNetwork(name) + } + return &networks[0], nil +} + +// AttachLoggerToResource attaches an io Writer to a container for a given pool. +func AttachLoggerToResource(pool *dockertest.Pool, outputStream io.Writer, containerID string) { + go func() { + ctx := context.Background() + opts := docker.LogsOptions{ + Context: ctx, + + Stderr: true, + Stdout: true, + Follow: true, + Timestamps: true, + RawTerminal: true, + + Container: containerID, + + OutputStream: outputStream, + } + err := pool.Client.Logs(opts) + if err != nil { + log.Errorf("unable to attach logger to resource: %s", err) + } + }() +}