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  }