|
| 1 | +package framework |
| 2 | + |
| 3 | +import ( |
| 4 | + "fmt" |
| 5 | + "os" |
| 6 | + "path/filepath" |
| 7 | + |
| 8 | + "github.com/rs/zerolog/log" |
| 9 | +) |
| 10 | + |
| 11 | +/* Templates */ |
| 12 | + |
| 13 | +const ( |
| 14 | + ProductComponentImpl = ` |
| 15 | + package services |
| 16 | +
|
| 17 | +/* |
| 18 | + * A simple template to add your project services to devenv. |
| 19 | + * Each service should have a file with Input/Output struct and a function deploying it. |
| 20 | + */ |
| 21 | +
|
| 22 | +import ( |
| 23 | + "context" |
| 24 | + "fmt" |
| 25 | + "os" |
| 26 | + "strconv" |
| 27 | +
|
| 28 | + "github.com/rs/zerolog" |
| 29 | + "github.com/rs/zerolog/log" |
| 30 | +
|
| 31 | + "github.com/docker/docker/api/types/container" |
| 32 | + "github.com/docker/go-connections/nat" |
| 33 | + "github.com/smartcontractkit/chainlink-testing-framework/framework" |
| 34 | + "github.com/testcontainers/testcontainers-go" |
| 35 | +) |
| 36 | +
|
| 37 | +var L = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}).Level(zerolog.DebugLevel).With().Fields(map[string]any{"component": "datastreams"}).Logger() |
| 38 | +
|
| 39 | +const ( |
| 40 | + DefaultExampleSvcName = "example-service" |
| 41 | + DefaultExampleSvcImage = "busybox:latest" |
| 42 | +) |
| 43 | +
|
| 44 | +// ExampleSvcInput example service input |
| 45 | +type ExampleSvcInput struct { |
| 46 | + Image string ` + "`" + `toml:"image"` + "`" + ` |
| 47 | + Port int ` + "`" + `toml:"port"` + "`" + ` |
| 48 | + ContainerName string ` + "`" + `toml:"container_name"` + "`" + ` |
| 49 | + EnvVars map[string]string ` + "`" + `toml:"env_vars"` + "`" + ` |
| 50 | + Out *ExampleSvcOutput ` + "`" + `toml:"out"` + "`" + ` |
| 51 | +} |
| 52 | +
|
| 53 | +// Default is a default input configuration |
| 54 | +func (s *ExampleSvcInput) Default() { |
| 55 | + if s.Image == "" { |
| 56 | + s.Image = DefaultExampleSvcImage |
| 57 | + } |
| 58 | + if s.Port == 0 { |
| 59 | + s.Port = 9501 |
| 60 | + } |
| 61 | + if s.ContainerName == "" { |
| 62 | + s.ContainerName = DefaultExampleSvcName |
| 63 | + } |
| 64 | +} |
| 65 | +
|
| 66 | +// ExampleSvcOutput represents service connection details which can be consumed by tests |
| 67 | +// or environment.go, or any other metadata your need to expose to clients |
| 68 | +type ExampleSvcOutput struct { |
| 69 | + ServiceURL string ` + "`" + `toml:"service_url"` + "`" + ` |
| 70 | +} |
| 71 | +
|
| 72 | +// NewService deploys an example service and populates output data |
| 73 | +func NewService(in *ExampleSvcInput) error { |
| 74 | + if in == nil || in.Out != nil { |
| 75 | + // either service key is not present in configuration |
| 76 | + // or it is already deployed because we have an output, skipping |
| 77 | + L.Info().Str("ServiceName", DefaultExampleSvcName).Msg("service is skipped or already deployed") |
| 78 | + return nil |
| 79 | + } |
| 80 | + ctx := context.Background() |
| 81 | + // read and apply default inputs |
| 82 | + in.Default() |
| 83 | + // create your service container, use static ports |
| 84 | + req := testcontainers.ContainerRequest{ |
| 85 | + Image: in.Image, |
| 86 | + Name: in.ContainerName, |
| 87 | + Labels: framework.DefaultTCLabels(), |
| 88 | + Networks: []string{framework.DefaultNetworkName}, |
| 89 | + NetworkAliases: map[string][]string{ |
| 90 | + framework.DefaultNetworkName: {in.ContainerName}, |
| 91 | + }, |
| 92 | + Env: in.EnvVars, |
| 93 | + Cmd: []string{"sleep", "infinity"}, |
| 94 | + // add more internal ports here with /tcp suffix, ex.: 9501/tcp |
| 95 | + ExposedPorts: []string{"9501/tcp"}, |
| 96 | + HostConfigModifier: func(h *container.HostConfig) { |
| 97 | + h.PortBindings = nat.PortMap{ |
| 98 | + // add more internal/external pairs here, ex.: 9501/tcp as a key and HostPort is the exposed port (no /tcp prefix!) |
| 99 | + "9501/tcp": []nat.PortBinding{ |
| 100 | + {HostPort: strconv.Itoa(in.Port)}, |
| 101 | + }, |
| 102 | + } |
| 103 | + }, |
| 104 | + // for complex services wait for specific log message |
| 105 | + // WaitingFor: wait.ForLog("Some log message"). |
| 106 | + // WithStartupTimeout(120 * time.Second). |
| 107 | + // WithPollInterval(3 * time.Second), |
| 108 | + } |
| 109 | + _, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ |
| 110 | + ContainerRequest: req, |
| 111 | + Started: true, |
| 112 | + }) |
| 113 | + if err != nil { |
| 114 | + return fmt.Errorf("failed to setup %s service", in.ContainerName) |
| 115 | + } |
| 116 | +
|
| 117 | + // write outputs on a shared struct |
| 118 | + in.Out = &ExampleSvcOutput{ |
| 119 | + ServiceURL: "https://example.com", |
| 120 | + } |
| 121 | + return nil |
| 122 | +} |
| 123 | + ` |
| 124 | +) |
| 125 | + |
| 126 | +// SvcImplParams service template params |
| 127 | +type SvcImplParams struct{} |
| 128 | + |
| 129 | +// GenerateServiceImpl generates example service implementation |
| 130 | +func (g *EnvCodegen) GenerateServiceImpl() (string, error) { |
| 131 | + log.Info().Msg("Generating service implementation") |
| 132 | + p := SvcImplParams{} |
| 133 | + return render(ProductComponentImpl, p) |
| 134 | +} |
| 135 | + |
| 136 | +// WriteServices write all things related to example service template |
| 137 | +func (g *EnvCodegen) WriteServices() error { |
| 138 | + servicesRoot := filepath.Join(g.cfg.outputDir, "services") |
| 139 | + if err := os.MkdirAll( //nolint:gosec |
| 140 | + servicesRoot, |
| 141 | + os.ModePerm, |
| 142 | + ); err != nil { |
| 143 | + return fmt.Errorf("failed to create services directory: %w", err) |
| 144 | + } |
| 145 | + |
| 146 | + // generate Docker wrapper for a service |
| 147 | + serviceImplContents, err := g.GenerateServiceImpl() |
| 148 | + if err != nil { |
| 149 | + return err |
| 150 | + } |
| 151 | + if err := os.WriteFile( //nolint:gosec |
| 152 | + filepath.Join(servicesRoot, "svc.go"), |
| 153 | + []byte(serviceImplContents), |
| 154 | + os.ModePerm, |
| 155 | + ); err != nil { |
| 156 | + return fmt.Errorf("failed to write service file: %w", err) |
| 157 | + } |
| 158 | + |
| 159 | + return nil |
| 160 | +} |
0 commit comments