2019-07-14 14:55:34 -08:00
|
|
|
package cicd
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
|
|
|
"log"
|
|
|
|
"os"
|
|
|
|
"os/exec"
|
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"geeks-accelerator/oss/saas-starter-kit/internal/platform/tests"
|
|
|
|
"github.com/aws/aws-sdk-go/aws/session"
|
|
|
|
"github.com/iancoleman/strcase"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
)
|
|
|
|
|
|
|
|
// serviceDeployRequest defines the details needed to execute a service deployment.
|
|
|
|
type serviceRequest struct {
|
|
|
|
ServiceName string `validate:"required"`
|
|
|
|
ServiceDir string `validate:"required"`
|
|
|
|
Env string `validate:"oneof=dev stage prod"`
|
|
|
|
ProjectRoot string `validate:"required"`
|
|
|
|
ProjectName string `validate:"required"`
|
|
|
|
DockerFile string `validate:"required"`
|
|
|
|
GoModFile string `validate:"required"`
|
|
|
|
GoModName string `validate:"required"`
|
|
|
|
|
|
|
|
AwsCreds awsCredentials `validate:"required,dive,required"`
|
|
|
|
_awsSession *session.Session
|
|
|
|
|
|
|
|
ReleaseImage string
|
|
|
|
}
|
|
|
|
|
|
|
|
// projectNameCamel takes a project name and returns the camel cased version.
|
|
|
|
func (r *serviceRequest) ProjectNameCamel() string {
|
|
|
|
s := strings.Replace(r.ProjectName, "_", " ", -1)
|
|
|
|
s = strings.Replace(s, "-", " ", -1)
|
|
|
|
s = strcase.ToCamel(s)
|
|
|
|
return s
|
|
|
|
}
|
|
|
|
|
|
|
|
// awsSession returns the current AWS session for the serviceDeployRequest.
|
|
|
|
func (r *serviceRequest) awsSession() *session.Session {
|
|
|
|
if r._awsSession == nil {
|
|
|
|
r._awsSession = r.AwsCreds.Session()
|
|
|
|
}
|
|
|
|
|
|
|
|
return r._awsSession
|
|
|
|
}
|
|
|
|
|
|
|
|
// init sets the basic details needed for both build and deploy for serviceRequest.
|
|
|
|
func (req *serviceRequest) init(log *log.Logger) error {
|
|
|
|
// When project root directory is empty or set to current working path, then search for the project root by locating
|
|
|
|
// the go.mod file.
|
|
|
|
log.Println("\tDetermining the project root directory.")
|
|
|
|
{
|
|
|
|
if req.ProjectRoot == "" || req.ProjectRoot == "." {
|
|
|
|
log.Println("\tAttempting to location project root directory from current working directory.")
|
|
|
|
|
|
|
|
var err error
|
|
|
|
req.GoModFile, err = findProjectGoModFile()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
req.ProjectRoot = filepath.Dir(req.GoModFile)
|
|
|
|
} else {
|
|
|
|
log.Printf("\t\tUsing supplied project root directory '%s'.\n", req.ProjectRoot)
|
|
|
|
req.GoModFile = filepath.Join(req.ProjectRoot, "go.mod")
|
|
|
|
}
|
|
|
|
log.Printf("\t\t\tproject root: %s", req.ProjectRoot)
|
|
|
|
log.Printf("\t\t\tgo.mod: %s", req.GoModFile)
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Println("\tExtracting go module name from go.mod.")
|
|
|
|
{
|
|
|
|
var err error
|
|
|
|
req.GoModName, err = loadGoModName(req.GoModFile)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
log.Printf("\t\t\tmodule name: %s", req.GoModName)
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Println("\tDetermining the project name.")
|
|
|
|
{
|
|
|
|
if req.ProjectName != "" {
|
|
|
|
log.Printf("\t\tUse provided value.")
|
|
|
|
} else {
|
|
|
|
req.ProjectName = filepath.Base(req.GoModName)
|
|
|
|
log.Printf("\t\tSet from go module.")
|
|
|
|
}
|
|
|
|
log.Printf("\t\t\tproject name: %s", req.ProjectName)
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Println("\tAttempting to locate service directory from project root directory.")
|
|
|
|
{
|
|
|
|
if req.DockerFile != "" {
|
|
|
|
req.DockerFile = req.DockerFile
|
|
|
|
log.Printf("\t\tUse provided value.")
|
|
|
|
|
|
|
|
} else {
|
|
|
|
log.Printf("\t\tFind from project root looking for Dockerfile.")
|
|
|
|
var err error
|
|
|
|
req.DockerFile, err = findServiceDockerFile(req.ProjectRoot, req.ServiceName)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
req.ServiceDir = filepath.Dir(req.DockerFile)
|
|
|
|
|
|
|
|
log.Printf("\t\t\tservice directory: %s", req.ServiceDir)
|
|
|
|
log.Printf("\t\t\tdockerfile: %s", req.DockerFile)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Verifies AWS credentials specified as environment variables.
|
|
|
|
log.Println("\tVerify AWS credentials.")
|
|
|
|
{
|
|
|
|
var err error
|
|
|
|
req.AwsCreds, err = GetAwsCredentials(req.Env)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if req.AwsCreds.UseRole {
|
|
|
|
log.Printf("\t\t\tUsing role")
|
|
|
|
} else {
|
|
|
|
log.Printf("\t\t\tAccessKeyID: '%s'", req.AwsCreds.AccessKeyID)
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Printf("\t\t\tRegion: '%s'", req.AwsCreds.Region)
|
|
|
|
log.Printf("\t%s\tAWS credentials valid.", tests.Success)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// ecrRepositoryName returns the name used for the AWS ECR Repository.
|
|
|
|
func ecrRepositoryName(projectName string) string {
|
|
|
|
return projectName
|
|
|
|
}
|
|
|
|
|
|
|
|
// releaseImage returns the name used for tagging a release image will always include one with environment and
|
|
|
|
// service name. If the env var CI_COMMIT_REF_NAME is set, it will be appended.
|
2019-07-14 19:13:09 -08:00
|
|
|
func releaseTag(env, serviceName string) string {
|
2019-07-14 14:55:34 -08:00
|
|
|
|
|
|
|
tag1 := env + "-" + serviceName
|
|
|
|
|
|
|
|
// Generate tags for the release image.
|
2019-07-14 19:13:09 -08:00
|
|
|
var releaseTag string
|
2019-07-14 14:55:34 -08:00
|
|
|
if v := os.Getenv("CI_COMMIT_REF_NAME"); v != "" {
|
|
|
|
tag2 := tag1 + "-" + v
|
2019-07-14 19:13:09 -08:00
|
|
|
releaseTag = tag2
|
2019-07-14 14:55:34 -08:00
|
|
|
} else {
|
2019-07-14 19:13:09 -08:00
|
|
|
releaseTag = tag1
|
2019-07-14 14:55:34 -08:00
|
|
|
}
|
2019-07-14 19:13:09 -08:00
|
|
|
return releaseTag
|
|
|
|
}
|
|
|
|
|
|
|
|
// releaseImage returns the name used for tagging a release image will always include one with environment and
|
|
|
|
// service name. If the env var CI_COMMIT_REF_NAME is set, it will be appended.
|
|
|
|
func releaseImage(env, serviceName, repositoryUri string) string {
|
|
|
|
return repositoryUri + ":" + releaseTag(env, serviceName)
|
2019-07-14 14:55:34 -08:00
|
|
|
}
|
|
|
|
|
2019-07-14 15:33:23 -08:00
|
|
|
// dBInstanceIdentifier returns the database name.
|
|
|
|
func dBInstanceIdentifier(projectName, env string) string {
|
|
|
|
return projectName + "-" + env
|
|
|
|
}
|
|
|
|
|
|
|
|
// secretID returns the secret name with a standard prefix.
|
|
|
|
func secretID(projectName, env, secretName string) string {
|
|
|
|
return filepath.Join(projectName, env, secretName)
|
|
|
|
}
|
|
|
|
|
2019-07-14 14:55:34 -08:00
|
|
|
// findProjectGoModFile finds the project root directory from the current working directory.
|
|
|
|
func findProjectGoModFile() (string, error) {
|
|
|
|
var err error
|
|
|
|
projectRoot, err := os.Getwd()
|
|
|
|
if err != nil {
|
|
|
|
return "", errors.WithMessage(err, "failed to get current working directory")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Try to find the project root for looking for the go.mod file in a parent directory.
|
|
|
|
var goModFile string
|
|
|
|
testDir := projectRoot
|
|
|
|
for i := 0; i < 3; i++ {
|
|
|
|
if goModFile != "" {
|
|
|
|
testDir = filepath.Join(testDir, "../")
|
|
|
|
}
|
|
|
|
goModFile = filepath.Join(testDir, "go.mod")
|
|
|
|
ok, _ := exists(goModFile)
|
|
|
|
if ok {
|
|
|
|
projectRoot = testDir
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Verify the go.mod file was found.
|
|
|
|
ok, err := exists(goModFile)
|
|
|
|
if err != nil {
|
|
|
|
return "", errors.WithMessagef(err, "failed to load go.mod for project using project root %s")
|
|
|
|
} else if !ok {
|
|
|
|
return "", errors.Errorf("failed to locate project go.mod in project root %s", projectRoot)
|
|
|
|
}
|
|
|
|
|
|
|
|
return goModFile, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// findServiceDockerFile finds the service directory.
|
|
|
|
func findServiceDockerFile(projectRoot, targetService string) (string, error) {
|
|
|
|
checkDirs := []string{
|
|
|
|
filepath.Join(projectRoot, "cmd", targetService),
|
|
|
|
filepath.Join(projectRoot, "tools", targetService),
|
|
|
|
}
|
|
|
|
|
|
|
|
var dockerFile string
|
|
|
|
for _, cd := range checkDirs {
|
|
|
|
// Check to see if directory contains Dockerfile.
|
|
|
|
tf := filepath.Join(cd, "Dockerfile")
|
|
|
|
|
|
|
|
ok, _ := exists(tf)
|
|
|
|
if ok {
|
|
|
|
dockerFile = tf
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if dockerFile == "" {
|
|
|
|
return "", errors.Errorf("failed to locate Dockerfile for service %s", targetService)
|
|
|
|
}
|
|
|
|
|
|
|
|
return dockerFile, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// getTargetEnv checks for an env var that is prefixed with the current target env.
|
|
|
|
func getTargetEnv(targetEnv, envName string) string {
|
|
|
|
k := fmt.Sprintf("%s_%s", strings.ToUpper(targetEnv), envName)
|
|
|
|
|
|
|
|
if v := os.Getenv(k); v != "" {
|
|
|
|
// Set the non prefixed env var with the prefixed value.
|
|
|
|
os.Setenv(envName, v)
|
|
|
|
return v
|
|
|
|
}
|
|
|
|
|
|
|
|
return os.Getenv(envName)
|
|
|
|
}
|
|
|
|
|
|
|
|
// loadGoModName parses out the module name from go.mod.
|
|
|
|
func loadGoModName(goModFile string) (string, error) {
|
|
|
|
ok, err := exists(goModFile)
|
|
|
|
if err != nil {
|
|
|
|
return "", errors.WithMessage(err, "Failed to load go.mod for project")
|
|
|
|
} else if !ok {
|
|
|
|
return "", errors.Errorf("Failed to locate project go.mod at %s", goModFile)
|
|
|
|
}
|
|
|
|
|
|
|
|
b, err := ioutil.ReadFile(goModFile)
|
|
|
|
if err != nil {
|
|
|
|
return "", errors.WithMessagef(err, "Failed to read go.mod at %s", goModFile)
|
|
|
|
}
|
|
|
|
|
|
|
|
var name string
|
|
|
|
lines := strings.Split(string(b), "\n")
|
|
|
|
for _, l := range lines {
|
|
|
|
if strings.HasPrefix(l, "module ") {
|
|
|
|
name = strings.TrimSpace(strings.Split(l, " ")[1])
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return name, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// exists returns a bool as to whether a file path exists.
|
|
|
|
func exists(path string) (bool, error) {
|
|
|
|
_, err := os.Stat(path)
|
|
|
|
if err == nil {
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
return true, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// execCmds executes a set of commands using the current env variables.
|
|
|
|
func execCmds(log *log.Logger, workDir string, cmds ...[]string) error {
|
|
|
|
for _, cmdVals := range cmds {
|
|
|
|
cmd := exec.Command(cmdVals[0], cmdVals[1:]...)
|
|
|
|
cmd.Dir = workDir
|
|
|
|
cmd.Env = os.Environ()
|
|
|
|
|
|
|
|
cmd.Stderr = log.Writer()
|
|
|
|
cmd.Stdout = log.Writer()
|
|
|
|
|
|
|
|
err := cmd.Run()
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return errors.WithMessagef(err, "failed to execute %s", strings.Join(cmdVals, " "))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|