From 37ecbee23710cf80abe55c61385300f819265aab Mon Sep 17 00:00:00 2001 From: Ferran Borreguero Date: Wed, 2 Apr 2025 11:23:08 +0100 Subject: [PATCH 1/2] Add inspect commadn --- internal/inspect.go | 127 +++++++++++++++++++++++++++++++++++++++ internal/local_runner.go | 29 +++++++-- main.go | 28 +++++++++ 3 files changed, 180 insertions(+), 4 deletions(-) create mode 100644 internal/inspect.go diff --git a/internal/inspect.go b/internal/inspect.go new file mode 100644 index 0000000..5cd2638 --- /dev/null +++ b/internal/inspect.go @@ -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. + 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 +} diff --git a/internal/local_runner.go b/internal/local_runner.go index 43dad46..bb4debf 100644 --- a/internal/local_runner.go +++ b/internal/local_runner.go @@ -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 { @@ -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, @@ -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 { diff --git a/main.go b/main.go index 8f8fbad..7c3f31c 100644 --- a/main.go +++ b/main.go @@ -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{}, @@ -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) From 632dfd4310b10c2a09e61683bef8af939383a305 Mon Sep 17 00:00:00 2001 From: Ferran Borreguero Date: Wed, 2 Apr 2025 11:25:20 +0100 Subject: [PATCH 2/2] Update README --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index 2390cc3..1505561 100644 --- a/README.md +++ b/README.md @@ -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 +``` + +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