Skip to content

Commit 6b896fa

Browse files
Atrax1EasterTheBunny
authored andcommitted
feat(local): add billing platform service
1 parent 7e4a950 commit 6b896fa

File tree

2 files changed

+322
-0
lines changed

2 files changed

+322
-0
lines changed
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
package billing_platform_service
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"net/http"
8+
"os"
9+
"strings"
10+
"time"
11+
12+
networkTypes "github.com/docker/docker/api/types/network"
13+
"github.com/docker/docker/client"
14+
"github.com/docker/go-connections/nat"
15+
"github.com/pkg/errors"
16+
"github.com/smartcontractkit/chainlink-testing-framework/framework"
17+
"github.com/testcontainers/testcontainers-go"
18+
"github.com/testcontainers/testcontainers-go/modules/compose"
19+
"github.com/testcontainers/testcontainers-go/wait"
20+
)
21+
22+
type Output struct {
23+
BillingPlatformService *BillingPlatformServiceOutput
24+
Postgres *PostgresOutput
25+
}
26+
27+
type BillingPlatformServiceOutput struct {
28+
GRPCInternalURL string
29+
GRPCExternalURL string
30+
}
31+
32+
type PostgresOutput struct {
33+
}
34+
35+
type Input struct {
36+
ComposeFile string `toml:"compose_file"`
37+
ExtraDockerNetworks []string `toml:"extra_docker_networks"`
38+
Output *Output `toml:"output"`
39+
UseCache bool `toml:"use_cache"`
40+
}
41+
42+
func defaultBillingPlatformService(in *Input) *Input {
43+
if in.ComposeFile == "" {
44+
in.ComposeFile = "./docker-compose.yml"
45+
}
46+
return in
47+
}
48+
49+
const (
50+
DEFAULT_STACK_NAME = "billing-platform-service"
51+
52+
DEFAULT_BILLING_PLATFORM_SERVICE_GRPC_PORT = "2022"
53+
DEFAULT_BILLING_PLATFORM_SERVICE_SERVICE_NAME = "billing-platform-service"
54+
)
55+
56+
func New(in *Input) (*Output, error) {
57+
if in == nil {
58+
return nil, errors.New("input is nil")
59+
}
60+
61+
if in.UseCache {
62+
if in.Output != nil {
63+
return in.Output, nil
64+
}
65+
}
66+
67+
in = defaultBillingPlatformService(in)
68+
identifier := framework.DefaultTCName(DEFAULT_STACK_NAME)
69+
framework.L.Debug().Str("Compose file", in.ComposeFile).
70+
Msgf("Starting Billing Platform Service stack with identifier %s",
71+
framework.DefaultTCName(DEFAULT_STACK_NAME))
72+
73+
cFilePath, fileErr := composeFilePath(in.ComposeFile)
74+
if fileErr != nil {
75+
return nil, errors.Wrap(fileErr, "failed to get compose file path")
76+
}
77+
78+
stack, stackErr := compose.NewDockerComposeWith(
79+
compose.WithStackFiles(cFilePath),
80+
compose.StackIdentifier(identifier),
81+
)
82+
if stackErr != nil {
83+
return nil, errors.Wrap(stackErr, "failed to create compose stack for Billing Platform Service")
84+
}
85+
86+
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
87+
defer cancel()
88+
89+
upErr := stack.Up(ctx)
90+
91+
if upErr != nil {
92+
return nil, errors.Wrap(upErr, "failed to start stack for Billing Platform Service")
93+
}
94+
95+
stack.WaitForService(DEFAULT_BILLING_PLATFORM_SERVICE_SERVICE_NAME,
96+
wait.ForAll(
97+
wait.ForLog("GRPC server is live").WithPollInterval(200*time.Millisecond),
98+
wait.ForListeningPort(DEFAULT_BILLING_PLATFORM_SERVICE_GRPC_PORT),
99+
).WithDeadline(1*time.Minute),
100+
)
101+
102+
billingContainer, billingErr := stack.ServiceContainer(ctx, DEFAULT_BILLING_PLATFORM_SERVICE_SERVICE_NAME)
103+
if billingErr != nil {
104+
return nil, errors.Wrap(billingErr, "failed to get billing-platform-service container")
105+
}
106+
107+
cli, cliErr := client.NewClientWithOpts(
108+
client.FromEnv,
109+
client.WithAPIVersionNegotiation(),
110+
)
111+
if cliErr != nil {
112+
return nil, errors.Wrap(cliErr, "failed to create docker client")
113+
}
114+
defer cli.Close()
115+
116+
// so let's try to connect to a Docker network a couple of times, there must be a race condition in Docker
117+
// and even when network sandbox has been created and container is running, this call can still fail
118+
// retrying is simpler than trying to figure out how to correctly wait for the network sandbox to be ready
119+
networks := []string{framework.DefaultNetworkName}
120+
networks = append(networks, in.ExtraDockerNetworks...)
121+
122+
for _, networkName := range networks {
123+
framework.L.Debug().Msgf("Connecting billing-platform-service to %s network", networkName)
124+
connectContex, connectCancel := context.WithTimeout(ctx, 30*time.Second)
125+
defer connectCancel()
126+
if connectErr := connectNetwork(connectContex, 30*time.Second, cli, billingContainer.ID, networkName, identifier); connectErr != nil {
127+
return nil, errors.Wrapf(connectErr, "failed to connect billing-platform-service to %s network", networkName)
128+
}
129+
// verify that the container is connected to framework's network
130+
inspected, inspectErr := cli.ContainerInspect(ctx, billingContainer.ID)
131+
if inspectErr != nil {
132+
return nil, errors.Wrapf(inspectErr, "failed to inspect container %s", billingContainer.ID)
133+
}
134+
135+
_, ok := inspected.NetworkSettings.Networks[networkName]
136+
if !ok {
137+
return nil, fmt.Errorf("container %s is NOT on network %s", billingContainer.ID, networkName)
138+
}
139+
140+
framework.L.Debug().Msgf("Container %s is connected to network %s", billingContainer.ID, networkName)
141+
}
142+
143+
// get hosts for billing platform service
144+
billingExternalHost, billingExternalHostErr := billingContainer.Host(ctx)
145+
if billingExternalHostErr != nil {
146+
return nil, errors.Wrap(billingExternalHostErr, "failed to get host for Billing Platform Service")
147+
}
148+
149+
// get mapped port for billing platform service
150+
billingExternalPort, billingExternalPortErr := findMappedPort(ctx, 20*time.Second, billingContainer, DEFAULT_BILLING_PLATFORM_SERVICE_GRPC_PORT)
151+
if billingExternalPortErr != nil {
152+
return nil, errors.Wrap(billingExternalPortErr, "failed to get mapped port for Chip Ingress")
153+
}
154+
155+
output := &Output{
156+
BillingPlatformService: &BillingPlatformServiceOutput{
157+
GRPCInternalURL: fmt.Sprintf("http://%s:%s", DEFAULT_BILLING_PLATFORM_SERVICE_SERVICE_NAME, DEFAULT_BILLING_PLATFORM_SERVICE_GRPC_PORT),
158+
GRPCExternalURL: fmt.Sprintf("http://%s:%s", billingExternalHost, billingExternalPort.Port()),
159+
},
160+
}
161+
162+
framework.L.Info().Msg("Chip Ingress stack start")
163+
164+
return output, nil
165+
}
166+
167+
func composeFilePath(rawFilePath string) (string, error) {
168+
// if it's not a URL, return it as is and assume it's a local file
169+
if !strings.HasPrefix(rawFilePath, "http") {
170+
return rawFilePath, nil
171+
}
172+
173+
resp, respErr := http.Get(rawFilePath)
174+
if respErr != nil {
175+
return "", errors.Wrap(respErr, "failed to download docker-compose file")
176+
}
177+
defer resp.Body.Close()
178+
179+
tempFile, tempErr := os.CreateTemp("", "billing-platform-service-docker-compose-*.yml")
180+
if tempErr != nil {
181+
return "", errors.Wrap(tempErr, "failed to create temp file")
182+
}
183+
defer tempFile.Close()
184+
185+
_, copyErr := io.Copy(tempFile, resp.Body)
186+
if copyErr != nil {
187+
tempFile.Close()
188+
return "", errors.Wrap(copyErr, "failed to write compose file")
189+
}
190+
191+
return tempFile.Name(), nil
192+
}
193+
194+
func findMappedPort(ctx context.Context, timeout time.Duration, container *testcontainers.DockerContainer, port nat.Port) (nat.Port, error) {
195+
forCtx, cancel := context.WithTimeout(ctx, timeout)
196+
defer cancel()
197+
198+
tickerInterval := 5 * time.Second
199+
ticker := time.NewTicker(5 * time.Second)
200+
defer ticker.Stop()
201+
202+
for {
203+
select {
204+
case <-forCtx.Done():
205+
return "", fmt.Errorf("timeout while waiting for mapped port for %s", port)
206+
case <-ticker.C:
207+
portCtx, portCancel := context.WithTimeout(ctx, tickerInterval)
208+
defer portCancel()
209+
mappedPort, mappedPortErr := container.MappedPort(portCtx, port)
210+
if mappedPortErr != nil {
211+
return "", errors.Wrapf(mappedPortErr, "failed to get mapped port for %s", port)
212+
}
213+
if mappedPort.Port() == "" {
214+
return "", fmt.Errorf("mapped port for %s is empty", port)
215+
}
216+
return mappedPort, nil
217+
}
218+
}
219+
}
220+
221+
func connectNetwork(connCtx context.Context, timeout time.Duration, dockerClient *client.Client, containerID, networkName, stackIdentifier string) error {
222+
ticker := time.NewTicker(500 * time.Millisecond)
223+
defer ticker.Stop()
224+
225+
networkCtx, networkCancel := context.WithTimeout(connCtx, timeout)
226+
defer networkCancel()
227+
228+
for {
229+
select {
230+
case <-networkCtx.Done():
231+
return fmt.Errorf("timeout while trying to connect billing-platform-service to default network")
232+
case <-ticker.C:
233+
if networkErr := dockerClient.NetworkConnect(
234+
connCtx,
235+
networkName,
236+
containerID,
237+
&networkTypes.EndpointSettings{
238+
Aliases: []string{stackIdentifier},
239+
},
240+
); networkErr != nil && !strings.Contains(networkErr.Error(), "already exists in network") {
241+
framework.L.Trace().Msgf("failed to connect to default network: %v", networkErr)
242+
continue
243+
}
244+
framework.L.Trace().Msgf("connected to %s network", networkName)
245+
return nil
246+
}
247+
}
248+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
services:
2+
billing-platform-service:
3+
restart: on-failure
4+
tty: true
5+
container_name: billing-platform-service
6+
image: ${BILLING_PLATFORM_SERVICE_IMAGE:-}
7+
command: ["grpc-service"]
8+
depends_on:
9+
postgres:
10+
condition: service_healthy
11+
migrations:
12+
condition: service_started
13+
ports:
14+
- "2112:2112"
15+
- "2222:2222"
16+
- "2223:2223"
17+
- "2257:2257"
18+
environment:
19+
PROMETHEUS_PORT: 2112
20+
BILLING_SERVER_PORT: 2222
21+
CREDIT_RESERVATION_SERVER_PORT: 2223
22+
WORKFLOW_OWNERSHIP_PROOF_SERVER_PORT: 2257
23+
WORKFLOW_OWNERSHIP_PROOF_SERVER_HOST: 0.0.0.0
24+
STREAMS_API_URL: ${STREAMS_API_URL}
25+
STREAMS_API_KEY: ${STREAMS_API_KEY}
26+
STREAMS_API_SECRET: ${STREAMS_API_SECRET}
27+
DB_HOST: postgres
28+
DB_PORT: 5432
29+
DB_NAME: billing_platform
30+
DB_USERNAME: postgres
31+
DB_PASSWORD: postgres
32+
healthcheck:
33+
test: ["CMD", "grpc_health_probe", "-addr=localhost:2222"]
34+
interval: 200ms
35+
timeout: 10s
36+
retries: 3
37+
38+
postgres:
39+
image: postgres:16-alpine
40+
container_name: postgres-billing-platform
41+
restart: always
42+
environment:
43+
POSTGRES_HOST_AUTH_METHOD: trust
44+
POSTGRES_DB: billing_platform
45+
POSTGRES_HOST: postgres
46+
POSTGRES_USER: postgres
47+
POSTGRES_PASSWORD: postgres
48+
healthcheck:
49+
test: ["CMD","pg_isready","-U","${POSTGRES_USER}","-d","${POSTGRES_DB}","-h","${POSTGRES_HOST}"]
50+
interval: 5s
51+
timeout: 10s
52+
retries: 3
53+
ports:
54+
- "5432:5432"
55+
56+
migrations:
57+
image: ${BILLING_PLATFORM_SERVICE_MIGRATION_IMAGE:-}
58+
container_name: db-migrations-billing-platform
59+
restart: on-failure
60+
environment:
61+
DBMATE_NO_DUMP_SCHEMA: true
62+
DBMATE_WAIT: true
63+
DATABASE_URL: postgres://postgres:postgres@postgres:postgres/billing_platform
64+
networks:
65+
- backend
66+
depends_on:
67+
postgres:
68+
condition: service_healthy
69+
entrypoint:
70+
- dbmate
71+
- up
72+
- --
73+
- --url=${DATABASE_URL}
74+
- --migrations-dir=/db

0 commit comments

Comments
 (0)