From f954f6d4a1be80f6eaa2234d8c6247da284b150e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20de=20la=20Pe=C3=B1a?= Date: Mon, 30 Sep 2024 18:16:33 +0200 Subject: [PATCH] feat: add etcd module (#2788) * chore: scaffolding for etcd module * feat: add etcd with cluster support * feat: support for AutoTLS * chore: add unit test for cmd * fix: remove non-existent module * chore: simplify removing auto-tls support * docs: remove old deprecated function * docs: document functions * docs: remove commented code * chore: more readable tests * chore: move logic to the functional option * chore: simplify passing current node opts * fix: return the parent node when creating the child nodes * chore: move to constant * chore: use zero values * fix: typo after wrong copy&paste * chore: remove must methods * chore: use private fields in the container * chore: use private fields in the options struct * chore: simplify public API * chore: make constants private * fix: lint * chore: rename field to childNodes * docs: enrich container comments * fix: assert on the right node * chore: bump dockercfg * feat: implement termination of the cluster * chore: use require.Zero * chore: proper test name * chore: preallocate slice * chore: do not allow single-node clusters * fix: join errors on terminate cluster * fix: check parent container is terminated * chore: extract to function * fix: check if container is not nil * chor: do not orphan the network * chore: simplify * docs: fix comment * chore: wrap errors in endpoint functions * Revert "chore: wrap errors in endpoint functions" This reverts commit 1c4361b2210d538438fbfb6bbea68dc949bc0379. * chore: simplify eval * fix: not needed * chore: simplify container initialisation * chore: simplify even more --- .github/workflows/ci.yml | 2 +- .vscode/.testcontainers-go.code-workspace | 4 + docs/modules/etcd.md | 95 ++++++++ mkdocs.yml | 1 + modules/etcd/Makefile | 5 + modules/etcd/cmd_test.go | 108 +++++++++ modules/etcd/etcd.go | 272 ++++++++++++++++++++++ modules/etcd/etcd_test.go | 60 +++++ modules/etcd/etcd_unit_test.go | 113 +++++++++ modules/etcd/examples_test.go | 97 ++++++++ modules/etcd/go.mod | 75 ++++++ modules/etcd/go.sum | 216 +++++++++++++++++ modules/etcd/options.go | 107 +++++++++ sonar-project.properties | 2 +- 14 files changed, 1155 insertions(+), 2 deletions(-) create mode 100644 docs/modules/etcd.md create mode 100644 modules/etcd/Makefile create mode 100644 modules/etcd/cmd_test.go create mode 100644 modules/etcd/etcd.go create mode 100644 modules/etcd/etcd_test.go create mode 100644 modules/etcd/etcd_unit_test.go create mode 100644 modules/etcd/examples_test.go create mode 100644 modules/etcd/go.mod create mode 100644 modules/etcd/go.sum create mode 100644 modules/etcd/options.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49dfc336c4..0fea7eaeb0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -94,7 +94,7 @@ jobs: matrix: go-version: [1.22.x, 1.x] platform: [ubuntu-latest] - module: [artemis, azurite, cassandra, chroma, clickhouse, cockroachdb, compose, consul, couchbase, databend, dolt, dynamodb, elasticsearch, gcloud, grafana-lgtm, inbucket, influxdb, k3s, k6, kafka, localstack, mariadb, milvus, minio, mockserver, mongodb, mssql, mysql, nats, neo4j, ollama, openfga, openldap, opensearch, postgres, pulsar, qdrant, rabbitmq, redis, redpanda, registry, surrealdb, valkey, vault, vearch, weaviate] + module: [artemis, azurite, cassandra, chroma, clickhouse, cockroachdb, compose, consul, couchbase, databend, dolt, dynamodb, elasticsearch, etcd, gcloud, grafana-lgtm, inbucket, influxdb, k3s, k6, kafka, localstack, mariadb, milvus, minio, mockserver, mongodb, mssql, mysql, nats, neo4j, ollama, openfga, openldap, opensearch, postgres, pulsar, qdrant, rabbitmq, redis, redpanda, registry, surrealdb, valkey, vault, vearch, weaviate] uses: ./.github/workflows/ci-test-go.yml with: go-version: ${{ matrix.go-version }} diff --git a/.vscode/.testcontainers-go.code-workspace b/.vscode/.testcontainers-go.code-workspace index df74747a48..68a2751a5d 100644 --- a/.vscode/.testcontainers-go.code-workspace +++ b/.vscode/.testcontainers-go.code-workspace @@ -65,6 +65,10 @@ "name": "module / elasticsearch", "path": "../modules/elasticsearch" }, + { + "name": "module / etcd", + "path": "../modules/etcd" + }, { "name": "module / gcloud", "path": "../modules/gcloud" diff --git a/docs/modules/etcd.md b/docs/modules/etcd.md new file mode 100644 index 0000000000..f83816c986 --- /dev/null +++ b/docs/modules/etcd.md @@ -0,0 +1,95 @@ +# etcd + +Not available until the next release of testcontainers-go :material-tag: main + +## Introduction + +The Testcontainers module for etcd. + +## Adding this module to your project dependencies + +Please run the following command to add the etcd module to your Go dependencies: + +``` +go get github.com/testcontainers/testcontainers-go/modules/etcd +``` + +## Usage example + + +[Creating a etcd container](../../modules/etcd/examples_test.go) inside_block:runetcdContainer + + +## Module Reference + +### Run function + +- Not available until the next release of testcontainers-go :material-tag: main + +The etcd module exposes one entrypoint function to create the etcd container, and this function receives three parameters: + +```golang +func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*etcdContainer, error) +``` + +- `context.Context`, the Go context. +- `string`, the Docker image to use. +- `testcontainers.ContainerCustomizer`, a variadic argument for passing options. + +### Container Options + +When starting the etcd container, you can pass options in a variadic way to configure it. + +#### Image + +If you need to set a different etcd Docker image, you can set a valid Docker image as the second argument in the `Run` function. +E.g. `Run(context.Background(), "bitnami/etcd:latest")`. + +{% include "../features/common_functional_options.md" %} + +#### WithAdditionalArgs + +- Not available until the next release of testcontainers-go :material-tag: main + +You can pass additional arguments to the etcd container by using the `WithAdditionalArgs` option. The arguments are passed to the CMD of the etcd container. + +#### WithDataDir + +- Not available until the next release of testcontainers-go :material-tag: main + +You can set the data directory for the etcd container by using the `WithDataDir` boolean option. The data directory where the etcd data is stored is `/data.etcd`. + +#### WithNodes + +- Not available until the next release of testcontainers-go :material-tag: main + +You can set the number of nodes for the etcd cluster by using the `WithNodes` option, passing the node names for each of the nodes. Single-node clusters are not allowed, +for that reason the functional option receives three string arguments: the first node, the second node, and a variadic argument for the rest of the nodes. +The module starts a container for each node, having the first node a reference to the other nodes. E.g. `WithNodes("etcd-1", "etcd-2")`, `WithNodes("etcd-1", "etcd-2", "etcd-3")` and so on. + +The module creates a Docker network for the etcd cluster, and the nodes are connected to this network, so that they can communicate with each other through the network. + +#### WithClusterToken + +- Not available until the next release of testcontainers-go :material-tag: main + +Sets the cluster token for the etcd cluster. The cluster token is used to identify the etcd cluster. The default value is `mys3cr3ttok3n`. +The etcd container holds a reference to the cluster token, so you can use it with e.g. `ctr.ClusterToken`. + +### Container Methods + +- Not available until the next release of testcontainers-go :material-tag: main + +The etcd container exposes the following methods: + +#### ClientEndpoint + +- Not available until the next release of testcontainers-go :material-tag: main + +Returns the client endpoint for the etcd container and an error, if any. In the case of a cluster, it returns the client endpoint for the first node. + +#### PeerEndpoint + +- Not available until the next release of testcontainers-go :material-tag: main + +Returns the peer endpoint for the etcd container and an error, if any. In the case of a cluster, it returns the peer endpoint for the first node. diff --git a/mkdocs.yml b/mkdocs.yml index f5dfafebdd..824f5091e1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -78,6 +78,7 @@ nav: - modules/dolt.md - modules/dynamodb.md - modules/elasticsearch.md + - modules/etcd.md - modules/gcloud.md - modules/grafana-lgtm.md - modules/inbucket.md diff --git a/modules/etcd/Makefile b/modules/etcd/Makefile new file mode 100644 index 0000000000..7531baef98 --- /dev/null +++ b/modules/etcd/Makefile @@ -0,0 +1,5 @@ +include ../../commons-test.mk + +.PHONY: test +test: + $(MAKE) test-etcd diff --git a/modules/etcd/cmd_test.go b/modules/etcd/cmd_test.go new file mode 100644 index 0000000000..918c68dc84 --- /dev/null +++ b/modules/etcd/cmd_test.go @@ -0,0 +1,108 @@ +package etcd + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_configureCMD(t *testing.T) { + t.Run("default", func(t *testing.T) { + got := configureCMD(options{}) + want := []string{"etcd", "--name=default"} + require.Equal(t, want, got) + }) + + t.Run("with-node", func(t *testing.T) { + got := configureCMD(options{ + nodeNames: []string{"node1"}, + }) + want := []string{ + "etcd", + "--name=node1", + "--initial-advertise-peer-urls=http://node1:2380", + "--advertise-client-urls=http://node1:2379", + "--listen-peer-urls=http://0.0.0.0:2380", + "--listen-client-urls=http://0.0.0.0:2379", + "--initial-cluster-state=new", + "--initial-cluster=node1=http://node1:2380", + } + require.Equal(t, want, got) + }) + + t.Run("with-node-datadir", func(t *testing.T) { + got := configureCMD(options{ + nodeNames: []string{"node1"}, + mountDataDir: true, + }) + want := []string{ + "etcd", + "--name=node1", + "--initial-advertise-peer-urls=http://node1:2380", + "--advertise-client-urls=http://node1:2379", + "--listen-peer-urls=http://0.0.0.0:2380", + "--listen-client-urls=http://0.0.0.0:2379", + "--initial-cluster-state=new", + "--initial-cluster=node1=http://node1:2380", + "--data-dir=/data.etcd", + } + require.Equal(t, want, got) + }) + + t.Run("with-node-datadir-additional-args", func(t *testing.T) { + got := configureCMD(options{ + nodeNames: []string{"node1"}, + mountDataDir: true, + additionalArgs: []string{"--auto-compaction-retention=1"}, + }) + want := []string{ + "etcd", + "--name=node1", + "--initial-advertise-peer-urls=http://node1:2380", + "--advertise-client-urls=http://node1:2379", + "--listen-peer-urls=http://0.0.0.0:2380", + "--listen-client-urls=http://0.0.0.0:2379", + "--initial-cluster-state=new", + "--initial-cluster=node1=http://node1:2380", + "--data-dir=/data.etcd", + "--auto-compaction-retention=1", + } + require.Equal(t, want, got) + }) + + t.Run("with-cluster", func(t *testing.T) { + got := configureCMD(options{ + nodeNames: []string{"node1", "node2"}, + }) + want := []string{ + "etcd", + "--name=node1", + "--initial-advertise-peer-urls=http://node1:2380", + "--advertise-client-urls=http://node1:2379", + "--listen-peer-urls=http://0.0.0.0:2380", + "--listen-client-urls=http://0.0.0.0:2379", + "--initial-cluster-state=new", + "--initial-cluster=node1=http://node1:2380,node2=http://node2:2380", + } + require.Equal(t, want, got) + }) + + t.Run("with-cluster-token", func(t *testing.T) { + got := configureCMD(options{ + nodeNames: []string{"node1", "node2"}, + clusterToken: "token", + }) + want := []string{ + "etcd", + "--name=node1", + "--initial-advertise-peer-urls=http://node1:2380", + "--advertise-client-urls=http://node1:2379", + "--listen-peer-urls=http://0.0.0.0:2380", + "--listen-client-urls=http://0.0.0.0:2379", + "--initial-cluster-state=new", + "--initial-cluster=node1=http://node1:2380,node2=http://node2:2380", + "--initial-cluster-token=token", + } + require.Equal(t, want, got) + }) +} diff --git a/modules/etcd/etcd.go b/modules/etcd/etcd.go new file mode 100644 index 0000000000..7ea78b4385 --- /dev/null +++ b/modules/etcd/etcd.go @@ -0,0 +1,272 @@ +package etcd + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/testcontainers/testcontainers-go" + tcnetwork "github.com/testcontainers/testcontainers-go/network" +) + +const ( + clientPort = "2379" + peerPort = "2380" + dataDir = "/data.etcd" + defaultClusterToken = "mys3cr3ttok3n" + scheme = "http" +) + +// EtcdContainer represents the etcd container type used in the module. It can be used to create a single-node instance or a cluster. +// For the cluster, the first node creates the cluster and the other nodes join it as child nodes. +type EtcdContainer struct { + testcontainers.Container + // childNodes contains the child nodes of the current node, forming a cluster + childNodes []*EtcdContainer + opts options +} + +// Terminate terminates the etcd container, its child nodes, and the network in which the cluster is running +// to communicate between the nodes. +func (c *EtcdContainer) Terminate(ctx context.Context) error { + var errs []error + + // child nodes has no other children + for i, child := range c.childNodes { + if err := child.Terminate(ctx); err != nil { + errs = append(errs, fmt.Errorf("terminate child node(%d): %w", i, err)) + } + } + + if c.Container != nil { + if err := c.Container.Terminate(ctx); err != nil { + errs = append(errs, fmt.Errorf("terminate cluster node: %w", err)) + } + } + + // remove the cluster network if it was created, but only for the first node + // we could check if the current node is the first one (index 0), + // and/or check that there are no child nodes + if c.opts.clusterNetwork != nil && c.opts.currentNode == 0 { + if err := c.opts.clusterNetwork.Remove(ctx); err != nil { + errs = append(errs, fmt.Errorf("remove cluster network: %w", err)) + } + } + + return errors.Join(errs...) +} + +// Run creates an instance of the etcd container type +func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*EtcdContainer, error) { + req := testcontainers.ContainerRequest{ + Image: img, + Cmd: []string{}, + } + + genericContainerReq := testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, + } + + settings := defaultOptions(&req) + for _, opt := range opts { + if apply, ok := opt.(Option); ok { + apply(&settings) + } + if err := opt.Customize(&genericContainerReq); err != nil { + return nil, err + } + } + + clusterOpts, err := configureCluster(ctx, &settings, opts) + if err != nil { + return nil, fmt.Errorf("configure cluster: %w", err) + } + + // configure CMD with the nodes + genericContainerReq.Cmd = configureCMD(settings) + + // Initialise the etcd container with the current settings. + // The cluster network, if needed, is already part of the settings, + // so the following error handling returns a partially initialised container, + // allowing the caller to clean up the resources with the Terminate method. + c := &EtcdContainer{opts: settings} + + if settings.clusterNetwork != nil { + // apply the network to the current node + err := tcnetwork.WithNetwork([]string{settings.nodeNames[settings.currentNode]}, settings.clusterNetwork)(&genericContainerReq) + if err != nil { + return c, fmt.Errorf("with network: %w", err) + } + } + + if c.Container, err = testcontainers.GenericContainer(ctx, genericContainerReq); err != nil { + return c, fmt.Errorf("generic container: %w", err) + } + + // only the first node creates the cluster + if settings.currentNode == 0 { + for i := 1; i < len(settings.nodeNames); i++ { + // move to the next node + childNode, err := Run(ctx, req.Image, append(clusterOpts, withCurrentNode(i))...) + if err != nil { + // return the parent cluster node and the error, so the caller can clean up. + return c, fmt.Errorf("run cluster node: %w", err) + } + + c.childNodes = append(c.childNodes, childNode) + } + } + + return c, nil +} + +// configureCluster configures the cluster settings, ensuring that the cluster is properly configured with the necessary network and options, +// avoiding duplicate application of options to be passed to the successive nodes. +func configureCluster(ctx context.Context, settings *options, opts []testcontainers.ContainerCustomizer) ([]testcontainers.ContainerCustomizer, error) { + var clusterOpts []testcontainers.ContainerCustomizer + if len(settings.nodeNames) == 0 { + return clusterOpts, nil + } + + // pass cluster options to each node + etcdOpts := []Option{} + for _, opt := range opts { + // if the option is of type Option, it won't be applied to the settings + // this prevents the same option from being applied multiple times (e.g. updating the current node) + if apply, ok := opt.(Option); ok { + etcdOpts = append(etcdOpts, apply) + } else { + clusterOpts = append(clusterOpts, opt) + } + } + + if settings.clusterNetwork == nil { // the first time the network is created + newNetwork, err := tcnetwork.New(ctx) + if err != nil { + return clusterOpts, fmt.Errorf("new network: %w", err) + } + + // set the network for the first node + settings.clusterNetwork = newNetwork + + clusterOpts = append(clusterOpts, withClusterNetwork(newNetwork)) // save the network for the next nodes + } + + // we finally need to re-apply all the etcd-specific options + clusterOpts = append(clusterOpts, withClusterOptions(etcdOpts)) + + return clusterOpts, nil +} + +// configureCMD configures the etcd command line arguments, based on the settings provided, +// in order to create a cluster or a single-node instance. +func configureCMD(settings options) []string { + cmds := []string{"etcd"} + + if len(settings.nodeNames) == 0 { + cmds = append(cmds, "--name=default") + } else { + clusterCmds := []string{ + "--name=" + settings.nodeNames[settings.currentNode], + "--initial-advertise-peer-urls=" + scheme + "://" + settings.nodeNames[settings.currentNode] + ":" + peerPort, + "--advertise-client-urls=" + scheme + "://" + settings.nodeNames[settings.currentNode] + ":" + clientPort, + "--listen-peer-urls=" + scheme + "://0.0.0.0:" + peerPort, + "--listen-client-urls=" + scheme + "://0.0.0.0:" + clientPort, + "--initial-cluster-state=new", + } + + clusterStateValues := make([]string, len(settings.nodeNames)) + for i, node := range settings.nodeNames { + clusterStateValues[i] = node + "=" + scheme + "://" + node + ":" + peerPort + } + clusterCmds = append(clusterCmds, "--initial-cluster="+strings.Join(clusterStateValues, ",")) + + if settings.clusterToken != "" { + clusterCmds = append(clusterCmds, "--initial-cluster-token="+settings.clusterToken) + } + + cmds = append(cmds, clusterCmds...) + } + + if settings.mountDataDir { + cmds = append(cmds, "--data-dir="+dataDir) + } + + cmds = append(cmds, settings.additionalArgs...) + + return cmds +} + +// ClientEndpoint returns the client endpoint for the etcd container, and an error if any. +// For a cluster, it returns the client endpoint of the first node. +func (c *EtcdContainer) ClientEndpoint(ctx context.Context) (string, error) { + host, err := c.Host(ctx) + if err != nil { + return "", err + } + + port, err := c.MappedPort(ctx, clientPort) + if err != nil { + return "", err + } + + return fmt.Sprintf("http://%s:%s", host, port.Port()), nil +} + +// ClientEndpoints returns the client endpoints for the etcd cluster. +func (c *EtcdContainer) ClientEndpoints(ctx context.Context) ([]string, error) { + endpoint, err := c.ClientEndpoint(ctx) + if err != nil { + return nil, err + } + + endpoints := []string{endpoint} + + for _, node := range c.childNodes { + endpoint, err := node.ClientEndpoint(ctx) + if err != nil { + return nil, err + } + endpoints = append(endpoints, endpoint) + } + + return endpoints, nil +} + +// PeerEndpoint returns the peer endpoint for the etcd container, and an error if any. +// For a cluster, it returns the peer endpoint of the first node. +func (c *EtcdContainer) PeerEndpoint(ctx context.Context) (string, error) { + host, err := c.Host(ctx) + if err != nil { + return "", err + } + + port, err := c.MappedPort(ctx, peerPort) + if err != nil { + return "", err + } + + return fmt.Sprintf("http://%s:%s", host, port.Port()), nil +} + +// PeerEndpoints returns the peer endpoints for the etcd cluster. +func (c *EtcdContainer) PeerEndpoints(ctx context.Context) ([]string, error) { + endpoint, err := c.PeerEndpoint(ctx) + if err != nil { + return nil, err + } + + endpoints := []string{endpoint} + + for _, node := range c.childNodes { + endpoint, err := node.PeerEndpoint(ctx) + if err != nil { + return nil, err + } + endpoints = append(endpoints, endpoint) + } + + return endpoints, nil +} diff --git a/modules/etcd/etcd_test.go b/modules/etcd/etcd_test.go new file mode 100644 index 0000000000..5095ba8429 --- /dev/null +++ b/modules/etcd/etcd_test.go @@ -0,0 +1,60 @@ +package etcd_test + +import ( + "context" + "io" + "testing" + "time" + + "github.com/stretchr/testify/require" + clientv3 "go.etcd.io/etcd/client/v3" + + "github.com/testcontainers/testcontainers-go" + tcexec "github.com/testcontainers/testcontainers-go/exec" + "github.com/testcontainers/testcontainers-go/modules/etcd" +) + +func TestRun(t *testing.T) { + ctx := context.Background() + + ctr, err := etcd.Run(ctx, "gcr.io/etcd-development/etcd:v3.5.14") + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) + + c, r, err := ctr.Exec(ctx, []string{"etcdctl", "member", "list"}, tcexec.Multiplexed()) + require.NoError(t, err) + require.Equal(t, 0, c) + + output, err := io.ReadAll(r) + require.NoError(t, err) + require.Contains(t, string(output), "default") +} + +func TestRun_PutGet(t *testing.T) { + ctx := context.Background() + + ctr, err := etcd.Run(ctx, "gcr.io/etcd-development/etcd:v3.5.14", etcd.WithNodes("etcd-1", "etcd-2", "etcd-3")) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) + + clientEndpoints, err := ctr.ClientEndpoints(ctx) + require.NoError(t, err) + + cli, err := clientv3.New(clientv3.Config{ + Endpoints: clientEndpoints, + DialTimeout: 5 * time.Second, + }) + require.NoError(t, err) + defer cli.Close() + + ctx, cancel := context.WithTimeout(ctx, 2*time.Second) + defer cancel() + _, err = cli.Put(ctx, "sample_key", "sample_value") + require.NoError(t, err) + + resp, err := cli.Get(ctx, "sample_key") + require.NoError(t, err) + + require.Len(t, resp.Kvs, 1) + require.Equal(t, "sample_value", string(resp.Kvs[0].Value)) +} diff --git a/modules/etcd/etcd_unit_test.go b/modules/etcd/etcd_unit_test.go new file mode 100644 index 0000000000..d32b9519f7 --- /dev/null +++ b/modules/etcd/etcd_unit_test.go @@ -0,0 +1,113 @@ +package etcd + +import ( + "context" + "fmt" + "io" + "testing" + + "github.com/docker/docker/api/types/network" + "github.com/docker/docker/errdefs" + "github.com/stretchr/testify/require" + + "github.com/testcontainers/testcontainers-go" + tcexec "github.com/testcontainers/testcontainers-go/exec" + tcnetwork "github.com/testcontainers/testcontainers-go/network" +) + +func TestRunCluster1Node(t *testing.T) { + ctx := context.Background() + + ctr, err := Run(ctx, "gcr.io/etcd-development/etcd:v3.5.14") + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) + + // the topology has only one node with no children + require.Empty(t, ctr.childNodes) + require.Equal(t, defaultClusterToken, ctr.opts.clusterToken) +} + +func TestRunClusterMultipleNodes(t *testing.T) { + t.Run("2-nodes", testCluster(t, "etcd-1", "etcd-2")) + t.Run("3-nodes", testCluster(t, "etcd-1", "etcd-2", "etcd-3")) +} + +func TestTerminate(t *testing.T) { + ctx := context.Background() + + ctr, err := Run(ctx, "gcr.io/etcd-development/etcd:v3.5.14", WithNodes("etcd-1", "etcd-2", "etcd-3")) + require.NoError(t, err) + require.NoError(t, ctr.Terminate(ctx)) + + // verify that the network and the containers does no longer exist + + cli, err := testcontainers.NewDockerClientWithOpts(context.Background()) + require.NoError(t, err) + defer cli.Close() + + _, err = cli.ContainerInspect(context.Background(), ctr.GetContainerID()) + require.True(t, errdefs.IsNotFound(err)) + + for _, child := range ctr.childNodes { + _, err := cli.ContainerInspect(context.Background(), child.GetContainerID()) + require.True(t, errdefs.IsNotFound(err)) + } + + _, err = cli.NetworkInspect(context.Background(), ctr.opts.clusterNetwork.ID, network.InspectOptions{}) + require.True(t, errdefs.IsNotFound(err)) +} + +func TestTerminate_partiallyInitialised(t *testing.T) { + newNetwork, err := tcnetwork.New(context.Background()) + require.NoError(t, err) + + ctr := &EtcdContainer{ + opts: options{ + clusterNetwork: newNetwork, + }, + } + + require.NoError(t, ctr.Terminate(context.Background())) + + cli, err := testcontainers.NewDockerClientWithOpts(context.Background()) + require.NoError(t, err) + defer cli.Close() + + _, err = cli.NetworkInspect(context.Background(), ctr.opts.clusterNetwork.ID, network.InspectOptions{}) + require.True(t, errdefs.IsNotFound(err)) +} + +// testCluster is a helper function to test the creation of an etcd cluster with the specified nodes. +func testCluster(t *testing.T, node1 string, node2 string, nodes ...string) func(t *testing.T) { + t.Helper() + + return func(tt *testing.T) { + const clusterToken string = "My-cluster-t0k3n" + + ctx := context.Background() + + ctr, err := Run(ctx, "gcr.io/etcd-development/etcd:v3.5.14", WithNodes(node1, node2, nodes...), WithClusterToken(clusterToken)) + testcontainers.CleanupContainer(t, ctr) + require.NoError(tt, err) + + require.Equal(tt, clusterToken, ctr.opts.clusterToken) + + // the topology has one parent node, one child node and optionally more child nodes + // depending on the number of nodes specified + require.Len(tt, ctr.childNodes, 1+len(nodes)) + + for i, node := range ctr.childNodes { + require.Empty(t, node.childNodes) // child nodes has no children + + c, r, err := node.Exec(ctx, []string{"etcdctl", "member", "list"}, tcexec.Multiplexed()) + require.NoError(tt, err) + + output, err := io.ReadAll(r) + require.NoError(t, err) + require.Contains(t, string(output), fmt.Sprintf("etcd-%d", i+1)) + + require.Zero(tt, c) + require.Equal(tt, clusterToken, node.opts.clusterToken) + } + } +} diff --git a/modules/etcd/examples_test.go b/modules/etcd/examples_test.go new file mode 100644 index 0000000000..950d5ecd05 --- /dev/null +++ b/modules/etcd/examples_test.go @@ -0,0 +1,97 @@ +package etcd_test + +import ( + "context" + "fmt" + "log" + "time" + + clientv3 "go.etcd.io/etcd/client/v3" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/etcd" +) + +func ExampleRun() { + // runetcdContainer { + ctx := context.Background() + + etcdContainer, err := etcd.Run(ctx, "gcr.io/etcd-development/etcd:v3.5.14") + defer func() { + if err := testcontainers.TerminateContainer(etcdContainer); err != nil { + log.Printf("failed to terminate container: %s", err) + } + }() + if err != nil { + log.Printf("failed to start container: %s", err) + return + } + // } + + state, err := etcdContainer.State(ctx) + if err != nil { + log.Printf("failed to get container state: %s", err) + return + } + + fmt.Println(state.Running) + + // Output: + // true +} + +func ExampleRun_cluster() { + ctx := context.Background() + + ctr, err := etcd.Run(ctx, "gcr.io/etcd-development/etcd:v3.5.14", etcd.WithNodes("etcd-1", "etcd-2", "etcd-3")) + if err != nil { + log.Printf("failed to start container: %s", err) + return + } + defer func() { + if err := testcontainers.TerminateContainer(ctr); err != nil { + log.Printf("failed to terminate container: %s", err) + } + }() + + clientEndpoints, err := ctr.ClientEndpoints(ctx) + if err != nil { + log.Printf("failed to get client endpoints: %s", err) + return + } + + // we have 3 nodes, 1 cluster node and 2 child nodes + fmt.Println(len(clientEndpoints)) + + cli, err := clientv3.New(clientv3.Config{ + Endpoints: clientEndpoints, + DialTimeout: 5 * time.Second, + }) + if err != nil { + log.Printf("failed to create etcd client: %s", err) + return + } + defer cli.Close() + + ctx, cancel := context.WithTimeout(ctx, 2*time.Second) + defer cancel() + _, err = cli.Put(ctx, "sample_key", "sample_value") + if err != nil { + log.Printf("failed to put key: %s", err) + return + } + + resp, err := cli.Get(ctx, "sample_key") + if err != nil { + log.Printf("failed to get key: %s", err) + return + } + + fmt.Println(len(resp.Kvs)) + fmt.Println(string(resp.Kvs[0].Value)) + + // Output: + // 3 + // 1 + // sample_value +} diff --git a/modules/etcd/go.mod b/modules/etcd/go.mod new file mode 100644 index 0000000000..fb5e7f39d8 --- /dev/null +++ b/modules/etcd/go.mod @@ -0,0 +1,75 @@ +module github.com/testcontainers/testcontainers-go/modules/etcd + +go 1.22 + +require ( + github.com/docker/docker v27.1.1+incompatible + github.com/stretchr/testify v1.9.0 + github.com/testcontainers/testcontainers-go v0.33.0 + go.etcd.io/etcd/client/v3 v3.5.16 +) + +require ( + dario.cat/mergo v1.0.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/containerd/containerd v1.7.18 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/coreos/go-semver v0.3.0 // indirect + github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/compress v1.17.4 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.5.0 // indirect + github.com/moby/sys/user v0.1.0 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/shirou/gopsutil/v3 v3.23.12 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/yusufpapurcu/wmi v1.2.3 // indirect + go.etcd.io/etcd/api/v3 v3.5.16 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.5.16 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + go.opentelemetry.io/otel v1.24.0 // indirect + go.opentelemetry.io/otel/metric v1.24.0 // indirect + go.opentelemetry.io/otel/trace v1.24.0 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + go.uber.org/zap v1.17.0 // indirect + golang.org/x/crypto v0.24.0 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect + google.golang.org/grpc v1.64.1 // indirect + google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/testcontainers/testcontainers-go => ../.. diff --git a/modules/etcd/go.sum b/modules/etcd/go.sum new file mode 100644 index 0000000000..3de88cadb9 --- /dev/null +++ b/modules/etcd/go.sum @@ -0,0 +1,216 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/containerd/containerd v1.7.18 h1:jqjZTQNfXGoEaZdW1WwPU0RqSn1Bm2Ay/KJPUuO8nao= +github.com/containerd/containerd v1.7.18/go.mod h1:IYEk9/IO6wAPUz2bCMVUbsfXjzw5UNP5fLz4PsUygQ4= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY= +github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= +github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= +github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= +github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= +github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= +github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= +github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.etcd.io/etcd/api/v3 v3.5.16 h1:WvmyJVbjWqK4R1E+B12RRHz3bRGy9XVfh++MgbN+6n0= +go.etcd.io/etcd/api/v3 v3.5.16/go.mod h1:1P4SlIP/VwkDmGo3OlOD7faPeP8KDIFhqvciH5EfN28= +go.etcd.io/etcd/client/pkg/v3 v3.5.16 h1:ZgY48uH6UvB+/7R9Yf4x574uCO3jIx0TRDyetSfId3Q= +go.etcd.io/etcd/client/pkg/v3 v3.5.16/go.mod h1:V8acl8pcEK0Y2g19YlOV9m9ssUe6MgiDSobSoaBAM0E= +go.etcd.io/etcd/client/v3 v3.5.16 h1:sSmVYOAHeC9doqi0gv7v86oY/BTld0SEFGaxsU9eRhE= +go.etcd.io/etcd/client/v3 v3.5.16/go.mod h1:X+rExSGkyqxvu276cr2OwPLBaeqFu1cIl4vmRjAD/50= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= +go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= +go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= +go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= +go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= +go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 h1:RFiFrvy37/mpSpdySBDrUdipW/dHwsRwh3J3+A9VgT4= +google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237/go.mod h1:Z5Iiy3jtmioajWHDGFk7CeugTyHtPvMHA4UTmUkyalE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= +google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= diff --git a/modules/etcd/options.go b/modules/etcd/options.go new file mode 100644 index 0000000000..1359e4a3b4 --- /dev/null +++ b/modules/etcd/options.go @@ -0,0 +1,107 @@ +package etcd + +import ( + "context" + "fmt" + + "github.com/testcontainers/testcontainers-go" + tcexec "github.com/testcontainers/testcontainers-go/exec" +) + +type options struct { + currentNode int + clusterNetwork *testcontainers.DockerNetwork + nodeNames []string + clusterToken string + additionalArgs []string + mountDataDir bool // flag needed to avoid extra calculations with the lifecycle hooks + containerRequest *testcontainers.ContainerRequest +} + +func defaultOptions(req *testcontainers.ContainerRequest) options { + return options{ + clusterToken: defaultClusterToken, + containerRequest: req, + } +} + +// Compiler check to ensure that Option implements the testcontainers.ContainerCustomizer interface. +var _ testcontainers.ContainerCustomizer = (Option)(nil) + +// Option is an option for the Etcd container. +type Option func(*options) + +// Customize is a NOOP. It's defined to satisfy the testcontainers.ContainerCustomizer interface. +func (o Option) Customize(*testcontainers.GenericContainerRequest) error { + // NOOP to satisfy interface. + return nil +} + +// WithAdditionalArgs is an option to pass additional arguments to the etcd container. +// They will be appended last to the command line. +func WithAdditionalArgs(args ...string) Option { + return func(o *options) { + o.additionalArgs = args + } +} + +// WithDataDir is an option to mount the data directory, which is located at /data.etcd. +// The option will add a lifecycle hook to the container to change the permissions of the data directory. +func WithDataDir() Option { + return func(o *options) { + // Avoid extra calculations with the lifecycle hooks + o.mountDataDir = true + + o.containerRequest.LifecycleHooks = append(o.containerRequest.LifecycleHooks, testcontainers.ContainerLifecycleHooks{ + PostStarts: []testcontainers.ContainerHook{ + func(ctx context.Context, c testcontainers.Container) error { + _, _, err := c.Exec(ctx, []string{"chmod", "o+rwx", "-R", dataDir}, tcexec.Multiplexed()) + if err != nil { + return fmt.Errorf("chmod etcd data dir: %w", err) + } + + return nil + }, + }, + }) + } +} + +// WithNodes is an option to set the nodes of the etcd cluster. +// It should be used to create a cluster with more than one node. +func WithNodes(node1 string, node2 string, nodes ...string) Option { + return func(o *options) { + o.nodeNames = append([]string{node1, node2}, nodes...) + } +} + +// withCurrentNode is an option to set the current node index. +// It's an internal option and should not be used by the user. +func withCurrentNode(i int) Option { + return func(o *options) { + o.currentNode = i + } +} + +// withClusterNetwork is an option to set the cluster network. +// It's an internal option and should not be used by the user. +func withClusterNetwork(n *testcontainers.DockerNetwork) Option { + return func(o *options) { + o.clusterNetwork = n + } +} + +// WithClusterToken is an option to set the cluster token. +func WithClusterToken(token string) Option { + return func(o *options) { + o.clusterToken = token + } +} + +func withClusterOptions(opts []Option) Option { + return func(o *options) { + for _, opt := range opts { + opt(o) + } + } +} diff --git a/sonar-project.properties b/sonar-project.properties index 8c7946ed8e..1b5cb92513 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -18,4 +18,4 @@ sonar.test.inclusions=**/*_test.go sonar.test.exclusions=**/vendor/** sonar.go.coverage.reportPaths=**/coverage.out -sonar.go.tests.reportPaths=TEST-unit.xml,examples/nginx/TEST-unit.xml,examples/toxiproxy/TEST-unit.xml,modulegen/TEST-unit.xml,modules/artemis/TEST-unit.xml,modules/azurite/TEST-unit.xml,modules/cassandra/TEST-unit.xml,modules/chroma/TEST-unit.xml,modules/clickhouse/TEST-unit.xml,modules/cockroachdb/TEST-unit.xml,modules/compose/TEST-unit.xml,modules/consul/TEST-unit.xml,modules/couchbase/TEST-unit.xml,modules/databend/TEST-unit.xml,modules/dolt/TEST-unit.xml,modules/dynamodb/TEST-unit.xml,modules/elasticsearch/TEST-unit.xml,modules/gcloud/TEST-unit.xml,modules/grafana-lgtm/TEST-unit.xml,modules/inbucket/TEST-unit.xml,modules/influxdb/TEST-unit.xml,modules/k3s/TEST-unit.xml,modules/k6/TEST-unit.xml,modules/kafka/TEST-unit.xml,modules/localstack/TEST-unit.xml,modules/mariadb/TEST-unit.xml,modules/milvus/TEST-unit.xml,modules/minio/TEST-unit.xml,modules/mockserver/TEST-unit.xml,modules/mongodb/TEST-unit.xml,modules/mssql/TEST-unit.xml,modules/mysql/TEST-unit.xml,modules/nats/TEST-unit.xml,modules/neo4j/TEST-unit.xml,modules/ollama/TEST-unit.xml,modules/openfga/TEST-unit.xml,modules/openldap/TEST-unit.xml,modules/opensearch/TEST-unit.xml,modules/postgres/TEST-unit.xml,modules/pulsar/TEST-unit.xml,modules/qdrant/TEST-unit.xml,modules/rabbitmq/TEST-unit.xml,modules/redis/TEST-unit.xml,modules/redpanda/TEST-unit.xml,modules/registry/TEST-unit.xml,modules/surrealdb/TEST-unit.xml,modules/valkey/TEST-unit.xml,modules/vault/TEST-unit.xml,modules/vearch/TEST-unit.xml,modules/weaviate/TEST-unit.xml +sonar.go.tests.reportPaths=TEST-unit.xml,examples/nginx/TEST-unit.xml,examples/toxiproxy/TEST-unit.xml,modulegen/TEST-unit.xml,modules/artemis/TEST-unit.xml,modules/azurite/TEST-unit.xml,modules/cassandra/TEST-unit.xml,modules/chroma/TEST-unit.xml,modules/clickhouse/TEST-unit.xml,modules/cockroachdb/TEST-unit.xml,modules/compose/TEST-unit.xml,modules/consul/TEST-unit.xml,modules/couchbase/TEST-unit.xml,modules/databend/TEST-unit.xml,modules/dolt/TEST-unit.xml,modules/dynamodb/TEST-unit.xml,modules/elasticsearch/TEST-unit.xml,modules/etcd/TEST-unit.xml,modules/gcloud/TEST-unit.xml,modules/grafana-lgtm/TEST-unit.xml,modules/inbucket/TEST-unit.xml,modules/influxdb/TEST-unit.xml,modules/k3s/TEST-unit.xml,modules/k6/TEST-unit.xml,modules/kafka/TEST-unit.xml,modules/localstack/TEST-unit.xml,modules/mariadb/TEST-unit.xml,modules/milvus/TEST-unit.xml,modules/minio/TEST-unit.xml,modules/mockserver/TEST-unit.xml,modules/mongodb/TEST-unit.xml,modules/mssql/TEST-unit.xml,modules/mysql/TEST-unit.xml,modules/nats/TEST-unit.xml,modules/neo4j/TEST-unit.xml,modules/ollama/TEST-unit.xml,modules/openfga/TEST-unit.xml,modules/openldap/TEST-unit.xml,modules/opensearch/TEST-unit.xml,modules/postgres/TEST-unit.xml,modules/pulsar/TEST-unit.xml,modules/qdrant/TEST-unit.xml,modules/rabbitmq/TEST-unit.xml,modules/redis/TEST-unit.xml,modules/redpanda/TEST-unit.xml,modules/registry/TEST-unit.xml,modules/surrealdb/TEST-unit.xml,modules/valkey/TEST-unit.xml,modules/vault/TEST-unit.xml,modules/vearch/TEST-unit.xml,modules/weaviate/TEST-unit.xml