github.com/mweagle/Sparta@v1.15.0/docker/docker.go (about)

     1  package docker
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/base64"
     6  	"fmt"
     7  	"os"
     8  	"os/exec"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/aws/aws-sdk-go/aws/session"
    13  	"github.com/aws/aws-sdk-go/service/ecr"
    14  	"github.com/aws/aws-sdk-go/service/sts"
    15  	"github.com/mweagle/Sparta/system"
    16  	"github.com/pkg/errors"
    17  	"github.com/sirupsen/logrus"
    18  )
    19  
    20  ////////////////////////////////////////////////////////////////////////////////
    21  // CONSTANTS
    22  ////////////////////////////////////////////////////////////////////////////////
    23  
    24  const (
    25  	// BinaryNameArgument is the argument provided to docker build that
    26  	// supplies the local statically built Go binary
    27  	BinaryNameArgument = "SPARTA_DOCKER_BINARY"
    28  )
    29  
    30  // BuildDockerImageWithFlags is an extended version of BuildDockerImage that includes
    31  // support for build time tags
    32  func BuildDockerImageWithFlags(serviceName string,
    33  	dockerFilepath string,
    34  	dockerTags map[string]string,
    35  	buildTags string,
    36  	linkFlags string,
    37  	logger *logrus.Logger) error {
    38  
    39  	// BEGIN DOCKER PRECONDITIONS
    40  	// Ensure that serviceName and tags are lowercase to make Docker happy
    41  	var dockerErrors []string
    42  	for eachKey, eachValue := range dockerTags {
    43  		if eachKey != strings.ToLower(eachKey) ||
    44  			eachValue != strings.ToLower(eachValue) {
    45  			dockerErrors = append(dockerErrors, fmt.Sprintf("--tag %s:%s MUST be lower case", eachKey, eachValue))
    46  		}
    47  	}
    48  
    49  	if len(dockerErrors) > 0 {
    50  		return errors.Errorf("Docker build errors: %s", strings.Join(dockerErrors[:], ", "))
    51  	}
    52  	// BEGIN Informational - output the docker version...
    53  	dockerVersionCmd := exec.Command("docker", "-v")
    54  	dockerVersionCmdErr := system.RunOSCommand(dockerVersionCmd, logger)
    55  	if dockerVersionCmdErr != nil {
    56  		return errors.Wrapf(dockerVersionCmdErr, "Attempting to get docker version")
    57  	}
    58  	// END Informational - output the docker version...
    59  
    60  	// END DOCKER PRECONDITIONS
    61  
    62  	// Compile this binary for minimal Docker size
    63  	// https://blog.codeship.com/building-minimal-docker-containers-for-go-applications/
    64  	currentTime := time.Now().UnixNano()
    65  	executableOutput := fmt.Sprintf("%s-%d-docker.lambda.amd64", serviceName, currentTime)
    66  	buildErr := system.BuildGoBinary(serviceName,
    67  		executableOutput,
    68  		false,
    69  		fmt.Sprintf("%d", currentTime),
    70  		buildTags,
    71  		linkFlags,
    72  		false,
    73  		logger)
    74  	if buildErr != nil {
    75  		return errors.Wrapf(buildErr, "Attempting to build Docker binary")
    76  	}
    77  	defer func() {
    78  		removeErr := os.Remove(executableOutput)
    79  		if removeErr != nil {
    80  			logger.WithFields(logrus.Fields{
    81  				"Path":  executableOutput,
    82  				"Error": removeErr,
    83  			}).Warn("Failed to delete temporary Docker binary")
    84  		}
    85  	}()
    86  
    87  	// ARG SPARTA_DOCKER_BINARY reference s.t. we can supply the binary
    88  	// name to the build..
    89  	// We need to build the static binary s.t. we can add it to the Docker container...
    90  	// Build the image...
    91  	dockerArgs := []string{
    92  		"build",
    93  		"--build-arg",
    94  		fmt.Sprintf("%s=%s", BinaryNameArgument, executableOutput),
    95  	}
    96  
    97  	if dockerFilepath != "" {
    98  		dockerArgs = append(dockerArgs, "--file", dockerFilepath)
    99  	}
   100  	// Add the latest tag
   101  	// dockerArgs = append(dockerArgs, "--tag", fmt.Sprintf("sparta/%s:latest", serviceName))
   102  	logger.WithFields(logrus.Fields{
   103  		"Tags": dockerTags,
   104  	}).Info("Creating Docker image")
   105  
   106  	for eachKey, eachValue := range dockerTags {
   107  		dockerArgs = append(dockerArgs, "--tag", fmt.Sprintf("%s:%s",
   108  			strings.ToLower(eachKey),
   109  			strings.ToLower(eachValue)))
   110  	}
   111  
   112  	dockerArgs = append(dockerArgs, ".")
   113  	dockerCmd := exec.Command("docker", dockerArgs...)
   114  	return system.RunOSCommand(dockerCmd, logger)
   115  }
   116  
   117  // BuildDockerImage creates the smallest docker image for this Golang binary
   118  // using the serviceName as the image name and including the supplied tags
   119  func BuildDockerImage(serviceName string,
   120  	dockerFilepath string,
   121  	tags map[string]string,
   122  	logger *logrus.Logger) error {
   123  
   124  	return BuildDockerImageWithFlags(serviceName,
   125  		dockerFilepath,
   126  		tags,
   127  		"",
   128  		"",
   129  		logger)
   130  }
   131  
   132  // PushDockerImageToECR pushes a local Docker image to an ECR repository
   133  func PushDockerImageToECR(localImageTag string,
   134  	ecrRepoName string,
   135  	awsSession *session.Session,
   136  	logger *logrus.Logger) (string, error) {
   137  
   138  	stsSvc := sts.New(awsSession)
   139  	ecrSvc := ecr.New(awsSession)
   140  
   141  	// 1. Get the caller identity s.t. we can get the ECR URL which includes the
   142  	// account name
   143  	stsIdentityOutput, stsIdentityErr := stsSvc.GetCallerIdentity(&sts.GetCallerIdentityInput{})
   144  	if stsIdentityErr != nil {
   145  		return "", errors.Wrapf(stsIdentityErr, "Attempting to get AWS caller identity")
   146  	}
   147  
   148  	// 2. Create the URL to which we're going to do the push
   149  	localImageTagParts := strings.Split(localImageTag, ":")
   150  	ecrTagValue := fmt.Sprintf("%s.dkr.ecr.%s.amazonaws.com/%s:%s",
   151  		*stsIdentityOutput.Account,
   152  		*awsSession.Config.Region,
   153  		ecrRepoName,
   154  		localImageTagParts[len(localImageTagParts)-1])
   155  
   156  	// 3. Tag the local image with the ECR tag
   157  	dockerTagCmd := exec.Command("docker", "tag", localImageTag, ecrTagValue)
   158  	dockerTagCmdErr := system.RunOSCommand(dockerTagCmd, logger)
   159  	if dockerTagCmdErr != nil {
   160  		return "", errors.Wrapf(dockerTagCmdErr, "Attempting to tag Docker image")
   161  	}
   162  
   163  	// 4. Push the image - if that fails attempt to reauthorize with the docker
   164  	// client and try again
   165  	var pushError error
   166  	dockerPushCmd := exec.Command("docker", "push", ecrTagValue)
   167  	pushError = system.RunOSCommand(dockerPushCmd, logger)
   168  	if pushError != nil {
   169  		logger.WithFields(logrus.Fields{
   170  			"Error": pushError,
   171  		}).Info("ECR push failed - reauthorizing")
   172  		ecrAuthTokenResult, ecrAuthTokenResultErr := ecrSvc.GetAuthorizationToken(&ecr.GetAuthorizationTokenInput{})
   173  		if ecrAuthTokenResultErr != nil {
   174  			pushError = ecrAuthTokenResultErr
   175  		} else {
   176  			authData := ecrAuthTokenResult.AuthorizationData[0]
   177  			authToken, authTokenErr := base64.StdEncoding.DecodeString(*authData.AuthorizationToken)
   178  			if authTokenErr != nil {
   179  				pushError = authTokenErr
   180  			} else {
   181  				authTokenString := string(authToken)
   182  				authTokenParts := strings.Split(authTokenString, ":")
   183  				dockerURL := fmt.Sprintf("https://%s.dkr.ecr.%s.amazonaws.com",
   184  					*stsIdentityOutput.Account,
   185  					*awsSession.Config.Region)
   186  				dockerLoginCmd := exec.Command("docker",
   187  					"login",
   188  					"-u",
   189  					authTokenParts[0],
   190  					"--password-stdin",
   191  					dockerURL)
   192  				dockerLoginCmd.Stdout = os.Stdout
   193  				dockerLoginCmd.Stdin = bytes.NewReader([]byte(fmt.Sprintf("%s\n", authTokenParts[1])))
   194  				dockerLoginCmd.Stderr = os.Stderr
   195  				dockerLoginCmdErr := system.RunOSCommand(dockerLoginCmd, logger)
   196  				if dockerLoginCmdErr != nil {
   197  					pushError = dockerLoginCmdErr
   198  				} else {
   199  					// Try it again...
   200  					dockerRetryPushCmd := exec.Command("docker", "push", ecrTagValue)
   201  					dockerRetryPushCmdErr := system.RunOSCommand(dockerRetryPushCmd, logger)
   202  					pushError = dockerRetryPushCmdErr
   203  				}
   204  			}
   205  		}
   206  	}
   207  	if pushError != nil {
   208  		pushError = errors.Wrapf(pushError, "Attempting to push Docker image")
   209  	}
   210  	return ecrTagValue, pushError
   211  }