Skip to content

Commit

Permalink
Add build command
Browse files Browse the repository at this point in the history
Port the majority of the current bash script that runs the existing CLI tool.

The allows people to run `circleci build` locally using the new CLI.
  • Loading branch information
marcomorain committed Jul 4, 2018
1 parent a5a853a commit b8d22fa
Show file tree
Hide file tree
Showing 6 changed files with 218 additions and 22 deletions.
206 changes: 206 additions & 0 deletions cmd/build.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
package cmd

import (
"bufio"
"context"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path"
"regexp"
"strconv"
"strings"

"github.com/CircleCI-Public/circleci-cli/settings"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)

var dockerAPIVersion = "1.23"

var job = "build"
var nodeTotal = 1
var checkoutKey = "~/.ssh/id_rsa"
var skipCheckout = true

func newBuildCommand() *cobra.Command {

command := &cobra.Command{
Use: "build",
Short: "Run a build",
RunE: runBuild,
}

/*
TODO: Support these flags:
--branch string Git branch
-e, --env -e VAR=VAL Set environment variables, e.g. -e VAR=VAL
--index int node index of parallelism
--repo-url string Git Url
--revision string Git Revision
-v, --volume stringSlice Volume bind-mounting
*/

flags := command.Flags()
flags.StringVarP(&dockerAPIVersion, "docker-api-version", "d", dockerAPIVersion, "The Docker API version to use")
flags.StringVar(&checkoutKey, "checkout-key", checkoutKey, "Git Checkout key")
flags.StringVar(&job, "job", job, "job to be executed")
flags.BoolVar(&skipCheckout, "skip-checkout", skipCheckout, "use local path as-is")
flags.IntVar(&nodeTotal, "node-total", nodeTotal, "total number of parallel nodes")

return command
}

var picardRepo = "circleci/picard"
var circleCiDir = path.Join(settings.UserHomeDir(), ".circleci")
var currentDigestFile = path.Join(circleCiDir, "current_picard_digest")
var latestDigestFile = path.Join(circleCiDir, "latest_picard_digest")

func getDigest(file string) string {

if _, err := os.Stat(file); !os.IsNotExist(err) {
digest, err := ioutil.ReadFile(file)

if err != nil {
Logger.Error("Could not load digest file", err)
} else {
return strings.TrimSpace(string(digest))
}
}

return "" // Unknown digest
}

func newPullLatestImageCommand() *exec.Cmd {
return exec.Command("docker", "pull", picardRepo)
}

func getLatestImageSha() (string, error) {

cmd := newPullLatestImageCommand()
Logger.Info("Pulling latest build image")
bytes, err := cmd.CombinedOutput()

if err != nil {
return "", errors.Wrap(err, "failed to pull latest image")
}

output := string(bytes)

sha256 := regexp.MustCompile("(?m)sha256.*$")

//latest_image=$(docker pull picardRepo | grep -o "sha256.*$")
//echo $latest_image > $LATEST_DIGEST_FILE
latest := sha256.FindString(output)

if latest == "" {
return latest, errors.New("Failed to find latest image")
}

return latest, nil
}

func picardImageSha1() (string, error) {
currentDigest := getDigest(currentDigestFile)

if currentDigest == "" {
Logger.Debug("no current digest stored - downloading latest image of picard")
Logger.Info("Downloading latest CircleCI build agent...")

// TODO - write the digest to a file so that we can
// use it again.
return getLatestImageSha()
}

// TODO - this should write to a file to record
// the fact that we have the latest image.
Logger.Debug("Pulling latest picard image in background")
// We don't wait for this command, we just `Start` it and run in the background.
if err := newPullLatestImageCommand().Start(); err != nil {
return "", errors.Wrap(err, "Fails to start background update of build image")
}

return currentDigest, nil

}

func picardImage() (string, error) {
sha, err := picardImageSha1()

if err != nil {
return "", err
}
Logger.Infof("Docker image digest: %s", sha)
return fmt.Sprintf("%s@%s", picardRepo, sha), nil
}

func streamOutout(stream io.Reader) {
scanner := bufio.NewScanner(stream)
go func() {
for scanner.Scan() {
Logger.Info(scanner.Text())
}
}()
}

func runBuild(cmd *cobra.Command, args []string) error {

ctx := context.Background()

pwd, err := os.Getwd()

if err != nil {
return errors.Wrap(err, "Could not find pwd")
}

image, err := picardImage()

if err != nil {
return errors.Wrap(err, "Could not find picard image")
}

// TODO: marc:
// I can't find a way to pass `-it` and have carriage return
// characters work correctly.
arguments := []string{"run", "--rm",
"-e", fmt.Sprintf("DOCKER_API_VERSION=%s", dockerAPIVersion),
"-v", "/var/run/docker.sock:/var/run/docker.sock",
"-v", fmt.Sprintf("%s:%s", pwd, pwd),
"-v", fmt.Sprintf("%s:/root/.circleci", circleCiDir),
"--workdir", pwd,
image, "circleci", "build",

// Proxied arguments
"--config", configPath,
"--skip-checkout", strconv.FormatBool(skipCheckout),
"--node-total", strconv.Itoa(nodeTotal),
"--checkout-key", checkoutKey,
"--job", job}

arguments = append(arguments, args...)

Logger.Debug(fmt.Sprintf("Starting docker with args: %s", arguments))

build := exec.CommandContext(ctx, "docker", arguments...)

build.Stdin = os.Stdin

stdout, err := build.StdoutPipe()

if err != nil {
return errors.Wrap(err, "Failed to connect to stdout")
}

stderr, err := build.StderrPipe()

if err != nil {
return errors.Wrap(err, "Failed to connect to stderr")
}

streamOutout(stdout)
streamOutout(stderr)

return build.Run()
}
4 changes: 0 additions & 4 deletions cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ import (
"github.com/spf13/cobra"
)

// Path to the config.yml file to operate on.
var configPath string

func newConfigCommand() *cobra.Command {
configCmd := &cobra.Command{
Use: "config",
Expand All @@ -29,7 +26,6 @@ func newConfigCommand() *cobra.Command {
RunE: expandConfig,
}

configCmd.PersistentFlags().StringVarP(&configPath, "path", "p", ".circleci/config.yml", "path to build config")
configCmd.AddCommand(validateCommand)
configCmd.AddCommand(expandCommand)

Expand Down
4 changes: 2 additions & 2 deletions cmd/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ var _ = Describe("Config", func() {
"config", "validate",
"-t", token,
"-e", testServer.URL(),
"-p", config.Path,
"-c", config.Path,
)
})

Expand Down Expand Up @@ -120,7 +120,7 @@ var _ = Describe("Config", func() {
"config", "expand",
"-t", token,
"-e", testServer.URL(),
"-p", config.Path,
"-c", config.Path,
)
})

Expand Down
11 changes: 5 additions & 6 deletions cmd/configure.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package cmd

import (
"fmt"
"strings"

"github.com/pkg/errors"
Expand Down Expand Up @@ -76,12 +75,12 @@ type testingUI struct {
}

func (ui testingUI) readStringFromUser(message string, defaultValue string) string {
fmt.Println(message)
Logger.Info(message)
return ui.input
}

func (ui testingUI) askUserToConfirm(message string) bool {
fmt.Println(message)
Logger.Info(message)
return ui.confirm
}

Expand All @@ -108,10 +107,10 @@ func configure(cmd *cobra.Command, args []string) error {

if shouldAskForToken(token, ui) {
viper.Set("token", ui.readStringFromUser("CircleCI API Token", ""))
fmt.Println("API token has been set.")
Logger.Info("API token has been set.")
}
viper.Set("endpoint", ui.readStringFromUser("CircleCI API End Point", viper.GetString("endpoint")))
fmt.Println("API endpoint has been set.")
Logger.Info("API endpoint has been set.")

// Marc: I can't find a way to prevent the verbose flag from
// being written to the config file, so set it to false in
Expand All @@ -122,6 +121,6 @@ func configure(cmd *cobra.Command, args []string) error {
return errors.Wrap(err, "Failed to save config file")
}

fmt.Println("Configuration has been saved.")
Logger.Info("Configuration has been saved.")
return nil
}
6 changes: 5 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import (

var defaultEndpoint = "https://circleci.com/graphql-unstable"

// Path to the config.yml file to operate on.
var configPath = ".circleci/config.yml"

// Execute adds all child commands to rootCmd and
// sets flags appropriately. This function is called
// by main.main(). It only needs to happen once to
Expand Down Expand Up @@ -42,10 +45,11 @@ func makeCommands() *cobra.Command {
rootCmd.AddCommand(newConfigureCommand())
rootCmd.AddCommand(newConfigCommand())
rootCmd.AddCommand(newOrbCommand())

rootCmd.AddCommand(newBuildCommand())
rootCmd.PersistentFlags().BoolP("verbose", "v", false, "Enable verbose logging.")
rootCmd.PersistentFlags().StringP("endpoint", "e", defaultEndpoint, "the endpoint of your CircleCI GraphQL API")
rootCmd.PersistentFlags().StringP("token", "t", "", "your token for using CircleCI")
rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", configPath, "path to build config")

for _, flag := range []string{"endpoint", "token", "verbose"} {
bindCobraFlagToViper(rootCmd, flag)
Expand Down
9 changes: 0 additions & 9 deletions logger/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,6 @@ func (l *Logger) Error(msg string, err error) {
}
}

// FatalOnError prints a message and error's message to os.Stderr then QUITS!
// Please be aware this method will exit the program via os.Exit(1).
// This method wraps log.Logger.Fatalln
func (l *Logger) FatalOnError(msg string, err error) {
if err != nil {
l.error.Fatalln(msg, err.Error())
}
}

// Prettyify accepts a map of data and pretty prints it.
// It's using json.MarshalIndent and printing with log.Logger.Infoln
func (l *Logger) Prettyify(data interface{}) {
Expand Down

0 comments on commit b8d22fa

Please sign in to comment.