Skip to content

Commit

Permalink
chore: support for adding runnable components
Browse files Browse the repository at this point in the history
  • Loading branch information
mdelapenya committed Sep 25, 2023
1 parent 03f5ac9 commit 56aadbd
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 30 deletions.
5 changes: 5 additions & 0 deletions docs/modules/dapr.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -66,8 +67,12 @@ The `Component` struct has the following fields:
<!--/codeinclude-->

- 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:
Expand Down
84 changes: 69 additions & 15 deletions modules/dapr/dapr.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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{
Expand All @@ -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
}
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions modules/dapr/dapr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"

"github.com/testcontainers/testcontainers-go"
)

Expand Down
3 changes: 1 addition & 2 deletions modules/dapr/examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
31 changes: 18 additions & 13 deletions modules/dapr/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -24,8 +23,7 @@ func defaultOptions() options {
Components: map[string]Component{
inMemoryStore.Key(): inMemoryStore,
},
ComponentsPath: defaultComponentsPath,
NetworkName: defaultDaprNetworkName,
NetworkName: defaultDaprNetworkName,
}
}

Expand Down Expand Up @@ -58,6 +56,7 @@ func WithNetworkName(name string) Option {
type Component struct {
Name string
Type string
Image string
Metadata map[string]string
}

Expand Down Expand Up @@ -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,
Expand All @@ -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) {
Expand All @@ -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
}
}

0 comments on commit 56aadbd

Please sign in to comment.