github.com/etecs-ru/gnomock@v0.13.2/preset/localstack/preset.go (about) 1 // Package localstack provides a Gnomock Preset for localstack project 2 // (https://github.com/localstack/localstack). It allows to easily setup local 3 // AWS stack for testing 4 package localstack 5 6 import ( 7 "context" 8 "crypto/tls" 9 "encoding/json" 10 "fmt" 11 "net/http" 12 "strconv" 13 "strings" 14 15 "github.com/etecs-ru/gnomock" 16 "github.com/etecs-ru/gnomock/internal/registry" 17 ) 18 19 const ( 20 webPort = "web" 21 22 // APIPort should be used to configure AWS SDK endpoint 23 APIPort = "api" 24 ) 25 26 const defaultVersion = "0.12.2" 27 28 func init() { 29 registry.Register("localstack", func() gnomock.Preset { return &P{} }) 30 } 31 32 // Preset creates a new localstack preset to use with gnomock.Start. See 33 // package docs for a list of exposed ports and services. It is legal to not 34 // provide any services using WithServices options, but in such case a new 35 // localstack container will be useless. 36 // 37 // This Preset cannot be used with localstack image prior to 0.11.0 38 func Preset(opts ...Option) gnomock.Preset { 39 p := &P{} 40 41 for _, opt := range opts { 42 opt(p) 43 } 44 45 return p 46 } 47 48 // P is a Gnomock Preset localstack implementation 49 type P struct { 50 Services []Service `json:"services"` 51 S3Path string `json:"s3_path"` 52 Version string `json:"version"` 53 } 54 55 // Image returns an image that should be pulled to create this container 56 func (p *P) Image() string { 57 return fmt.Sprintf("docker.io/localstack/localstack:%s", p.Version) 58 } 59 60 // Ports returns ports that should be used to access this container 61 func (p *P) Ports() gnomock.NamedPorts { 62 return gnomock.NamedPorts{ 63 webPort: {Protocol: "tcp", Port: 8080}, 64 APIPort: {Protocol: "tcp", Port: 4566}, 65 } 66 } 67 68 // Options returns a list of options to configure this container 69 func (p *P) Options() []gnomock.Option { 70 p.setDefaults() 71 72 svcStrings := make([]string, len(p.Services)) 73 for i, svc := range p.Services { 74 svcStrings[i] = string(svc) 75 } 76 77 svcEnv := strings.Join(svcStrings, ",") 78 79 opts := []gnomock.Option{ 80 gnomock.WithHealthCheck(p.healthcheck(svcStrings)), 81 gnomock.WithEnv("SERVICES=" + svcEnv), 82 gnomock.WithInit(p.initf()), 83 } 84 85 return opts 86 } 87 88 func (p *P) setDefaults() { 89 if p.Version == "" { 90 p.Version = defaultVersion 91 } 92 } 93 94 func (p *P) healthcheck(services []string) gnomock.HealthcheckFunc { 95 return func(ctx context.Context, c *gnomock.Container) (err error) { 96 addr := p.healthCheckAddress(c) 97 98 client := &http.Client{ 99 Transport: &http.Transport{ 100 TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // nolint:gosec // allow for tests 101 }, 102 } 103 104 req, err := http.NewRequestWithContext(ctx, http.MethodGet, addr, nil) 105 if err != nil { 106 return err 107 } 108 109 res, err := client.Do(req) 110 if err != nil { 111 return err 112 } 113 114 defer func() { 115 closeErr := res.Body.Close() 116 if err == nil && closeErr != nil { 117 err = closeErr 118 } 119 }() 120 121 var hr healthResponse 122 123 decoder := json.NewDecoder(res.Body) 124 125 err = decoder.Decode(&hr) 126 if err != nil { 127 return err 128 } 129 130 if len(hr.Services) < len(services) { 131 return fmt.Errorf( 132 "not enough active services: want %d got %d [%s]", 133 len(services), len(hr.Services), hr.Services, 134 ) 135 } 136 137 for _, service := range services { 138 status := hr.Services[service] 139 if status != "running" { 140 return fmt.Errorf("service '%s' is not running", service) 141 } 142 } 143 144 return nil 145 } 146 } 147 148 // healthCheckAddress returns the address of `/health` endpoint of a running 149 // localstack container. Before version 0.11.3, the endpoint was available at 150 // port 8080. In 0.11.3, the endpoint was moved to the default port (4566). 151 func (p *P) healthCheckAddress(c *gnomock.Container) string { 152 defaultPath := fmt.Sprintf("http://%s/health", c.Address(APIPort)) 153 154 if p.Version == defaultVersion { 155 return defaultPath 156 } 157 158 versionParts := strings.Split(p.Version, ".") 159 if len(versionParts) != 3 { 160 return defaultPath 161 } 162 163 major, err := strconv.Atoi(versionParts[0]) 164 if err != nil { 165 return defaultPath 166 } 167 168 if major > 0 { 169 return defaultPath 170 } 171 172 minor, err := strconv.Atoi(versionParts[1]) 173 if err != nil { 174 return defaultPath 175 } 176 177 if minor > 11 { 178 return defaultPath 179 } 180 181 patch, err := strconv.Atoi(versionParts[2]) 182 if err != nil { 183 return defaultPath 184 } 185 186 if minor == 11 && patch > 2 { 187 return defaultPath 188 } 189 190 return fmt.Sprintf("http://%s/health", c.Address(webPort)) 191 } 192 193 type healthResponse struct { 194 Services map[string]string `json:"services"` 195 } 196 197 func (p *P) initf() gnomock.InitFunc { 198 return func(ctx context.Context, c *gnomock.Container) error { 199 for _, s := range p.Services { 200 if s == S3 { 201 err := p.initS3(c) 202 if err != nil { 203 return fmt.Errorf("can't init s3 storage: %w", err) 204 } 205 } 206 } 207 208 return nil 209 } 210 }