Skip to content

Commit

Permalink
Docker copy and P2P fixes (#1349)
Browse files Browse the repository at this point in the history
docker copy command, fix p2p
  • Loading branch information
skudasov authored Nov 18, 2024
1 parent 9c9821d commit 0dd918b
Show file tree
Hide file tree
Showing 18 changed files with 132 additions and 123 deletions.
1 change: 1 addition & 0 deletions book/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
- [Components Cleanup](framework/components/cleanup.md)
- [Components Caching](framework/components/caching.md)
- [Mocking Services](framework/components/mocking.md)
- [Copying Files](framework/copying_files.md)
- [External Environment](framework/components/external.md)
- [Secrets]()
- [Observability Stack](framework/observability/observability_stack.md)
Expand Down
1 change: 1 addition & 0 deletions book/src/framework/components/state.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ When deploying a component, you can explicitly configure port ranges if the defa
Defaults are:
- [NodeSet](../components/chainlink/nodeset.md) (Node HTTP API): `10000..100XX`
- [NodeSet](../components/chainlink/nodeset.md) (Node P2P API): `12000..120XX`
- Shared `PostgreSQL` volume is called `postgresql_data`
```
[nodeset]
# HTTP API port range start, each new node get port incremented (host machine)
Expand Down
15 changes: 15 additions & 0 deletions book/src/framework/copying_files.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copying Files

You can copy files to containers by using the `ContainerName` from the output and specifying the source `src` and destination `dst` paths.

However, using this API is discouraged and will be **deprecated** in the future, as it violates the principles of "black-box" testing. If your service relies on this functionality, consider designing a configuration or API to address the requirement instead.

```go
bc, err := blockchain.NewBlockchainNetwork(&blockchain.Input{
...
})
require.NoError(t, err)

err = dockerClient.CopyFile(bc.ContainerName, "local_file.txt", "/home")
require.NoError(t, err)
```
3 changes: 3 additions & 0 deletions framework/.changeset/v0.2.7.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- Do not expose P2P URL to prevent confusion
- Add container names into outputs
- Add `CopyFile` command for Docker
5 changes: 3 additions & 2 deletions framework/components/blockchain/anvil.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,9 @@ func deployAnvil(in *Input) (*Output, error) {
return nil, err
}
return &Output{
UseCache: true,
ChainID: in.ChainID,
UseCache: true,
ChainID: in.ChainID,
ContainerName: containerName,
Nodes: []*Node{
{
HostWSUrl: fmt.Sprintf("ws://%s:%s", host, mp.Port()),
Expand Down
7 changes: 4 additions & 3 deletions framework/components/blockchain/blockchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ type Input struct {

// Output is a blockchain network output, ChainID and one or more nodes that forms the network
type Output struct {
UseCache bool `toml:"use_cache"`
ChainID string `toml:"chain_id"`
Nodes []*Node `toml:"nodes"`
UseCache bool `toml:"use_cache"`
ContainerName string `toml:"container_name"`
ChainID string `toml:"chain_id"`
Nodes []*Node `toml:"nodes"`
}

// Node represents blockchain node output, URLs required for connection locally and inside docker network
Expand Down
22 changes: 7 additions & 15 deletions framework/components/clnode/clnode.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ type Output struct {
type NodeOut struct {
APIAuthUser string `toml:"api_auth_user"`
APIAuthPassword string `toml:"api_auth_password"`
ContainerName string `toml:"container_name"`
HostURL string `toml:"url"`
HostP2PURL string `toml:"p2p_url"`
DockerURL string `toml:"docker_internal_url"`
DockerP2PUrl string `toml:"p2p_docker_internal_url"`
}
Expand Down Expand Up @@ -121,7 +121,7 @@ func newNode(in *Input, pgOut *postgres.Output) (*NodeOut, error) {
if err != nil {
return nil, err
}
cfgPath, err := writeDefaultConfig(in)
cfgPath, err := writeDefaultConfig()
if err != nil {
return nil, err
}
Expand All @@ -147,7 +147,6 @@ func newNode(in *Input, pgOut *postgres.Output) (*NodeOut, error) {
}

httpPort := fmt.Sprintf("%s/tcp", DefaultHTTPPort)
p2pPort := fmt.Sprintf("%s/udp", DefaultP2PPort)
var containerName string
if in.Node.Name != "" {
containerName = in.Node.Name
Expand All @@ -158,7 +157,7 @@ func newNode(in *Input, pgOut *postgres.Output) (*NodeOut, error) {
for _, p := range in.Node.CustomPorts {
customPorts = append(customPorts, fmt.Sprintf("%d/tcp", p))
}
exposedPorts := []string{httpPort, p2pPort}
exposedPorts := []string{httpPort}
exposedPorts = append(exposedPorts, customPorts...)

portBindings := nat.PortMap{
Expand All @@ -168,12 +167,6 @@ func newNode(in *Input, pgOut *postgres.Output) (*NodeOut, error) {
HostPort: fmt.Sprintf("%d/tcp", in.Node.HTTPPort),
},
},
nat.Port(p2pPort): []nat.PortBinding{
{
HostIP: "0.0.0.0",
HostPort: fmt.Sprintf("%d/udp", in.Node.P2PPort),
},
},
}
for _, p := range customPorts {
portBindings[nat.Port(p)] = []nat.PortBinding{
Expand Down Expand Up @@ -283,13 +276,12 @@ func newNode(in *Input, pgOut *postgres.Output) (*NodeOut, error) {
}

mp := nat.Port(fmt.Sprintf("%d/tcp", in.Node.HTTPPort))
mpP2P := nat.Port(fmt.Sprintf("%d/udp", in.Node.P2PPort))

return &NodeOut{
APIAuthUser: DefaultAPIUser,
APIAuthPassword: DefaultAPIPassword,
ContainerName: containerName,
HostURL: fmt.Sprintf("http://%s:%s", host, mp.Port()),
HostP2PURL: fmt.Sprintf("http://%s:%s", host, mpP2P.Port()),
DockerURL: fmt.Sprintf("http://%s:%s", containerName, DefaultHTTPPort),
DockerP2PUrl: fmt.Sprintf("http://%s:%s", containerName, DefaultP2PPort),
}, nil
Expand All @@ -300,7 +292,7 @@ type DefaultCLNodeConfig struct {
SecureCookies bool
}

func generateDefaultConfig(in *Input) (string, error) {
func generateDefaultConfig() (string, error) {
config := DefaultCLNodeConfig{
HTTPPort: DefaultHTTPPort,
SecureCookies: false,
Expand Down Expand Up @@ -348,8 +340,8 @@ func writeDefaultSecrets(pgOut *postgres.Output) (*os.File, error) {
return WriteTmpFile(secretsOverrides, "secrets.toml")
}

func writeDefaultConfig(in *Input) (*os.File, error) {
cfg, err := generateDefaultConfig(in)
func writeDefaultConfig() (*os.File, error) {
cfg, err := generateDefaultConfig()
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion framework/components/clnode/clnode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func checkBasicOutputs(t *testing.T, output *clnode.Output) {
require.Contains(t, output.Node.DockerP2PUrl, "cl-node")
require.NotNil(t, output.PostgreSQL)
require.Contains(t, output.PostgreSQL.Url, "postgresql://chainlink:thispasswordislongenough@127.0.0.1")
require.Contains(t, output.PostgreSQL.DockerInternalURL, "postgresql://chainlink:thispasswordislongenough@postgresql-")
require.Contains(t, output.PostgreSQL.DockerInternalURL, "postgresql://chainlink:thispasswordislongenough@ns-postgresql")
}

func TestComponentDockerNodeWithSharedDB(t *testing.T) {
Expand Down
4 changes: 3 additions & 1 deletion framework/components/postgres/postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,15 @@ type Input struct {

type Output struct {
Url string `toml:"url"`
ContainerName string `toml:"container_name"`
DockerInternalURL string `toml:"docker_internal_url"`
}

func NewPostgreSQL(in *Input) (*Output, error) {
ctx := context.Background()

bindPort := fmt.Sprintf("%s/tcp", Port)
containerName := framework.DefaultTCName("postgresql")
containerName := framework.DefaultTCName("ns-postgresql")

var sqlCommands []string
for i := 0; i <= in.Databases; i++ {
Expand Down Expand Up @@ -126,6 +127,7 @@ func NewPostgreSQL(in *Input) (*Output, error) {
return nil, err
}
return &Output{
ContainerName: containerName,
DockerInternalURL: fmt.Sprintf(
"postgresql://%s:%s@%s:%s/%s?sslmode=disable",
User,
Expand Down
1 change: 0 additions & 1 deletion framework/components/simple_node_set/node_set.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ func printURLs(out *Output) {
httpURLs, p2pURLs, pgURLs := make([]string, 0), make([]string, 0), make([]string, 0)
for _, n := range out.CLNodes {
httpURLs = append(httpURLs, n.Node.HostURL)
p2pURLs = append(p2pURLs, n.Node.HostP2PURL)
pgURLs = append(pgURLs, n.PostgreSQL.Url)
}
framework.L.Info().Any("UI", httpURLs).Send()
Expand Down
21 changes: 4 additions & 17 deletions framework/components/simple_node_set/nodeset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (

type testCase struct {
name string
fakeURL string
funding float64
bcInput *blockchain.Input
nodeSetInput *ns.Input
Expand All @@ -25,13 +24,13 @@ func checkBasicOutputs(t *testing.T, output *ns.Output) {
require.NotNil(t, output.CLNodes)
require.Len(t, output.CLNodes, 2)
require.Contains(t, output.CLNodes[0].PostgreSQL.Url, "postgresql://chainlink:thispasswordislongenough@127.0.0.1")
require.Contains(t, output.CLNodes[0].PostgreSQL.DockerInternalURL, "postgresql://chainlink:thispasswordislongenough@postgresql-")
require.Contains(t, output.CLNodes[0].PostgreSQL.DockerInternalURL, "postgresql://chainlink:thispasswordislongenough@ns-postgresql-")
require.Contains(t, output.CLNodes[0].Node.HostURL, "127.0.0.1")
require.Contains(t, output.CLNodes[0].Node.DockerURL, "node")
require.Contains(t, output.CLNodes[0].Node.DockerP2PUrl, "node")

require.Contains(t, output.CLNodes[1].PostgreSQL.Url, "postgresql://chainlink:thispasswordislongenough@127.0.0.1")
require.Contains(t, output.CLNodes[1].PostgreSQL.DockerInternalURL, "postgresql://chainlink:thispasswordislongenough@postgresql-")
require.Contains(t, output.CLNodes[1].PostgreSQL.DockerInternalURL, "postgresql://chainlink:thispasswordislongenough@ns-postgresql-")
require.Contains(t, output.CLNodes[1].Node.HostURL, "127.0.0.1")
require.Contains(t, output.CLNodes[1].Node.DockerURL, "node")
require.Contains(t, output.CLNodes[1].Node.DockerP2PUrl, "node")
Expand All @@ -40,8 +39,7 @@ func checkBasicOutputs(t *testing.T, output *ns.Output) {
func TestComponentDockerNodeSetSharedDB(t *testing.T) {
testCases := []testCase{
{
name: "2 nodes cluster, override mode 'all'",
fakeURL: "http://example.com",
name: "2 nodes cluster, override mode 'all'",
bcInput: &blockchain.Input{
Type: "anvil",
Image: "f4hrenh9it/foundry",
Expand All @@ -56,9 +54,6 @@ func TestComponentDockerNodeSetSharedDB(t *testing.T) {
},
NodeSpecs: []*clnode.Input{
{
DbInput: &postgres.Input{
Image: "postgres:15.6",
},
Node: &clnode.NodeInput{
Image: "public.ecr.aws/chainlink/chainlink:v2.17.0",
Name: "cl-node",
Expand All @@ -71,8 +66,7 @@ func TestComponentDockerNodeSetSharedDB(t *testing.T) {
},
},
{
name: "2 nodes cluster, override mode 'each'",
fakeURL: "http://example.com",
name: "2 nodes cluster, override mode 'each'",
bcInput: &blockchain.Input{
Type: "anvil",
Image: "f4hrenh9it/foundry",
Expand All @@ -90,10 +84,6 @@ func TestComponentDockerNodeSetSharedDB(t *testing.T) {
},
NodeSpecs: []*clnode.Input{
{
DbInput: &postgres.Input{
Image: "postgres:15.6",
Port: 14000,
},
Node: &clnode.NodeInput{
Image: "public.ecr.aws/chainlink/chainlink:v2.17.0",
Name: "cl-node-1",
Expand All @@ -104,9 +94,6 @@ level = 'info'
},
},
{
DbInput: &postgres.Input{
Image: "postgres:15.6",
},
Node: &clnode.NodeInput{
Image: "public.ecr.aws/chainlink/chainlink:v2.17.0",
Name: "cl-node-2",
Expand Down
2 changes: 1 addition & 1 deletion framework/components/simple_node_set/reload.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
// UpgradeNodeSet updates nodes configuration TOML files
// this API is discouraged, however, you can use it if nodes require restart or configuration updates, temporarily!
func UpgradeNodeSet(in *Input, bc *blockchain.Output, wait time.Duration) (*Output, error) {
_, err := chaos.ExecPumba("rm --volumes=false re2:node.*|postgresql.*", wait)
_, err := chaos.ExecPumba("rm --volumes=false re2:node.*|ns-postgresql.*", wait)
if err != nil {
return nil, err
}
Expand Down
87 changes: 87 additions & 0 deletions framework/docker.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package framework

import (
"archive/tar"
"bytes"
"context"
"errors"
"fmt"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"github.com/docker/go-connections/nat"
"github.com/google/uuid"
tc "github.com/testcontainers/testcontainers-go"
"io"
"os"
"os/exec"
"strings"
Expand Down Expand Up @@ -132,3 +136,86 @@ func RebuildDockerImage(once *sync.Once, dockerfile string, buildContext string,
}
return fmt.Sprintf("localhost:5050/%s:latest", imageName), nil
}

// DockerClient wraps a Docker API client and provides convenience methods
type DockerClient struct {
cli *client.Client
}

// NewDockerClient creates a new instance of DockerClient
func NewDockerClient() (*DockerClient, error) {
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return nil, fmt.Errorf("failed to create Docker client: %w", err)
}
return &DockerClient{cli: cli}, nil
}

// CopyFile copies a file into a container by name
func (dc *DockerClient) CopyFile(containerName, sourceFile, targetPath string) error {
ctx := context.Background()
containerID, err := dc.findContainerIDByName(ctx, containerName)
if err != nil {
return fmt.Errorf("failed to find container ID by name: %s", containerName)
}
return dc.copyToContainer(containerID, sourceFile, targetPath)
}

// findContainerIDByName finds a container ID by its name
func (dc *DockerClient) findContainerIDByName(ctx context.Context, containerName string) (string, error) {
containers, err := dc.cli.ContainerList(ctx, container.ListOptions{
All: true,
})
if err != nil {
return "", fmt.Errorf("failed to list containers: %w", err)
}
for _, c := range containers {
for _, name := range c.Names {
if name == "/"+containerName {
return c.ID, nil
}
}
}
return "", fmt.Errorf("container with name %s not found", containerName)
}

// copyToContainer copies a file into a container
func (dc *DockerClient) copyToContainer(containerID, sourceFile, targetPath string) error {
ctx := context.Background()
src, err := os.Open(sourceFile)
if err != nil {
return fmt.Errorf("could not open source file: %w", err)
}
defer src.Close()

// Create a tar archive containing the file
var buf bytes.Buffer
tw := tar.NewWriter(&buf)
info, err := src.Stat()
if err != nil {
return fmt.Errorf("could not stat source file: %w", err)
}

// Add file to tar
header := &tar.Header{
Name: info.Name(),
Size: info.Size(),
Mode: int64(info.Mode()),
}
if err := tw.WriteHeader(header); err != nil {
return fmt.Errorf("could not write tar header: %w", err)
}
if _, err := io.Copy(tw, src); err != nil {
return fmt.Errorf("could not write file to tar archive: %w", err)
}
tw.Close()

// Copy the tar archive to the container
err = dc.cli.CopyToContainer(ctx, containerID, targetPath, &buf, container.CopyToContainerOptions{
AllowOverwriteDirWithFile: true,
})
if err != nil {
return fmt.Errorf("could not copy file to container: %w", err)
}
return nil
}
1 change: 1 addition & 0 deletions framework/examples/myproject/.envrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export LOKI_TENANT_ID=promtail
export LOKI_URL=http://localhost:3030/loki/api/v1/push
export TESTCONTAINERS_RYUK_DISABLED=true
export CTF_LOG_LEVEL=info
Loading

0 comments on commit 0dd918b

Please sign in to comment.