|  | @@ -304,93 +304,102 @@ func (s sdk) ListStacks(ctx context.Context, name string) ([]compose.Stack, erro
 | 
	
		
			
				|  |  |  	}
 | 
	
		
			
				|  |  |  	stacks := []compose.Stack{}
 | 
	
		
			
				|  |  |  	for _, stack := range cfStacks.Stacks {
 | 
	
		
			
				|  |  | -		skip := true
 | 
	
		
			
				|  |  |  		for _, t := range stack.Tags {
 | 
	
		
			
				|  |  |  			if *t.Key == compose.ProjectTag {
 | 
	
		
			
				|  |  | -				skip = false
 | 
	
		
			
				|  |  | +				status := compose.RUNNING
 | 
	
		
			
				|  |  | +				switch aws.StringValue(stack.StackStatus) {
 | 
	
		
			
				|  |  | +				case "CREATE_IN_PROGRESS":
 | 
	
		
			
				|  |  | +					status = compose.STARTING
 | 
	
		
			
				|  |  | +				case "DELETE_IN_PROGRESS":
 | 
	
		
			
				|  |  | +					status = compose.REMOVING
 | 
	
		
			
				|  |  | +				case "UPDATE_IN_PROGRESS":
 | 
	
		
			
				|  |  | +					status = compose.UPDATING
 | 
	
		
			
				|  |  | +				default:
 | 
	
		
			
				|  |  | +				}
 | 
	
		
			
				|  |  | +				stacks = append(stacks, compose.Stack{
 | 
	
		
			
				|  |  | +					ID:     aws.StringValue(stack.StackId),
 | 
	
		
			
				|  |  | +					Name:   aws.StringValue(stack.StackName),
 | 
	
		
			
				|  |  | +					Status: status,
 | 
	
		
			
				|  |  | +				})
 | 
	
		
			
				|  |  |  				break
 | 
	
		
			
				|  |  |  			}
 | 
	
		
			
				|  |  |  		}
 | 
	
		
			
				|  |  | -		if skip {
 | 
	
		
			
				|  |  | -			continue
 | 
	
		
			
				|  |  | -		}
 | 
	
		
			
				|  |  | -		status := compose.RUNNING
 | 
	
		
			
				|  |  | -		reason := ""
 | 
	
		
			
				|  |  | -		switch aws.StringValue(stack.StackStatus) {
 | 
	
		
			
				|  |  | -		case "CREATE_IN_PROGRESS":
 | 
	
		
			
				|  |  | -			status = compose.STARTING
 | 
	
		
			
				|  |  | -		case "DELETE_IN_PROGRESS":
 | 
	
		
			
				|  |  | -			status = compose.REMOVING
 | 
	
		
			
				|  |  | -		case "UPDATE_IN_PROGRESS":
 | 
	
		
			
				|  |  | -			status = compose.UPDATING
 | 
	
		
			
				|  |  | -		}
 | 
	
		
			
				|  |  | -		if status == compose.STARTING {
 | 
	
		
			
				|  |  | -			if err := s.CheckStackState(ctx, aws.StringValue(stack.StackName)); err != nil {
 | 
	
		
			
				|  |  | -				status = compose.FAILED
 | 
	
		
			
				|  |  | -				reason = err.Error()
 | 
	
		
			
				|  |  | -			}
 | 
	
		
			
				|  |  | -		}
 | 
	
		
			
				|  |  | -		stacks = append(stacks, compose.Stack{
 | 
	
		
			
				|  |  | -			ID:     aws.StringValue(stack.StackId),
 | 
	
		
			
				|  |  | -			Name:   aws.StringValue(stack.StackName),
 | 
	
		
			
				|  |  | -			Status: status,
 | 
	
		
			
				|  |  | -			Reason: reason,
 | 
	
		
			
				|  |  | -		})
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  |  	}
 | 
	
		
			
				|  |  |  	return stacks, nil
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -func (s sdk) CheckStackState(ctx context.Context, name string) error {
 | 
	
		
			
				|  |  | +func (s sdk) GetStackClusterID(ctx context.Context, stack string) (string, error) {
 | 
	
		
			
				|  |  |  	resources, err := s.CF.ListStackResourcesWithContext(ctx, &cloudformation.ListStackResourcesInput{
 | 
	
		
			
				|  |  | -		StackName: aws.String(name),
 | 
	
		
			
				|  |  | +		StackName: aws.String(stack),
 | 
	
		
			
				|  |  |  	})
 | 
	
		
			
				|  |  |  	if err != nil {
 | 
	
		
			
				|  |  | -		return err
 | 
	
		
			
				|  |  | +		return "", err
 | 
	
		
			
				|  |  |  	}
 | 
	
		
			
				|  |  | -	services := []*string{}
 | 
	
		
			
				|  |  | -	serviceNames := []string{}
 | 
	
		
			
				|  |  | -	var cluster *string
 | 
	
		
			
				|  |  |  	for _, r := range resources.StackResourceSummaries {
 | 
	
		
			
				|  |  |  		if aws.StringValue(r.ResourceType) == "AWS::ECS::Cluster" {
 | 
	
		
			
				|  |  | -			cluster = r.PhysicalResourceId
 | 
	
		
			
				|  |  | -			continue
 | 
	
		
			
				|  |  | -		}
 | 
	
		
			
				|  |  | -		if aws.StringValue(r.ResourceType) == "AWS::ECS::Service" {
 | 
	
		
			
				|  |  | -			if r.PhysicalResourceId == nil {
 | 
	
		
			
				|  |  | -				continue
 | 
	
		
			
				|  |  | -			}
 | 
	
		
			
				|  |  | -			services = append(services, r.PhysicalResourceId)
 | 
	
		
			
				|  |  | -			serviceNames = append(serviceNames, *r.LogicalResourceId)
 | 
	
		
			
				|  |  | +			return aws.StringValue(r.PhysicalResourceId), nil
 | 
	
		
			
				|  |  |  		}
 | 
	
		
			
				|  |  |  	}
 | 
	
		
			
				|  |  | -	for i, service := range services {
 | 
	
		
			
				|  |  | -		err := s.CheckTaskState(ctx, aws.StringValue(cluster), aws.StringValue(service))
 | 
	
		
			
				|  |  | -		if err != nil {
 | 
	
		
			
				|  |  | -			return fmt.Errorf("%s error: %s", serviceNames[i], err.Error())
 | 
	
		
			
				|  |  | -		}
 | 
	
		
			
				|  |  | -	}
 | 
	
		
			
				|  |  | -	return nil
 | 
	
		
			
				|  |  | +	return "", nil
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -func (s sdk) CheckTaskState(ctx context.Context, cluster string, serviceName string) error {
 | 
	
		
			
				|  |  | -	tasks, err := s.ECS.ListTasksWithContext(ctx, &ecs.ListTasksInput{
 | 
	
		
			
				|  |  | -		Cluster:     aws.String(cluster),
 | 
	
		
			
				|  |  | -		ServiceName: aws.String(serviceName),
 | 
	
		
			
				|  |  | +func (s sdk) GetServiceTaskDefinition(ctx context.Context, cluster string, serviceArns []string) (map[string]string, error) {
 | 
	
		
			
				|  |  | +	defs := map[string]string{}
 | 
	
		
			
				|  |  | +	svc := []*string{}
 | 
	
		
			
				|  |  | +	for _, s := range serviceArns {
 | 
	
		
			
				|  |  | +		svc = append(svc, aws.String(s))
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +	services, err := s.ECS.DescribeServicesWithContext(ctx, &ecs.DescribeServicesInput{
 | 
	
		
			
				|  |  | +		Cluster:  aws.String(cluster),
 | 
	
		
			
				|  |  | +		Services: svc,
 | 
	
		
			
				|  |  |  	})
 | 
	
		
			
				|  |  |  	if err != nil {
 | 
	
		
			
				|  |  | -		return err
 | 
	
		
			
				|  |  | +		return nil, err
 | 
	
		
			
				|  |  |  	}
 | 
	
		
			
				|  |  | -	if len(tasks.TaskArns) > 0 {
 | 
	
		
			
				|  |  | -		return nil
 | 
	
		
			
				|  |  | +	for _, s := range services.Services {
 | 
	
		
			
				|  |  | +		defs[aws.StringValue(s.ServiceArn)] = aws.StringValue(s.TaskDefinition)
 | 
	
		
			
				|  |  |  	}
 | 
	
		
			
				|  |  | -	tasks, err = s.ECS.ListTasksWithContext(ctx, &ecs.ListTasksInput{
 | 
	
		
			
				|  |  | +	return defs, nil
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +func (s sdk) ListStackServices(ctx context.Context, stack string) ([]string, error) {
 | 
	
		
			
				|  |  | +	arns := []string{}
 | 
	
		
			
				|  |  | +	var nextToken *string
 | 
	
		
			
				|  |  | +	for {
 | 
	
		
			
				|  |  | +		response, err := s.CF.ListStackResourcesWithContext(ctx, &cloudformation.ListStackResourcesInput{
 | 
	
		
			
				|  |  | +			StackName: aws.String(stack),
 | 
	
		
			
				|  |  | +			NextToken: nextToken,
 | 
	
		
			
				|  |  | +		})
 | 
	
		
			
				|  |  | +		if err != nil {
 | 
	
		
			
				|  |  | +			return nil, err
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +		for _, r := range response.StackResourceSummaries {
 | 
	
		
			
				|  |  | +			if aws.StringValue(r.ResourceType) == "AWS::ECS::Service" {
 | 
	
		
			
				|  |  | +				if r.PhysicalResourceId != nil {
 | 
	
		
			
				|  |  | +					arns = append(arns, aws.StringValue(r.PhysicalResourceId))
 | 
	
		
			
				|  |  | +				}
 | 
	
		
			
				|  |  | +			}
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +		nextToken = response.NextToken
 | 
	
		
			
				|  |  | +		if nextToken == nil {
 | 
	
		
			
				|  |  | +			break
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +	return arns, nil
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +func (s sdk) GetServiceTasks(ctx context.Context, cluster string, service string, stopped bool) ([]*ecs.Task, error) {
 | 
	
		
			
				|  |  | +	state := "RUNNING"
 | 
	
		
			
				|  |  | +	if stopped {
 | 
	
		
			
				|  |  | +		state = "STOPPED"
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +	tasks, err := s.ECS.ListTasksWithContext(ctx, &ecs.ListTasksInput{
 | 
	
		
			
				|  |  |  		Cluster:       aws.String(cluster),
 | 
	
		
			
				|  |  | -		ServiceName:   aws.String(serviceName),
 | 
	
		
			
				|  |  | -		DesiredStatus: aws.String("STOPPED"),
 | 
	
		
			
				|  |  | +		ServiceName:   aws.String(service),
 | 
	
		
			
				|  |  | +		DesiredStatus: aws.String(state),
 | 
	
		
			
				|  |  |  	})
 | 
	
		
			
				|  |  |  	if err != nil {
 | 
	
		
			
				|  |  | -		return err
 | 
	
		
			
				|  |  | +		return nil, err
 | 
	
		
			
				|  |  |  	}
 | 
	
		
			
				|  |  |  	if len(tasks.TaskArns) > 0 {
 | 
	
		
			
				|  |  |  		taskDescriptions, err := s.ECS.DescribeTasksWithContext(ctx, &ecs.DescribeTasksInput{
 | 
	
	
		
			
				|  | @@ -398,17 +407,30 @@ func (s sdk) CheckTaskState(ctx context.Context, cluster string, serviceName str
 | 
	
		
			
				|  |  |  			Tasks:   tasks.TaskArns,
 | 
	
		
			
				|  |  |  		})
 | 
	
		
			
				|  |  |  		if err != nil {
 | 
	
		
			
				|  |  | -			return err
 | 
	
		
			
				|  |  | -		}
 | 
	
		
			
				|  |  | -		if len(taskDescriptions.Tasks) > 0 {
 | 
	
		
			
				|  |  | -			recentTask := taskDescriptions.Tasks[0]
 | 
	
		
			
				|  |  | -			switch aws.StringValue(recentTask.StopCode) {
 | 
	
		
			
				|  |  | -			case "TaskFailedToStart":
 | 
	
		
			
				|  |  | -				return fmt.Errorf(aws.StringValue(recentTask.StoppedReason))
 | 
	
		
			
				|  |  | -			}
 | 
	
		
			
				|  |  | +			return nil, err
 | 
	
		
			
				|  |  |  		}
 | 
	
		
			
				|  |  | +		return taskDescriptions.Tasks, nil
 | 
	
		
			
				|  |  |  	}
 | 
	
		
			
				|  |  | -	return nil
 | 
	
		
			
				|  |  | +	return nil, nil
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +func (s sdk) GetTaskStoppedReason(ctx context.Context, cluster string, taskArn string) (string, error) {
 | 
	
		
			
				|  |  | +	taskDescriptions, err := s.ECS.DescribeTasksWithContext(ctx, &ecs.DescribeTasksInput{
 | 
	
		
			
				|  |  | +		Cluster: aws.String(cluster),
 | 
	
		
			
				|  |  | +		Tasks:   []*string{aws.String(taskArn)},
 | 
	
		
			
				|  |  | +	})
 | 
	
		
			
				|  |  | +	if err != nil {
 | 
	
		
			
				|  |  | +		return "", err
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +	if len(taskDescriptions.Tasks) == 0 {
 | 
	
		
			
				|  |  | +		return "", nil
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +	task := taskDescriptions.Tasks[0]
 | 
	
		
			
				|  |  | +	return fmt.Sprintf(
 | 
	
		
			
				|  |  | +		"%s: %s",
 | 
	
		
			
				|  |  | +		aws.StringValue(task.StopCode),
 | 
	
		
			
				|  |  | +		aws.StringValue(task.StoppedReason)), nil
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  func (s sdk) DescribeStackEvents(ctx context.Context, stackID string) ([]*cloudformation.StackEvent, error) {
 | 
	
	
		
			
				|  | @@ -423,6 +445,7 @@ func (s sdk) DescribeStackEvents(ctx context.Context, stackID string) ([]*cloudf
 | 
	
		
			
				|  |  |  		if err != nil {
 | 
	
		
			
				|  |  |  			return nil, err
 | 
	
		
			
				|  |  |  		}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |  		events = append(events, resp.StackEvents...)
 | 
	
		
			
				|  |  |  		if resp.NextToken == nil {
 | 
	
		
			
				|  |  |  			return events, nil
 | 
	
	
		
			
				|  | @@ -609,46 +632,43 @@ func (s sdk) GetLogs(ctx context.Context, name string, consumer func(service, co
 | 
	
		
			
				|  |  |  	}
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -func (s sdk) DescribeServices(ctx context.Context, cluster string, arns []string) ([]compose.ServiceStatus, error) {
 | 
	
		
			
				|  |  | +func (s sdk) DescribeService(ctx context.Context, cluster string, arn string) (compose.ServiceStatus, error) {
 | 
	
		
			
				|  |  |  	services, err := s.ECS.DescribeServicesWithContext(ctx, &ecs.DescribeServicesInput{
 | 
	
		
			
				|  |  |  		Cluster:  aws.String(cluster),
 | 
	
		
			
				|  |  | -		Services: aws.StringSlice(arns),
 | 
	
		
			
				|  |  | +		Services: []*string{aws.String(arn)},
 | 
	
		
			
				|  |  |  		Include:  aws.StringSlice([]string{"TAGS"}),
 | 
	
		
			
				|  |  |  	})
 | 
	
		
			
				|  |  |  	if err != nil {
 | 
	
		
			
				|  |  | -		return nil, err
 | 
	
		
			
				|  |  | +		return compose.ServiceStatus{}, err
 | 
	
		
			
				|  |  |  	}
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -	status := []compose.ServiceStatus{}
 | 
	
		
			
				|  |  | -	for _, service := range services.Services {
 | 
	
		
			
				|  |  | -		var name string
 | 
	
		
			
				|  |  | -		for _, t := range service.Tags {
 | 
	
		
			
				|  |  | -			if *t.Key == compose.ServiceTag {
 | 
	
		
			
				|  |  | -				name = aws.StringValue(t.Value)
 | 
	
		
			
				|  |  | -			}
 | 
	
		
			
				|  |  | -		}
 | 
	
		
			
				|  |  | -		if name == "" {
 | 
	
		
			
				|  |  | -			return nil, fmt.Errorf("service %s doesn't have a %s tag", *service.ServiceArn, compose.ServiceTag)
 | 
	
		
			
				|  |  | +	service := services.Services[0]
 | 
	
		
			
				|  |  | +	var name string
 | 
	
		
			
				|  |  | +	for _, t := range service.Tags {
 | 
	
		
			
				|  |  | +		if *t.Key == compose.ServiceTag {
 | 
	
		
			
				|  |  | +			name = aws.StringValue(t.Value)
 | 
	
		
			
				|  |  |  		}
 | 
	
		
			
				|  |  | -		targetGroupArns := []string{}
 | 
	
		
			
				|  |  | -		for _, lb := range service.LoadBalancers {
 | 
	
		
			
				|  |  | -			targetGroupArns = append(targetGroupArns, *lb.TargetGroupArn)
 | 
	
		
			
				|  |  | -		}
 | 
	
		
			
				|  |  | -		// getURLwithPortMapping makes 2 queries
 | 
	
		
			
				|  |  | -		// one to get the target groups and another for load balancers
 | 
	
		
			
				|  |  | -		loadBalancers, err := s.getURLWithPortMapping(ctx, targetGroupArns)
 | 
	
		
			
				|  |  | -		if err != nil {
 | 
	
		
			
				|  |  | -			return nil, err
 | 
	
		
			
				|  |  | -		}
 | 
	
		
			
				|  |  | -		status = append(status, compose.ServiceStatus{
 | 
	
		
			
				|  |  | -			ID:         aws.StringValue(service.ServiceName),
 | 
	
		
			
				|  |  | -			Name:       name,
 | 
	
		
			
				|  |  | -			Replicas:   int(aws.Int64Value(service.RunningCount)),
 | 
	
		
			
				|  |  | -			Desired:    int(aws.Int64Value(service.DesiredCount)),
 | 
	
		
			
				|  |  | -			Publishers: loadBalancers,
 | 
	
		
			
				|  |  | -		})
 | 
	
		
			
				|  |  |  	}
 | 
	
		
			
				|  |  | -	return status, nil
 | 
	
		
			
				|  |  | +	if name == "" {
 | 
	
		
			
				|  |  | +		return compose.ServiceStatus{}, fmt.Errorf("service %s doesn't have a %s tag", *service.ServiceArn, compose.ServiceTag)
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +	targetGroupArns := []string{}
 | 
	
		
			
				|  |  | +	for _, lb := range service.LoadBalancers {
 | 
	
		
			
				|  |  | +		targetGroupArns = append(targetGroupArns, *lb.TargetGroupArn)
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +	// getURLwithPortMapping makes 2 queries
 | 
	
		
			
				|  |  | +	// one to get the target groups and another for load balancers
 | 
	
		
			
				|  |  | +	loadBalancers, err := s.getURLWithPortMapping(ctx, targetGroupArns)
 | 
	
		
			
				|  |  | +	if err != nil {
 | 
	
		
			
				|  |  | +		return compose.ServiceStatus{}, err
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +	return compose.ServiceStatus{
 | 
	
		
			
				|  |  | +		ID:         aws.StringValue(service.ServiceName),
 | 
	
		
			
				|  |  | +		Name:       name,
 | 
	
		
			
				|  |  | +		Replicas:   int(aws.Int64Value(service.RunningCount)),
 | 
	
		
			
				|  |  | +		Desired:    int(aws.Int64Value(service.DesiredCount)),
 | 
	
		
			
				|  |  | +		Publishers: loadBalancers,
 | 
	
		
			
				|  |  | +	}, nil
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  func (s sdk) getURLWithPortMapping(ctx context.Context, targetGroupArns []string) ([]compose.PortPublisher, error) {
 |