Skip to content
130 changes: 130 additions & 0 deletions cmd/dependabot/internal/cmd/graph.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package cmd

import (
"context"
"errors"
"fmt"
"io"
"log"
"os"
"slices"

"github.com/MakeNowJust/heredoc"
"github.com/dependabot/cli/internal/infra"
"github.com/spf13/cobra"
)

var graphSupportedEcosystems = []string{
"bundler",
"go_modules",
}

var graphCmd = NewGraphCommand()

func init() {
rootCmd.AddCommand(graphCmd)
}

func NewGraphCommand() *cobra.Command {
var flags UpdateFlags

cmd := &cobra.Command{
Use: "graph [<package_manager> <repo> | -f <input.yml>] [flags]",
Short: "[Experimental] List the dependencies of a manifest/lockfile",
Example: heredoc.Doc(`
NOTE: This command is a work in progress.

It will only work with some package managers and the dependency list
may be incomplete.

$ dependabot graph bundler dependabot/dependabot-core
$ dependabot graph bundler --local .
$ dependabot graph -f input.yml
`),
RunE: func(cmd *cobra.Command, args []string) error {
var outFile *os.File
if flags.output != "" {
var err error
outFile, err = os.Create(flags.output)
if err != nil {
return fmt.Errorf("failed to create output file: %w", err)
}
defer outFile.Close()
}

input, err := extractInput(cmd, &flags)
if err != nil {
return err
}

processInput(input, &flags)

if !slices.Contains(graphSupportedEcosystems, input.Job.PackageManager) {
return fmt.Errorf(
"package manager '%s' is not supported for graphing. Supported ecosystems: %v",
input.Job.PackageManager,
graphSupportedEcosystems,
)
}

var writer io.Writer
if !flags.debugging {
writer = os.Stdout
}

if err := infra.Run(infra.RunParams{
Command: infra.UpdateGraphCommand,
CacheDir: flags.cache,
CollectorConfigPath: flags.collectorConfigPath,
CollectorImage: collectorImage,
Creds: input.Credentials,
Debug: flags.debugging,
Flamegraph: flags.flamegraph,
Expected: nil, // graph subcommand doesn't use expectations
ExtraHosts: flags.extraHosts,
InputName: flags.file,
Job: &input.Job,
LocalDir: flags.local,
Output: flags.output,
ProxyCertPath: flags.proxyCertPath,
ProxyImage: proxyImage,
PullImages: flags.pullImages,
Timeout: flags.timeout,
UpdaterImage: updaterImage,
Volumes: flags.volumes,
Writer: writer,
ApiUrl: flags.apiUrl,
}); err != nil {
if errors.Is(err, context.DeadlineExceeded) {
log.Fatalf("update timed out after %s", flags.timeout)
}
log.Fatalf("updater failure: %v", err)
}

return nil
},
}

cmd.Flags().StringVarP(&flags.file, "file", "f", "", "path to input file")

cmd.Flags().StringVarP(&flags.provider, "provider", "p", "github", "provider of the repository")
cmd.Flags().StringVarP(&flags.branch, "branch", "b", "", "target branch to update")
cmd.Flags().StringVarP(&flags.directory, "directory", "d", "/", "directory to update")
cmd.Flags().StringVarP(&flags.commit, "commit", "", "", "commit to update")

cmd.Flags().StringVarP(&flags.output, "output", "o", "", "write scenario to file")
cmd.Flags().StringVar(&flags.cache, "cache", "", "cache import/export directory")
cmd.Flags().StringVar(&flags.local, "local", "", "local directory to use as fetched source")
cmd.Flags().StringVar(&flags.proxyCertPath, "proxy-cert", "", "path to a certificate the proxy will trust")
cmd.Flags().StringVar(&flags.collectorConfigPath, "collector-config", "", "path to an OpenTelemetry collector config file")
cmd.Flags().BoolVar(&flags.pullImages, "pull", true, "pull the image if it isn't present")
cmd.Flags().BoolVar(&flags.debugging, "debug", false, "run an interactive shell inside the updater")
cmd.Flags().BoolVar(&flags.flamegraph, "flamegraph", false, "generate a flamegraph and other metrics")
cmd.Flags().StringArrayVarP(&flags.volumes, "volume", "v", nil, "mount volumes in Docker")
cmd.Flags().StringArrayVar(&flags.extraHosts, "extra-hosts", nil, "Docker extra hosts setting on the proxy")
cmd.Flags().DurationVarP(&flags.timeout, "timeout", "t", 0, "max time to run an update")
cmd.Flags().IntVar(&flags.inputServerPort, "input-port", 0, "port to use for securely passing input to the updater")
cmd.Flags().StringVarP(&flags.apiUrl, "api-url", "a", "", "the api dependabot should connect to.")

return cmd
}
17 changes: 15 additions & 2 deletions internal/infra/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,23 @@ import (
"gopkg.in/yaml.v3"
)

type RunCommand int

const (
UpdateFilesCommand RunCommand = iota
UpdateGraphCommand
)

var runCmds = map[RunCommand]string{
UpdateFilesCommand: "bin/run fetch_files && bin/run update_files",
UpdateGraphCommand: "bin/run fetch_files && bin/run update_graph",
}

type RunParams struct {
// Input file
Input string
// Which command to use, this will default to UpdateFilesCommand
Command RunCommand
// job definition passed to the updater
Job *model.Job
// expectations asserted at the end of a test
Expand Down Expand Up @@ -452,8 +466,7 @@ func runContainers(ctx context.Context, params RunParams) (err error) {
if params.Flamegraph {
env = append(env, "FLAMEGRAPH=1")
}
const cmd = "bin/run fetch_files && bin/run update_files"
if err := updater.RunCmd(ctx, cmd, dependabot, env...); err != nil {
if err := updater.RunCmd(ctx, runCmds[params.Command], dependabot, env...); err != nil {
return err
}
if params.Flamegraph {
Expand Down
10 changes: 10 additions & 0 deletions internal/model/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ type UpdatePullRequest struct {
DependencyGroup map[string]any `json:"dependency-group" yaml:"dependency-group,omitempty"`
}

type DependencySubmissionRequest struct {
Version int8 `json:"version" yaml:"version"`
Sha string `json:"sha" yaml:"sha"`
Ref string `json:"ref" yaml:"ref"`
Job map[string]any `json:"job" yaml:"job"`
Detector map[string]any `json:"detector" yaml:"detector"`
Scanned string `json:"scanned" yaml:"scanned"`
Manifests map[string]any `json:"manifests" yaml:"manifests"`
}

type DependencyFile struct {
Content string `json:"content" yaml:"content"`
ContentEncoding string `json:"content_encoding" yaml:"content_encoding"`
Expand Down
7 changes: 7 additions & 0 deletions internal/server/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,8 @@ func decodeWrapper(kind string, data []byte) (actual *model.UpdateWrapper, err e
actual.Data, err = decode[model.UpdateDependencyList](data)
case "create_pull_request":
actual.Data, err = decode[model.CreatePullRequest](data)
case "create_dependency_submission":
actual.Data, err = decode[model.DependencySubmissionRequest](data)
case "update_pull_request":
actual.Data, err = decode[model.UpdatePullRequest](data)
case "close_pull_request":
Expand Down Expand Up @@ -280,6 +282,11 @@ func decode[T any](data []byte) (T, error) {
return wrapper.Data, nil
}

// TODO(brrygrdn): Add model.DependencySubmissionRequest to support smoke tests
//
// The test command only expects to run with `update` operations right now so
// we will need to incorporate which run command is expected as well, but we
// don't need regression coverage yet.
func compare(expect, actual *model.UpdateWrapper) error {
switch v := expect.Data.(type) {
case model.UpdateDependencyList:
Expand Down
Loading