You've already forked golang-saas-starter-kit
mirror of
https://github.com/raseels-repos/golang-saas-starter-kit.git
synced 2025-06-17 00:17:59 +02:00
checkpoint
This commit is contained in:
152
example-project/tools/truss/cmd/devops/devops.go
Normal file
152
example-project/tools/truss/cmd/devops/devops.go
Normal file
@ -0,0 +1,152 @@
|
||||
package devops
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
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")
|
||||
|
||||
fmt.Println(tf)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
type EnvVars []string
|
||||
|
||||
// execCmds executes a set of commands.
|
||||
func execCmds(workDir string, envVars *EnvVars, cmds ...[]string) ([]string, error) {
|
||||
if envVars == nil {
|
||||
ev := EnvVars(os.Environ())
|
||||
envVars = &ev
|
||||
}
|
||||
|
||||
var results []string
|
||||
for _, cmdVals := range cmds {
|
||||
cmd := exec.Command(cmdVals[0], cmdVals[1:]...)
|
||||
cmd.Dir = workDir
|
||||
cmd.Env = *envVars
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return results, errors.WithMessagef(err, "failed to execute %s - %s\n%s", strings.Join(cmdVals, " "), string(out))
|
||||
}
|
||||
results = append(results, string(out))
|
||||
|
||||
// Update the current env vars after command has been executed.
|
||||
ev := EnvVars(cmd.Env)
|
||||
envVars = &ev
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
213
example-project/tools/truss/cmd/devops/service_build.go
Normal file
213
example-project/tools/truss/cmd/devops/service_build.go
Normal file
@ -0,0 +1,213 @@
|
||||
package devops
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/tests"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// requiredCmdsBuild proves a list of required executables for completing build.
|
||||
var requiredCmdsBuild = [][]string{
|
||||
[]string{"docker", "version", "-f", "{{.Client.Version}}"},
|
||||
}
|
||||
|
||||
// Run is the main entrypoint for building a service for a given target env.
|
||||
func ServiceBuild(log *log.Logger, projectRoot, targetService, targetEnv, releaseImage string, noPush, noCache bool) error {
|
||||
|
||||
log.SetPrefix(log.Prefix() + " build : ")
|
||||
|
||||
//
|
||||
log.Println("Verify required commands are installed.")
|
||||
for _, cmdVals := range requiredCmdsBuild {
|
||||
cmd := exec.Command(cmdVals[0], cmdVals[1:]...)
|
||||
cmd.Env = os.Environ()
|
||||
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return errors.WithMessagef(err, "failed to execute %s - %s\n%s", strings.Join(cmdVals, " "), string(out))
|
||||
}
|
||||
|
||||
log.Printf("\t%s\t%s - %s", tests.Success, cmdVals[0], string(out))
|
||||
}
|
||||
|
||||
// When project root directory is empty or set to current working path, then search for the project root by locating
|
||||
// the go.mod file.
|
||||
var goModFile string
|
||||
if projectRoot == "" || projectRoot == "." {
|
||||
log.Println("Attempting to location project root directory from current working directory.")
|
||||
|
||||
var err error
|
||||
goModFile, err = findProjectGoModFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
projectRoot = filepath.Dir(goModFile)
|
||||
} else {
|
||||
log.Println("Using supplied project root directory.")
|
||||
goModFile = filepath.Join(projectRoot, "go.mod")
|
||||
}
|
||||
|
||||
log.Printf("\t\tproject root: %s", projectRoot)
|
||||
log.Printf("\t\tgo.mod: %s", goModFile)
|
||||
log.Printf("\t%s\tFound project root directory.", tests.Success)
|
||||
|
||||
log.Println("Extracting go module name from go.mod.")
|
||||
modName, err := loadGoModName(goModFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("\t\tmodule name: %s", modName)
|
||||
log.Printf("\t%s\tgo module name.", tests.Success)
|
||||
|
||||
log.Println("Determining the project name.")
|
||||
projectName := getTargetEnv(targetEnv, "PROJECT_NAME")
|
||||
if projectName != "" {
|
||||
log.Printf("\t\tproject name: %s", projectName)
|
||||
log.Printf("\t%s\tFound env variable.", tests.Success)
|
||||
} else {
|
||||
projectName = filepath.Base(modName)
|
||||
log.Printf("\t\tproject name: %s", projectName)
|
||||
log.Printf("\t%s\tSet from go module.", tests.Success)
|
||||
}
|
||||
|
||||
log.Println("Attempting to locate service directory from project root directory.")
|
||||
dockerFile, err := findServiceDockerFile(projectRoot, targetService)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
serviceDir := filepath.Dir(dockerFile)
|
||||
|
||||
log.Printf("\t\tservice directory: %s", serviceDir)
|
||||
log.Printf("\t\tdockerfile: %s", dockerFile)
|
||||
log.Printf("\t%s\tFound service directory.", tests.Success)
|
||||
|
||||
|
||||
log.Println("Verify release image.")
|
||||
if releaseImage == "" {
|
||||
if v := os.Getenv("CI_REGISTRY_IMAGE"); v != "" {
|
||||
releaseImage = fmt.Sprintf("%s:%s-%s", v, targetService, targetEnv)
|
||||
} else {
|
||||
releaseImage = fmt.Sprintf("%s/%s:latest", targetService, targetEnv)
|
||||
noPush = true
|
||||
}
|
||||
}
|
||||
log.Printf("\t\trelease image: %s", releaseImage)
|
||||
log.Printf("\t%s\tRelease image valid.", tests.Success)
|
||||
|
||||
// Load the AWS
|
||||
log.Println("Verify AWS credentials.")
|
||||
awsCreds, err := GetAwsCredentials(targetEnv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("\t\tAccessKeyID: %s", awsCreds.AccessKeyID)
|
||||
log.Printf("\t\tRegion: %s", awsCreds.Region)
|
||||
|
||||
// Set default AWS Registry Name if needed.
|
||||
if awsCreds.RepositoryName == "" {
|
||||
awsCreds.RepositoryName = projectName
|
||||
log.Printf("\t\tSetting Repository Name to Project Name.")
|
||||
}
|
||||
log.Printf("\t\tRepository Name: %s", awsCreds.RepositoryName)
|
||||
log.Printf("\t%s\tAWS credentials valid.", tests.Success)
|
||||
|
||||
// Pull the current env variables to be passed in for command execution.
|
||||
envVars := EnvVars(os.Environ())
|
||||
|
||||
// Do the docker build.
|
||||
{
|
||||
cmdVals := []string{
|
||||
"docker",
|
||||
"build",
|
||||
"--file=" + dockerFile,
|
||||
"--build-arg", "service=" + targetService,
|
||||
"--build-arg", "env=" + targetEnv,
|
||||
"-t", releaseImage,
|
||||
}
|
||||
|
||||
if noCache {
|
||||
cmdVals = append(cmdVals, "--no-cache")
|
||||
}
|
||||
cmdVals = append(cmdVals, ".")
|
||||
|
||||
log.Printf("starting docker build: \n\t%s\n", strings.Join(cmdVals, " "))
|
||||
out, err := execCmds(projectRoot, &envVars, cmdVals)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("build complete\n\t%s\n", string(out[0]))
|
||||
}
|
||||
|
||||
// Push the newly built docker container to the registry.
|
||||
if !noPush {
|
||||
log.Println("Push release image.")
|
||||
_, err = execCmds(projectRoot, &envVars, []string{"docker", "push", releaseImage})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if awsCreds.RepositoryName != "" {
|
||||
awsRepo, err := EcrGetOrCreateRepository(awsCreds, projectName, targetEnv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
maxImages := defaultAwsRegistryMaxImages
|
||||
if v := getTargetEnv(targetEnv, "AWS_REPOSITORY_MAX_IMAGES"); v != "" {
|
||||
maxImages, err = strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return errors.WithMessagef(err, "Failed to parse max ECR images")
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("Purging old ECR images.")
|
||||
err = EcrPurgeImages(log, awsCreds, maxImages)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Println("Retrieve ECR authorization token used for docker login.")
|
||||
dockerLogin, err := GetEcrLogin(awsCreds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
awsRegistryImage := *awsRepo.RepositoryUri
|
||||
|
||||
// Login to AWS docker registry and pull release image locally.
|
||||
log.Println("Push release image to ECR.")
|
||||
log.Printf("\t\t%s", awsRegistryImage)
|
||||
_, err = execCmds(projectRoot, &envVars, dockerLogin, []string{"docker", "tag", releaseImage, awsRegistryImage}, []string{"docker", "push", awsRegistryImage})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tag1 := targetEnv+"-"+targetService
|
||||
log.Printf("\t\ttagging as %s", tag1)
|
||||
_, err = execCmds(projectRoot, &envVars, []string{"docker", "tag", releaseImage, awsRegistryImage+":"+tag1}, []string{"docker", "push", awsRegistryImage+":"+tag1})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if v := os.Getenv("CI_COMMIT_REF_NAME"); v != "" {
|
||||
tag2 := tag1 + "-" + v
|
||||
log.Printf("\t\ttagging as %s", tag2)
|
||||
_, err = execCmds(projectRoot, &envVars, []string{"docker", "tag", releaseImage, awsRegistryImage+ ":" + tag2}, []string{"docker", "push", awsRegistryImage + ":" + tag2})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
195
example-project/tools/truss/cmd/devops/service_deploy.go
Normal file
195
example-project/tools/truss/cmd/devops/service_deploy.go
Normal file
@ -0,0 +1,195 @@
|
||||
package devops
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/tests"
|
||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||
"github.com/aws/aws-sdk-go/service/secretsmanager"
|
||||
"github.com/pkg/errors"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
secretsmanager:GetSecretValue
|
||||
ecr:GetAuthorizationToken
|
||||
ecr:ListImages
|
||||
ecr:DescribeRepositories
|
||||
CreateRepository
|
||||
*/
|
||||
|
||||
// requiredCmdsBuild proves a list of required executables for completing build.
|
||||
var requiredCmdsDeploy = [][]string{
|
||||
[]string{"docker", "version", "-f", "{{.Client.Version}}"},
|
||||
}
|
||||
|
||||
|
||||
// Run is the main entrypoint for deploying a service for a given target env.
|
||||
func ServiceDeploy(log *log.Logger, projectRoot, targetService, targetEnv, releaseImage, ecsCluster string, enableVpc bool, noBuild, noDeploy, noCache bool) error {
|
||||
|
||||
log.SetPrefix(log.Prefix() + " deploy : ")
|
||||
|
||||
//
|
||||
log.Println("Verify required commands are installed.")
|
||||
for _, cmdVals := range requiredCmdsDeploy {
|
||||
cmd := exec.Command(cmdVals[0], cmdVals[1:]...)
|
||||
cmd.Env = os.Environ()
|
||||
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return errors.WithMessagef(err, "failed to execute %s - %s\n%s", strings.Join(cmdVals, " "), string(out))
|
||||
}
|
||||
|
||||
log.Printf("\t%s\t%s - %s", tests.Success, cmdVals[0], string(out))
|
||||
}
|
||||
|
||||
// When project root directory is empty or set to current working path, then search for the project root by locating
|
||||
// the go.mod file.
|
||||
var goModFile string
|
||||
if projectRoot == "" || projectRoot == "." {
|
||||
log.Println("Attempting to location project root directory from current working directory.")
|
||||
|
||||
var err error
|
||||
goModFile, err = findProjectGoModFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
projectRoot = filepath.Dir(goModFile)
|
||||
} else {
|
||||
log.Println("Using supplied project root directory.")
|
||||
goModFile = filepath.Join(projectRoot, "go.mod")
|
||||
}
|
||||
|
||||
log.Printf("\t\tproject root: %s", projectRoot)
|
||||
log.Printf("\t\tgo.mod: %s", goModFile)
|
||||
log.Printf("\t%s\tFound project root directory.", tests.Success)
|
||||
|
||||
log.Println("Extracting go module name from go.mod.")
|
||||
modName, err := loadGoModName(goModFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("\t\tmodule name: %s", modName)
|
||||
log.Printf("\t%s\tgo module name.", tests.Success)
|
||||
|
||||
log.Println("Determining the project name.")
|
||||
projectName := getTargetEnv(targetEnv, "PROJECT_NAME")
|
||||
if projectName != "" {
|
||||
log.Printf("\t\tproject name: %s", projectName)
|
||||
log.Printf("\t%s\tFound env variable.", tests.Success)
|
||||
} else {
|
||||
projectName = filepath.Base(modName)
|
||||
log.Printf("\t\tproject name: %s", projectName)
|
||||
log.Printf("\t%s\tSet from go module.", tests.Success)
|
||||
}
|
||||
|
||||
log.Println("Attempting to locate service directory from project root directory.")
|
||||
dockerFile, err := findServiceDockerFile(projectRoot, targetService)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
serviceDir := filepath.Dir(dockerFile)
|
||||
|
||||
log.Printf("\t\tservice directory: %s", serviceDir)
|
||||
log.Printf("\t\tdockerfile: %s", dockerFile)
|
||||
log.Printf("\t%s\tFound service directory.", tests.Success)
|
||||
|
||||
log.Println("Verify release image.")
|
||||
var noPull bool
|
||||
if releaseImage == "" {
|
||||
if v := os.Getenv("CI_REGISTRY_IMAGE"); v != "" {
|
||||
releaseImage = fmt.Sprintf("%s:%s-%s", v, targetService, targetEnv)
|
||||
} else {
|
||||
releaseImage = fmt.Sprintf("%s/%s:latest", targetService, targetEnv)
|
||||
noPull = true
|
||||
}
|
||||
}
|
||||
log.Printf("\t\trelease image: %s", releaseImage)
|
||||
log.Printf("\t%s\tRelease image valid.", tests.Success)
|
||||
|
||||
log.Println("Verify AWS credentials.")
|
||||
awsCreds, err := GetAwsCredentials(targetEnv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ecsCluster == "" {
|
||||
ecsCluster = filepath.Base(goModFile) + "-" + targetEnv
|
||||
log.Printf("AWS ECS cluster not set, assigning default value %s.", ecsCluster)
|
||||
}
|
||||
|
||||
// Create default service name used for deployment.
|
||||
serviceName := targetService + "-" + targetEnv
|
||||
_ = serviceName
|
||||
|
||||
|
||||
|
||||
awsRepo, err := EcrGetOrCreateRepository(awsCreds, projectName, targetEnv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
envVars := EnvVars(os.Environ())
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if !noPull {
|
||||
log.Println("Retrieve ECR authorization token used for docker login.")
|
||||
dockerLogin, err := GetEcrLogin(awsCreds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Login to AWS docker registry and pull release image locally.
|
||||
log.Println("Pull Release image from ECR.")
|
||||
log.Printf("\t\t%s", releaseImage)
|
||||
_, err = execCmds(projectRoot, &envVars, dockerLogin, []string{"docker", "pull", releaseImage})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDatadogApiKey returns the Datadog API Key which can be either stored in an env var or in AWS Secrets Manager.
|
||||
func GetDatadogApiKey(targetEnv string, creds *AwsCredentials) (string, error) {
|
||||
|
||||
// 1. Check env vars for [DEV|STAGE|PROD]_DD_API_KEY and DD_API_KEY
|
||||
apiKey := getTargetEnv(targetEnv, "DD_API_KEY")
|
||||
if apiKey != "" {
|
||||
return apiKey, nil
|
||||
}
|
||||
|
||||
// 2. Check AWS Secrets Manager for datadog entry prefixed with target env.
|
||||
prefixedSecretId := strings.ToUpper(targetEnv) + "/DATADOG"
|
||||
var err error
|
||||
apiKey, err = GetAwsSecretValue(creds, prefixedSecretId)
|
||||
if err != nil {
|
||||
if aerr, ok := errors.Cause(err).(awserr.Error); !ok || aerr.Code() != secretsmanager.ErrCodeResourceNotFoundException {
|
||||
return "", err
|
||||
}
|
||||
} else if apiKey != "" {
|
||||
return apiKey, nil
|
||||
}
|
||||
|
||||
// 3. Check AWS Secrets Manager for datadog entry.
|
||||
secretId := "DATADOG"
|
||||
apiKey, err = GetAwsSecretValue(creds, secretId)
|
||||
if err != nil {
|
||||
if aerr, ok := errors.Cause(err).(awserr.Error); !ok || aerr.Code() != secretsmanager.ErrCodeResourceNotFoundException {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
return apiKey, nil
|
||||
}
|
Reference in New Issue
Block a user