From 56aadbde86d9ef4441a8661bb51e8b7b03a52c76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20de=20la=20Pe=C3=B1a?= Date: Mon, 25 Sep 2023 17:29:08 +0200 Subject: [PATCH] chore: support for adding runnable components --- docs/modules/dapr.md | 5 +++ modules/dapr/dapr.go | 84 ++++++++++++++++++++++++++++------- modules/dapr/dapr_test.go | 1 + modules/dapr/examples_test.go | 3 +- modules/dapr/options.go | 31 +++++++------ 5 files changed, 94 insertions(+), 30 deletions(-) diff --git a/docs/modules/dapr.md b/docs/modules/dapr.md index 664ead1e3a..2f41cb14ef 100644 --- a/docs/modules/dapr.md +++ b/docs/modules/dapr.md @@ -35,6 +35,7 @@ This entrypoint function will create: - a Dapr network in which the Dapr container and all its components will be attached. Default name is `dapr-network`. - a Dapr container. +- A component container for each component that has a Docker image defined. See [Components](#components) for more information. ### Container Options @@ -66,8 +67,12 @@ The `Component` struct has the following fields: - The key used to internally identify a Component is the component name. E.g. `statestore`. +- The image is the Docker image used by the component. E.g. `redis:6-alpine`. - Metadata it's a map of strings, where the key is the metadata name and the value is the metadata value. It will be used to render a YAML file with the component configuration. +!!! info + Those components with a Docker image will be run as a separate container in the Dapr network, and using Dapr's container ID as the container network mode. + Each component will result in a configuration file that will be uploaded to the Dapr container, under the `/components` directory. It's possible to change this file path with the `WithComponentsPath(path string)` functional option. If not passed, the default value is `/components`. The file will be named as the component name, and the content will be a YAML file with the following structure: diff --git a/modules/dapr/dapr.go b/modules/dapr/dapr.go index c2a478ae58..8c623962b5 100644 --- a/modules/dapr/dapr.go +++ b/modules/dapr/dapr.go @@ -8,7 +8,9 @@ import ( "path/filepath" "time" + "github.com/docker/docker/api/types/container" "github.com/docker/go-connections/nat" + "github.com/testcontainers/testcontainers-go" ) @@ -33,8 +35,9 @@ var ( // DaprContainer represents the Dapr container type used in the module type DaprContainer struct { testcontainers.Container - Network testcontainers.Network - Settings options + Network testcontainers.Network + ComponentContainers map[string]testcontainers.Container + Settings options } // GRPCPort returns the port used by the Dapr container @@ -47,20 +50,35 @@ func (c *DaprContainer) GRPCPort(ctx context.Context) (int, error) { return port.Int(), nil } -// Terminate terminates the Dapr container and removes the Dapr network +// Terminate terminates the Dapr container and removes the component containers and the Dapr network, +// in that particular order. func (c *DaprContainer) Terminate(ctx context.Context) error { if err := c.Container.Terminate(ctx); err != nil { - return err + return fmt.Errorf("failed to terminate Dapr container %w", err) + } + + for key, componentContainer := range c.ComponentContainers { + // do not terminate the component container if it has no image defined + if c.Settings.Components[key].Image == "" { + continue + } + + if err := componentContainer.Terminate(ctx); err != nil { + return fmt.Errorf("failed to terminate component container %w", err) + } } if err := c.Network.Remove(ctx); err != nil { - return err + return fmt.Errorf("failed to terminate Dapr network %w", err) } return nil } -// RunContainer creates an instance of the Dapr container type +// RunContainer creates an instance of the Dapr container type, creating the following elements: +// - a Dapr network +// - a Dapr container +// - a component container for each component defined in the options. The component must have an image defined. func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomizer) (*DaprContainer, error) { componentsTmpDir = filepath.Join(os.TempDir(), fmt.Sprintf("%d", time.Now().UnixMilli()), "components") err := os.MkdirAll(componentsTmpDir, 0o700) @@ -96,7 +114,7 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize return nil, err } - genericContainerReq.Cmd = []string{"./daprd", "-app-id", settings.AppName, "--dapr-listen-addresses=0.0.0.0", "-components-path", settings.ComponentsPath} + genericContainerReq.Cmd = []string{"./daprd", "-app-id", settings.AppName, "--dapr-listen-addresses=0.0.0.0", "-components-path", defaultComponentsPath} nw, err := testcontainers.GenericNetwork(ctx, testcontainers.GenericNetworkRequest{ NetworkRequest: testcontainers.NetworkRequest{ @@ -114,26 +132,62 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize settings.NetworkName: {settings.AppName}, } - container, err := testcontainers.GenericContainer(ctx, genericContainerReq) + daprContainer, err := testcontainers.GenericContainer(ctx, genericContainerReq) if err != nil { return nil, err } + // we must start the component containers in container mode, so that they can connect to the Dapr container + networkMode := fmt.Sprintf("container:%v", daprContainer.GetContainerID()) + + componentContainers := map[string]testcontainers.Container{} + for _, component := range settings.Components { + if component.Image == "" { + continue + } + + componentContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Image: component.Image, + Networks: []string{settings.NetworkName}, + NetworkAliases: map[string][]string{ + settings.NetworkName: {component.Name}, + }, + HostConfigModifier: func(hc *container.HostConfig) { + hc.NetworkMode = container.NetworkMode(networkMode) + }, + }, + Started: true, + }) + if err != nil { + return nil, err + } + + componentContainers[component.Key()] = componentContainer + } + return &DaprContainer{ - Container: container, - Settings: settings, - Network: nw, + Container: daprContainer, + Settings: settings, + ComponentContainers: componentContainers, + Network: nw, }, nil } // renderComponents renders the configuration file for each component, creating a temporary file for each one under a default -// temporary directory. The entire directory is then uploaded to the container. +// temporary directory. The entire directory is then uploaded to the container, including the +// right permissions (0o777) for Dapr to access the files. func renderComponents(settings options, req *testcontainers.GenericContainerRequest) error { + execPermissions := os.FileMode(0o777) + for _, component := range settings.Components { content, err := component.Render() + if err != nil { + return err + } tmpComponentFile := filepath.Join(componentsTmpDir, component.FileName()) - err = os.WriteFile(tmpComponentFile, content, 0o600) + err = os.WriteFile(tmpComponentFile, content, execPermissions) if err != nil { return err } @@ -142,8 +196,8 @@ func renderComponents(settings options, req *testcontainers.GenericContainerRequ req.Files = append(req.Files, testcontainers.ContainerFile{ HostFilePath: componentsTmpDir, - ContainerFilePath: settings.ComponentsPath, - FileMode: 0o600, + ContainerFilePath: defaultComponentsPath, + FileMode: int64(execPermissions), }) return nil diff --git a/modules/dapr/dapr_test.go b/modules/dapr/dapr_test.go index 64c616f7fc..e7072efbe5 100644 --- a/modules/dapr/dapr_test.go +++ b/modules/dapr/dapr_test.go @@ -6,6 +6,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" + "github.com/testcontainers/testcontainers-go" ) diff --git a/modules/dapr/examples_test.go b/modules/dapr/examples_test.go index b35d8ace70..5570483422 100644 --- a/modules/dapr/examples_test.go +++ b/modules/dapr/examples_test.go @@ -18,9 +18,8 @@ func ExampleRunContainer() { dapr.WithNetworkName("dapr-network"), dapr.WithComponents( dapr.NewComponent("pubsub", "pubsub.in-memory", map[string]string{"foo": "bar", "bar": "baz"}), - dapr.NewComponent("statestore", "statestore.in-memory", map[string]string{"baz": "qux", "quux": "quuz"}), + dapr.NewComponentWithImage("statestore", "state.redis", "redis:6-alpine", map[string]string{"baz": "qux", "quux": "quuz"}), ), - dapr.WithComponentsPath("/components"), ) if err != nil { panic(err) diff --git a/modules/dapr/options.go b/modules/dapr/options.go index c62cb55e79..08e39b0266 100644 --- a/modules/dapr/options.go +++ b/modules/dapr/options.go @@ -9,10 +9,9 @@ import ( ) type options struct { - AppName string - Components map[string]Component - ComponentsPath string - NetworkName string + AppName string + Components map[string]Component + NetworkName string } // defaultOptions returns the default options for the Dapr container, including an in-memory state store. @@ -24,8 +23,7 @@ func defaultOptions() options { Components: map[string]Component{ inMemoryStore.Key(): inMemoryStore, }, - ComponentsPath: defaultComponentsPath, - NetworkName: defaultDaprNetworkName, + NetworkName: defaultDaprNetworkName, } } @@ -58,6 +56,7 @@ func WithNetworkName(name string) Option { type Component struct { Name string Type string + Image string Metadata map[string]string } @@ -89,6 +88,8 @@ func (c Component) Render() ([]byte, error) { return componentConfig.Bytes(), nil } +// NewComponentWithImage returns a new Component without its Docker image. +// Those components without a Docker image won't be run as a separate container in the Dapr network. func NewComponent(name string, t string, metadata map[string]string) Component { return Component{ Name: name, @@ -97,6 +98,17 @@ func NewComponent(name string, t string, metadata map[string]string) Component { } } +// NewComponentWithImage returns a new Component including its Docker image. +// Those components with a Docker image will be run as a separate container in the Dapr network, +// and using Dapr's container ID as the container network mode. +func NewComponentWithImage(name string, t string, image string, metadata map[string]string) Component { + c := NewComponent(name, t, metadata) + + c.Image = image + + return c +} + // WithComponents defines the components added to the dapr config, using a variadic list of Component. func WithComponents(component ...Component) Option { return func(o *options) { @@ -105,10 +117,3 @@ func WithComponents(component ...Component) Option { } } } - -// WithComponentsPath defines the container path where the components will be stored. -func WithComponentsPath(path string) Option { - return func(o *options) { - o.ComponentsPath = path - } -}