1
0
mirror of https://github.com/raseels-repos/golang-saas-starter-kit.git synced 2025-06-25 00:46:51 +02:00

replace internal devops package

This commit is contained in:
Lee Brown
2019-09-03 15:00:47 -08:00
parent a54b3a9153
commit ac080e0ce2
10 changed files with 24 additions and 642 deletions

View File

@ -497,7 +497,7 @@ func NewConfig(log *log.Logger, targetEnv Env, awsCredentials devdeploy.AwsCrede
// Append all the defined services to the config. // Append all the defined services to the config.
for _, n := range ServiceNames { for _, n := range ServiceNames {
srv, err := NewService(n, cfg) srv, err := NewService(log, n, cfg)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -506,7 +506,7 @@ func NewConfig(log *log.Logger, targetEnv Env, awsCredentials devdeploy.AwsCrede
// Append all the defined functions to the config. // Append all the defined functions to the config.
for _, n := range FunctionNames { for _, n := range FunctionNames {
fn, err := NewFunction(n, cfg) fn, err := NewFunction(log, n, cfg)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -25,7 +25,7 @@ var FunctionNames = []Function{
} }
// NewFunction returns the *devdeploy.ProjectFunction. // NewFunction returns the *devdeploy.ProjectFunction.
func NewFunction(funcName string, cfg *devdeploy.Config) (*devdeploy.ProjectFunction, error) { func NewFunction(log *log.Logger, funcName string, cfg *devdeploy.Config) (*devdeploy.ProjectFunction, error) {
ctx := &devdeploy.ProjectFunction{ ctx := &devdeploy.ProjectFunction{
Name: fmt.Sprintf("%s-%s-%s", cfg.Env, cfg.ProjectName, funcName), Name: fmt.Sprintf("%s-%s-%s", cfg.Env, cfg.ProjectName, funcName),
@ -213,7 +213,7 @@ func BuildFunctionForTargetEnv(log *log.Logger, awsCredentials devdeploy.AwsCred
return err return err
} }
targetFunc, err := NewFunction(functionName, cfg) targetFunc, err := NewFunction(log, functionName, cfg)
if err != nil { if err != nil {
return err return err
} }
@ -269,7 +269,7 @@ func DeployFunctionForTargetEnv(log *log.Logger, awsCredentials devdeploy.AwsCre
return err return err
} }
targetFunc, err := NewFunction(functionName, cfg) targetFunc, err := NewFunction(log, functionName, cfg)
if err != nil { if err != nil {
return err return err
} }

View File

@ -23,7 +23,7 @@ var ImageNames = []Image{
} }
// NewImage returns the *devdeploy.ProjectImage. // NewImage returns the *devdeploy.ProjectImage.
func NewImage(imageName string, cfg *devdeploy.Config) (*devdeploy.ProjectImage, error) { func NewImage(log *log.Logger, imageName string, cfg *devdeploy.Config) (*devdeploy.ProjectImage, error) {
ctx := &devdeploy.ProjectImage{ ctx := &devdeploy.ProjectImage{
Name: fmt.Sprintf("%s-%s-%s", cfg.Env, cfg.ProjectName, imageName), Name: fmt.Sprintf("%s-%s-%s", cfg.Env, cfg.ProjectName, imageName),
@ -61,7 +61,7 @@ func BuildImageForTargetEnv(log *log.Logger, awsCredentials devdeploy.AwsCredent
return err return err
} }
targetImage, err := NewImage(imageName, cfg) targetImage, err := NewImage(log, imageName, cfg)
if err != nil { if err != nil {
return err return err
} }

View File

@ -154,7 +154,7 @@ func (c ServiceContext) BaseUrl() string {
} }
// NewService returns the ProjectService for a service that is configured for the target deployment env. // NewService returns the ProjectService for a service that is configured for the target deployment env.
func NewService(serviceName string, cfg *devdeploy.Config) (*devdeploy.ProjectService, error) { func NewService(log *log.Logger, serviceName string, cfg *devdeploy.Config) (*devdeploy.ProjectService, error) {
ctx, err := NewServiceContext(serviceName, cfg) ctx, err := NewServiceContext(serviceName, cfg)
if err != nil { if err != nil {
@ -774,7 +774,7 @@ func BuildServiceForTargetEnv(log *log.Logger, awsCredentials devdeploy.AwsCrede
return err return err
} }
targetSvc, err := NewService(serviceName, cfg) targetSvc, err := NewService(log, serviceName, cfg)
if err != nil { if err != nil {
return err return err
} }
@ -830,7 +830,7 @@ func DeployServiceForTargetEnv(log *log.Logger, awsCredentials devdeploy.AwsCred
return err return err
} }
targetSvc, err := NewService(serviceName, cfg) targetSvc, err := NewService(log, serviceName, cfg)
if err != nil { if err != nil {
return err return err
} }

View File

@ -24,7 +24,6 @@ import (
"geeks-accelerator/oss/saas-starter-kit/internal/account/account_preference" "geeks-accelerator/oss/saas-starter-kit/internal/account/account_preference"
"geeks-accelerator/oss/saas-starter-kit/internal/mid" "geeks-accelerator/oss/saas-starter-kit/internal/mid"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth" "geeks-accelerator/oss/saas-starter-kit/internal/platform/auth"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/devops"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/flag" "geeks-accelerator/oss/saas-starter-kit/internal/platform/flag"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/notify" "geeks-accelerator/oss/saas-starter-kit/internal/platform/notify"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext" "geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
@ -45,6 +44,7 @@ import (
"github.com/kelseyhightower/envconfig" "github.com/kelseyhightower/envconfig"
"github.com/lib/pq" "github.com/lib/pq"
"github.com/pkg/errors" "github.com/pkg/errors"
"gitlab.com/geeks-accelerator/oss/devops/pkg/devdeploy"
"golang.org/x/crypto/acme" "golang.org/x/crypto/acme"
"golang.org/x/crypto/acme/autocert" "golang.org/x/crypto/acme/autocert"
awstrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/aws/aws-sdk-go/aws" awstrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/aws/aws-sdk-go/aws"
@ -303,8 +303,8 @@ func main() {
// If AWS is enabled, check the Secrets Manager for the session key. // If AWS is enabled, check the Secrets Manager for the session key.
if awsSession != nil { if awsSession != nil {
cfg.Project.SharedSecretKey, err = devops.SecretManagerGetString(awsSession, secretID) cfg.Project.SharedSecretKey, err = devdeploy.SecretManagerGetString(awsSession, secretID)
if err != nil && errors.Cause(err) != devops.ErrSecreteNotFound { if err != nil && errors.Cause(err) != devdeploy.ErrSecreteNotFound {
log.Fatalf("main : Session : %+v", err) log.Fatalf("main : Session : %+v", err)
} }
} }
@ -314,7 +314,7 @@ func main() {
cfg.Project.SharedSecretKey = string(securecookie.GenerateRandomKey(32)) cfg.Project.SharedSecretKey = string(securecookie.GenerateRandomKey(32))
if awsSession != nil { if awsSession != nil {
err = devops.SecretManagerPutString(awsSession, secretID, cfg.Project.SharedSecretKey) err = devdeploy.SecretManagerPutString(awsSession, secretID, cfg.Project.SharedSecretKey)
if err != nil { if err != nil {
log.Fatalf("main : Session : %+v", err) log.Fatalf("main : Session : %+v", err)
} }
@ -510,7 +510,7 @@ func main() {
// ========================================================================= // =========================================================================
// ECS Task registration for services that don't use an AWS Elastic Load Balancer. // ECS Task registration for services that don't use an AWS Elastic Load Balancer.
err = devops.EcsServiceTaskInit(log, awsSession) err = devdeploy.EcsServiceTaskInit(log, awsSession)
if err != nil { if err != nil {
log.Fatalf("main : Ecs Service Task init : %+v", err) log.Fatalf("main : Ecs Service Task init : %+v", err)
} }
@ -588,7 +588,7 @@ func main() {
// Local file cache to reduce requests hitting Secret Manager. // Local file cache to reduce requests hitting Secret Manager.
localCache := autocert.DirCache(os.TempDir()) localCache := autocert.DirCache(os.TempDir())
cache, err := devops.NewSecretManagerAutocertCache(log, awsSession, secretPrefix, localCache) cache, err := devdeploy.NewSecretManagerAutocertCache(log, awsSession, secretPrefix, localCache)
if err != nil { if err != nil {
log.Fatalf("main : HTTPS : %+v", err) log.Fatalf("main : HTTPS : %+v", err)
} }
@ -621,7 +621,7 @@ func main() {
log.Printf("main : %v : Start shutdown..", sig) log.Printf("main : %v : Start shutdown..", sig)
// Ensure the public IP address for the task is removed from Route53. // Ensure the public IP address for the task is removed from Route53.
err = devops.EcsServiceTaskTaskShutdown(log, awsSession) err = devdeploy.EcsServiceTaskTaskShutdown(log, awsSession)
if err != nil { if err != nil {
log.Fatalf("main : Ecs Service Task shutdown : %+v", err) log.Fatalf("main : Ecs Service Task shutdown : %+v", err)
} }

View File

@ -26,7 +26,6 @@ import (
"geeks-accelerator/oss/saas-starter-kit/internal/geonames" "geeks-accelerator/oss/saas-starter-kit/internal/geonames"
"geeks-accelerator/oss/saas-starter-kit/internal/mid" "geeks-accelerator/oss/saas-starter-kit/internal/mid"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth" "geeks-accelerator/oss/saas-starter-kit/internal/platform/auth"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/devops"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/flag" "geeks-accelerator/oss/saas-starter-kit/internal/platform/flag"
img_resize "geeks-accelerator/oss/saas-starter-kit/internal/platform/img-resize" img_resize "geeks-accelerator/oss/saas-starter-kit/internal/platform/img-resize"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/notify" "geeks-accelerator/oss/saas-starter-kit/internal/platform/notify"
@ -52,6 +51,7 @@ import (
"github.com/kelseyhightower/envconfig" "github.com/kelseyhightower/envconfig"
"github.com/lib/pq" "github.com/lib/pq"
"github.com/pkg/errors" "github.com/pkg/errors"
"gitlab.com/geeks-accelerator/oss/devops/pkg/devdeploy"
"golang.org/x/crypto/acme" "golang.org/x/crypto/acme"
"golang.org/x/crypto/acme/autocert" "golang.org/x/crypto/acme/autocert"
awstrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/aws/aws-sdk-go/aws" awstrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/aws/aws-sdk-go/aws"
@ -306,8 +306,8 @@ func main() {
// If AWS is enabled, check the Secrets Manager for the session key. // If AWS is enabled, check the Secrets Manager for the session key.
if awsSession != nil { if awsSession != nil {
cfg.Project.SharedSecretKey, err = devops.SecretManagerGetString(awsSession, secretID) cfg.Project.SharedSecretKey, err = devdeploy.SecretManagerGetString(awsSession, secretID)
if err != nil && errors.Cause(err) != devops.ErrSecreteNotFound { if err != nil && errors.Cause(err) != devdeploy.ErrSecreteNotFound {
log.Fatalf("main : Session : %+v", err) log.Fatalf("main : Session : %+v", err)
} }
} }
@ -317,7 +317,7 @@ func main() {
cfg.Project.SharedSecretKey = string(securecookie.GenerateRandomKey(32)) cfg.Project.SharedSecretKey = string(securecookie.GenerateRandomKey(32))
if awsSession != nil { if awsSession != nil {
err = devops.SecretManagerPutString(awsSession, secretID, cfg.Project.SharedSecretKey) err = devdeploy.SecretManagerPutString(awsSession, secretID, cfg.Project.SharedSecretKey)
if err != nil { if err != nil {
log.Fatalf("main : Session : %+v", err) log.Fatalf("main : Session : %+v", err)
} }
@ -511,7 +511,7 @@ func main() {
// a publicly available image URL. // a publicly available image URL.
var staticS3UrlFormatter func(string) string var staticS3UrlFormatter func(string) string
if cfg.Service.StaticFiles.S3Enabled || cfg.Service.StaticFiles.CloudFrontEnabled || cfg.Service.StaticFiles.ImgResizeEnabled { if cfg.Service.StaticFiles.S3Enabled || cfg.Service.StaticFiles.CloudFrontEnabled || cfg.Service.StaticFiles.ImgResizeEnabled {
s3UrlFormatter, err := devops.S3UrlFormatter(awsSession, cfg.Aws.S3BucketPublic, cfg.Service.StaticFiles.S3Prefix, cfg.Service.StaticFiles.CloudFrontEnabled) s3UrlFormatter, err := devdeploy.S3UrlFormatter(awsSession, cfg.Aws.S3BucketPublic, cfg.Service.StaticFiles.S3Prefix, cfg.Service.StaticFiles.CloudFrontEnabled)
if err != nil { if err != nil {
log.Fatalf("main : S3UrlFormatter failed : %+v", err) log.Fatalf("main : S3UrlFormatter failed : %+v", err)
} }
@ -993,7 +993,7 @@ func main() {
// ========================================================================= // =========================================================================
// ECS Task registration for services that don't use an AWS Elastic Load Balancer. // ECS Task registration for services that don't use an AWS Elastic Load Balancer.
err = devops.EcsServiceTaskInit(log, awsSession) err = devdeploy.EcsServiceTaskInit(log, awsSession)
if err != nil { if err != nil {
log.Fatalf("main : Ecs Service Task init : %+v", err) log.Fatalf("main : Ecs Service Task init : %+v", err)
} }
@ -1058,7 +1058,7 @@ func main() {
// Local file cache to reduce requests hitting Secret Manager. // Local file cache to reduce requests hitting Secret Manager.
localCache := autocert.DirCache(os.TempDir()) localCache := autocert.DirCache(os.TempDir())
cache, err := devops.NewSecretManagerAutocertCache(log, awsSession, secretPrefix, localCache) cache, err := devdeploy.NewSecretManagerAutocertCache(log, awsSession, secretPrefix, localCache)
if err != nil { if err != nil {
log.Fatalf("main : HTTPS : %+v", err) log.Fatalf("main : HTTPS : %+v", err)
} }

View File

@ -1,137 +0,0 @@
package devops
import (
"context"
"log"
"path/filepath"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/secretsmanager"
"github.com/pkg/errors"
"golang.org/x/crypto/acme/autocert"
)
// SecretManagerAutocertCache implements the autocert.Cache interface for AWS Secrets Manager that is used by Manager
// to store and retrieve previously obtained certificates and other account data as opaque blobs.
type SecretManagerAutocertCache struct {
awsSession *session.Session
log *log.Logger
secretPrefix string
cache autocert.Cache
}
// NewSecretManagerAutocertCache provides the functionality to keep config files sync'd between running tasks and across deployments.
func NewSecretManagerAutocertCache(log *log.Logger, awsSession *session.Session, secretPrefix string, cache autocert.Cache) (*SecretManagerAutocertCache, error) {
return &SecretManagerAutocertCache{
awsSession,
log,
secretPrefix,
cache,
}, nil
}
// Get returns a certificate data for the specified key.
// If there's no such key, Get returns ErrCacheMiss.
func (c *SecretManagerAutocertCache) Get(ctx context.Context, key string) ([]byte, error) {
// Check short term cache.
if c.cache != nil {
v, err := c.cache.Get(ctx, key)
if err != nil && err != autocert.ErrCacheMiss {
return nil, errors.WithStack(err)
} else if len(v) > 0 {
return v, nil
}
}
secretID := filepath.Join(c.secretPrefix, key)
// Load the secret by ID from Secrets Manager.
res, err := SecretManagerGetString(c.awsSession, secretID)
if err != nil {
if err == ErrSecreteNotFound {
return nil, autocert.ErrCacheMiss
}
return nil, err
}
log.Printf("AWS Secrets Manager : Secret %s found", secretID)
return []byte(res), nil
}
// Put stores the data in the cache under the specified key.
// Underlying implementations may use any data storage format,
// as long as the reverse operation, Get, results in the original data.
func (c *SecretManagerAutocertCache) Put(ctx context.Context, key string, data []byte) error {
secretID := filepath.Join(c.secretPrefix, key)
err := SecretManagerPutString(c.awsSession, secretID, string(data))
if err != nil {
return err
}
log.Printf("AWS Secrets Manager : Secret %s updated", secretID)
if c.cache != nil {
err = c.cache.Put(ctx, key, data)
if err != nil {
return errors.WithStack(err)
}
}
return nil
}
// Delete removes a certificate data from the cache under the specified key.
// If there's no such key in the cache, Delete returns nil.
func (c *SecretManagerAutocertCache) Delete(ctx context.Context, key string) error {
svc := secretsmanager.New(c.awsSession)
secretID := filepath.Join(c.secretPrefix, key)
// Create the new entry in AWS Secret Manager for the file.
_, err := svc.DeleteSecret(&secretsmanager.DeleteSecretInput{
SecretId: aws.String(secretID),
// (Optional) Specifies that the secret is to be deleted without any recovery
// window. You can't use both this parameter and the RecoveryWindowInDays parameter
// in the same API call.
//
// An asynchronous background process performs the actual deletion, so there
// can be a short delay before the operation completes. If you write code to
// delete and then immediately recreate a secret with the same name, ensure
// that your code includes appropriate back off and retry logic.
//
// Use this parameter with caution. This parameter causes the operation to skip
// the normal waiting period before the permanent deletion that AWS would normally
// impose with the RecoveryWindowInDays parameter. If you delete a secret with
// the ForceDeleteWithouRecovery parameter, then you have no opportunity to
// recover the secret. It is permanently lost.
ForceDeleteWithoutRecovery: aws.Bool(false),
// (Optional) Specifies the number of days that Secrets Manager waits before
// it can delete the secret. You can't use both this parameter and the ForceDeleteWithoutRecovery
// parameter in the same API call.
//
// This value can range from 7 to 30 days.
RecoveryWindowInDays: aws.Int64(30),
})
if err != nil {
return errors.Wrapf(err, "autocert failed to delete secret %s", secretID)
}
log.Printf("AWS Secrets Manager : Secret %s deleted", secretID)
if c.cache != nil {
err = c.cache.Delete(ctx, key)
if err != nil {
return errors.WithStack(err)
}
}
return nil
}

View File

@ -1,163 +0,0 @@
package devops
import (
"fmt"
"net/url"
"path/filepath"
"strings"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/cloudfront"
"github.com/pkg/errors"
)
func CloudFrontDistribution(awsSession *session.Session, s3Bucket string) (*cloudfront.DistributionSummary, error) {
// Init new CloudFront using provided AWS session.
cloudFront := cloudfront.New(awsSession)
// Loop through all the cloudfront distributions and find the one that matches the
// S3 Bucket name. AWS doesn't current support multiple distributions per bucket
// so this should always be a one to one match.
var distribution *cloudfront.DistributionSummary
err := cloudFront.ListDistributionsPages(&cloudfront.ListDistributionsInput{},
func(page *cloudfront.ListDistributionsOutput, lastPage bool) bool {
if page.DistributionList != nil {
for _, v := range page.DistributionList.Items {
if v.DomainName == nil || v.Origins == nil || v.Origins.Items == nil {
continue
}
for _, o := range v.Origins.Items {
if o.DomainName == nil || !strings.HasPrefix(*o.DomainName, s3Bucket+".") {
continue
}
distribution = v
break
}
if distribution != nil {
break
}
}
}
if distribution != nil {
return false
}
return !lastPage
},
)
if err != nil {
return nil, err
}
if distribution == nil {
return nil, errors.Errorf("aws cloud front deployment does not exist for s3 bucket %s.", s3Bucket)
}
return distribution, nil
}
// NewAuthenticator creates an *Authenticator for use.
// key expiration is optional to filter out old keys
// It will error if:
// - The aws session is nil.
// - The aws s3 bucket is blank.
func S3UrlFormatter(awsSession *session.Session, s3Bucket, s3KeyPrefix string, enableCloudFront bool) (func(string) string, error) {
if awsSession == nil {
return nil, errors.New("aws session cannot be nil")
}
if s3Bucket == "" {
return nil, errors.New("aws s3 bucket cannot be empty")
}
var (
baseS3Url string
baseS3Origin string
)
if enableCloudFront {
dist, err := CloudFrontDistribution(awsSession, s3Bucket)
if err != nil {
return nil, err
}
// Format the domain as an HTTPS url, "dzuyel7n94hma.cloudfront.net"
baseS3Url = fmt.Sprintf("https://%s/", *dist.DomainName)
// The origin used for the cloudfront needs to be striped from the path
// provided, the URL shouldn't have one, but "/public"
baseS3Origin = *dist.Origins.Items[0].OriginPath
} else {
// The static files are upload to a specific prefix, so need to ensure
// the path reference includes this prefix
s3Path := filepath.Join(s3Bucket, s3KeyPrefix)
if *awsSession.Config.Region == "us-east-1" {
// US East (N.Virginia) region endpoint, http://s3.amazonaws.com/bucket or
// http://s3-external-1.amazonaws.com/bucket/
baseS3Url = fmt.Sprintf("https://s3.amazonaws.com/%s/", s3Path)
} else {
// Region-specific endpoint, http://s3-aws-region.amazonaws.com/bucket
baseS3Url = fmt.Sprintf("https://s3-%s.amazonaws.com/%s/", *awsSession.Config.Region, s3Path)
}
baseS3Origin = s3KeyPrefix
}
f := func(p string) string {
return S3Url(baseS3Url, baseS3Origin, p)
}
return f, nil
}
// S3Url formats a path to include either the S3 URL or a CloudFront
// URL instead of serving the file from local file system.
func S3Url(baseS3Url, baseS3Origin, p string) string {
// If its already a URL, then don't format it
if strings.HasPrefix(p, "http") {
return p
}
// Drop the beginning forward slash
p = strings.TrimLeft(p, "/")
// In the case of cloudfront, the base URL may not match S3,
// removing the origin from the path provided
// ie. The s3 bucket + path of
// gitw-corp-web.s3.amazonaws.com/public
// maps to dzuyel7n94hma.cloudfront.net
// where the path prefix of '/public' needs to be dropped.
org := strings.Trim(baseS3Origin, "/")
if org != "" {
p = strings.Replace(p, org+"/", "", 1)
}
// Parse out the querystring from the path
var pathQueryStr string
if strings.Contains(p, "?") {
pts := strings.Split(p, "?")
p = pts[0]
if len(pts) > 1 {
pathQueryStr = pts[1]
}
}
u, err := url.Parse(baseS3Url)
if err != nil {
return "?"
}
ldir := filepath.Base(u.Path)
if strings.HasPrefix(p, ldir) {
p = strings.Replace(p, ldir+"/", "", 1)
}
u.Path = filepath.Join(u.Path, p)
u.RawQuery = pathQueryStr
return u.String()
}

View File

@ -1,242 +0,0 @@
package devops
import (
"encoding/base64"
"encoding/json"
"log"
"os"
"strconv"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/ecs"
"github.com/aws/aws-sdk-go/service/route53"
"github.com/pkg/errors"
)
// EcsServiceTaskInit allows newly spun up ECS Service Tasks to register their public IP with Route 53.
func EcsServiceTaskInit(log *log.Logger, awsSession *session.Session) error {
if awsSession == nil {
return nil
}
ecsClusterName := os.Getenv("ECS_CLUSTER")
ecsServiceName := os.Getenv("ECS_SERVICE")
// If both env variables are empty, this instance of the services is not running on AWS ECS.
if ecsClusterName == "" && ecsServiceName == "" {
return nil
}
var zoneArecNames = map[string][]string{}
if v := os.Getenv("ROUTE53_ZONES"); v != "" {
dat, err := base64.RawURLEncoding.DecodeString(v)
if err != nil {
return errors.Wrapf(err, "failed to base64 URL decode zones")
}
err = json.Unmarshal(dat, &zoneArecNames)
if err != nil {
return errors.Wrapf(err, "failed to json decode zones - %s", string(dat))
}
}
var registerServiceTasks bool
if v := os.Getenv("ROUTE53_UPDATE_TASK_IPS"); v != "" {
var err error
registerServiceTasks, err = strconv.ParseBool(v)
if err != nil {
return errors.Wrapf(err, "failed to parse ROUTE53_UPDATE_TASK_IPS value '%s' to bool", v)
}
}
if registerServiceTasks {
if err := RegisterEcsServiceTasksRoute53(log, awsSession, ecsClusterName, ecsServiceName, zoneArecNames); err != nil {
return err
}
}
return nil
}
// EcsServiceTaskTaskShutdown allows ECS Service Tasks that are spinning down to deregister their public IP with Route 53.
func EcsServiceTaskTaskShutdown(log *log.Logger, awsSession *session.Session) error {
// TODO: Should lookup the IP for the current running task and remove that specific IP.
// For now just run the init since it removes all non-running tasks.
return EcsServiceTaskInit(log, awsSession)
}
// RegisterEcsServiceTasksRoute53 registers the public IPs for a ECS Service Task with Route 53.
func RegisterEcsServiceTasksRoute53(log *log.Logger, awsSession *session.Session, ecsClusterName, ecsServiceName string, zoneArecNames map[string][]string) error {
if awsSession == nil {
return nil
}
var networkInterfaceIds []string
for a := 0; a <= 3; a++ {
svc := ecs.New(awsSession)
serviceRes, err := svc.DescribeServices(&ecs.DescribeServicesInput{
Cluster: aws.String(ecsClusterName),
Services: []*string{aws.String(ecsServiceName)},
})
if err != nil {
return errors.Wrapf(err, "failed to describe service '%s'", ecsServiceName)
}
service := serviceRes.Services[0]
servceTaskRes, err := svc.ListTasks(&ecs.ListTasksInput{
Cluster: aws.String(ecsClusterName),
ServiceName: aws.String(ecsServiceName),
DesiredStatus: aws.String("RUNNING"),
})
if err != nil {
return errors.Wrapf(err, "failed to list tasks for cluster '%s' service '%s'", ecsClusterName, ecsServiceName)
}
if len(servceTaskRes.TaskArns) == 0 {
continue
}
taskRes, err := svc.DescribeTasks(&ecs.DescribeTasksInput{
Cluster: aws.String(ecsClusterName),
Tasks: servceTaskRes.TaskArns,
})
if err != nil {
return errors.Wrapf(err, "failed to describe %d tasks for cluster '%s'", len(servceTaskRes.TaskArns), ecsClusterName)
}
for _, t := range taskRes.Tasks {
if *t.TaskDefinitionArn != *service.TaskDefinition || *t.DesiredStatus != "RUNNING" {
continue
}
if t.Attachments == nil {
continue
}
for _, c := range t.Containers {
if *c.Name != ecsServiceName {
continue
}
if c.NetworkInterfaces == nil || len(c.NetworkInterfaces) == 0 || c.NetworkInterfaces[0].AttachmentId == nil {
continue
}
for _, a := range t.Attachments {
if a.Details == nil || *a.Id != *c.NetworkInterfaces[0].AttachmentId {
continue
}
for _, ad := range a.Details {
if ad.Name != nil && *ad.Name == "networkInterfaceId" {
networkInterfaceIds = append(networkInterfaceIds, *ad.Value)
}
}
}
break
}
}
if len(networkInterfaceIds) > 0 {
log.Printf("Found %d network interface IDs.\n", len(networkInterfaceIds))
break
}
// Found no network interfaces, try again.
log.Println("Found no network interfaces.")
time.Sleep((time.Duration(a) * time.Second * 10) * time.Duration(a))
}
if len(networkInterfaceIds) == 0 {
return errors.New("Unable to update public IPs. No network interfaces found.")
}
log.Println("Get public IPs for network interface IDs.")
var publicIps []string
for a := 0; a <= 3; a++ {
svc := ec2.New(awsSession)
log.Println("\t\tDescribe network interfaces.")
res, err := svc.DescribeNetworkInterfaces(&ec2.DescribeNetworkInterfacesInput{
NetworkInterfaceIds: aws.StringSlice(networkInterfaceIds),
})
if err != nil {
return errors.Wrap(err, "failed to describe network interfaces")
}
for _, ni := range res.NetworkInterfaces {
if ni.Association == nil || ni.Association.PublicIp == nil {
continue
}
publicIps = append(publicIps, *ni.Association.PublicIp)
}
if len(publicIps) > 0 {
log.Printf("Found %d public IPs.\n", len(publicIps))
break
}
// Found no public IPs, try again.
log.Println("Found no public IPs.")
time.Sleep((time.Duration(a) * time.Second * 10) * time.Duration(a))
}
if len(publicIps) > 0 {
log.Println("Update public IPs for hosted zones.")
svc := route53.New(awsSession)
// Public IPs to be served as round robin.
log.Printf("\tPublic IPs:\n")
rrs := []*route53.ResourceRecord{}
for _, ip := range publicIps {
log.Printf("\t\t%s\n", ip)
rrs = append(rrs, &route53.ResourceRecord{Value: aws.String(ip)})
}
for zoneId, aNames := range zoneArecNames {
log.Printf("\tChange zone '%s'.\n", zoneId)
input := &route53.ChangeResourceRecordSetsInput{
ChangeBatch: &route53.ChangeBatch{
Changes: []*route53.Change{},
},
HostedZoneId: aws.String(zoneId),
}
// Add all the A record names with the same set of public IPs.
addedNames := make(map[string]bool)
for _, aName := range aNames {
if addedNames[aName] {
continue
}
log.Printf("\t\tAdd A record for '%s'.\n", aName)
input.ChangeBatch.Changes = append(input.ChangeBatch.Changes, &route53.Change{
Action: aws.String("UPSERT"),
ResourceRecordSet: &route53.ResourceRecordSet{
Name: aws.String(aName),
ResourceRecords: rrs,
TTL: aws.Int64(60),
Type: aws.String("A"),
},
})
addedNames[aName] = true
}
_, err := svc.ChangeResourceRecordSets(input)
if err != nil {
return errors.Wrapf(err, "failed to update A records for zone '%s'", zoneId)
}
}
log.Printf("DNS entries updated.\n")
}
return nil
}

View File

@ -1,76 +0,0 @@
package devops
import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/secretsmanager"
"github.com/pkg/errors"
)
var ErrSecreteNotFound = errors.New("secret not found")
// SecretManagerGetString loads a key from AWS Secrets Manager.
// when UnrecognizedClientException its likely the AWS IAM permissions are not correct.
func SecretManagerGetString(awsSession *session.Session, secretID string) (string, error) {
svc := secretsmanager.New(awsSession)
// Load the secret by ID from Secrets Manager.
res, err := svc.GetSecretValue(&secretsmanager.GetSecretValueInput{
SecretId: aws.String(secretID),
})
if err != nil {
if aerr, ok := err.(awserr.Error); ok && (aerr.Code() == secretsmanager.ErrCodeResourceNotFoundException || aerr.Code() == secretsmanager.ErrCodeInvalidRequestException) {
return "", ErrSecreteNotFound
}
return "", errors.Wrapf(err, "failed to get value for secret id %s", secretID)
}
return *res.SecretString, nil
}
// SecretManagerPutString saves a value to AWS Secrets Manager.
// If the secret ID does not exist, it will create it.
// If the secret ID was deleted, it will restore it and then update the value.
func SecretManagerPutString(awsSession *session.Session, secretID, value string) error {
svc := secretsmanager.New(awsSession)
// Create the new entry in AWS Secret Manager for the file.
_, err := svc.CreateSecret(&secretsmanager.CreateSecretInput{
Name: aws.String(secretID),
SecretString: aws.String(value),
})
if err != nil {
aerr, ok := err.(awserr.Error)
if ok && aerr.Code() == secretsmanager.ErrCodeInvalidRequestException {
// InvalidRequestException: You can't create this secret because a secret with this
// name is already scheduled for deletion.
// Restore secret after it was already previously deleted.
_, err = svc.RestoreSecret(&secretsmanager.RestoreSecretInput{
SecretId: aws.String(secretID),
})
if err != nil {
return errors.Wrapf(err, "failed to restore secret %s", secretID)
}
} else if !ok || aerr.Code() != secretsmanager.ErrCodeResourceExistsException {
return errors.Wrapf(err, "failed to create secret %s", secretID)
}
// If where was a resource exists error for create, then need to update the secret instead.
_, err = svc.UpdateSecret(&secretsmanager.UpdateSecretInput{
SecretId: aws.String(secretID),
SecretString: aws.String(value),
})
if err != nil {
return errors.Wrapf(err, "failed to update secret %s", secretID)
}
}
return nil
}