Skip to content

Add inspect command #95

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,23 @@ $ builder-playground cook l1 --latest-fork --output ~/my-builder-testnet --genes

To stop the playground, press `Ctrl+C`.

## Inspect

Builder-playground supports inspecting the connection of a service to a specific port.

```bash
$ builder-playground inspect <service> <port>
```

Example:

```bash
$ builder-playground cook opstack
$ builder-playground inspect op-geth authrpc
```

This command starts a `tcpflow` container in the same network interface as the service and captures the traffic to the specified port.

## Internals

### Execution Flow
Expand Down
127 changes: 127 additions & 0 deletions internal/inspect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package internal

import (
"context"
"fmt"
"io"
"os"

"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
)

// Inspect incldues the logic for the inspect command
func Inspect(ctx context.Context, serviceName, portName string) error {
client, err := newDockerClient()
if err != nil {
return fmt.Errorf("failed to create docker client: %w", err)
}

serviceID, portNum, err := retrieveContainerDetails(client, serviceName, portName)
if err != nil {
return fmt.Errorf("failed to retrieve container details: %w", err)
}

return runTcpFlow(ctx, client, serviceID, portNum)
}

func retrieveContainerDetails(client *client.Client, serviceName, portName string) (string, string, error) {
// Get the service by name
containers, err := client.ContainerList(context.Background(), container.ListOptions{
Filters: filters.NewArgs(filters.Arg("label", "service="+serviceName)),
All: true,
})
if err != nil {
return "", "", fmt.Errorf("error getting container list: %w", err)
}

size := len(containers)
if size == 0 {
return "", "", fmt.Errorf("no containers found for service %s", serviceName)
} else if size > 1 {
return "", "", fmt.Errorf("multiple containers found for service %s", serviceName)
}

container := containers[0]

// Get the container details to find the port mapping in the labels as port.<name>
containerDetails, err := client.ContainerInspect(context.Background(), container.ID)
if err != nil {
return "", "", fmt.Errorf("error inspecting container %s: %w", container.ID, err)
}

// Check if the port name is in the labels
portLabel := fmt.Sprintf("port.%s", portName)
portNum, ok := containerDetails.Config.Labels[portLabel]
if !ok {
return "", "", fmt.Errorf("port %s not found in container %s", portName, container.ID)
}

return container.ID, portNum, nil
}

func runTcpFlow(ctx context.Context, client *client.Client, containerID, portName string) error {
// Create container config for tcpflow
config := &container.Config{
Image: "appropriate/tcpflow:latest",
Cmd: []string{"-c", "-p", "-i", "eth0", "port", portName},
Tty: true,
AttachStdout: true,
AttachStderr: true,
}

// Host config with network mode and capabilities
hostConfig := &container.HostConfig{
NetworkMode: container.NetworkMode("container:" + containerID),
CapAdd: []string{"NET_ADMIN"},
}

// Create the container
resp, err := client.ContainerCreate(ctx, config, hostConfig, nil, nil, "")
if err != nil {
return fmt.Errorf("failed to create container: %w", err)
}

// Start the container
if err := client.ContainerStart(ctx, resp.ID, container.StartOptions{}); err != nil {
return fmt.Errorf("failed to start container: %w", err)
}

// Get container logs and stream them
logOptions := container.LogsOptions{
ShowStdout: true,
ShowStderr: true,
Follow: true,
Timestamps: false,
}

logs, err := client.ContainerLogs(ctx, resp.ID, logOptions)
if err != nil {
return fmt.Errorf("failed to get container logs: %w", err)
}
defer logs.Close()

// Start copying logs to stdout
go func() {
_, err := io.Copy(os.Stdout, logs)
if err != nil && err != io.EOF {
fmt.Fprintf(os.Stderr, "Error copying logs: %v\n", err)
}
}()

// Wait for interrupt signal
<-ctx.Done()

// Cleanup: stop and remove the container
timeout := 5
if err := client.ContainerStop(context.Background(), resp.ID, container.StopOptions{Timeout: &timeout}); err != nil {
fmt.Fprintf(os.Stderr, "Error stopping container: %v\n", err)
}

if err := client.ContainerRemove(context.Background(), resp.ID, container.RemoveOptions{Force: true}); err != nil {
fmt.Fprintf(os.Stderr, "Error removing container: %v\n", err)
}

return nil
}
29 changes: 25 additions & 4 deletions internal/local_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,19 @@ type taskUI struct {
style lipgloss.Style
}

func NewLocalRunner(out *output, manifest *Manifest, overrides map[string]string, interactive bool) (*LocalRunner, error) {
func newDockerClient() (*client.Client, error) {
client, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return nil, fmt.Errorf("failed to create docker client: %w", err)
}
return client, nil
}

func NewLocalRunner(out *output, manifest *Manifest, overrides map[string]string, interactive bool) (*LocalRunner, error) {
client, err := newDockerClient()
if err != nil {
return nil, fmt.Errorf("failed to create docker client: %w", err)
}

// merge the overrides with the manifest overrides
if overrides == nil {
Expand Down Expand Up @@ -494,6 +502,21 @@ func (d *LocalRunner) toDockerComposeService(s *service) (map[string]interface{}
return nil, fmt.Errorf("failed to validate image %s: %w", imageName, err)
}

labels := map[string]string{
// It is important to use the playground label to identify the containers
// during the cleanup process
"playground": "true",
"service": s.Name,
}

// add the local ports exposed by the service as labels
// we have to do this for now since we do not store the manifest in JSON yet.
// Otherwise, we could use that directly
for _, port := range s.ports {
labels[fmt.Sprintf("port.%s", port.Name)] = fmt.Sprintf("%d", port.Port)
}

// add the ports to the labels as well
service := map[string]interface{}{
"image": imageName,
"command": args,
Expand All @@ -503,9 +526,7 @@ func (d *LocalRunner) toDockerComposeService(s *service) (map[string]interface{}
},
// Add the ethereum network
"networks": []string{networkName},
// It is important to use the playground label to identify the containers
// during the cleanup process
"labels": map[string]string{"playground": "true"},
"labels": labels,
}

if len(envs) > 0 {
Expand Down
28 changes: 28 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,33 @@ var artifactsAllCmd = &cobra.Command{
},
}

var inspectCmd = &cobra.Command{
Use: "inspect",
Short: "Inspect a connection between two services",
RunE: func(cmd *cobra.Command, args []string) error {
// two arguments, the name of the service and the name of the connection
if len(args) != 2 {
return fmt.Errorf("please specify a service name and a connection name")
}
serviceName := args[0]
connectionName := args[1]

sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt)

ctx, cancel := context.WithCancel(context.Background())
go func() {
<-sig
cancel()
}()

if err := internal.Inspect(ctx, serviceName, connectionName); err != nil {
return fmt.Errorf("failed to inspect connection: %w", err)
}
return nil
},
}

var recipes = []internal.Recipe{
&internal.L1Recipe{},
&internal.OpRecipe{},
Expand Down Expand Up @@ -151,6 +178,7 @@ func main() {
rootCmd.AddCommand(cookCmd)
rootCmd.AddCommand(artifactsCmd)
rootCmd.AddCommand(artifactsAllCmd)
rootCmd.AddCommand(inspectCmd)

if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
Expand Down