runner/setup.go

Summary

Maintainability
C
1 day
Test Coverage
package runner

import (
    "context"
    "fmt"
    "io"
    "time"

    "github.com/efritz/ij/config"
    "github.com/efritz/ij/environment"
    "github.com/efritz/ij/logging"
    "github.com/efritz/ij/network"
    "github.com/efritz/ij/options"
    "github.com/efritz/ij/registry"
    "github.com/efritz/ij/scratch"
    "github.com/efritz/ij/ssh"
    "github.com/efritz/ij/util"
)

func SetupRunner(
    cfg *config.Config,
    appOptions *options.AppOptions,
    runOptions *options.RunOptions,
) (runner *Runner, err error) {
    var (
        cleanup           = NewCleanup()
        ctx, cancel       = setupContext(runOptions.Context, runOptions.PlanTimeout)
        logger            logging.Logger
        loggerFactory     *logging.LoggerFactory
        runID             string
        scratch           *scratch.ScratchSpace
        taskRunnerFactory TaskRunnerFactory
    )

    if runID, err = setupRunID(); err != nil {
        return
    }

    enableHostSSHAgent, err := shouldEnableHostSSHAgent(
        runOptions.EnableContainerSSHAgent,
        cfg.Options.SSHIdentities,
    )

    if err != nil {
        return
    }

    scratch, err = setupScratch(
        runID,
        appOptions.ProjectDir,
        appOptions.ScratchRoot,
        cleanup,
        runOptions.KeepWorkspace,
    )

    if err != nil {
        return
    }

    defer func() {
        if err == nil {
            return
        }

        if err := scratch.Teardown(); err != nil {
            logging.EmergencyLog(
                "error: failed to teardown scratch directory: %s",
                err.Error(),
            )
        }
    }()

    logger, loggerFactory, err = setupLogger(
        cleanup,
        scratch,
        appOptions.Quiet,
        appOptions.Verbose,
        !appOptions.DisableColor,
        appOptions.FileFactory,
    )

    if err != nil {
        return
    }

    cleanup.Register(func() {
        scratch.Prune(logger)
    })

    _, err = setupNetwork(
        ctx,
        runID,
        cleanup,
        logger,
    )

    if err != nil {
        return
    }

    containerLists := setupContainerLists(
        runID,
        cleanup,
        logger,
    )

    if runOptions.EnableContainerSSHAgent {
        logger.Info(
            nil,
            "Starting ssh-agent container",
        )

        err = startSSHAgent(
            runID,
            cfg.Options.SSHIdentities,
            scratch,
            containerLists,
            logger,
        )

        if err != nil {
            reportError(
                ctx,
                logger,
                nil,
                "error: failed to setup ssh-agent: %s",
                err.Error(),
            )

            return
        }
    }

    err = setupRegistries(
        ctx,
        cfg,
        cleanup,
        logger,
        appOptions.Env,
        runOptions.Login,
    )

    if err != nil {
        return
    }

    taskRunnerFactory = func(
        context *RunContext,
        task config.Task,
        prefix *logging.Prefix,
        env environment.Environment,
    ) TaskRunner {
        switch t := task.(type) {
        case *config.BuildTask:
            return NewBuildTaskRunnerFactory(
                ctx,
                scratch.Workspace(),
                logger,
            )(
                t,
                env,
                prefix,
            )

        case *config.PushTask:
            return NewPushTaskRunnerFactory(
                ctx,
                logger,
            )(
                context,
                t,
                env,
                prefix,
            )

        case *config.RemoveTask:
            return NewRemoveTaskRunnerFactory(
                ctx,
                logger,
            )(
                context,
                t,
                env,
                prefix,
            )

        case *config.RunTask:
            containerOptions := &containerOptions{
                EnableHostSSHAgent:      enableHostSSHAgent,
                EnableContainerSSHAgent: runOptions.EnableContainerSSHAgent,
                CPUShares:               runOptions.CPUShares,
                Memory:                  runOptions.Memory,
            }

            return NewRunTaskRunnerFactory(
                ctx,
                cfg,
                runID,
                scratch,
                containerLists,
                containerOptions,
                logger,
                loggerFactory,
            )(
                t,
                env,
                prefix,
            )

        case *config.PlanTask:
            runner := NewPlanRunner(
                ctx,
                cfg,
                taskRunnerFactory,
                logger,
                appOptions.Env,
            )

            return NewPlanTaskRunnerFactory(
                runner,
                logger,
            )(
                t,
                env,
                prefix,
            )
        }

        panic("unexpected task type")
    }

    runner = NewRunner(
        ctx,
        logger,
        cfg,
        taskRunnerFactory,
        scratch,
        cleanup,
        runID,
        cancel,
        appOptions.Env,
    )

    return
}

//
// Setup Functions

func setupContext(ctx context.Context, timeout time.Duration) (context.Context, func()) {
    if timeout == 0 {
        return context.WithCancel(ctx)
    }

    return context.WithTimeout(ctx, timeout)
}

func setupRunID() (string, error) {
    id, err := util.MakeID()
    if err != nil {
        logging.EmergencyLog(
            "error: failed to generate run id: %s",
            err.Error(),
        )

        return "", err
    }

    return id, nil
}

func setupScratch(
    runID string,
    projectDir string,
    scratchRoot string,
    cleanup *Cleanup,
    keepWorkspace bool,
) (*scratch.ScratchSpace, error) {
    scratch := scratch.NewScratchSpace(
        runID,
        projectDir,
        scratchRoot,
        keepWorkspace,
    )

    if err := scratch.Setup(); err != nil {
        logging.EmergencyLog(
            "error: failed to create scratch directory: %s",
            err.Error(),
        )

        return nil, err
    }

    return scratch, nil
}

func setupLogger(
    cleanup *Cleanup,
    scratch *scratch.ScratchSpace,
    quiet bool,
    verbose bool,
    colorize bool,
    fileFactory logging.FileFactory,
) (logging.Logger, *logging.LoggerFactory, error) {
    logProcessor := logging.NewProcessor(quiet, verbose, colorize)
    logProcessor.Start()
    cleanup.Register(logProcessor.Shutdown)

    //
    // Create Logger Factory

    if fileFactory == nil {
        fileFactory = func(prefix string) (io.WriteCloser, io.WriteCloser, error) {
            return scratch.MakeLogFiles(prefix)
        }
    }

    loggerFactory := logging.NewLoggerFactory(logProcessor, fileFactory)

    //
    // Create Base Logger

    logger, err := loggerFactory.Logger("ij", true)
    if err != nil {
        logging.EmergencyLog(
            "error: failed to create log files: %s",
            err.Error(),
        )

        return nil, nil, err
    }

    return logger, loggerFactory, nil
}

func setupContainerLists(
    runID string,
    cleanup *Cleanup,
    logger logging.Logger,
) *ContainerLists {
    containerStopper := NewContainerStopper(logger)
    networkDisconnector := NewNetworkDisconnector(runID, logger)

    cleanup.Register(containerStopper.Execute)
    cleanup.Register(networkDisconnector.Execute)

    return &ContainerLists{
        ContainerStopper:    containerStopper,
        NetworkDisconnector: networkDisconnector,
    }
}

func setupRegistries(
    ctx context.Context,
    cfg *config.Config,
    cleanup *Cleanup,
    logger logging.Logger,
    env []string,
    login bool,
) error {
    if !login {
        return nil
    }

    registryEnv := environment.Merge(
        environment.New(cfg.Environment),
        environment.New(env),
    )

    registrySet, err := registry.NewRegistrySet(
        ctx,
        logger,
        registryEnv,
        cfg.Registries,
    )

    if err != nil {
        reportError(
            ctx,
            logger,
            nil,
            "error: failed to resolve registries: %s",
            err.Error(),
        )

        return err
    }

    if err = registrySet.Login(); err != nil {
        reportError(
            ctx,
            logger,
            nil,
            "error: failed to log into registries: %s",
            err.Error(),
        )

        return err
    }

    cleanup.Register(registrySet.Logout)
    return nil
}

func setupNetwork(
    ctx context.Context,
    runID string,
    cleanup *Cleanup,
    logger logging.Logger,
) (*network.Network, error) {
    network, err := network.NewNetwork(ctx, runID, logger)
    if err != nil {
        reportError(
            ctx,
            logger,
            nil,
            "error: failed to create network: %s",
            err.Error(),
        )

        return nil, err
    }

    cleanup.Register(network.Teardown)
    return network, nil
}

//
// Helpers

func shouldEnableHostSSHAgent(enableContainerSSHAgent bool, identities []string) (bool, error) {
    if enableContainerSSHAgent {
        return false, nil
    }

    enable, err := ssh.EnsureKeysAvailable(identities)
    if err != nil {
        return false, fmt.Errorf(
            "failed to validate ssh keys: %s",
            err.Error(),
        )
    }

    return enable, nil
}

func reportError(
    ctx context.Context,
    logger logging.Logger,
    prefix *logging.Prefix,
    format string,
    args ...interface{},
) {
    select {
    case <-ctx.Done():
        return
    default:
    }

    logger.Error(prefix, format, args...)
}