diff --git a/cmd/dependabot/internal/cmd/root.go b/cmd/dependabot/internal/cmd/root.go index 2ee59ac..cd87c63 100644 --- a/cmd/dependabot/internal/cmd/root.go +++ b/cmd/dependabot/internal/cmd/root.go @@ -12,17 +12,19 @@ import ( ) var ( - file string - cache string - debugging bool - proxyCertPath string - extraHosts []string - output string - pullImages bool - volumes []string - timeout time.Duration - updaterImage string - proxyImage string + file string + cache string + debugging bool + proxyCertPath string + collectorConfigPath string + extraHosts []string + output string + pullImages bool + volumes []string + timeout time.Duration + updaterImage string + proxyImage string + collectorImage string ) // rootCmd represents the base command when called without any subcommands @@ -50,4 +52,5 @@ func init() { rootCmd.PersistentFlags().StringVar(&updaterImage, "updater-image", "", "container image to use for the updater") rootCmd.PersistentFlags().StringVar(&proxyImage, "proxy-image", infra.ProxyImageName, "container image to use for the proxy") + rootCmd.PersistentFlags().StringVar(&collectorImage, "collector-image", infra.CollectorImageName, "container image to use for the OpenTelemetry collector") } diff --git a/cmd/dependabot/internal/cmd/test.go b/cmd/dependabot/internal/cmd/test.go index 4103335..827615c 100644 --- a/cmd/dependabot/internal/cmd/test.go +++ b/cmd/dependabot/internal/cmd/test.go @@ -37,22 +37,24 @@ var testCmd = &cobra.Command{ processInput(&scenario.Input) if err := infra.Run(infra.RunParams{ - CacheDir: cache, - Creds: scenario.Input.Credentials, - Debug: debugging, - Expected: scenario.Output, - ExtraHosts: extraHosts, - InputName: file, - InputRaw: inputRaw, - Job: &scenario.Input.Job, - LocalDir: local, - Output: output, - ProxyCertPath: proxyCertPath, - ProxyImage: proxyImage, - PullImages: pullImages, - Timeout: timeout, - UpdaterImage: updaterImage, - Volumes: volumes, + CacheDir: cache, + CollectorConfigPath: collectorConfigPath, + CollectorImage: collectorImage, + Creds: scenario.Input.Credentials, + Debug: debugging, + Expected: scenario.Output, + ExtraHosts: extraHosts, + InputName: file, + InputRaw: inputRaw, + Job: &scenario.Input.Job, + LocalDir: local, + Output: output, + ProxyCertPath: proxyCertPath, + ProxyImage: proxyImage, + PullImages: pullImages, + Timeout: timeout, + UpdaterImage: updaterImage, + Volumes: volumes, }); err != nil { log.Fatal(err) } @@ -88,6 +90,7 @@ func init() { testCmd.Flags().StringVar(&cache, "cache", "", "cache import/export directory") testCmd.Flags().StringVar(&local, "local", "", "local directory to use as fetched source") testCmd.Flags().StringVar(&proxyCertPath, "proxy-cert", "", "path to a certificate the proxy will trust") + testCmd.Flags().StringVar(&collectorConfigPath, "collector-config", "", "path to an OpenTelemetry collector config file") testCmd.Flags().BoolVar(&pullImages, "pull", true, "pull the image if it isn't present") testCmd.Flags().BoolVar(&debugging, "debug", false, "run an interactive shell inside the updater") testCmd.Flags().StringArrayVarP(&volumes, "volume", "v", nil, "mount volumes in Docker") diff --git a/cmd/dependabot/internal/cmd/update.go b/cmd/dependabot/internal/cmd/update.go index 745e549..74b5127 100644 --- a/cmd/dependabot/internal/cmd/update.go +++ b/cmd/dependabot/internal/cmd/update.go @@ -61,22 +61,24 @@ func NewUpdateCommand() *cobra.Command { } if err := infra.Run(infra.RunParams{ - CacheDir: cache, - Creds: input.Credentials, - Debug: debugging, - Expected: nil, // update subcommand doesn't use expectations - ExtraHosts: extraHosts, - InputName: file, - Job: &input.Job, - LocalDir: local, - Output: output, - ProxyCertPath: proxyCertPath, - ProxyImage: proxyImage, - PullImages: pullImages, - Timeout: timeout, - UpdaterImage: updaterImage, - Writer: writer, - Volumes: volumes, + CacheDir: cache, + CollectorConfigPath: collectorConfigPath, + CollectorImage: collectorImage, + Creds: input.Credentials, + Debug: debugging, + Expected: nil, // update subcommand doesn't use expectations + ExtraHosts: extraHosts, + InputName: file, + Job: &input.Job, + LocalDir: local, + Output: output, + ProxyCertPath: proxyCertPath, + ProxyImage: proxyImage, + PullImages: pullImages, + Timeout: timeout, + UpdaterImage: updaterImage, + Volumes: volumes, + Writer: writer, }); err != nil { log.Fatalf("failed to run updater: %v", err) } @@ -96,6 +98,7 @@ func NewUpdateCommand() *cobra.Command { cmd.Flags().StringVar(&cache, "cache", "", "cache import/export directory") cmd.Flags().StringVar(&local, "local", "", "local directory to use as fetched source") cmd.Flags().StringVar(&proxyCertPath, "proxy-cert", "", "path to a certificate the proxy will trust") + cmd.Flags().StringVar(&collectorConfigPath, "collector-config", "", "path to an OpenTelemetry collector config file") cmd.Flags().BoolVar(&pullImages, "pull", true, "pull the image if it isn't present") cmd.Flags().BoolVar(&debugging, "debug", false, "run an interactive shell inside the updater") cmd.Flags().StringArrayVarP(&volumes, "volume", "v", nil, "mount volumes in Docker") diff --git a/internal/infra/open_telemetry.go b/internal/infra/open_telemetry.go new file mode 100644 index 0000000..b59da0c --- /dev/null +++ b/internal/infra/open_telemetry.go @@ -0,0 +1,104 @@ +package infra + +import ( + "context" + "fmt" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/mount" + "github.com/docker/docker/api/types/network" + "github.com/moby/moby/client" + "os" + "path" + "path/filepath" +) + +// CollectorImageName is the default Docker image used +const CollectorImageName = "ghcr.io/open-telemetry/opentelemetry-collector-releases/opentelemetry-collector-contrib:latest" + +const CollectorConfigPath = "/etc/otelcol-contrib/config.yaml" + +const sslCertificates = "/etc/ssl/certs/ca-certificates.crt" + +type Collector struct { + cli *client.Client + containerID string +} + +// NewCollector starts the OpenTelemetry collector container. +func NewCollector(ctx context.Context, cli *client.Client, net *Networks, params *RunParams, proxy *Proxy) (*Collector, error) { + hostCfg := &container.HostConfig{ + AutoRemove: false, + } + + containerCfg := &container.Config{ + Image: params.CollectorImage, + Env: []string{ + fmt.Sprintf("HTTP_PROXY=%s", proxy.url), + fmt.Sprintf("HTTPS_PROXY=%s", proxy.url), + }, + } + + netCfg := &network.NetworkingConfig{ + EndpointsConfig: map[string]*network.EndpointSettings{ + net.noInternetName: { + NetworkID: net.NoInternet.ID, + }, + }, + } + + if params.CollectorConfigPath != "" { + if !filepath.IsAbs(params.CollectorConfigPath) { + // needs to be absolute, assume it is relative to the working directory + var dir string + dir, err := os.Getwd() + if err != nil { + return nil, fmt.Errorf("couldn't get working directory: %w", err) + } + params.CollectorConfigPath = path.Join(dir, params.CollectorConfigPath) + } + hostCfg.Mounts = append(hostCfg.Mounts, mount.Mount{ + Type: mount.TypeBind, + Source: params.CollectorConfigPath, + Target: CollectorConfigPath, + ReadOnly: true, + }) + } + + collectorContainer, err := cli.ContainerCreate(ctx, containerCfg, hostCfg, netCfg, nil, "") + if err != nil { + return nil, fmt.Errorf("failed to create collector container: %w", err) + } + + collector := &Collector{ + cli: cli, + containerID: collectorContainer.ID, + } + + opt := types.CopyToContainerOptions{} + if t, err := tarball(sslCertificates, proxy.ca.Cert); err != nil { + return nil, fmt.Errorf("failed to create cert tarball: %w", err) + } else if err = cli.CopyToContainer(ctx, collector.containerID, "/", t, opt); err != nil { + return nil, fmt.Errorf("failed to copy cert to container: %w", err) + } + + if err = cli.ContainerStart(ctx, collectorContainer.ID, types.ContainerStartOptions{}); err != nil { + collector.Close() + return nil, fmt.Errorf("failed to start collector container: %w", err) + } + + return collector, nil + +} + +// Close stops and removes the container. +func (c *Collector) Close() error { + timeout := 5 + _ = c.cli.ContainerStop(context.Background(), c.containerID, container.StopOptions{Timeout: &timeout}) + + err := c.cli.ContainerRemove(context.Background(), c.containerID, types.ContainerRemoveOptions{Force: true}) + if err != nil { + return fmt.Errorf("failed to remove collector container: %w", err) + } + return nil +} diff --git a/internal/infra/run.go b/internal/infra/run.go index 47594ec..d1f83b4 100644 --- a/internal/infra/run.go +++ b/internal/infra/run.go @@ -57,6 +57,10 @@ type RunParams struct { UpdaterImage string // ProxyImage is the image to use for the proxy ProxyImage string + // CollectorImage is the image to use for the OpenTelemetry collector + CollectorImage string + // CollectorConfigPath is the path to the OpenTelemetry collector configuration file + CollectorConfigPath string // Writer is where API calls will be written to Writer io.Writer InputName string @@ -256,6 +260,9 @@ func setImageNames(params *RunParams) error { if params.ProxyImage == "" { params.ProxyImage = ProxyImageName } + if params.CollectorImage == "" { + params.CollectorImage = CollectorImageName + } if params.UpdaterImage == "" { pm, ok := packageManagerLookup[params.Job.PackageManager] if !ok { @@ -326,6 +333,13 @@ func runContainers(ctx context.Context, params RunParams, api *server.API) error return err } + if params.CollectorConfigPath != "" { + err = pullImage(ctx, cli, params.CollectorImage) + if err != nil { + return err + } + } + err = pullImage(ctx, cli, params.UpdaterImage) if err != nil { return err @@ -349,6 +363,14 @@ func runContainers(ctx context.Context, params RunParams, api *server.API) error go prox.TailLogs(ctx, cli) } + if params.CollectorConfigPath != "" { + collector, err := NewCollector(ctx, cli, networks, ¶ms, prox) + if err != nil { + return err + } + defer collector.Close() + } + updater, err := NewUpdater(ctx, cli, networks, ¶ms, prox) if err != nil { return err diff --git a/internal/infra/updater.go b/internal/infra/updater.go index 477a8bd..533a0be 100644 --- a/internal/infra/updater.go +++ b/internal/infra/updater.go @@ -53,6 +53,11 @@ func NewUpdater(ctx context.Context, cli *client.Client, net *Networks, params * Cmd: []string{"/bin/sh"}, Tty: true, // prevent container from stopping } + + if params.CollectorConfigPath != "" { + containerCfg.Env = append(containerCfg.Env, "OTEL_ENABLED=true") + } + hostCfg := &container.HostConfig{} var err error for _, v := range params.Volumes {