Skip to content
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

hack: dependabot ls command #325

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
91 changes: 91 additions & 0 deletions cmd/dependabot/internal/cmd/ls.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package cmd

import (
"context"
"errors"
"github.com/MakeNowJust/heredoc"
"github.com/dependabot/cli/internal/infra"
"github.com/spf13/cobra"
"log"
)

var listCmd = NewListCommand()

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

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

cmd := &cobra.Command{
Use: "ls [<package_manager> <repo>] [flags]",
Short: "List the dependencies of a manifest/lockfile",
Example: heredoc.Doc(`
$ dependabot ls go_modules rsc/quote
$ dependabot ls go_modules --local .
`),
RunE: func(cmd *cobra.Command, args []string) error {
input, err := readArguments(cmd, &flags)
if err != nil {
return err
}

processInput(input, &flags)

input.Job.Source.Provider = "github" // TODO why isn't this being set?

if err := infra.Run(infra.RunParams{
CacheDir: flags.cache,
CollectorConfigPath: flags.collectorConfigPath,
CollectorImage: collectorImage,
Creds: input.Credentials,
Debug: flags.debugging,
Flamegraph: flags.flamegraph,
Expected: nil, // update subcommand doesn't use expectations
ExtraHosts: flags.extraHosts,
InputName: flags.file,
Job: &input.Job,
ListDependencies: true, // list dependencies, then exit
LocalDir: flags.local,
Output: flags.output,
ProxyCertPath: flags.proxyCertPath,
ProxyImage: proxyImage,
PullImages: flags.pullImages,
Timeout: flags.timeout,
UpdaterImage: updaterImage,
Volumes: flags.volumes,
Writer: nil, // prevent outputting all API responses to stdout, we only want dependencies
}); err != nil {
if errors.Is(err, context.DeadlineExceeded) {
log.Fatalf("update timed out after %s", flags.timeout)
}
// HACK: we cancel context to stop the containers, so we don't know if there was a failure.
// A correct solution would involve changes with dependabot-core, which is good, but
// I am just hacking this together right now.
log.Printf("HACK: suppressing updater failure: %v", err)
return nil
}

return nil
},
}

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")

return cmd
}
15 changes: 15 additions & 0 deletions internal/infra/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ type RunParams struct {
Job *model.Job
// expectations asserted at the end of a test
Expected []model.Output
// if true, the containers will be stopped once the dependencies are listed
ListDependencies bool
// directory to copy into the updater container as the repo
LocalDir string
// credentials passed to the proxy
Expand Down Expand Up @@ -107,6 +109,19 @@ func Run(params RunParams) error {
api := server.NewAPI(params.Expected, params.Writer)
defer api.Stop()

if params.ListDependencies {
go func() {
dependencyList := <-api.UpdateDependencyList
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", " ")
err := encoder.Encode(dependencyList.Dependencies)
if err != nil {
log.Printf("failed to write dependency list: %v\n", err)
}
cancel()
}()
}

var outFile *os.File
if params.Output != "" {
var err error
Expand Down
22 changes: 16 additions & 6 deletions internal/server/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ type API struct {
hasExpectations bool
port int
writer io.Writer

// UpdateDependencyList is a channel you can listen on to get the dependencies as soon as they're published
UpdateDependencyList chan model.UpdateDependencyList
}

// NewAPI creates a new API instance and starts the server
Expand Down Expand Up @@ -63,12 +66,13 @@ func NewAPI(expected []model.Output, writer io.Writer) *API {
IdleTimeout: 60 * time.Second,
}
api := &API{
server: server,
Expectations: expected,
writer: writer,
cursor: 0,
hasExpectations: len(expected) > 0,
port: l.Addr().(*net.TCPAddr).Port,
server: server,
Expectations: expected,
writer: writer,
cursor: 0,
hasExpectations: len(expected) > 0,
port: l.Addr().(*net.TCPAddr).Port,
UpdateDependencyList: make(chan model.UpdateDependencyList, 1), // buffer of 1 to prevent blocking
}
server.Handler = api

Expand Down Expand Up @@ -122,6 +126,12 @@ func (a *API) ServeHTTP(w http.ResponseWriter, r *http.Request) {
a.pushError(err)
}

switch object := actual.Data.(type) {
case model.UpdateDependencyList:
a.UpdateDependencyList <- object
close(a.UpdateDependencyList)
}

if actual == nil {
// indicates the kind (endpoint) isn't implemented in decodeWrapper, so return a 501
w.WriteHeader(http.StatusNotImplemented)
Expand Down
Loading