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

checkpoint

This commit is contained in:
Lee Brown
2019-07-10 16:24:10 -08:00
parent acd1bca501
commit 4125a19156
9 changed files with 759 additions and 287 deletions

View File

@ -18,6 +18,8 @@ import (
"strings"
"time"
"geeks-accelerator/oss/saas-starter-kit/example-project/tools/truss/internal/retry"
"github.com/aws/aws-sdk-go/service/servicediscovery"
"github.com/aws/aws-sdk-go/service/rds"
"github.com/pborman/uuid"
"github.com/aws/aws-sdk-go/service/elasticache"
@ -43,71 +45,6 @@ import (
"gopkg.in/go-playground/validator.v9"
)
// baseServicePolicyDocument defines the default permissions required to access AWS services for all deployed services.
var baseServicePolicyDocument = IamPolicyDocument{
Version: "2012-10-17",
Statement: []IamStatementEntry{
IamStatementEntry{
Sid: "DefaultServiceAccess",
Effect: "Allow",
Action: []string{
"s3:HeadBucket",
"ec2:DescribeNetworkInterfaces",
"ec2:DeleteNetworkInterface",
"ecs:ListTasks",
"ecs:DescribeTasks",
"ec2:DescribeNetworkInterfaces",
"route53:ListHostedZones",
"route53:ListResourceRecordSets",
"route53:ChangeResourceRecordSets",
"ecs:UpdateService",
"ses:SendEmail",
},
Resource: "*",
},
IamStatementEntry{
Sid: "ServiceInvokeLambda",
Effect: "Allow",
Action: []string{
"iam:GetRole",
"lambda:InvokeFunction",
"lambda:ListVersionsByFunction",
"lambda:GetFunction",
"lambda:InvokeAsync",
"lambda:GetFunctionConfiguration",
"iam:PassRole",
"lambda:GetAlias",
"lambda:GetPolicy",
},
Resource: []string{
"arn:aws:iam:::role/*",
"arn:aws:lambda:::function:*",
},
},
IamStatementEntry{
Sid: "datadoglambda",
Effect: "Allow",
Action: []string{
"cloudwatch:Get*",
"cloudwatch:List*",
"ec2:Describe*",
"support:*",
"tag:GetResources",
"tag:GetTagKeys",
"tag:GetTagValues",
},
Resource: "*",
},
},
}
/*
// requiredCmdsBuild proves a list of required executables for completing build.
var requiredCmdsDeploy = [][]string{
[]string{"docker", "version", "-f", "{{.Client.Version}}"},
}
*/
// NewServiceDeployRequest generated a new request for executing deploy for a given set of flags.
func NewServiceDeployRequest(log *log.Logger, flags ServiceDeployFlags) (*serviceDeployRequest, error) {
@ -254,8 +191,73 @@ func NewServiceDeployRequest(log *log.Logger, flags ServiceDeployFlags) (*servic
// Set default AWS ECS Task Policy Name.
req.EcsTaskPolicyName = fmt.Sprintf("%s%sServices", req.ProjectNameCamel(), strcase.ToCamel(req.Env))
req.EcsTaskPolicy = &iam.CreatePolicyInput{
PolicyName: aws.String(req.EcsTaskPolicyName),
Description: aws.String(fmt.Sprintf("Defines access for %s services. ", req.ProjectName)),
}
log.Printf("\t\t\tSet ECS Task Policy Name to '%s'.", req.EcsTaskPolicyName)
// EcsTaskPolicyDocument defines the default document policy used to create the AWS ECS Task Policy. If the
// policy already exists, the permissions will be used to add new required actions, but not for removal.
// The policy document grants the permissions required for deployed services to access AWS services.
req.EcsTaskPolicyDocument = IamPolicyDocument{
Version: "2012-10-17",
Statement: []IamStatementEntry{
IamStatementEntry{
Sid: "DefaultServiceAccess",
Effect: "Allow",
Action: []string{
"s3:HeadBucket",
"ec2:DescribeNetworkInterfaces",
"ec2:DeleteNetworkInterface",
"ecs:ListTasks",
"ecs:DescribeTasks",
"ec2:DescribeNetworkInterfaces",
"route53:ListHostedZones",
"route53:ListResourceRecordSets",
"route53:ChangeResourceRecordSets",
"ecs:UpdateService",
"ses:SendEmail",
},
Resource: "*",
},
IamStatementEntry{
Sid: "ServiceInvokeLambda",
Effect: "Allow",
Action: []string{
"iam:GetRole",
"lambda:InvokeFunction",
"lambda:ListVersionsByFunction",
"lambda:GetFunction",
"lambda:InvokeAsync",
"lambda:GetFunctionConfiguration",
"iam:PassRole",
"lambda:GetAlias",
"lambda:GetPolicy",
},
Resource: []string{
"arn:aws:iam:::role/*",
"arn:aws:lambda:::function:*",
},
},
IamStatementEntry{
Sid: "datadoglambda",
Effect: "Allow",
Action: []string{
"cloudwatch:Get*",
"cloudwatch:List*",
"ec2:Describe*",
"support:*",
"tag:GetResources",
"tag:GetTagKeys",
"tag:GetTagValues",
},
Resource: "*",
},
},
}
// Set default Cloudwatch Log Group Name.
req.CloudWatchLogGroupName = fmt.Sprintf("logs/env_%s/aws/ecs/cluster_%s/service_%s", req.Env, req.EcsClusterName, req.ServiceName)
log.Printf("\t\t\tSet CloudWatch Log Group Name to '%s'.", req.CloudWatchLogGroupName)
@ -264,6 +266,7 @@ func NewServiceDeployRequest(log *log.Logger, flags ServiceDeployFlags) (*servic
req.Ec2SecurityGroupName = req.EcsClusterName
log.Printf("\t\t\tSet ECS Security Group Name to '%s'.", req.Ec2SecurityGroupName)
// Set default ELB Load Balancer Name when ELB is enabled.
if req.EnableEcsElb {
if !strings.Contains(req.EcsClusterName, req.Env) && !strings.Contains(req.ServiceName, req.Env) {
@ -295,6 +298,64 @@ func NewServiceDeployRequest(log *log.Logger, flags ServiceDeployFlags) (*servic
// S3 temp prefix, a life cycle policy will be applied to this.
req.S3BucketTempPrefix = "tmp/"
// Service Discovery Namespace settings.
req.SDNamepsace = &servicediscovery.CreatePrivateDnsNamespaceInput{
Name: aws.String(req.EcsClusterName),
Description: aws.String(fmt.Sprintf("Private DNS namespace used for services running on the ECS Cluster %s", req.EcsClusterName)),
// A unique string that identifies the request and that allows failed CreatePrivateDnsNamespace
// requests to be retried without the risk of executing the operation twice.
// CreatorRequestId can be any unique string, for example, a date/time stamp.
CreatorRequestId: aws.String("truss-deploy"),
}
// Service Discovery Service settings.
req.SDService = &servicediscovery.CreateServiceInput{
Name: aws.String(req.EcsServiceName),
Description: aws.String(fmt.Sprintf("Service %s running on the ECS Cluster %s", req.EcsServiceName, req.EcsClusterName)),
// A complex type that contains information about the Amazon Route 53 records
// that you want AWS Cloud Map to create when you register an instance.
DnsConfig: &servicediscovery.DnsConfig{
DnsRecords: []*servicediscovery.DnsRecord{
{
// The amount of time, in seconds, that you want DNS resolvers to cache the
// settings for this record.
TTL: aws.Int64(300),
// The type of the resource, which indicates the type of value that Route 53
// returns in response to DNS queries.
Type: aws.String("A"),
},
},
},
// A complex type that contains information about an optional custom health
// check.
//
// If you specify a health check configuration, you can specify either HealthCheckCustomConfig
// or HealthCheckConfig but not both.
HealthCheckCustomConfig: &servicediscovery.HealthCheckCustomConfig{
// The number of 30-second intervals that you want Cloud Map to wait after receiving
// an UpdateInstanceCustomHealthStatus request before it changes the health
// status of a service instance. For example, suppose you specify a value of
// 2 for FailureTheshold, and then your application sends an UpdateInstanceCustomHealthStatus
// request. Cloud Map waits for approximately 60 seconds (2 x 30) before changing
// the status of the service instance based on that request.
//
// Sending a second or subsequent UpdateInstanceCustomHealthStatus request with
// the same value before FailureThreshold x 30 seconds has passed doesn't accelerate
// the change. Cloud Map still waits FailureThreshold x 30 seconds after the
// first request to make the change.
FailureThreshold: aws.Int64(3),
},
// A unique string that identifies the request and that allows failed CreatePrivateDnsNamespace
// requests to be retried without the risk of executing the operation twice.
// CreatorRequestId can be any unique string, for example, a date/time stamp.
CreatorRequestId: aws.String("truss-deploy"),
}
// Elastic Cache settings for a Redis cache cluster. Could defined different settings by env.
req.CacheCluster = &elasticache.CreateCacheClusterInput{
AutoMinorVersionUpgrade: aws.Bool(true),
@ -342,6 +403,8 @@ func NewServiceDeployRequest(log *log.Logger, flags ServiceDeployFlags) (*servic
},
}
log.Printf("\t%s\tDefaults set.", tests.Success)
}
@ -359,26 +422,7 @@ func NewServiceDeployRequest(log *log.Logger, flags ServiceDeployFlags) (*servic
// Run is the main entrypoint for deploying a service for a given target env.
func ServiceDeploy(log *log.Logger, req *serviceDeployRequest) error {
/*
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))
}
// Pull the current env variables to be passed in for command execution.
envVars := EnvVars(os.Environ())
*/
startTime := time.Now()
// Load the AWS ECR repository. Try to find by name else create new one.
@ -526,10 +570,12 @@ func ServiceDeploy(log *log.Logger, req *serviceDeployRequest) error {
return errors.Wrap(err, "failed to create docker build context")
}
_, err = docker.ImageBuild(context.Background(), buildCtx, buildOpts)
res, err := docker.ImageBuild(context.Background(), buildCtx, buildOpts)
if err != nil {
return errors.Wrap(err, "failed to build docker image")
}
io.Copy(os.Stdout, res.Body)
res.Body.Close()
// Push the newly built docker container to the registry.
if req.NoPush == false {
@ -855,39 +901,22 @@ func ServiceDeploy(log *log.Logger, req *serviceDeployRequest) error {
svc := ec2.New(req.awsSession())
log.Println("\t\tFind all subnets are that default for each available AZ.")
var subnets []*ec2.Subnet
if true { // len(req.ec2SubnetIds) == 0 {
log.Println("\t\tFind all subnets are that default for each available AZ.")
err := svc.DescribeSubnetsPages(&ec2.DescribeSubnetsInput{}, func(res *ec2.DescribeSubnetsOutput, lastPage bool) bool {
for _, s := range res.Subnets {
if *s.DefaultForAz {
subnets = append(subnets, s)
}
}
return !lastPage
})
if err != nil {
return errors.Wrap(err, "failed to find default subnets")
}
/*} else {
log.Println("\t\tFind all subnets for the IDs provided.")
err := svc.DescribeSubnetsPages(&ec2.DescribeSubnetsInput{
SubnetIds: aws.StringSlice(flags.Ec2SubnetIds),
}, func(res *ec2.DescribeSubnetsOutput, lastPage bool) bool {
for _, s := range res.Subnets {
err := svc.DescribeSubnetsPages(&ec2.DescribeSubnetsInput{}, func(res *ec2.DescribeSubnetsOutput, lastPage bool) bool {
for _, s := range res.Subnets {
if *s.DefaultForAz {
subnets = append(subnets, s)
}
return !lastPage
})
if err != nil {
return errors.Wrapf(err, "failed to find subnets: %s", strings.Join(flags.Ec2SubnetIds, ", "))
} else if len(flags.Ec2SubnetIds) != len(subnets) {
return errors.Errorf("failed to find all subnets, expected %d, got %d", len(flags.Ec2SubnetIds) != len(subnets))
}*/
}
return !lastPage
})
if err != nil {
return errors.Wrap(err, "failed to find default subnets")
}
if len(subnets) == 0 {
return errors.New("failed to find any subnets, expected at least 1")
}
@ -1330,6 +1359,310 @@ func ServiceDeploy(log *log.Logger, req *serviceDeployRequest) error {
log.Printf("\t%s\tUsing Cache Cluster '%s'.\n", tests.Success, *cacheCluster.CacheClusterId)
}
// Route 53 zone lookup when hostname is set. Supports both top level domains or sub domains.
var zoneArecNames = map[string][]string{}
if req.ServiceDomainName != "" {
log.Println("Route 53 - Get or create hosted zones.")
svc := route53.New(req.awsSession())
log.Println("\tList all hosted zones.")
var zones []*route53.HostedZone
err := svc.ListHostedZonesPages(&route53.ListHostedZonesInput{},
func(res *route53.ListHostedZonesOutput, lastPage bool) bool {
for _, z := range res.HostedZones {
zones = append(zones, z)
}
return !lastPage
})
if err != nil {
return errors.Wrap(err, "failed list route 53 hosted zones")
}
// Generate a slice with the primary domain name and include all the alternative domain names.
lookupDomains := []string{req.ServiceDomainName}
for _, dn := range req.ServiceDomainNameAliases {
lookupDomains = append(lookupDomains, dn)
}
// Loop through all the defined domain names and find the associated zone even when they are a sub domain.
for _, dn := range lookupDomains {
log.Printf("\t\tFind zone for domain '%s'", dn)
// Get the top level domain from url.
zoneName := domainutil.Domain(dn)
var subdomain string
if zoneName == "" {
// Handle domain names that have weird TDL: ie .tech
zoneName = dn
log.Printf("\t\t\tNon-standard Level Domain: '%s'", zoneName)
} else {
log.Printf("\t\t\tTop Level Domain: '%s'", zoneName)
// Check if url has subdomain.
if domainutil.HasSubdomain(dn) {
subdomain = domainutil.Subdomain(dn)
log.Printf("\t\t\tsubdomain: '%s'", subdomain)
}
}
// Start at the top level domain and try to find a hosted zone. Search until a match is found or there are
// no more domain levels to search for.
var zoneId string
for {
log.Printf("\t\t\tChecking zone '%s' for associated hosted zone.", zoneName)
// Loop over each one of hosted zones and try to find match.
for _, z := range zones {
zn := strings.TrimRight(*z.Name, ".")
log.Printf("\t\t\t\tChecking if '%s' matches '%s'", zn, zoneName)
if zn == zoneName {
zoneId = *z.Id
break
}
}
if zoneId != "" || zoneName == dn {
// Found a matching zone or have search all possibilities!
break
}
// If we have not found a hosted zone, append the next level from the domain to the zone.
pts := strings.Split(subdomain, ".")
subs := []string{}
for idx, sn := range pts {
if idx == len(pts)-1 {
zoneName = sn + "." + zoneName
} else {
subs = append(subs, sn)
}
}
subdomain = strings.Join(subs, ".")
}
var aName string
if zoneId == "" {
// Get the top level domain from url again.
zoneName := domainutil.Domain(dn)
if zoneName == "" {
// Handle domain names that have weird TDL: ie .tech
zoneName = dn
}
log.Printf("\t\t\tNo hosted zone found for '%s', create '%s'.", dn, zoneName)
createRes, err := svc.CreateHostedZone(&route53.CreateHostedZoneInput{
Name: aws.String(zoneName),
HostedZoneConfig: &route53.HostedZoneConfig{
Comment: aws.String(fmt.Sprintf("Public hosted zone created by saas-starter-kit.")),
},
// A unique string that identifies the request and that allows failed CreateHostedZone
// requests to be retried without the risk of executing the operation twice.
// You must use a unique CallerReference string every time you submit a CreateHostedZone
// request. CallerReference can be any unique string, for example, a date/time
// stamp.
//
// CallerReference is a required field
CallerReference: aws.String("truss-deploy"),
})
if err != nil {
return errors.Wrapf(err, "failed to create route 53 hosted zone '%s' for domain '%s'", zoneName, dn)
}
zoneId = *createRes.HostedZone.Id
log.Printf("\t\t\tCreated hosted zone '%s'", zoneId)
// The fully qualified A record name.
aName = dn
} else {
log.Printf("\t\t\tFound hosted zone '%s'", zoneId)
// The fully qualified A record name.
if subdomain != "" {
aName = subdomain + "." + zoneName
} else {
aName = zoneName
}
}
// Add the A record to be maintained for the zone.
if _, ok := zoneArecNames[zoneId]; !ok {
zoneArecNames[zoneId] = []string{}
}
zoneArecNames[zoneId] = append(zoneArecNames[zoneId], aName)
log.Printf("\t%s\tZone '%s' found with A record name '%s'.\n", tests.Success, zoneId, aName)
}
}
// Setup service discovery.
var sdService *servicediscovery.Service
{
log.Println("SD - Get or Create Namespace")
svc := servicediscovery.New(req.awsSession())
log.Println("\t\tList all the private namespaces and try to find an existing entry.")
listNamespaces := func() (*servicediscovery.NamespaceSummary, error) {
var found *servicediscovery.NamespaceSummary
err := svc.ListNamespacesPages(&servicediscovery.ListNamespacesInput{
Filters: []*servicediscovery.NamespaceFilter{
&servicediscovery.NamespaceFilter{
Name: aws.String("TYPE"),
Condition: aws.String("EQ"),
Values: aws.StringSlice([]string{"DNS_PRIVATE"}),
},
},
}, func(res *servicediscovery.ListNamespacesOutput, lastPage bool) bool {
for _, n := range res.Namespaces {
if *n.Name == *req.SDNamepsace.Name {
found = n
return false
}
}
return !lastPage
})
if err != nil {
return nil, errors.Wrap(err, "failed to list namespaces")
}
return found, nil
}
sdNamespace, err := listNamespaces()
if err != nil {
return err
}
if sdNamespace == nil {
// Link the namespace to the VPC.
req.SDNamepsace.Vpc = aws.String(vpcId)
log.Println("\t\tCreate private namespace.")
// If no namespace was found, create one.
createRes, err := svc.CreatePrivateDnsNamespace(req.SDNamepsace)
if err != nil {
return errors.Wrapf(err, "failed to create namespace '%s'", *req.SDNamepsace.Name)
}
operationId := createRes.OperationId
log.Println("\t\tWait for create operation to finish.")
retryFunc := func() (bool, error) {
opRes, err := svc.GetOperation(&servicediscovery.GetOperationInput{
OperationId: operationId,
})
if err != nil {
return true, err
}
log.Printf("\t\t\tStatus: %s.", *opRes.Operation.Status)
// The status of the operation. Values include the following:
// * SUBMITTED: This is the initial state immediately after you submit a
// request.
// * PENDING: AWS Cloud Map is performing the operation.
// * SUCCESS: The operation succeeded.
// * FAIL: The operation failed. For the failure reason, see ErrorMessage.
if *opRes.Operation.Status == "SUCCESS" {
return true, nil
} else if *opRes.Operation.Status == "FAIL" {
err = errors.Errorf("operation failed")
err = awserr.New(*opRes.Operation.ErrorCode, *opRes.Operation.ErrorMessage, err)
return true, err
}
return false, nil
}
err = retry.Retry(context.Background(), nil, retryFunc)
if err != nil {
return errors.Wrapf(err, "failed to get operation for namespace '%s'", *req.SDNamepsace.Name)
}
// Now that the create operation is complete, try to find the namespace again.
sdNamespace, err = listNamespaces()
if err != nil {
return err
}
log.Printf("\t\tCreated: %s.", *sdNamespace.Arn)
} else {
log.Printf("\t\tFound: %s.", *sdNamespace.Arn)
// The number of services that are associated with the namespace.
if sdNamespace.ServiceCount != nil {
log.Printf("\t\t\tServiceCount: %d.", *sdNamespace.ServiceCount)
}
}
log.Printf("\t%s\tUsing Service Discovery Namespace '%s'.\n", tests.Success, *sdNamespace.Id)
// Try to find an existing entry for the current service.
var existingService *servicediscovery.ServiceSummary
err = svc.ListServicesPages(&servicediscovery.ListServicesInput{
Filters: []*servicediscovery.ServiceFilter{
&servicediscovery.ServiceFilter{
Name: aws.String("NAMESPACE_ID"),
Condition: aws.String("EQ"),
Values: aws.StringSlice([]string{*sdNamespace.Id}),
},
},
}, func(res *servicediscovery.ListServicesOutput, lastPage bool) bool {
for _, n := range res.Services {
if *n.Name == req.EcsServiceName {
existingService = n
return false
}
}
return !lastPage
})
if err != nil {
return errors.Wrapf(err, "failed to list services for namespace '%s'", *sdNamespace.Id)
}
if existingService == nil {
// Link the service to the namespace.
req.SDService.NamespaceId = sdNamespace.Id
// If no namespace was found, create one.
createRes, err := svc.CreateService(req.SDService)
if err != nil {
return errors.Wrapf(err, "failed to create service '%s'", *req.SDService.Name)
}
sdService = createRes.Service
log.Printf("\t\tCreated: %s.", *sdService.Arn)
} else {
// If no namespace was found, create one.
getRes, err := svc.GetService(&servicediscovery.GetServiceInput{
Id: existingService.Id,
})
if err != nil {
return errors.Wrapf(err, "failed to get service '%s'", *req.SDService.Name)
}
sdService = getRes.Service
log.Printf("\t\tFound: %s.", *sdService.Arn)
// The number of instances that are currently associated with the service. Instances
// that were previously associated with the service but that have been deleted
// are not included in the count.
if sdService.InstanceCount != nil {
log.Printf("\t\t\tInstanceCount: %d.", *sdService.InstanceCount)
}
}
log.Printf("\t%s\tUsing Service Discovery Service '%s'.\n", tests.Success, *sdService.Id)
}
// Try to find AWS ECS Cluster by name or create new one.
var ecsCluster *ecs.Cluster
{
@ -1805,7 +2138,7 @@ func ServiceDeploy(log *log.Logger, req *serviceDeployRequest) error {
}
var updateDoc bool
for _, baseStmt := range baseServicePolicyDocument.Statement {
for _, baseStmt := range req.EcsTaskPolicyDocument.Statement {
var found bool
for curIdx, curStmt := range curDoc.Statement {
if baseStmt.Sid != curStmt.Sid {
@ -1857,17 +2190,14 @@ func ServiceDeploy(log *log.Logger, req *serviceDeployRequest) error {
}
}
} else {
dat, err := json.Marshal(baseServicePolicyDocument)
dat, err := json.Marshal(req.EcsTaskPolicyDocument)
if err != nil {
return errors.Wrap(err, "failed to json encode policy document")
}
req.EcsTaskPolicy.PolicyDocument = aws.String(string(dat))
// If no repository was found, create one.
res, err := svc.CreatePolicy(&iam.CreatePolicyInput{
PolicyName: aws.String(req.EcsTaskPolicyName),
Description: aws.String(fmt.Sprintf("Defines access for %s services. ", req.ProjectName)),
PolicyDocument: aws.String(string(dat)),
})
res, err := svc.CreatePolicy(req.EcsTaskPolicy)
if err != nil {
return errors.Wrapf(err, "failed to create task policy '%s'", req.EcsTaskPolicyName)
}
@ -2004,11 +2334,17 @@ func ServiceDeploy(log *log.Logger, req *serviceDeployRequest) error {
recreateService = true
forceDelete = true
} else if req.EnableEcsElb && (ecsService.LoadBalancers == nil || len(ecsService.LoadBalancers) == 0) {
// Service was created with no ELB and now ELB is enabled.
// Service was created without ELB and now ELB is enabled.
recreateService = true
} else if !req.EnableEcsElb && (ecsService.LoadBalancers != nil && len(ecsService.LoadBalancers) > 0) {
// Service was created with ELB and now ELB is disabled.
recreateService = true
} else if sdService != nil && sdService.Arn != nil && (ecsService.ServiceRegistries == nil || len(ecsService.ServiceRegistries) == 0) {
// Service was created without Service Discovery and now Service Discovery is enabled.
recreateService = true
} else if (sdService == nil || sdService.Arn == nil) && (ecsService.ServiceRegistries != nil && len(ecsService.ServiceRegistries) > 0) {
// Service was created with Service Discovery and now Service Discovery is disabled.
recreateService = true
}
if recreateService {
@ -2016,7 +2352,30 @@ func ServiceDeploy(log *log.Logger, req *serviceDeployRequest) error {
svc := ecs.New(req.awsSession())
_, err := svc.DeleteService(&ecs.DeleteServiceInput{
// The service cannot be stopped while it is scaled above 0
if ecsService.DesiredCount != nil && *ecsService.DesiredCount > 0 {
log.Println("\t\tScaling service down to zero.")
_, err := svc.UpdateService(&ecs.UpdateServiceInput{
Cluster: ecsService.ClusterArn,
Service: ecsService.ServiceArn,
DesiredCount: aws.Int64(int64(0)),
})
if err != nil {
return errors.Wrapf(err, "failed to update service '%s'", ecsService.ServiceName)
}
log.Println("\t\tWait for the service to scale down.")
err = svc.WaitUntilServicesStable(&ecs.DescribeServicesInput{
Cluster: ecsCluster.ClusterArn,
Services: aws.StringSlice([]string{*ecsService.ServiceArn}),
})
if err != nil {
return errors.Wrapf(err, "failed to wait for service '%s' to enter stable state", *ecsService.ServiceName)
}
}
log.Println("\t\tDelete Service.")
res, err := svc.DeleteService(&ecs.DeleteServiceInput{
Cluster: ecsService.ClusterArn,
Service: ecsService.ServiceArn,
@ -2026,8 +2385,9 @@ func ServiceDeploy(log *log.Logger, req *serviceDeployRequest) error {
Force: aws.Bool(forceDelete),
})
if err != nil {
return errors.Wrapf(err, "failed to create security group '%s'", req.Ec2SecurityGroupName)
return errors.Wrapf(err, "failed to delete service '%s'", ecsService.ServiceName)
}
ecsService = res.Service
log.Printf("\t%s\tDelete Service.\n", tests.Success)
}
@ -2611,6 +2971,18 @@ func ServiceDeploy(log *log.Logger, req *serviceDeployRequest) error {
},
}
// Add the Service Discovery registry to the ECS service.
if sdService != nil {
if serviceInput.ServiceRegistries == nil {
serviceInput.ServiceRegistries = []*ecs.ServiceRegistry{}
}
serviceInput.ServiceRegistries = append(serviceInput.ServiceRegistries, &ecs.ServiceRegistry{
RegistryArn: sdService.Arn,
})
}
createRes, err := svc.CreateService(serviceInput)
// If tags aren't enabled for the account, try the request again without them.
@ -2867,133 +3239,6 @@ func ServiceDeploy(log *log.Logger, req *serviceDeployRequest) error {
log.Printf("\t%s\tService running.\n", tests.Success)
}
// Route 53 zone lookup when hostname is set. Supports both top level domains or sub domains.
var zoneArecNames = map[string][]string{}
if req.ServiceDomainName != "" {
log.Println("Route 53 - Get or create hosted zones.")
svc := route53.New(req.awsSession())
log.Println("\tList all hosted zones.")
var zones []*route53.HostedZone
err := svc.ListHostedZonesPages(&route53.ListHostedZonesInput{},
func(res *route53.ListHostedZonesOutput, lastPage bool) bool {
for _, z := range res.HostedZones {
zones = append(zones, z)
}
return !lastPage
})
if err != nil {
return errors.Wrap(err, "failed list route 53 hosted zones")
}
// Generate a slice with the primary domain name and include all the alternative domain names.
lookupDomains := []string{req.ServiceDomainName}
for _, dn := range req.ServiceDomainNameAliases {
lookupDomains = append(lookupDomains, dn)
}
// Loop through all the defined domain names and find the associated zone even when they are a sub domain.
for _, dn := range lookupDomains {
log.Printf("\t\tFind zone for domain '%s'", dn)
// Get the top level domain from url.
zoneName := domainutil.Domain(dn)
log.Printf("\t\t\tTop Level Domain: '%s'", zoneName)
// Check if url has subdomain.
var subdomain string
if domainutil.HasSubdomain(dn) {
subdomain = domainutil.Subdomain(dn)
log.Printf("\t\t\tsubdomain: '%s'", subdomain)
}
// Start at the top level domain and try to find a hosted zone. Search until a match is found or there are
// no more domain levels to search for.
var zoneId string
for {
log.Printf("\t\t\tChecking zone '%s' for associated hosted zone.", zoneName)
// Loop over each one of hosted zones and try to find match.
for _, z := range zones {
log.Printf("\t\t\t\tChecking if %s matches %s", *z.Name, zoneName)
if strings.TrimRight(*z.Name, ".") == zoneName {
zoneId = *z.Id
break
}
}
if zoneId != "" || zoneName == dn {
// Found a matching zone or have search all possibilities!
break
}
// If we have not found a hosted zone, append the next level from the domain to the zone.
pts := strings.Split(subdomain, ".")
subs := []string{}
for idx, sn := range pts {
if idx == len(pts)-1 {
zoneName = sn + "." + zoneName
} else {
subs = append(subs, sn)
}
}
subdomain = strings.Join(subs, ".")
}
var aName string
if zoneId == "" {
// Get the top level domain from url again.
zoneName := domainutil.Domain(dn)
log.Printf("\t\t\tNo hosted zone found for '%s', create '%s'.", dn, zoneName)
createRes, err := svc.CreateHostedZone(&route53.CreateHostedZoneInput{
Name: aws.String(zoneName),
HostedZoneConfig: &route53.HostedZoneConfig{
Comment: aws.String(fmt.Sprintf("Public hosted zone created by saas-starter-kit.")),
},
// A unique string that identifies the request and that allows failed CreateHostedZone
// requests to be retried without the risk of executing the operation twice.
// You must use a unique CallerReference string every time you submit a CreateHostedZone
// request. CallerReference can be any unique string, for example, a date/time
// stamp.
//
// CallerReference is a required field
CallerReference: aws.String("truss-deploy"),
})
if err != nil {
return errors.Wrapf(err, "failed to create route 53 hosted zone '%s' for domain '%s'", zoneName, dn)
}
zoneId = *createRes.HostedZone.Id
log.Printf("\t\t\tCreated hosted zone '%s'", zoneId)
// The fully qualified A record name.
aName = dn
} else {
log.Printf("\t\t\tFound hosted zone '%s'", zoneId)
// The fully qualified A record name.
if subdomain != "" {
aName = subdomain + "." + zoneName
} else {
aName = zoneName
}
}
// Add the A record to be maintained for the zone.
if _, ok := zoneArecNames[zoneId]; !ok {
zoneArecNames[zoneId] = []string{}
}
zoneArecNames[zoneId] = append(zoneArecNames[zoneId], aName)
log.Printf("\t%s\tZone '%s' found with A record name '%s'.\n", tests.Success, zoneId, aName)
}
}
if req.EnableEcsElb {
// TODO: Need to connect ELB to route53
} else {
@ -3006,6 +3251,7 @@ func ServiceDeploy(log *log.Logger, req *serviceDeployRequest) error {
servceTaskRes, err := svc.ListTasks(&ecs.ListTasksInput{
Cluster: aws.String(req.EcsClusterName),
ServiceName: aws.String(req.EcsServiceName),
DesiredStatus: aws.String("RUNNING"),
})
if err != nil {
return errors.Wrapf(err, "failed to list tasks for cluster '%s' service '%s'", req.EcsClusterName, req.EcsServiceName)
@ -3020,43 +3266,18 @@ func ServiceDeploy(log *log.Logger, req *serviceDeployRequest) error {
return errors.Wrapf(err, "failed to describe %d tasks for cluster '%s'", len(servceTaskRes.TaskArns), req.EcsClusterName)
}
var failures []*ecs.Failure
var taskArns []string
for _, t := range taskRes.Tasks {
if t.TaskDefinitionArn != taskDef.TaskDefinitionArn {
if *t.TaskDefinitionArn != *taskDef.TaskDefinitionArn {
continue
}
log.Printf("\t\t\t%s: %s\n", *t.TaskArn, *t.LastStatus)
taskArns = append(taskArns, *t.TaskArn)
// Limit failures to only the current task definition.
for _, f := range taskRes.Failures {
if *f.Arn == *t.TaskArn {
failures = append(failures, f)
}
}
}
if len(failures) > 0 {
for _, t := range failures {
log.Printf("\t%s\tTask %s failed with %s.\n", tests.Failed, *t.Arn, *t.Reason)
}
} else {
log.Printf("\t%s\tTasks founds.\n", tests.Success)
}
log.Println("\t\tWaiting for tasks to enter running state.")
{
var err error
err = svc.WaitUntilTasksRunning(&ecs.DescribeTasksInput{
Cluster: ecsCluster.ClusterArn,
Tasks: aws.StringSlice(taskArns),
})
if err != nil {
return errors.Wrapf(err, "failed to wait for tasks to enter running state for cluster '%s'", req.EcsClusterName)
}
log.Printf("\t%s\tTasks running.\n", tests.Success)
}
log.Printf("\t%s\tTasks founds.\n", tests.Success)
log.Println("\t\tDescribe tasks for running tasks.")
{
@ -3073,6 +3294,8 @@ func ServiceDeploy(log *log.Logger, req *serviceDeployRequest) error {
continue
}
// t.Containers[0].NetworkInterfaces[0].Ipv6Address
for _, a := range t.Attachments {
if a.Details == nil {
continue