jkawamoto/loci

View on GitHub
command/run.go

Summary

Maintainability
D
2 days
Test Coverage
//
// command/run.go
//
// Copyright (c) 2016-2017 Junpei Kawamoto
//
// This software is released under the MIT License.
//
// http://opensource.org/licenses/mit-license.php
//

package command

import (
    "context"
    "crypto/md5"
    "encoding/binary"
    "fmt"
    "io"
    "io/ioutil"
    "os"
    "os/signal"
    "path"
    "path/filepath"
    "strconv"
    "strings"
    "sync"
    "syscall"

    colorable "github.com/mattn/go-colorable"
    gitconfig "github.com/tcnksm/go-gitconfig"
    "github.com/ttacon/chalk"
    "github.com/urfave/cli"
)

// SourceArchive defines a name of source archive file.
const SourceArchive = "source.tar.gz"

// RunOpt defines a option parameter for run function.
type RunOpt struct {
    // Same options as DockerfileOpt.
    *DockerfileOpt
    // Travis configuration file.
    Filename string
    // Container name.
    Name string
    // Runtime version to which only versions matching will be run.
    Version string
    // Image tag.
    Tag string
    // Max processors to be used.
    Processors int
    // If true, logging information to be stored to files.
    OutputLog bool
    // If true, not using cache during buidling a docker image.
    NoCache bool
    // If true, omit printing color codes.
    NoColor bool
    // Printed on the header.
    Title string
}

// Run implements the action of this command.
func Run(c *cli.Context) error {

    opt := RunOpt{
        DockerfileOpt: &DockerfileOpt{
            BaseImage:  c.String("base"),
            AptProxy:   c.String("apt-proxy"),
            PypiProxy:  c.String("pypi-proxy"),
            HTTPProxy:  c.String("http-proxy"),
            HTTPSProxy: c.String("https-proxy"),
            NoProxy:    c.String("no-proxy"),
        },
        Filename:   c.Args().First(),
        Name:       c.String("name"),
        Version:    c.String("select"),
        Tag:        c.String("tag"),
        Processors: c.Int("max-processors"),
        OutputLog:  c.Bool("log"),
        NoCache:    c.Bool("no-cache"),
        NoColor:    c.Bool("no-color"),
        Title:      fmt.Sprintf("%v %v", c.App.Name, c.App.Version),
    }
    if err := run(&opt); err != nil {
        return cli.NewExitError(err.Error(), 1)
    }
    return nil
}

func run(opt *RunOpt) (err error) {

    // Prepare to be canceled.
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    sig := make(chan os.Signal, 1)
    signal.Notify(sig, os.Interrupt, os.Kill, syscall.SIGQUIT)
    go func() {
        <-sig
        cancel()
    }()

    // Prepare interface.
    display, ctx, err := NewDisplay(ctx, opt.Title, opt.Processors)
    if err != nil {
        return
    }
    defer display.Close()
    logger := display.Header.Logger

    var stdout io.Writer
    if opt.NoColor {
        stdout = colorable.NewNonColorable(os.Stdout)
        logger = colorable.NewNonColorable(logger)
        cli.ErrWriter = colorable.NewNonColorable(cli.ErrWriter)
    } else {
        stdout = colorable.NewColorableStdout()
    }

    // Load a Travis's script file.
    if opt.Filename == "" {
        opt.Filename = ".travis.yml"
    }
    fmt.Fprintln(logger, chalk.Cyan.Color("Loading .travis.yml"))

    travis, err := NewTravisFromFile(opt.Filename)
    if err != nil {
        return
    }

    // Get repository information.
    fmt.Fprintln(logger, chalk.Cyan.Color("Checking repository information"))
    origin, err := gitconfig.OriginURL()
    if err != nil {
        return
    }
    opt.Repository = getRepository(origin)

    // Set up the tag name of the container image.
    if opt.Tag == "" {
        opt.Tag = fmt.Sprintf("loci/%s", strings.ToLower(path.Base(opt.Repository)))
    }

    // Prepare docker images.
    fmt.Fprintln(logger, chalk.Cyan.Color("Preparing docker images for sandbox containers"))
    err = PrepareBaseImage(ctx, opt.BaseImage, logger)
    if err != nil {
        return
    }

    // Archive source files.
    fmt.Fprintln(logger, chalk.Cyan.Color("Archiving source code"))
    tempDir := filepath.Join(os.TempDir(), opt.Tag)
    if err = os.MkdirAll(tempDir, 0777); err != nil {
        return
    }
    defer os.RemoveAll(tempDir)
    pwd, err := os.Getwd()
    if err != nil {
        return
    }
    if err = Archive(ctx, pwd, filepath.Join(tempDir, SourceArchive)); err != nil {
        return
    }

    // Create Dockerfile.
    fmt.Fprintln(logger, chalk.Cyan.Color("Creating Dockerfile"))
    docker, err := Dockerfile(travis, opt.DockerfileOpt, SourceArchive)
    if err != nil {
        return
    }
    if err = ioutil.WriteFile(filepath.Join(tempDir, "Dockerfile"), docker, 0644); err != nil {
        return
    }

    // Create entrypoint.sh.
    fmt.Fprintln(logger, chalk.Cyan.Color("Creating entrypoint.sh"))
    entry, err := Entrypoint(travis)
    if err != nil {
        return
    }
    if err = ioutil.WriteFile(filepath.Join(tempDir, "entrypoint.sh"), entry, 0644); err != nil {
        return
    }

    argset, err := travis.ArgumentSet(logger)
    if err != nil {
        return
    }

    // Start testing with goroutines.
    fmt.Fprintln(logger, chalk.Cyan.Color("Building sandbox images and running tests"))
    var i int
    var wg sync.WaitGroup
    semaphore := make(chan struct{}, opt.Processors)
    errs := NewErrorSet()
    for version, set := range argset {

        if opt.Version != "" && version != opt.Version {
            continue
        }

        wg.Add(1)
        go func(version string, set []TestCase) (err error) {
            semaphore <- struct{}{}
            defer func() {
                <-semaphore
                wg.Done()
            }()

            // Build a container image.
            sec := display.AddSection(fmt.Sprintf("Building a docker image for %v", version))
            defer display.DeleteSection(sec)

            var output io.Writer
            writer := sec.Writer()
            defer writer.Close()
            output = writer
            if opt.NoColor {
                output = colorable.NewNonColorable(output)
            }

            if opt.OutputLog {
                var fp *os.File
                fp, err = os.OpenFile(fmt.Sprintf("loci-build-%v.log", version), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
                if err != nil {
                    errs.Add(version, err)
                    return
                }
                defer fp.Close()
                output = io.MultiWriter(output, colorable.NewColorable(fp))
            }

            tag := fmt.Sprintf("%v/%v", opt.Tag, version)
            err = Build(ctx, tempDir, tag, version, opt.NoCache, output)
            if err == context.Canceled {
                errs.Add("", err)
                return
            } else if err != nil {
                msg := fmt.Sprintf(chalk.Red.Color("Faild to build a docker image for %v"), version)
                errs.Add(
                    version,
                    fmt.Errorf("%v\n%v\n%v\n", msg, err.Error(), sec.String()))
                fmt.Fprintln(logger, msg)
                return
            }
            fmt.Fprintln(logger, chalk.Green.Color(fmt.Sprintf("Built a docker image for %v", version)))

            for _, c := range set {

                wg.Add(1)
                go func(envs []string) {
                    semaphore <- struct{}{}
                    defer func() {
                        <-semaphore
                        wg.Done()
                    }()

                    // Run tests in a sandbox.
                    sec := display.AddSection(fmt.Sprintf("Running tests (%v: %v)", version, envs))
                    defer display.DeleteSection(sec)

                    var output io.Writer
                    writer := sec.Writer()
                    defer writer.Close()
                    output = writer
                    if opt.NoColor {
                        output = colorable.NewNonColorable(output)
                    }

                    if opt.OutputLog {
                        var fp *os.File
                        hash := md5.Sum([]byte(strings.Join(envs, "-")))
                        fp, err = os.OpenFile(
                            fmt.Sprintf("loci-%v-%v.log", version, strconv.FormatInt(int64(binary.BigEndian.Uint64(hash[:])), 36)),
                            os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
                        if err != nil {
                            errs.Add(fmt.Sprintf("%v:%v", version, envs), err)
                            return
                        }
                        defer fp.Close()

                        fmt.Fprintln(fp, "* Environment variables *")
                        for _, v := range envs {
                            fmt.Fprintln(fp, v)
                        }
                        fmt.Fprintln(fp, "")

                        output = io.MultiWriter(output, colorable.NewColorable(fp))
                    }

                    name := opt.Name
                    if name != "" {
                        i++
                        name = fmt.Sprintf("%s-%d", name, i)
                    }

                    err = Start(ctx, tag, name, envs, output)
                    if err == context.Canceled {
                        errs.Add("", err)
                    } else if err != nil {
                        errs.Add(fmt.Sprintf("%v:%v", version, envs), fmt.Errorf("%s\n%s", chalk.Red.Color(err.Error()), sec.String()))
                        fmt.Fprintln(logger, chalk.Red.Color(fmt.Sprintf("Failed tests (%v: %v) ", version, envs)))
                    } else {
                        fmt.Fprintln(logger, chalk.Green.Color(fmt.Sprintf("Passed tests (%v: %v) ", version, envs)))
                    }
                    return

                }(c.Slice())

            }

            return

        }(version, set)

    }

    wg.Wait()
    err = display.Close()
    if err != nil {
        errs.Add("", err)
    }

    if errs.Size() == 0 {
        fmt.Fprintln(stdout, chalk.Green.Color("All tests have been passed."))
    } else {
        errList := errs.GetList()
        err = cli.NewMultiError(errList...)
    }
    return

}

// getRepository returns the repository path from a given remote URL of
// origin repository. The repository path consists of a URL without
// sheme, user name, password, and .git suffix.
func getRepository(origin string) (res string) {

    switch {
    case strings.Contains(origin, "@"):
        res = strings.Replace(strings.Split(origin, "@")[1], ":", "/", 1)
    case strings.HasPrefix(origin, "http://"):
        res = origin[len("http://"):]
    case strings.HasPrefix(origin, "https://"):
        res = origin[len("https://"):]
    default:
        res = strings.Replace(origin, ":", "/", 1)
    }
    if strings.HasSuffix(res, ".git") {
        res = res[:len(res)-len(".git")]
    }

    return

}