diff --git a/pkg/cmd/agent/build.go b/pkg/cmd/agent/build.go index e58d052..b6410b5 100644 --- a/pkg/cmd/agent/build.go +++ b/pkg/cmd/agent/build.go @@ -20,7 +20,6 @@ import ( "github.com/diambra/cli/pkg/container" "github.com/diambra/cli/pkg/log" - dclient "github.com/docker/docker/client" "github.com/go-kit/log/level" "github.com/spf13/cobra" ) @@ -35,13 +34,8 @@ func NewBuildCmd(logger *log.Logger) *cobra.Command { if len(args) == 0 { args = []string{"."} } - client, err := dclient.NewClientWithOpts(dclient.FromEnv, dclient.WithAPIVersionNegotiation()) - if err != nil { - level.Error(logger).Log("msg", "failed to create docker client", "err", err) - os.Exit(1) - } - runner, err := container.NewDockerRunner(logger, client, false) + runner, err := container.NewDockerRunner(logger, false) if err != nil { level.Error(logger).Log("msg", "failed to create docker runner", "err", err) os.Exit(1) diff --git a/pkg/cmd/agent/build_and_push.go b/pkg/cmd/agent/build_and_push.go index ae63db6..3fa4827 100644 --- a/pkg/cmd/agent/build_and_push.go +++ b/pkg/cmd/agent/build_and_push.go @@ -24,7 +24,6 @@ import ( "github.com/diambra/cli/pkg/container" "github.com/diambra/cli/pkg/diambra/client" "github.com/diambra/cli/pkg/log" - dclient "github.com/docker/docker/client" "github.com/go-kit/log/level" "github.com/spf13/cobra" ) @@ -45,13 +44,8 @@ func NewBuildAndPushCmd(logger *log.Logger) *cobra.Command { if len(args) == 0 { args = []string{"."} } - dc, err := dclient.NewClientWithOpts(dclient.FromEnv, dclient.WithAPIVersionNegotiation()) - if err != nil { - level.Error(logger).Log("msg", "failed to create docker client", "err", err) - os.Exit(1) - } - runner, err := container.NewDockerRunner(logger, dc, false) + runner, err := container.NewDockerRunner(logger, false) if err != nil { level.Error(logger).Log("msg", "failed to create docker runner", "err", err) os.Exit(1) diff --git a/pkg/cmd/agent/submit.go b/pkg/cmd/agent/submit.go index 05fccac..bd95c81 100644 --- a/pkg/cmd/agent/submit.go +++ b/pkg/cmd/agent/submit.go @@ -17,11 +17,15 @@ package agent import ( "fmt" + "net/url" "os" "path/filepath" + "time" + "github.com/diambra/cli/pkg/container" "github.com/diambra/cli/pkg/diambra" "github.com/diambra/cli/pkg/diambra/client" + "github.com/diambra/cli/pkg/git" "github.com/diambra/cli/pkg/log" "github.com/go-kit/log/level" "github.com/spf13/cobra" @@ -29,9 +33,14 @@ import ( ) func NewSubmitCmd(logger *log.Logger) *cobra.Command { - dump := false - submissionConfig := diambra.SubmissionConfig{} + var ( + dump = false + submissionConfig = diambra.SubmissionConfig{} + name = "" + version = "" + ) submissionConfig.RegisterCredentialsProviders() + c, err := diambra.NewConfig(logger) if err != nil { level.Error(logger).Log("msg", err.Error()) @@ -39,9 +48,9 @@ func NewSubmitCmd(logger *log.Logger) *cobra.Command { } cmd := &cobra.Command{ - Use: "submit [flags] {--submission.manifest submission-manifest.yaml | docker-image} [args/command(s) ...]", + Use: "submit [flags] (directory | --submission.manifest=submission-manifest.yaml | docker-image) [args/command(s) ...]", Short: "Submits an agent for evaluation", - Long: `This takes a docker image or submission manifest and submits it for evaluation.`, + Long: `This takes a directory, existing docker image or submission manifest and submits it for evaluation.`, Run: func(cmd *cobra.Command, args []string) { if err := diambra.EnsureCredentials(logger, c.CredPath); err != nil { level.Error(logger).Log("msg", err.Error()) @@ -61,11 +70,67 @@ func NewSubmitCmd(logger *log.Logger) *cobra.Command { fmt.Println(string(b)) return } + cl, err := client.NewClient(logger, c.CredPath) if err != nil { level.Error(logger).Log("msg", "failed to create client", "err", err.Error()) os.Exit(1) } + // If submission.Image is a directory, we build and push it, then update the name to the resulting image + if stat, err := os.Stat(submission.Manifest.Image); err == nil && stat.IsDir() { + context := submission.Manifest.Image + level.Info(logger).Log("msg", "Building and pushing image", "context", context) + + if name == "" { + name, err = container.TagFromDir(context) + if err != nil { + level.Error(logger).Log("msg", "failed to get tag from dir", "err", err) + os.Exit(1) + } + } + + if version == "" { + version, err = git.GitHeadSHAShort(context, 0) + if err != nil { + level.Warn(logger).Log("msg", "failed to get git head sha", "err", err) + version = time.Now().Format("20060102-150405") + } + } + level.Info(logger).Log("msg", "Building agent", "name", name, "version", version) + runner, err := container.NewDockerRunner(logger, false) + if err != nil { + level.Error(logger).Log("msg", "failed to create docker runner", "err", err) + os.Exit(1) + } + credentials, err := cl.Credentials() + if err != nil { + level.Error(logger).Log("msg", "failed to get credentials", "err", err.Error()) + os.Exit(1) + } + + repositoryURL, err := url.Parse(credentials.Repository) + if err != nil { + level.Error(logger).Log("msg", "failed to parse repository URL", "err", err) + os.Exit(1) + } + + tag := fmt.Sprintf("%s%s:%s-%s", repositoryURL.Host, repositoryURL.Path, name, version) + + if err := runner.Build(context, tag); err != nil { + level.Error(logger).Log("msg", "failed to build and push image", "err", err.Error()) + os.Exit(1) + } + if err := runner.Push(tag, credentials.Username, credentials.Password, repositoryURL.Host); err != nil { + level.Error(logger).Log("msg", "failed to push agent", "err", err) + os.Exit(1) + } + + submission.Manifest.Image = tag + } else { + level.Warn(logger).Log("msg", "Using existing images or submission manifest is not recommended and might get deprecated in the future") + } + os.Exit(1) + id, err := cl.Submit(submission) if err != nil { level.Error(logger).Log("msg", "failed to submit agent", "err", err.Error()) @@ -79,5 +144,7 @@ func NewSubmitCmd(logger *log.Logger) *cobra.Command { cmd.Flags().StringVar(&c.CredPath, "path.credentials", filepath.Join(c.Home, ".diambra/credentials"), "Path to credentials file") cmd.Flags().BoolVar(&dump, "dump", false, "Dump the manifest to stdout instead of submitting") cmd.Flags().SetInterspersed(false) + cmd.Flags().StringVar(&name, "name", name, "Name of the agent image (only used when giving a directory)") + cmd.Flags().StringVar(&version, "version", version, "Version of the agent image (only used when giving a directory)") return cmd } diff --git a/pkg/cmd/agent/test.go b/pkg/cmd/agent/test.go index 01839bb..4130106 100644 --- a/pkg/cmd/agent/test.go +++ b/pkg/cmd/agent/test.go @@ -13,7 +13,6 @@ import ( "github.com/diambra/cli/pkg/diambra" "github.com/diambra/cli/pkg/diambra/client" "github.com/diambra/cli/pkg/log" - dclient "github.com/docker/docker/client" "github.com/go-kit/log/level" "github.com/spf13/cobra" ) @@ -58,11 +57,7 @@ func NewTestCmd(logger *log.Logger) *cobra.Command { func TestFn(logger *log.Logger, c *diambra.EnvConfig, submission *client.Submission) error { level.Debug(logger).Log("manifest", fmt.Sprintf("%#v", submission.Manifest), "config", fmt.Sprintf("%#v", c)) - client, err := dclient.NewClientWithOpts(dclient.FromEnv, dclient.WithAPIVersionNegotiation()) - if err != nil { - return err - } - runner, err := container.NewDockerRunner(logger, client, c.AutoRemove) + runner, err := container.NewDockerRunner(logger, c.AutoRemove) if err != nil { return err } diff --git a/pkg/cmd/arena/down.go b/pkg/cmd/arena/down.go index 7e57c37..1f5969c 100644 --- a/pkg/cmd/arena/down.go +++ b/pkg/cmd/arena/down.go @@ -20,7 +20,6 @@ import ( "github.com/diambra/cli/pkg/container" "github.com/diambra/cli/pkg/log" - "github.com/docker/docker/client" "github.com/go-kit/log/level" "github.com/spf13/cobra" ) @@ -31,12 +30,7 @@ func NewDownCmd(logger *log.Logger) *cobra.Command { Short: "Stop DIAMBRA Arena", Long: `This stops a DIAMBRA Arena running in the background.`, Run: func(_ *cobra.Command, _ []string) { - client, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) - if err != nil { - level.Error(logger).Log("msg", "failed to create docker client", "err", err.Error()) - os.Exit(1) - } - runner, err := container.NewDockerRunner(logger, client, true) + runner, err := container.NewDockerRunner(logger, true) if err != nil { level.Error(logger).Log("msg", "msg", "failed to create runner", "err", err.Error()) os.Exit(1) diff --git a/pkg/cmd/arena/up.go b/pkg/cmd/arena/up.go index 635abfa..fc1e4d6 100644 --- a/pkg/cmd/arena/up.go +++ b/pkg/cmd/arena/up.go @@ -24,7 +24,6 @@ import ( "github.com/diambra/cli/pkg/container" "github.com/diambra/cli/pkg/diambra" "github.com/diambra/cli/pkg/log" - "github.com/docker/docker/client" "github.com/go-kit/log/level" "github.com/spf13/cobra" ) @@ -68,11 +67,7 @@ func NewUpCmd(logger *log.Logger) *cobra.Command { func RunFn(logger *log.Logger, c *diambra.EnvConfig, args []string) error { level.Debug(logger).Log("config", fmt.Sprintf("%#v", c)) - client, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) - if err != nil { - return err - } - runner, err := container.NewDockerRunner(logger, client, c.AutoRemove) + runner, err := container.NewDockerRunner(logger, c.AutoRemove) if err != nil { return err } diff --git a/pkg/cmd/run.go b/pkg/cmd/run.go index 947224c..6f282e3 100644 --- a/pkg/cmd/run.go +++ b/pkg/cmd/run.go @@ -28,7 +28,6 @@ import ( "github.com/diambra/cli/pkg/diambra" "github.com/diambra/cli/pkg/log" - "github.com/docker/docker/client" "github.com/go-kit/log/level" "github.com/spf13/cobra" ) @@ -76,11 +75,7 @@ The flag --agent-image can be used to run the commands in the given image.`, func RunFn(logger *log.Logger, c *diambra.EnvConfig, args []string) error { level.Debug(logger).Log("config", fmt.Sprintf("%#v", c)) - client, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) - if err != nil { - return err - } - runner, err := container.NewDockerRunner(logger, client, c.AutoRemove) + runner, err := container.NewDockerRunner(logger, c.AutoRemove) if err != nil { return err } diff --git a/pkg/container/docker.go b/pkg/container/docker.go index bc13494..a8079d3 100644 --- a/pkg/container/docker.go +++ b/pkg/container/docker.go @@ -48,14 +48,19 @@ type DockerRunner struct { AutoRemove bool } -func NewDockerRunner(logger log.Logger, client *client.Client, autoRemove bool) (*DockerRunner, error) { - _, err := client.Ping(context.TODO()) +func NewDockerRunner(logger log.Logger, autoRemove bool) (*DockerRunner, error) { + cl, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + level.Error(logger).Log("msg", "failed to create docker client", "err", err.Error()) + os.Exit(1) + } + _, err = cl.Ping(context.TODO()) if err != nil { return nil, fmt.Errorf("couldn't connect to docker. Make sure your user has docker access: %w", err) } return &DockerRunner{ Logger: logger, - Client: client, + Client: cl, TimeoutStop: 10 * time.Second, AutoRemove: autoRemove, }, nil diff --git a/pkg/diambra/config.go b/pkg/diambra/config.go index 2519e57..cf5e54a 100644 --- a/pkg/diambra/config.go +++ b/pkg/diambra/config.go @@ -232,7 +232,7 @@ const ( DifficultyHard Difficulty = "hard" ) -var ErrInvalidArgs = errors.New("either image, manifest path or submission id must be provided") +var ErrInvalidArgs = errors.New("either directory, image, manifest path or submission id must be provided") type SubmissionConfig struct { Mode string @@ -296,7 +296,7 @@ func (c *SubmissionConfig) Submission(config *EnvConfig, args []string) (*client } default: if nargs == 0 { - return nil, fmt.Errorf("either image, manifest path or submission id must be provided") + return nil, fmt.Errorf("either directory, image, manifest path or submission id must be provided") } // If we don't have a manifest, args are image and args manifest = &client.Manifest{} diff --git a/pkg/git/git.go b/pkg/git/git.go new file mode 100644 index 0000000..a8d443a --- /dev/null +++ b/pkg/git/git.go @@ -0,0 +1,66 @@ +package git + +import ( + "fmt" + "os" + "path/filepath" + "strings" +) + +const RefPrefix = "ref: " + +func findGitDir(startDir string) (string, error) { + dir, err := filepath.Abs(startDir) + if err != nil { + return "", fmt.Errorf("unable to resolve absolute path: %w", err) + } + + for { + gitPath := filepath.Join(dir, ".git") + if info, err := os.Stat(gitPath); err == nil && info.IsDir() { + return gitPath, nil + } + + parent := filepath.Dir(dir) + // root + if parent == dir { + break + } + dir = parent + } + return "", fmt.Errorf("no .git directory found in parents of %s", dir) +} + +func GitHeadSHA(dir string) (string, error) { + dir, err := findGitDir(dir) + if err != nil { + return "", err + } + file, err := os.ReadFile(filepath.Join(dir, "HEAD")) + if err != nil { + return "", err + } + if !strings.HasPrefix(string(file), RefPrefix) { + return string(file), nil + } + + refFile, err := os.ReadFile(filepath.Join(dir, strings.TrimSpace(string(file)[len(RefPrefix):]))) + if err != nil { + return "", err + } + return strings.TrimSpace(string(refFile)), nil +} + +func GitHeadSHAShort(dir string, n int) (string, error) { + if n <= 0 { + n = 7 + } + sha, err := GitHeadSHA(dir) + if err != nil { + return "", err + } + if len(sha) < n { + return sha, nil + } + return sha[:n], nil +}