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

Add build command #13

Merged
merged 6 commits into from
Jul 4, 2018
Merged
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
184 changes: 184 additions & 0 deletions cmd/build.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
package cmd

import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"regexp"
"syscall"

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

func newBuildCommand() *cobra.Command {

updateCommand := &cobra.Command{
Use: "update",
Short: "Update the build agent to the latest version",
RunE: updateBuildAgentToLatest,
}

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

buildCommand.AddCommand(updateCommand)

return buildCommand
}

var picardRepo = "circleci/picard"
var circleCiDir = path.Join(settings.UserHomeDir(), ".circleci")
var buildAgentSettingsPath = path.Join(circleCiDir, "build_agent_settings.json")

type buildAgentSettings struct {
LatestSha256 string
}

func storeBuildAgentSha(sha256 string) error {
settings := buildAgentSettings{
LatestSha256: sha256,
}

settingsJSON, err := json.Marshal(settings)

if err != nil {
return errors.Wrap(err, "Failed to serialize build agent settings")
}

err = ioutil.WriteFile(buildAgentSettingsPath, settingsJSON, 0644)

return errors.Wrap(err, "Failed to write build agent settings file")
}

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

latestSha256, err := findLatestPicardSha()

if err != nil {
return err
}

Logger.Infof("Latest build agent is version %s", latestSha256)

return nil
}

func findLatestPicardSha() (string, error) {

outputBytes, err := exec.Command("docker", "pull", picardRepo).CombinedOutput() // #nosec

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

output := string(outputBytes)
sha256 := regexp.MustCompile("(?m)sha256.*$")
latest := sha256.FindString(output)

if latest == "" {
return "", errors.New("failed to parse sha256 from docker pull output")
}

err = storeBuildAgentSha(latest)

if err != nil {
return "", err
}

return latest, nil

}

func loadCurrentBuildAgentSha() string {
if _, err := os.Stat(buildAgentSettingsPath); os.IsNotExist(err) {
return ""
}

settingsJSON, err := ioutil.ReadFile(buildAgentSettingsPath)

if err != nil {
Logger.Error("Faild to load build agent settings JSON", err)
return ""
}

var settings buildAgentSettings

err = json.Unmarshal(settingsJSON, &settings)

if err != nil {
Logger.Error("Faild to parse build agent settings JSON", err)
return ""
}

return settings.LatestSha256
}

func picardImage() (string, error) {

sha := loadCurrentBuildAgentSha()

if sha == "" {

Logger.Info("Downloading latest CircleCI build agent...")

var err error

sha, err = findLatestPicardSha()

if err != nil {
return "", err
}

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

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

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:
// We are passing the current environment to picard,
// so DOCKER_API_VERSION is only passed when it is set
// explicitly. The old bash script sets this to `1.23`
// when not explicitly set. Is this OK?
arguments := []string{"docker", "run", "--interactive", "--tty", "--rm",
"--volume", "/var/run/docker.sock:/var/run/docker.sock",
"--volume", fmt.Sprintf("%s:%s", pwd, pwd),
"--volume", fmt.Sprintf("%s:/root/.circleci", circleCiDir),
"--workdir", pwd,
image, "circleci", "build"}

arguments = append(arguments, args...)

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

dockerPath, err := exec.LookPath("docker")

if err != nil {
return errors.Wrap(err, "Could not find a `docker` executable on $PATH; please ensure that docker installed")
}

err = syscall.Exec(dockerPath, arguments, os.Environ()) // #nosec
return errors.Wrap(err, "failed to execute docker")
}
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
}
11 changes: 8 additions & 3 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@ 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
// the RootCmd.
func Execute() {
command := makeCommands()
command := MakeCommands()
if err := command.Execute(); err != nil {
os.Exit(-1)
}
Expand All @@ -28,7 +31,8 @@ func Execute() {
// This allows us to print to the log at anytime from within the `cmd` package.
var Logger *logger.Logger

func makeCommands() *cobra.Command {
// MakeCommands creates the top level commands
func MakeCommands() *cobra.Command {

rootCmd := &cobra.Command{
Use: "cli",
Expand All @@ -42,10 +46,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
11 changes: 11 additions & 0 deletions cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,24 @@ package cmd_test
import (
"os/exec"

"github.com/CircleCI-Public/circleci-cli/cmd"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gbytes"
"github.com/onsi/gomega/gexec"
)

var _ = Describe("Root", func() {

Describe("subcommands", func() {

It("can create commands", func() {
commands := cmd.MakeCommands()
Expect(len(commands.Commands())).To(Equal(7))
})

})

Describe("when run with --help", func() {
It("return exit code 0 with help message", func() {
command := exec.Command(pathCLI, "--help")
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