Skip to content

Commit 80b8807

Browse files
authored
Add inspect command (#95)
* Add inspect commadn * Update README
1 parent 1fee7b4 commit 80b8807

File tree

4 files changed

+197
-4
lines changed

4 files changed

+197
-4
lines changed

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,23 @@ $ builder-playground cook l1 --latest-fork --output ~/my-builder-testnet --genes
6868

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

71+
## Inspect
72+
73+
Builder-playground supports inspecting the connection of a service to a specific port.
74+
75+
```bash
76+
$ builder-playground inspect <service> <port>
77+
```
78+
79+
Example:
80+
81+
```bash
82+
$ builder-playground cook opstack
83+
$ builder-playground inspect op-geth authrpc
84+
```
85+
86+
This command starts a `tcpflow` container in the same network interface as the service and captures the traffic to the specified port.
87+
7188
## Internals
7289

7390
### Execution Flow

internal/inspect.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package internal
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"os"
8+
9+
"github.com/docker/docker/api/types/container"
10+
"github.com/docker/docker/api/types/filters"
11+
"github.com/docker/docker/client"
12+
)
13+
14+
// Inspect incldues the logic for the inspect command
15+
func Inspect(ctx context.Context, serviceName, portName string) error {
16+
client, err := newDockerClient()
17+
if err != nil {
18+
return fmt.Errorf("failed to create docker client: %w", err)
19+
}
20+
21+
serviceID, portNum, err := retrieveContainerDetails(client, serviceName, portName)
22+
if err != nil {
23+
return fmt.Errorf("failed to retrieve container details: %w", err)
24+
}
25+
26+
return runTcpFlow(ctx, client, serviceID, portNum)
27+
}
28+
29+
func retrieveContainerDetails(client *client.Client, serviceName, portName string) (string, string, error) {
30+
// Get the service by name
31+
containers, err := client.ContainerList(context.Background(), container.ListOptions{
32+
Filters: filters.NewArgs(filters.Arg("label", "service="+serviceName)),
33+
All: true,
34+
})
35+
if err != nil {
36+
return "", "", fmt.Errorf("error getting container list: %w", err)
37+
}
38+
39+
size := len(containers)
40+
if size == 0 {
41+
return "", "", fmt.Errorf("no containers found for service %s", serviceName)
42+
} else if size > 1 {
43+
return "", "", fmt.Errorf("multiple containers found for service %s", serviceName)
44+
}
45+
46+
container := containers[0]
47+
48+
// Get the container details to find the port mapping in the labels as port.<name>
49+
containerDetails, err := client.ContainerInspect(context.Background(), container.ID)
50+
if err != nil {
51+
return "", "", fmt.Errorf("error inspecting container %s: %w", container.ID, err)
52+
}
53+
54+
// Check if the port name is in the labels
55+
portLabel := fmt.Sprintf("port.%s", portName)
56+
portNum, ok := containerDetails.Config.Labels[portLabel]
57+
if !ok {
58+
return "", "", fmt.Errorf("port %s not found in container %s", portName, container.ID)
59+
}
60+
61+
return container.ID, portNum, nil
62+
}
63+
64+
func runTcpFlow(ctx context.Context, client *client.Client, containerID, portName string) error {
65+
// Create container config for tcpflow
66+
config := &container.Config{
67+
Image: "appropriate/tcpflow:latest",
68+
Cmd: []string{"-c", "-p", "-i", "eth0", "port", portName},
69+
Tty: true,
70+
AttachStdout: true,
71+
AttachStderr: true,
72+
}
73+
74+
// Host config with network mode and capabilities
75+
hostConfig := &container.HostConfig{
76+
NetworkMode: container.NetworkMode("container:" + containerID),
77+
CapAdd: []string{"NET_ADMIN"},
78+
}
79+
80+
// Create the container
81+
resp, err := client.ContainerCreate(ctx, config, hostConfig, nil, nil, "")
82+
if err != nil {
83+
return fmt.Errorf("failed to create container: %w", err)
84+
}
85+
86+
// Start the container
87+
if err := client.ContainerStart(ctx, resp.ID, container.StartOptions{}); err != nil {
88+
return fmt.Errorf("failed to start container: %w", err)
89+
}
90+
91+
// Get container logs and stream them
92+
logOptions := container.LogsOptions{
93+
ShowStdout: true,
94+
ShowStderr: true,
95+
Follow: true,
96+
Timestamps: false,
97+
}
98+
99+
logs, err := client.ContainerLogs(ctx, resp.ID, logOptions)
100+
if err != nil {
101+
return fmt.Errorf("failed to get container logs: %w", err)
102+
}
103+
defer logs.Close()
104+
105+
// Start copying logs to stdout
106+
go func() {
107+
_, err := io.Copy(os.Stdout, logs)
108+
if err != nil && err != io.EOF {
109+
fmt.Fprintf(os.Stderr, "Error copying logs: %v\n", err)
110+
}
111+
}()
112+
113+
// Wait for interrupt signal
114+
<-ctx.Done()
115+
116+
// Cleanup: stop and remove the container
117+
timeout := 5
118+
if err := client.ContainerStop(context.Background(), resp.ID, container.StopOptions{Timeout: &timeout}); err != nil {
119+
fmt.Fprintf(os.Stderr, "Error stopping container: %v\n", err)
120+
}
121+
122+
if err := client.ContainerRemove(context.Background(), resp.ID, container.RemoveOptions{Force: true}); err != nil {
123+
fmt.Fprintf(os.Stderr, "Error removing container: %v\n", err)
124+
}
125+
126+
return nil
127+
}

internal/local_runner.go

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,19 @@ type taskUI struct {
8080
style lipgloss.Style
8181
}
8282

83-
func NewLocalRunner(out *output, manifest *Manifest, overrides map[string]string, interactive bool) (*LocalRunner, error) {
83+
func newDockerClient() (*client.Client, error) {
8484
client, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
8585
if err != nil {
8686
return nil, fmt.Errorf("failed to create docker client: %w", err)
8787
}
88+
return client, nil
89+
}
90+
91+
func NewLocalRunner(out *output, manifest *Manifest, overrides map[string]string, interactive bool) (*LocalRunner, error) {
92+
client, err := newDockerClient()
93+
if err != nil {
94+
return nil, fmt.Errorf("failed to create docker client: %w", err)
95+
}
8896

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

505+
labels := map[string]string{
506+
// It is important to use the playground label to identify the containers
507+
// during the cleanup process
508+
"playground": "true",
509+
"service": s.Name,
510+
}
511+
512+
// add the local ports exposed by the service as labels
513+
// we have to do this for now since we do not store the manifest in JSON yet.
514+
// Otherwise, we could use that directly
515+
for _, port := range s.ports {
516+
labels[fmt.Sprintf("port.%s", port.Name)] = fmt.Sprintf("%d", port.Port)
517+
}
518+
519+
// add the ports to the labels as well
497520
service := map[string]interface{}{
498521
"image": imageName,
499522
"command": args,
@@ -503,9 +526,7 @@ func (d *LocalRunner) toDockerComposeService(s *service) (map[string]interface{}
503526
},
504527
// Add the ethereum network
505528
"networks": []string{networkName},
506-
// It is important to use the playground label to identify the containers
507-
// during the cleanup process
508-
"labels": map[string]string{"playground": "true"},
529+
"labels": labels,
509530
}
510531

511532
if len(envs) > 0 {

main.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,33 @@ var artifactsAllCmd = &cobra.Command{
113113
},
114114
}
115115

116+
var inspectCmd = &cobra.Command{
117+
Use: "inspect",
118+
Short: "Inspect a connection between two services",
119+
RunE: func(cmd *cobra.Command, args []string) error {
120+
// two arguments, the name of the service and the name of the connection
121+
if len(args) != 2 {
122+
return fmt.Errorf("please specify a service name and a connection name")
123+
}
124+
serviceName := args[0]
125+
connectionName := args[1]
126+
127+
sig := make(chan os.Signal, 1)
128+
signal.Notify(sig, os.Interrupt)
129+
130+
ctx, cancel := context.WithCancel(context.Background())
131+
go func() {
132+
<-sig
133+
cancel()
134+
}()
135+
136+
if err := internal.Inspect(ctx, serviceName, connectionName); err != nil {
137+
return fmt.Errorf("failed to inspect connection: %w", err)
138+
}
139+
return nil
140+
},
141+
}
142+
116143
var recipes = []internal.Recipe{
117144
&internal.L1Recipe{},
118145
&internal.OpRecipe{},
@@ -151,6 +178,7 @@ func main() {
151178
rootCmd.AddCommand(cookCmd)
152179
rootCmd.AddCommand(artifactsCmd)
153180
rootCmd.AddCommand(artifactsAllCmd)
181+
rootCmd.AddCommand(inspectCmd)
154182

155183
if err := rootCmd.Execute(); err != nil {
156184
fmt.Println(err)

0 commit comments

Comments
 (0)