<commit_before>/*
Copyright 2017 WALLIX

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package awsdriver

import (
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"math/rand"
	"mime"
	"os"
	"os/exec"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/awserr"
	"github.com/aws/aws-sdk-go/aws/client"
	"github.com/aws/aws-sdk-go/aws/request"
	"github.com/aws/aws-sdk-go/service/autoscaling"
	"github.com/aws/aws-sdk-go/service/cloudfront"
	"github.com/aws/aws-sdk-go/service/cloudwatch"
	"github.com/aws/aws-sdk-go/service/ec2"
	"github.com/aws/aws-sdk-go/service/ecr"
	"github.com/aws/aws-sdk-go/service/ecs"
	"github.com/aws/aws-sdk-go/service/elbv2"
	"github.com/aws/aws-sdk-go/service/iam"
	"github.com/aws/aws-sdk-go/service/rds"
	"github.com/aws/aws-sdk-go/service/route53"
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/mitchellh/ioprogress"
	"github.com/wallix/awless/cloud"
	"github.com/wallix/awless/console"
	"github.com/wallix/awless/logger"
	"github.com/wallix/awless/template/driver"
)

const (
	notFoundState = "not-found"
)

func (d *Ec2Driver) Attach_Securitygroup_DryRun(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	if _, ok := params["id"]; !ok {
		return nil, errors.New("attach securitygroup: missing required params 'id'")
	}

	_, hasInstance := params["instance"]

	if !hasInstance {
		return nil, errors.New("attach securitygroup: missing 'instance' param")
	}

	d.logger.Verbose("params dry run: attach securitygroup ok")
	return nil, nil
}

func (d *Ec2Driver) Attach_Securitygroup(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	instance, hasInstance := params["instance"].(string)

	switch {
	case hasInstance:
		groups, err := d.fetchInstanceSecurityGroups(instance)
		if err != nil {
			return nil, fmt.Errorf("fetching securitygroups for instance %s: %s", instance, err)
		}

		groups = append(groups, fmt.Sprint(params["id"]))
		if len(groups) == 0 {
			d.logger.Errorf("AWS instances must have at least one securitygroup")
		}
		call := &driverCall{
			d:      d,
			fn:     d.ModifyInstanceAttribute,
			logger: d.logger,
			setters: []setter{
				{val: instance, fieldPath: "InstanceID", fieldType: awsstr},
				{val: groups, fieldPath: "Groups", fieldType: awsstringslice},
			},
			desc: "attach securitygroup",
		}
		return call.execute(&ec2.ModifyInstanceAttributeInput{})
	}

	return nil, errors.New("missing 'instance' param")
}

func (d *Ec2Driver) Detach_Securitygroup_DryRun(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	if _, ok := params["id"]; !ok {
		return nil, errors.New("detach securitygroup: missing required params 'id'")
	}

	_, hasInstance := params["instance"]

	if !hasInstance {
		return nil, errors.New("detach securitygroup: missing 'instance' param")
	}

	d.logger.Verbose("params dry run: detach securitygroup ok")
	return nil, nil
}

func (d *Ec2Driver) Detach_Securitygroup(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	instance, hasInstance := params["instance"].(string)

	switch {
	case hasInstance:
		groups, err := d.fetchInstanceSecurityGroups(instance)
		if err != nil {
			return nil, fmt.Errorf("fetching securitygroups for instance %s: %s", instance, err)
		}

		cleaned := removeString(groups, fmt.Sprint(params["id"]))

		if len(cleaned) == 0 {
			d.logger.Errorf("AWS instances must have at least one securitygroup")
		}
		call := &driverCall{
			d:      d,
			fn:     d.ModifyInstanceAttribute,
			logger: d.logger,
			setters: []setter{
				{val: instance, fieldPath: "InstanceID", fieldType: awsstr},
				{val: cleaned, fieldPath: "Groups", fieldType: awsstringslice},
			},
			desc: "detach securitygroup",
		}
		return call.execute(&ec2.ModifyInstanceAttributeInput{})
	}

	return nil, errors.New("missing 'instance' param")
}

func (d *Ec2Driver) fetchInstanceSecurityGroups(id string) ([]string, error) {
	params := &ec2.DescribeInstanceAttributeInput{
		Attribute:  aws.String("groupSet"),
		InstanceId: aws.String(id),
	}
	resp, err := d.DescribeInstanceAttribute(params)
	if err != nil {
		return nil, err
	}

	var groups []string
	for _, g := range resp.Groups {
		groups = append(groups, aws.StringValue(g.GroupId))
	}

	return groups, nil
}

func removeString(arr []string, s string) (out []string) {
	for _, e := range arr {
		if e != s {
			out = append(out, e)
		}
	}
	return
}

func (d *Ec2Driver) Attach_Instanceprofile_DryRun(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	instanceId, ok := params["instance"].(string)
	if !ok {
		return nil, errors.New("attach instanceprofile: missing required params 'instance'")
	}

	if _, ok := params["name"]; !ok {
		return nil, errors.New("attach instanceprofile: missing required params 'name'")
	}

	replace, _ := strconv.ParseBool(fmt.Sprint(params["replace"]))
	if replace {
		in := &ec2.DescribeIamInstanceProfileAssociationsInput{
			Filters: []*ec2.Filter{
				{Name: aws.String("instance-id"), Values: []*string{aws.String(instanceId)}},
			},
		}
		out, err := d.DescribeIamInstanceProfileAssociations(in)
		if err != nil {
			return nil, fmt.Errorf("dry run: replace mode on: cannot get : %s", err)
		}
		if assocs := out.IamInstanceProfileAssociations; len(assocs) > 0 {
			for _, ass := range assocs {
				d.logger.ExtraVerbosef("dry run: attach instanceprofile: existing instanceprofile %s (state: %s) on instance %s", aws.StringValue(ass.IamInstanceProfile.Id), aws.StringValue(ass.State), instanceId)

			}
		}
	}

	d.logger.Verbose("params dry run: attach instanceprofile ok")
	return fakeDryRunId("instanceprofile"), nil
}

func (d *Ec2Driver) Attach_Instanceprofile(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	instanceId, _ := params["instance"].(string)
	profileName, _ := params["name"].(string)

	replace, _ := strconv.ParseBool(fmt.Sprint(params["replace"]))
	if replace {
		out, err := d.DescribeIamInstanceProfileAssociations(
			&ec2.DescribeIamInstanceProfileAssociationsInput{
				Filters: []*ec2.Filter{
					{Name: aws.String("instance-id"), Values: []*string{aws.String(instanceId)}},
					{Name: aws.String("state"), Values: []*string{aws.String("associated")}},
				},
			})
		if err != nil {
			logger.Warningf("attach instanceprofile: replace mode on: dry run was ok but now cannot get instance profile association")
		}
		assoc := out.IamInstanceProfileAssociations
		if len(assoc) > 0 {
			start := time.Now()
			assocId := aws.StringValue(assoc[0].AssociationId)
			assocInstId := aws.StringValue(assoc[0].InstanceId)
			oldProfileArn := aws.StringValue(assoc[0].IamInstanceProfile.Arn)
			d.logger.ExtraVerbosef("attach profile: found existing profile to replace with %s", profileName)
			if assocInstId == instanceId {
				out, err := d.ReplaceIamInstanceProfileAssociation(
					&ec2.ReplaceIamInstanceProfileAssociationInput{
						AssociationId: aws.String(assocId),
						IamInstanceProfile: &ec2.IamInstanceProfileSpecification{
							Name: aws.String(profileName),
						},
					})
				if err != nil {
					return nil, fmt.Errorf("attach instanceprofile: replace mode on: cannot replace with new instance profile: %s", err)
				}

				d.logger.Verbosef("attach profile: replaced profile '%s' with '%s' on instance %s", oldProfileArn, profileName, instanceId)
				d.logger.ExtraVerbosef("ec2.ReplaceIamInstanceProfileAssociation call took %s", time.Since(start))

				return out, nil
			}
		}
	}

	input := &ec2.AssociateIamInstanceProfileInput{}
	if err := setFieldWithType(instanceId, input, "InstanceId", awsstr, ctx); err != nil {
		return nil, err
	}
	if err := setFieldWithType(profileName, input, "IamInstanceProfile.Name", awsstr, ctx); err != nil {
		return nil, err
	}

	start := time.Now()
	output, err := d.AssociateIamInstanceProfile(input)
	if err != nil {
		return nil, fmt.Errorf("attach instanceprofile: %s", err)
	}
	d.logger.ExtraVerbosef("ec2.AssociateIamInstanceProfile call took %s", time.Since(start))
	d.logger.Info("attach instanceprofile done")
	return output, nil
}

func (d *Ec2Driver) Detach_Instanceprofile_DryRun(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	if _, ok := params["instance"]; !ok {
		return nil, errors.New("detach instanceprofile: missing required params 'instance'")
	}

	if _, ok := params["name"]; !ok {
		return nil, errors.New("detach instanceprofile: missing required params 'name'")
	}

	d.logger.Verbose("params dry run: detach instanceprofile ok")
	return fakeDryRunId("instanceprofile"), nil
}

func (d *Ec2Driver) Detach_Instanceprofile(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	instanceId, _ := params["instance"].(string)
	profileName, _ := params["name"].(string)

	out, err := d.DescribeIamInstanceProfileAssociations(
		&ec2.DescribeIamInstanceProfileAssociationsInput{
			Filters: []*ec2.Filter{
				{Name: aws.String("instance-id"), Values: []*string{aws.String(instanceId)}},
			},
		})
	if err != nil {
		return nil, fmt.Errorf("detach instance profile: cannot list profile on instance %s: %s", instanceId, err)
	}

	assocs := out.IamInstanceProfileAssociations
	if len(assocs) < 1 {
		d.logger.Infof("detach instanceprofile: nothing to be detached on instance %s", instanceId)
		return nil, nil
	}

	var lastId string
	for _, ass := range assocs {
		if strings.Contains(aws.StringValue(ass.IamInstanceProfile.Arn), profileName) {
			input := &ec2.DisassociateIamInstanceProfileInput{
				AssociationId: ass.AssociationId,
			}

			start := time.Now()
			output, err := d.DisassociateIamInstanceProfile(input)
			if err != nil {
				return nil, fmt.Errorf("detach instanceprofile: %s", err)
			}
			d.logger.ExtraVerbosef("ec2.DisassociateIamInstanceProfile call took %s", time.Since(start))
			id := aws.StringValue(output.IamInstanceProfileAssociation.IamInstanceProfile.Id)
			d.logger.Infof("detach instanceprofile '%s' done", id)
			lastId = id
		}
	}

	return lastId, nil
}

func (d *EcsDriver) Attach_Containertask_DryRun(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	if _, ok := params["name"]; !ok {
		return nil, errors.New("attach containertask: missing required params 'name'")
	}
	if _, ok := params["container-name"]; !ok {
		return nil, errors.New("attach containertask: missing required params 'container-name'")
	}
	if _, ok := params["image"]; !ok {
		return nil, errors.New("attach containertask: missing required params 'image'")
	}
	if _, ok := params["memory-hard-limit"]; !ok {
		return nil, errors.New("attach containertask: missing required params 'memory-hard-limit'")
	}
	d.logger.Verbose("params dry run: attach containertask ok")
	return nil, nil
}

func (d *EcsDriver) Attach_Containertask(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	var taskDefinitionInput *ecs.RegisterTaskDefinitionInput
	taskDefinitionName := fmt.Sprint(params["name"])

	taskdefOutput, err := d.DescribeTaskDefinition(&ecs.DescribeTaskDefinitionInput{
		TaskDefinition: aws.String(taskDefinitionName),
	})
	if awserr, ok := err.(awserr.Error); err != nil && ok {
		if awserr.Code() == "ClientException" && strings.Contains(strings.ToLower(awserr.Message()), "unable to describe task definition") {
			d.logger.Verbosef("service %s does not exist: creating service", taskDefinitionName)
			taskDefinitionInput = &ecs.RegisterTaskDefinitionInput{
				Family: aws.String(taskDefinitionName),
			}
		} else {
			return nil, err
		}
	} else if err != nil {
		return nil, err
	} else {
		taskDefinitionInput = &ecs.RegisterTaskDefinitionInput{
			ContainerDefinitions: taskdefOutput.TaskDefinition.ContainerDefinitions,
			Family:               taskdefOutput.TaskDefinition.Family,
			NetworkMode:          taskdefOutput.TaskDefinition.NetworkMode,
			PlacementConstraints: taskdefOutput.TaskDefinition.PlacementConstraints,
			TaskRoleArn:          taskdefOutput.TaskDefinition.TaskRoleArn,
			Volumes:              taskdefOutput.TaskDefinition.Volumes,
		}
	}

	container := &ecs.ContainerDefinition{}
	if err = setFieldWithType(params["container-name"], container, "Name", awsstr); err != nil {
		return nil, err
	}
	if err = setFieldWithType(params["image"], container, "Image", awsstr); err != nil {
		return nil, err
	}
	if err = setFieldWithType(params["memory-hard-limit"], container, "Memory", awsint64); err != nil {
		return nil, err
	}
	if command, ok := params["command"]; ok {
		switch cc := command.(type) {
		case string:
			if err = setFieldWithType(strings.Split(cc, " "), container, "Command", awsstringslice); err != nil {
				return nil, err
			}
		default:
			if err = setFieldWithType(cc, container, "Command", awsstringslice); err != nil {
				return nil, err
			}
		}
	}
	if env, ok := params["env"]; ok {
		if err = setFieldWithType(env, container, "Environment", awsecskeyvalue); err != nil {
			return nil, err
		}
	}
	if priv, ok := params["privileged"]; ok && fmt.Sprint(priv) == "true" {
		if err = setFieldWithType(true, container, "Privileged", awsbool); err != nil {
			return nil, err
		}
	}
	if workdir, ok := params["workdir"]; ok {
		if err = setFieldWithType(workdir, container, "WorkingDirectory", awsstr); err != nil {
			return nil, err
		}
	}
	if ports, ok := params["ports"]; ok {
		if err = setFieldWithType(ports, container, "PortMappings", awsportmappings); err != nil {
			return nil, err
		}
	}

	taskDefinitionInput.ContainerDefinitions = append(taskDefinitionInput.ContainerDefinitions, container)

	start := time.Now()

	taskDefOutput, err := d.RegisterTaskDefinition(taskDefinitionInput)
	if err != nil {
		return nil, fmt.Errorf("attach containertask: register task definition: %s", err)
	}
	d.logger.ExtraVerbosef("ecs.RegisterTaskDefinitionOutput call took %s", time.Since(start))
	d.logger.ExtraVerbosef("attach containertask: register task definition '%s' done", aws.StringValue(taskDefOutput.TaskDefinition.Family))

	d.logger.Infof("attach container '%s' done", aws.StringValue(taskDefOutput.TaskDefinition.TaskDefinitionArn))
	return nil, nil
}

func (d *EcsDriver) Detach_Containertask_DryRun(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	if _, ok := params["name"]; !ok {
		return nil, errors.New("detach containertask: missing required params 'name'")
	}
	if _, ok := params["container-name"]; !ok {
		return nil, errors.New("detach containertask: missing required params 'container-name'")
	}
	d.logger.Verbose("params dry run: detach containertask ok")
	return nil, nil
}

func (d *EcsDriver) Detach_Containertask(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	taskDefinitionName := fmt.Sprint(params["name"])

	taskdefOutput, err := d.DescribeTaskDefinition(&ecs.DescribeTaskDefinitionInput{
		TaskDefinition: aws.String(taskDefinitionName),
	})
	if err != nil {
		return nil, err
	}

	var containerDefinitions []*ecs.ContainerDefinition
	var found bool
	var containerNames []string
	for _, def := range taskdefOutput.TaskDefinition.ContainerDefinitions {
		name := aws.StringValue(def.Name)
		containerNames = append(containerNames, name)
		if name == fmt.Sprint(params["container-name"]) || aws.StringValue(def.Image) == fmt.Sprint(params["container-name"]) {
			found = true
		} else {
			containerDefinitions = append(containerDefinitions, def)
		}
	}
	if !found {
		return nil, fmt.Errorf("did not find any container called '%s': found: '%s'", fmt.Sprint(params["container-name"]), strings.Join(containerNames, "','"))
	}

	if len(containerDefinitions) > 0 { //At least one container remaining
		taskDefinitionInput := &ecs.RegisterTaskDefinitionInput{
			ContainerDefinitions: containerDefinitions,
			Family:               taskdefOutput.TaskDefinition.Family,
			NetworkMode:          taskdefOutput.TaskDefinition.NetworkMode,
			PlacementConstraints: taskdefOutput.TaskDefinition.PlacementConstraints,
			TaskRoleArn:          taskdefOutput.TaskDefinition.TaskRoleArn,
			Volumes:              taskdefOutput.TaskDefinition.Volumes,
		}
		start := time.Now()

		if _, err := d.RegisterTaskDefinition(taskDefinitionInput); err != nil {
			return nil, fmt.Errorf("detach containertask: register task definition: %s", err)
		}
		d.logger.ExtraVerbosef("ecs.RegisterTaskDefinition call took %s", time.Since(start))

	} else {
		d.logger.Verbosef("no container remaining in service %s: deleting service", taskDefinitionName)
		taskDefinitionInput := &ecs.DeregisterTaskDefinitionInput{
			TaskDefinition: taskdefOutput.TaskDefinition.TaskDefinitionArn,
		}
		start := time.Now()

		if _, err := d.DeregisterTaskDefinition(taskDefinitionInput); err != nil {
			return nil, fmt.Errorf("detach containertask: deregister task definition: %s", err)
		}
		d.logger.ExtraVerbosef("ecs.DeregisterTaskDefinition call took %s", time.Since(start))
	}

	d.logger.Infof("detach containertask '%s' done", aws.StringValue(taskdefOutput.TaskDefinition.Family))
	return nil, nil
}

func (d *EcsDriver) Delete_Containertask_DryRun(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	if _, ok := params["name"]; !ok {
		return nil, errors.New("delete containertask: missing required params 'name'")
	}
	taskDefinitionName := fmt.Sprint(params["name"])

	taskDefOutput, err := d.ListTaskDefinitions(&ecs.ListTaskDefinitionsInput{
		FamilyPrefix: aws.String(taskDefinitionName),
	})
	if err != nil {
		return nil, fmt.Errorf("delete containertask dry run: %s", err)
	}
	switch len(taskDefOutput.TaskDefinitionArns) {
	case 0:
		return nil, fmt.Errorf("delete containertask dry run: no containertask found with name '%s'", taskDefinitionName)
	case 1:
		d.logger.Verbosef("only one version found for containertask '%s', will delete '%s'.", taskDefinitionName, aws.StringValue(taskDefOutput.TaskDefinitionArns[0]))
	default:
		if deleteAll := fmt.Sprint(params["all-versions"]); deleteAll == "true" {
			d.logger.Warningf("multiple versions found for containertask '%s'. Will delete '%s'", taskDefinitionName, strings.Join(aws.StringValueSlice(taskDefOutput.TaskDefinitionArns), "','"))
		} else {
			d.logger.Infof("multiple versions found for containertask '%s'", taskDefinitionName)
			d.logger.Warningf("will delete only latest version: '%s'. Add param `all-versions=true` to delete all versions", aws.StringValue(taskDefOutput.TaskDefinitionArns[len(taskDefOutput.TaskDefinitionArns)-1]))
		}
	}
	d.logger.Verbose("params dry run: delete containertask ok")
	return nil, nil
}

func (d *EcsDriver) Delete_Containertask(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	taskDefinitionName := fmt.Sprint(params["name"])

	if deleteAll := fmt.Sprint(params["all-versions"]); deleteAll == "true" {
		taskDefOutput, err := d.ListTaskDefinitions(&ecs.ListTaskDefinitionsInput{
			FamilyPrefix: aws.String(taskDefinitionName),
		})
		if err != nil {
			return nil, err
		}
		for _, task := range taskDefOutput.TaskDefinitionArns {
			d.logger.ExtraVerbosef("deleting '%s'", aws.StringValue(task))
			start := time.Now()
			if _, err := d.DeregisterTaskDefinition(&ecs.DeregisterTaskDefinitionInput{TaskDefinition: task}); err != nil {
				return nil, fmt.Errorf("delete containertask: deregister task definition: %s", err)
			}
			d.logger.ExtraVerbosef("ecs.DeregisterTaskDefinition call took %s", time.Since(start))
		}
	} else {
		taskDefOutput, err := d.DescribeTaskDefinition(&ecs.DescribeTaskDefinitionInput{
			TaskDefinition: aws.String(taskDefinitionName),
		})
		if err != nil {
			return nil, fmt.Errorf("delete containertask: %s", err)
		}
		d.logger.ExtraVerbosef("deleting '%s'", aws.StringValue(taskDefOutput.TaskDefinition.TaskDefinitionArn))
		start := time.Now()
		if _, err := d.DeregisterTaskDefinition(&ecs.DeregisterTaskDefinitionInput{TaskDefinition: taskDefOutput.TaskDefinition.TaskDefinitionArn}); err != nil {
			return nil, fmt.Errorf("delete containertask: deregister task definition: %s", err)
		}
		d.logger.ExtraVerbosef("ecs.DeregisterTaskDefinition call took %s", time.Since(start))
	}

	d.logger.Infof("delete containertask '%s' done", taskDefinitionName)
	return nil, nil
}

func (d *EcsDriver) Start_Containertask_DryRun(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	if _, ok := params["cluster"]; !ok {
		return nil, errors.New("start containertask: missing required params 'cluster'")
	}

	if _, ok := params["desired-count"]; !ok {
		return nil, errors.New("start containertask: missing required params 'desired-count'")
	}

	if _, ok := params["name"]; !ok {
		return nil, errors.New("start containertask: missing required params 'name'")
	}

	types := map[string]struct{}{
		"task":    {},
		"service": {},
	}

	if t, ok := params["type"].(string); !ok {
		return nil, errors.New("start containertask: missing required params 'type'")
	} else {
		if _, stok := types[t]; !stok {
			return nil, fmt.Errorf("start containertask: invalid type '%s'", t)
		}
		if t == "service" {
			if _, ok := params["deployment-name"]; !ok {
				return nil, errors.New("start containertask: missing required params for type=service: 'deployment-name'")
			}
		}
	}

	d.logger.Verbose("params dry run: start containertask ok")
	return fakeDryRunId("containertask"), nil
}

func (d *EcsDriver) Start_Containertask(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	typ := fmt.Sprint(params["type"])
	switch typ {
	case "service":
		setters := []setter{
			{val: params["cluster"], fieldPath: "Cluster", fieldType: awsstr},
			{val: params["name"], fieldPath: "TaskDefinition", fieldType: awsstr},
			{val: params["deployment-name"], fieldPath: "ServiceName", fieldType: awsstr},
			{val: params["desired-count"], fieldPath: "DesiredCount", fieldType: awsint64},
		}
		if _, ok := params["role"]; ok {
			setters = append(setters, setter{val: params["role"], fieldPath: "Role", fieldType: awsstr})
		}
		if _, ok := params["loadbalancer.container-name"]; ok {
			setters = append(setters, setter{val: params["loadbalancer.container-name"], fieldPath: "LoadBalancers[0]ContainerName", fieldType: awsslicestruct})
		}
		if _, ok := params["loadbalancer.container-port"]; ok {
			setters = append(setters, setter{val: params["loadbalancer.container-port"], fieldPath: "LoadBalancers[0]ContainerPort", fieldType: awsslicestructint64})
		}
		if _, ok := params["loadbalancer.targetgroup"]; ok {
			setters = append(setters, setter{val: params["loadbalancer.targetgroup"], fieldPath: "LoadBalancers[0]TargetGroupArn", fieldType: awsslicestruct})
		}

		call := &driverCall{
			d:       d,
			desc:    "start containertask",
			fn:      d.CreateService,
			logger:  d.logger,
			setters: setters,
		}

		output, err := call.execute(&ecs.CreateServiceInput{})
		if err != nil {
			return nil, err
		}

		return aws.StringValue(output.(*ecs.CreateServiceOutput).Service.ServiceArn), nil
	case "task":
		call := &driverCall{
			d:      d,
			desc:   "start containertask",
			fn:     d.RunTask,
			logger: d.logger,
			setters: []setter{
				{val: params["cluster"], fieldPath: "Cluster", fieldType: awsstr},
				{val: params["name"], fieldPath: "TaskDefinition", fieldType: awsstr},
				{val: params["desired-count"], fieldPath: "Count", fieldType: awsint64},
			},
		}

		output, err := call.execute(&ecs.RunTaskInput{})
		if err != nil {
			return nil, err
		}
		if len(output.(*ecs.RunTaskOutput).Failures) > 0 {
			return nil, fmt.Errorf("start containertask: fail to run task: %s", aws.StringValue(output.(*ecs.RunTaskOutput).Failures[0].Reason))
		}

		if len(output.(*ecs.RunTaskOutput).Tasks) > 0 {
			return aws.StringValue(output.(*ecs.RunTaskOutput).Tasks[0].TaskArn), nil
		}
		return nil, fmt.Errorf("no task started successfully")
	}
	return nil, fmt.Errorf("start containertask: invalid type '%s'", typ)
}

func (d *EcsDriver) Stop_Containertask_DryRun(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	if _, ok := params["cluster"]; !ok {
		return nil, errors.New("stop containertask: missing required params 'cluster'")
	}

	types := map[string]struct{}{
		"task":    {},
		"service": {},
	}

	if t, ok := params["type"].(string); !ok {
		return nil, errors.New("stop containertask: missing required params 'type'")
	} else {
		if _, stok := types[t]; !stok {
			return nil, fmt.Errorf("stop containertask: invalid type '%s'", t)
		}
		if t == "service" {
			if _, ok := params["deployment-name"]; !ok {
				return nil, errors.New("stop containertask: missing required params for type=service: 'deployment-name'")
			}
		}
		if t == "task" {
			if _, ok := params["run-arn"]; !ok {
				return nil, errors.New("stop containertask: missing required params for type=task: 'run-arn'")
			}
		}
	}

	d.logger.Verbose("params dry run: stop containertask ok")
	return nil, nil
}

func (d *EcsDriver) Stop_Containertask(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	typ := fmt.Sprint(params["type"])
	switch typ {
	case "service":
		call := &driverCall{
			d:      d,
			desc:   "stop containertask",
			fn:     d.DeleteService,
			logger: d.logger,
			setters: []setter{
				{val: params["cluster"], fieldPath: "Cluster", fieldType: awsstr},
				{val: params["deployment-name"], fieldPath: "Service", fieldType: awsstr},
			},
		}
		_, err := call.execute(&ecs.DeleteServiceInput{})
		return nil, err
	case "task":
		call := &driverCall{
			d:      d,
			desc:   "stop containertask",
			fn:     d.StopTask,
			logger: d.logger,
			setters: []setter{
				{val: params["cluster"], fieldPath: "Cluster", fieldType: awsstr},
				{val: params["run-arn"], fieldPath: "Task", fieldType: awsstr},
			},
		}

		_, err := call.execute(&ecs.StopTaskInput{})
		return nil, err
	}
	return nil, fmt.Errorf("stop containertask: invalid type '%s'", typ)
}

func (d *IamDriver) Create_Policy_DryRun(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	_, effect := params["effect"]
	_, action := params["action"]
	_, resource := params["resource"]

	if !effect || !action || !resource {
		return nil, errors.New("create policy: effect, action and resource are required values")
	}

	d.logger.Verbose("params dry run: create policy ok")
	return fakeDryRunId("policy"), nil
}

func (d *IamDriver) Create_Policy(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	effect, _ := params["effect"].(string)
	resource, _ := params["resource"].(string)

	if resource == "all" {
		resource = "*"
	}

	stat := policyStatement{Effect: strings.Title(effect), Resource: resource}

	if actions, ok := params["action"]; ok {
		stat.Actions = castStringSlice(actions)
	}

	policy := &policyBody{
		Version:   "2012-10-17",
		Statement: []policyStatement{stat},
	}

	b, err := json.MarshalIndent(policy, "", " ")
	if err != nil {
		return nil, errors.New("cannot marshal policy document")
	}

	d.logger.ExtraVerbosef("policy document json:\n%s\n", string(b))

	call := &driverCall{
		d:      d,
		desc:   "create policy",
		fn:     d.CreatePolicy,
		logger: d.logger,
		setters: []setter{
			{val: params["name"], fieldPath: "PolicyName", fieldType: awsstr},
			{val: params["description"], fieldPath: "Description", fieldType: awsstr},
			{val: string(b), fieldPath: "PolicyDocument", fieldType: awsstr},
		},
	}

	output, err := call.execute(&iam.CreatePolicyInput{})
	if err != nil {
		return nil, err
	}

	return aws.StringValue(output.(*iam.CreatePolicyOutput).Policy.Arn), nil
}

func (d *IamDriver) Delete_Role_DryRun(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	if _, ok := params["name"]; !ok {
		return nil, errors.New("delete role: missing required params 'name'")
	}

	d.logger.Verbose("params dry run: delete role ok")
	return nil, nil
}

func (d *IamDriver) Delete_Role(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	d.Detach_Role(ctx, map[string]interface{}{
		"name":            params["name"],
		"instanceprofile": params["name"],
	})
	d.Delete_Instanceprofile(ctx, params)

	input := &iam.DeleteRoleInput{}

	if err := setFieldWithType(params["name"], input, "RoleName", awsstr); err != nil {
		return nil, err
	}

	start := time.Now()
	output, err := d.DeleteRole(input)
	if err != nil {
		return nil, fmt.Errorf("delete role: %s", err)
	}
	d.logger.ExtraVerbosef("iam.DeleteRole call took %s", time.Since(start))
	d.logger.Info("delete role done")

	return output, nil
}

func (d *IamDriver) Create_Role_DryRun(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	_, pAccount := params["principal-account"]
	_, pService := params["principal-service"]
	_, pUser := params["principal-user"]

	if !pAccount && !pService && !pUser {
		return nil, errors.New("create role: missing principal (either a user, service or account)")
	}

	d.logger.Verbose("params dry run: create role ok")
	return nil, nil
}

type principal struct {
	AWS     interface{} `json:",omitempty"`
	Service interface{} `json:",omitempty"`
}

type policyStatement struct {
	Effect    string     `json:",omitempty"`
	Actions   []string   `json:"Action,omitempty"`
	Resource  string     `json:",omitempty"`
	Principal *principal `json:",omitempty"`
}

type policyBody struct {
	Version   string
	Statement []policyStatement
}

func (d *IamDriver) Create_Role(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	pAccount, _ := params["principal-account"]
	pService, _ := params["principal-service"]
	pUser, _ := params["principal-user"]
<commit_msg>Fix: dryrun of created role injected fail as the fake return id is nil.<commit_after>/*
Copyright 2017 WALLIX

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package awsdriver

import (
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"math/rand"
	"mime"
	"os"
	"os/exec"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/awserr"
	"github.com/aws/aws-sdk-go/aws/client"
	"github.com/aws/aws-sdk-go/aws/request"
	"github.com/aws/aws-sdk-go/service/autoscaling"
	"github.com/aws/aws-sdk-go/service/cloudfront"
	"github.com/aws/aws-sdk-go/service/cloudwatch"
	"github.com/aws/aws-sdk-go/service/ec2"
	"github.com/aws/aws-sdk-go/service/ecr"
	"github.com/aws/aws-sdk-go/service/ecs"
	"github.com/aws/aws-sdk-go/service/elbv2"
	"github.com/aws/aws-sdk-go/service/iam"
	"github.com/aws/aws-sdk-go/service/rds"
	"github.com/aws/aws-sdk-go/service/route53"
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/mitchellh/ioprogress"
	"github.com/wallix/awless/cloud"
	"github.com/wallix/awless/console"
	"github.com/wallix/awless/logger"
	"github.com/wallix/awless/template/driver"
)

const (
	notFoundState = "not-found"
)

func (d *Ec2Driver) Attach_Securitygroup_DryRun(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	if _, ok := params["id"]; !ok {
		return nil, errors.New("attach securitygroup: missing required params 'id'")
	}

	_, hasInstance := params["instance"]

	if !hasInstance {
		return nil, errors.New("attach securitygroup: missing 'instance' param")
	}

	d.logger.Verbose("params dry run: attach securitygroup ok")
	return nil, nil
}

func (d *Ec2Driver) Attach_Securitygroup(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	instance, hasInstance := params["instance"].(string)

	switch {
	case hasInstance:
		groups, err := d.fetchInstanceSecurityGroups(instance)
		if err != nil {
			return nil, fmt.Errorf("fetching securitygroups for instance %s: %s", instance, err)
		}

		groups = append(groups, fmt.Sprint(params["id"]))
		if len(groups) == 0 {
			d.logger.Errorf("AWS instances must have at least one securitygroup")
		}
		call := &driverCall{
			d:      d,
			fn:     d.ModifyInstanceAttribute,
			logger: d.logger,
			setters: []setter{
				{val: instance, fieldPath: "InstanceID", fieldType: awsstr},
				{val: groups, fieldPath: "Groups", fieldType: awsstringslice},
			},
			desc: "attach securitygroup",
		}
		return call.execute(&ec2.ModifyInstanceAttributeInput{})
	}

	return nil, errors.New("missing 'instance' param")
}

func (d *Ec2Driver) Detach_Securitygroup_DryRun(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	if _, ok := params["id"]; !ok {
		return nil, errors.New("detach securitygroup: missing required params 'id'")
	}

	_, hasInstance := params["instance"]

	if !hasInstance {
		return nil, errors.New("detach securitygroup: missing 'instance' param")
	}

	d.logger.Verbose("params dry run: detach securitygroup ok")
	return nil, nil
}

func (d *Ec2Driver) Detach_Securitygroup(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	instance, hasInstance := params["instance"].(string)

	switch {
	case hasInstance:
		groups, err := d.fetchInstanceSecurityGroups(instance)
		if err != nil {
			return nil, fmt.Errorf("fetching securitygroups for instance %s: %s", instance, err)
		}

		cleaned := removeString(groups, fmt.Sprint(params["id"]))

		if len(cleaned) == 0 {
			d.logger.Errorf("AWS instances must have at least one securitygroup")
		}
		call := &driverCall{
			d:      d,
			fn:     d.ModifyInstanceAttribute,
			logger: d.logger,
			setters: []setter{
				{val: instance, fieldPath: "InstanceID", fieldType: awsstr},
				{val: cleaned, fieldPath: "Groups", fieldType: awsstringslice},
			},
			desc: "detach securitygroup",
		}
		return call.execute(&ec2.ModifyInstanceAttributeInput{})
	}

	return nil, errors.New("missing 'instance' param")
}

func (d *Ec2Driver) fetchInstanceSecurityGroups(id string) ([]string, error) {
	params := &ec2.DescribeInstanceAttributeInput{
		Attribute:  aws.String("groupSet"),
		InstanceId: aws.String(id),
	}
	resp, err := d.DescribeInstanceAttribute(params)
	if err != nil {
		return nil, err
	}

	var groups []string
	for _, g := range resp.Groups {
		groups = append(groups, aws.StringValue(g.GroupId))
	}

	return groups, nil
}

func removeString(arr []string, s string) (out []string) {
	for _, e := range arr {
		if e != s {
			out = append(out, e)
		}
	}
	return
}

func (d *Ec2Driver) Attach_Instanceprofile_DryRun(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	instanceId, ok := params["instance"].(string)
	if !ok {
		return nil, errors.New("attach instanceprofile: missing required params 'instance'")
	}

	if _, ok := params["name"]; !ok {
		return nil, errors.New("attach instanceprofile: missing required params 'name'")
	}

	replace, _ := strconv.ParseBool(fmt.Sprint(params["replace"]))
	if replace {
		in := &ec2.DescribeIamInstanceProfileAssociationsInput{
			Filters: []*ec2.Filter{
				{Name: aws.String("instance-id"), Values: []*string{aws.String(instanceId)}},
			},
		}
		out, err := d.DescribeIamInstanceProfileAssociations(in)
		if err != nil {
			return nil, fmt.Errorf("dry run: replace mode on: cannot get : %s", err)
		}
		if assocs := out.IamInstanceProfileAssociations; len(assocs) > 0 {
			for _, ass := range assocs {
				d.logger.ExtraVerbosef("dry run: attach instanceprofile: existing instanceprofile %s (state: %s) on instance %s", aws.StringValue(ass.IamInstanceProfile.Id), aws.StringValue(ass.State), instanceId)

			}
		}
	}

	d.logger.Verbose("params dry run: attach instanceprofile ok")
	return fakeDryRunId("instanceprofile"), nil
}

func (d *Ec2Driver) Attach_Instanceprofile(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	instanceId, _ := params["instance"].(string)
	profileName, _ := params["name"].(string)

	replace, _ := strconv.ParseBool(fmt.Sprint(params["replace"]))
	if replace {
		out, err := d.DescribeIamInstanceProfileAssociations(
			&ec2.DescribeIamInstanceProfileAssociationsInput{
				Filters: []*ec2.Filter{
					{Name: aws.String("instance-id"), Values: []*string{aws.String(instanceId)}},
					{Name: aws.String("state"), Values: []*string{aws.String("associated")}},
				},
			})
		if err != nil {
			logger.Warningf("attach instanceprofile: replace mode on: dry run was ok but now cannot get instance profile association")
		}
		assoc := out.IamInstanceProfileAssociations
		if len(assoc) > 0 {
			start := time.Now()
			assocId := aws.StringValue(assoc[0].AssociationId)
			assocInstId := aws.StringValue(assoc[0].InstanceId)
			oldProfileArn := aws.StringValue(assoc[0].IamInstanceProfile.Arn)
			d.logger.ExtraVerbosef("attach profile: found existing profile to replace with %s", profileName)
			if assocInstId == instanceId {
				out, err := d.ReplaceIamInstanceProfileAssociation(
					&ec2.ReplaceIamInstanceProfileAssociationInput{
						AssociationId: aws.String(assocId),
						IamInstanceProfile: &ec2.IamInstanceProfileSpecification{
							Name: aws.String(profileName),
						},
					})
				if err != nil {
					return nil, fmt.Errorf("attach instanceprofile: replace mode on: cannot replace with new instance profile: %s", err)
				}

				d.logger.Verbosef("attach profile: replaced profile '%s' with '%s' on instance %s", oldProfileArn, profileName, instanceId)
				d.logger.ExtraVerbosef("ec2.ReplaceIamInstanceProfileAssociation call took %s", time.Since(start))

				return out, nil
			}
		}
	}

	input := &ec2.AssociateIamInstanceProfileInput{}
	if err := setFieldWithType(instanceId, input, "InstanceId", awsstr, ctx); err != nil {
		return nil, err
	}
	if err := setFieldWithType(profileName, input, "IamInstanceProfile.Name", awsstr, ctx); err != nil {
		return nil, err
	}

	start := time.Now()
	output, err := d.AssociateIamInstanceProfile(input)
	if err != nil {
		return nil, fmt.Errorf("attach instanceprofile: %s", err)
	}
	d.logger.ExtraVerbosef("ec2.AssociateIamInstanceProfile call took %s", time.Since(start))
	d.logger.Info("attach instanceprofile done")
	return output, nil
}

func (d *Ec2Driver) Detach_Instanceprofile_DryRun(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	if _, ok := params["instance"]; !ok {
		return nil, errors.New("detach instanceprofile: missing required params 'instance'")
	}

	if _, ok := params["name"]; !ok {
		return nil, errors.New("detach instanceprofile: missing required params 'name'")
	}

	d.logger.Verbose("params dry run: detach instanceprofile ok")
	return fakeDryRunId("instanceprofile"), nil
}

func (d *Ec2Driver) Detach_Instanceprofile(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	instanceId, _ := params["instance"].(string)
	profileName, _ := params["name"].(string)

	out, err := d.DescribeIamInstanceProfileAssociations(
		&ec2.DescribeIamInstanceProfileAssociationsInput{
			Filters: []*ec2.Filter{
				{Name: aws.String("instance-id"), Values: []*string{aws.String(instanceId)}},
			},
		})
	if err != nil {
		return nil, fmt.Errorf("detach instance profile: cannot list profile on instance %s: %s", instanceId, err)
	}

	assocs := out.IamInstanceProfileAssociations
	if len(assocs) < 1 {
		d.logger.Infof("detach instanceprofile: nothing to be detached on instance %s", instanceId)
		return nil, nil
	}

	var lastId string
	for _, ass := range assocs {
		if strings.Contains(aws.StringValue(ass.IamInstanceProfile.Arn), profileName) {
			input := &ec2.DisassociateIamInstanceProfileInput{
				AssociationId: ass.AssociationId,
			}

			start := time.Now()
			output, err := d.DisassociateIamInstanceProfile(input)
			if err != nil {
				return nil, fmt.Errorf("detach instanceprofile: %s", err)
			}
			d.logger.ExtraVerbosef("ec2.DisassociateIamInstanceProfile call took %s", time.Since(start))
			id := aws.StringValue(output.IamInstanceProfileAssociation.IamInstanceProfile.Id)
			d.logger.Infof("detach instanceprofile '%s' done", id)
			lastId = id
		}
	}

	return lastId, nil
}

func (d *EcsDriver) Attach_Containertask_DryRun(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	if _, ok := params["name"]; !ok {
		return nil, errors.New("attach containertask: missing required params 'name'")
	}
	if _, ok := params["container-name"]; !ok {
		return nil, errors.New("attach containertask: missing required params 'container-name'")
	}
	if _, ok := params["image"]; !ok {
		return nil, errors.New("attach containertask: missing required params 'image'")
	}
	if _, ok := params["memory-hard-limit"]; !ok {
		return nil, errors.New("attach containertask: missing required params 'memory-hard-limit'")
	}
	d.logger.Verbose("params dry run: attach containertask ok")
	return nil, nil
}

func (d *EcsDriver) Attach_Containertask(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	var taskDefinitionInput *ecs.RegisterTaskDefinitionInput
	taskDefinitionName := fmt.Sprint(params["name"])

	taskdefOutput, err := d.DescribeTaskDefinition(&ecs.DescribeTaskDefinitionInput{
		TaskDefinition: aws.String(taskDefinitionName),
	})
	if awserr, ok := err.(awserr.Error); err != nil && ok {
		if awserr.Code() == "ClientException" && strings.Contains(strings.ToLower(awserr.Message()), "unable to describe task definition") {
			d.logger.Verbosef("service %s does not exist: creating service", taskDefinitionName)
			taskDefinitionInput = &ecs.RegisterTaskDefinitionInput{
				Family: aws.String(taskDefinitionName),
			}
		} else {
			return nil, err
		}
	} else if err != nil {
		return nil, err
	} else {
		taskDefinitionInput = &ecs.RegisterTaskDefinitionInput{
			ContainerDefinitions: taskdefOutput.TaskDefinition.ContainerDefinitions,
			Family:               taskdefOutput.TaskDefinition.Family,
			NetworkMode:          taskdefOutput.TaskDefinition.NetworkMode,
			PlacementConstraints: taskdefOutput.TaskDefinition.PlacementConstraints,
			TaskRoleArn:          taskdefOutput.TaskDefinition.TaskRoleArn,
			Volumes:              taskdefOutput.TaskDefinition.Volumes,
		}
	}

	container := &ecs.ContainerDefinition{}
	if err = setFieldWithType(params["container-name"], container, "Name", awsstr); err != nil {
		return nil, err
	}
	if err = setFieldWithType(params["image"], container, "Image", awsstr); err != nil {
		return nil, err
	}
	if err = setFieldWithType(params["memory-hard-limit"], container, "Memory", awsint64); err != nil {
		return nil, err
	}
	if command, ok := params["command"]; ok {
		switch cc := command.(type) {
		case string:
			if err = setFieldWithType(strings.Split(cc, " "), container, "Command", awsstringslice); err != nil {
				return nil, err
			}
		default:
			if err = setFieldWithType(cc, container, "Command", awsstringslice); err != nil {
				return nil, err
			}
		}
	}
	if env, ok := params["env"]; ok {
		if err = setFieldWithType(env, container, "Environment", awsecskeyvalue); err != nil {
			return nil, err
		}
	}
	if priv, ok := params["privileged"]; ok && fmt.Sprint(priv) == "true" {
		if err = setFieldWithType(true, container, "Privileged", awsbool); err != nil {
			return nil, err
		}
	}
	if workdir, ok := params["workdir"]; ok {
		if err = setFieldWithType(workdir, container, "WorkingDirectory", awsstr); err != nil {
			return nil, err
		}
	}
	if ports, ok := params["ports"]; ok {
		if err = setFieldWithType(ports, container, "PortMappings", awsportmappings); err != nil {
			return nil, err
		}
	}

	taskDefinitionInput.ContainerDefinitions = append(taskDefinitionInput.ContainerDefinitions, container)

	start := time.Now()

	taskDefOutput, err := d.RegisterTaskDefinition(taskDefinitionInput)
	if err != nil {
		return nil, fmt.Errorf("attach containertask: register task definition: %s", err)
	}
	d.logger.ExtraVerbosef("ecs.RegisterTaskDefinitionOutput call took %s", time.Since(start))
	d.logger.ExtraVerbosef("attach containertask: register task definition '%s' done", aws.StringValue(taskDefOutput.TaskDefinition.Family))

	d.logger.Infof("attach container '%s' done", aws.StringValue(taskDefOutput.TaskDefinition.TaskDefinitionArn))
	return nil, nil
}

func (d *EcsDriver) Detach_Containertask_DryRun(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	if _, ok := params["name"]; !ok {
		return nil, errors.New("detach containertask: missing required params 'name'")
	}
	if _, ok := params["container-name"]; !ok {
		return nil, errors.New("detach containertask: missing required params 'container-name'")
	}
	d.logger.Verbose("params dry run: detach containertask ok")
	return nil, nil
}

func (d *EcsDriver) Detach_Containertask(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	taskDefinitionName := fmt.Sprint(params["name"])

	taskdefOutput, err := d.DescribeTaskDefinition(&ecs.DescribeTaskDefinitionInput{
		TaskDefinition: aws.String(taskDefinitionName),
	})
	if err != nil {
		return nil, err
	}

	var containerDefinitions []*ecs.ContainerDefinition
	var found bool
	var containerNames []string
	for _, def := range taskdefOutput.TaskDefinition.ContainerDefinitions {
		name := aws.StringValue(def.Name)
		containerNames = append(containerNames, name)
		if name == fmt.Sprint(params["container-name"]) || aws.StringValue(def.Image) == fmt.Sprint(params["container-name"]) {
			found = true
		} else {
			containerDefinitions = append(containerDefinitions, def)
		}
	}
	if !found {
		return nil, fmt.Errorf("did not find any container called '%s': found: '%s'", fmt.Sprint(params["container-name"]), strings.Join(containerNames, "','"))
	}

	if len(containerDefinitions) > 0 { //At least one container remaining
		taskDefinitionInput := &ecs.RegisterTaskDefinitionInput{
			ContainerDefinitions: containerDefinitions,
			Family:               taskdefOutput.TaskDefinition.Family,
			NetworkMode:          taskdefOutput.TaskDefinition.NetworkMode,
			PlacementConstraints: taskdefOutput.TaskDefinition.PlacementConstraints,
			TaskRoleArn:          taskdefOutput.TaskDefinition.TaskRoleArn,
			Volumes:              taskdefOutput.TaskDefinition.Volumes,
		}
		start := time.Now()

		if _, err := d.RegisterTaskDefinition(taskDefinitionInput); err != nil {
			return nil, fmt.Errorf("detach containertask: register task definition: %s", err)
		}
		d.logger.ExtraVerbosef("ecs.RegisterTaskDefinition call took %s", time.Since(start))

	} else {
		d.logger.Verbosef("no container remaining in service %s: deleting service", taskDefinitionName)
		taskDefinitionInput := &ecs.DeregisterTaskDefinitionInput{
			TaskDefinition: taskdefOutput.TaskDefinition.TaskDefinitionArn,
		}
		start := time.Now()

		if _, err := d.DeregisterTaskDefinition(taskDefinitionInput); err != nil {
			return nil, fmt.Errorf("detach containertask: deregister task definition: %s", err)
		}
		d.logger.ExtraVerbosef("ecs.DeregisterTaskDefinition call took %s", time.Since(start))
	}

	d.logger.Infof("detach containertask '%s' done", aws.StringValue(taskdefOutput.TaskDefinition.Family))
	return nil, nil
}

func (d *EcsDriver) Delete_Containertask_DryRun(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	if _, ok := params["name"]; !ok {
		return nil, errors.New("delete containertask: missing required params 'name'")
	}
	taskDefinitionName := fmt.Sprint(params["name"])

	taskDefOutput, err := d.ListTaskDefinitions(&ecs.ListTaskDefinitionsInput{
		FamilyPrefix: aws.String(taskDefinitionName),
	})
	if err != nil {
		return nil, fmt.Errorf("delete containertask dry run: %s", err)
	}
	switch len(taskDefOutput.TaskDefinitionArns) {
	case 0:
		return nil, fmt.Errorf("delete containertask dry run: no containertask found with name '%s'", taskDefinitionName)
	case 1:
		d.logger.Verbosef("only one version found for containertask '%s', will delete '%s'.", taskDefinitionName, aws.StringValue(taskDefOutput.TaskDefinitionArns[0]))
	default:
		if deleteAll := fmt.Sprint(params["all-versions"]); deleteAll == "true" {
			d.logger.Warningf("multiple versions found for containertask '%s'. Will delete '%s'", taskDefinitionName, strings.Join(aws.StringValueSlice(taskDefOutput.TaskDefinitionArns), "','"))
		} else {
			d.logger.Infof("multiple versions found for containertask '%s'", taskDefinitionName)
			d.logger.Warningf("will delete only latest version: '%s'. Add param `all-versions=true` to delete all versions", aws.StringValue(taskDefOutput.TaskDefinitionArns[len(taskDefOutput.TaskDefinitionArns)-1]))
		}
	}
	d.logger.Verbose("params dry run: delete containertask ok")
	return nil, nil
}

func (d *EcsDriver) Delete_Containertask(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	taskDefinitionName := fmt.Sprint(params["name"])

	if deleteAll := fmt.Sprint(params["all-versions"]); deleteAll == "true" {
		taskDefOutput, err := d.ListTaskDefinitions(&ecs.ListTaskDefinitionsInput{
			FamilyPrefix: aws.String(taskDefinitionName),
		})
		if err != nil {
			return nil, err
		}
		for _, task := range taskDefOutput.TaskDefinitionArns {
			d.logger.ExtraVerbosef("deleting '%s'", aws.StringValue(task))
			start := time.Now()
			if _, err := d.DeregisterTaskDefinition(&ecs.DeregisterTaskDefinitionInput{TaskDefinition: task}); err != nil {
				return nil, fmt.Errorf("delete containertask: deregister task definition: %s", err)
			}
			d.logger.ExtraVerbosef("ecs.DeregisterTaskDefinition call took %s", time.Since(start))
		}
	} else {
		taskDefOutput, err := d.DescribeTaskDefinition(&ecs.DescribeTaskDefinitionInput{
			TaskDefinition: aws.String(taskDefinitionName),
		})
		if err != nil {
			return nil, fmt.Errorf("delete containertask: %s", err)
		}
		d.logger.ExtraVerbosef("deleting '%s'", aws.StringValue(taskDefOutput.TaskDefinition.TaskDefinitionArn))
		start := time.Now()
		if _, err := d.DeregisterTaskDefinition(&ecs.DeregisterTaskDefinitionInput{TaskDefinition: taskDefOutput.TaskDefinition.TaskDefinitionArn}); err != nil {
			return nil, fmt.Errorf("delete containertask: deregister task definition: %s", err)
		}
		d.logger.ExtraVerbosef("ecs.DeregisterTaskDefinition call took %s", time.Since(start))
	}

	d.logger.Infof("delete containertask '%s' done", taskDefinitionName)
	return nil, nil
}

func (d *EcsDriver) Start_Containertask_DryRun(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	if _, ok := params["cluster"]; !ok {
		return nil, errors.New("start containertask: missing required params 'cluster'")
	}

	if _, ok := params["desired-count"]; !ok {
		return nil, errors.New("start containertask: missing required params 'desired-count'")
	}

	if _, ok := params["name"]; !ok {
		return nil, errors.New("start containertask: missing required params 'name'")
	}

	types := map[string]struct{}{
		"task":    {},
		"service": {},
	}

	if t, ok := params["type"].(string); !ok {
		return nil, errors.New("start containertask: missing required params 'type'")
	} else {
		if _, stok := types[t]; !stok {
			return nil, fmt.Errorf("start containertask: invalid type '%s'", t)
		}
		if t == "service" {
			if _, ok := params["deployment-name"]; !ok {
				return nil, errors.New("start containertask: missing required params for type=service: 'deployment-name'")
			}
		}
	}

	d.logger.Verbose("params dry run: start containertask ok")
	return fakeDryRunId("containertask"), nil
}

func (d *EcsDriver) Start_Containertask(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	typ := fmt.Sprint(params["type"])
	switch typ {
	case "service":
		setters := []setter{
			{val: params["cluster"], fieldPath: "Cluster", fieldType: awsstr},
			{val: params["name"], fieldPath: "TaskDefinition", fieldType: awsstr},
			{val: params["deployment-name"], fieldPath: "ServiceName", fieldType: awsstr},
			{val: params["desired-count"], fieldPath: "DesiredCount", fieldType: awsint64},
		}
		if _, ok := params["role"]; ok {
			setters = append(setters, setter{val: params["role"], fieldPath: "Role", fieldType: awsstr})
		}
		if _, ok := params["loadbalancer.container-name"]; ok {
			setters = append(setters, setter{val: params["loadbalancer.container-name"], fieldPath: "LoadBalancers[0]ContainerName", fieldType: awsslicestruct})
		}
		if _, ok := params["loadbalancer.container-port"]; ok {
			setters = append(setters, setter{val: params["loadbalancer.container-port"], fieldPath: "LoadBalancers[0]ContainerPort", fieldType: awsslicestructint64})
		}
		if _, ok := params["loadbalancer.targetgroup"]; ok {
			setters = append(setters, setter{val: params["loadbalancer.targetgroup"], fieldPath: "LoadBalancers[0]TargetGroupArn", fieldType: awsslicestruct})
		}

		call := &driverCall{
			d:       d,
			desc:    "start containertask",
			fn:      d.CreateService,
			logger:  d.logger,
			setters: setters,
		}

		output, err := call.execute(&ecs.CreateServiceInput{})
		if err != nil {
			return nil, err
		}

		return aws.StringValue(output.(*ecs.CreateServiceOutput).Service.ServiceArn), nil
	case "task":
		call := &driverCall{
			d:      d,
			desc:   "start containertask",
			fn:     d.RunTask,
			logger: d.logger,
			setters: []setter{
				{val: params["cluster"], fieldPath: "Cluster", fieldType: awsstr},
				{val: params["name"], fieldPath: "TaskDefinition", fieldType: awsstr},
				{val: params["desired-count"], fieldPath: "Count", fieldType: awsint64},
			},
		}

		output, err := call.execute(&ecs.RunTaskInput{})
		if err != nil {
			return nil, err
		}
		if len(output.(*ecs.RunTaskOutput).Failures) > 0 {
			return nil, fmt.Errorf("start containertask: fail to run task: %s", aws.StringValue(output.(*ecs.RunTaskOutput).Failures[0].Reason))
		}

		if len(output.(*ecs.RunTaskOutput).Tasks) > 0 {
			return aws.StringValue(output.(*ecs.RunTaskOutput).Tasks[0].TaskArn), nil
		}
		return nil, fmt.Errorf("no task started successfully")
	}
	return nil, fmt.Errorf("start containertask: invalid type '%s'", typ)
}

func (d *EcsDriver) Stop_Containertask_DryRun(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	if _, ok := params["cluster"]; !ok {
		return nil, errors.New("stop containertask: missing required params 'cluster'")
	}

	types := map[string]struct{}{
		"task":    {},
		"service": {},
	}

	if t, ok := params["type"].(string); !ok {
		return nil, errors.New("stop containertask: missing required params 'type'")
	} else {
		if _, stok := types[t]; !stok {
			return nil, fmt.Errorf("stop containertask: invalid type '%s'", t)
		}
		if t == "service" {
			if _, ok := params["deployment-name"]; !ok {
				return nil, errors.New("stop containertask: missing required params for type=service: 'deployment-name'")
			}
		}
		if t == "task" {
			if _, ok := params["run-arn"]; !ok {
				return nil, errors.New("stop containertask: missing required params for type=task: 'run-arn'")
			}
		}
	}

	d.logger.Verbose("params dry run: stop containertask ok")
	return nil, nil
}

func (d *EcsDriver) Stop_Containertask(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	typ := fmt.Sprint(params["type"])
	switch typ {
	case "service":
		call := &driverCall{
			d:      d,
			desc:   "stop containertask",
			fn:     d.DeleteService,
			logger: d.logger,
			setters: []setter{
				{val: params["cluster"], fieldPath: "Cluster", fieldType: awsstr},
				{val: params["deployment-name"], fieldPath: "Service", fieldType: awsstr},
			},
		}
		_, err := call.execute(&ecs.DeleteServiceInput{})
		return nil, err
	case "task":
		call := &driverCall{
			d:      d,
			desc:   "stop containertask",
			fn:     d.StopTask,
			logger: d.logger,
			setters: []setter{
				{val: params["cluster"], fieldPath: "Cluster", fieldType: awsstr},
				{val: params["run-arn"], fieldPath: "Task", fieldType: awsstr},
			},
		}

		_, err := call.execute(&ecs.StopTaskInput{})
		return nil, err
	}
	return nil, fmt.Errorf("stop containertask: invalid type '%s'", typ)
}

func (d *IamDriver) Create_Policy_DryRun(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	_, effect := params["effect"]
	_, action := params["action"]
	_, resource := params["resource"]

	if !effect || !action || !resource {
		return nil, errors.New("create policy: effect, action and resource are required values")
	}

	d.logger.Verbose("params dry run: create policy ok")
	return fakeDryRunId("policy"), nil
}

func (d *IamDriver) Create_Policy(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	effect, _ := params["effect"].(string)
	resource, _ := params["resource"].(string)

	if resource == "all" {
		resource = "*"
	}

	stat := policyStatement{Effect: strings.Title(effect), Resource: resource}

	if actions, ok := params["action"]; ok {
		stat.Actions = castStringSlice(actions)
	}

	policy := &policyBody{
		Version:   "2012-10-17",
		Statement: []policyStatement{stat},
	}

	b, err := json.MarshalIndent(policy, "", " ")
	if err != nil {
		return nil, errors.New("cannot marshal policy document")
	}

	d.logger.ExtraVerbosef("policy document json:\n%s\n", string(b))

	call := &driverCall{
		d:      d,
		desc:   "create policy",
		fn:     d.CreatePolicy,
		logger: d.logger,
		setters: []setter{
			{val: params["name"], fieldPath: "PolicyName", fieldType: awsstr},
			{val: params["description"], fieldPath: "Description", fieldType: awsstr},
			{val: string(b), fieldPath: "PolicyDocument", fieldType: awsstr},
		},
	}

	output, err := call.execute(&iam.CreatePolicyInput{})
	if err != nil {
		return nil, err
	}

	return aws.StringValue(output.(*iam.CreatePolicyOutput).Policy.Arn), nil
}

func (d *IamDriver) Delete_Role_DryRun(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	if _, ok := params["name"]; !ok {
		return nil, errors.New("delete role: missing required params 'name'")
	}

	d.logger.Verbose("params dry run: delete role ok")
	return nil, nil
}

func (d *IamDriver) Delete_Role(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	d.Detach_Role(ctx, map[string]interface{}{
		"name":            params["name"],
		"instanceprofile": params["name"],
	})
	d.Delete_Instanceprofile(ctx, params)

	input := &iam.DeleteRoleInput{}

	if err := setFieldWithType(params["name"], input, "RoleName", awsstr); err != nil {
		return nil, err
	}

	start := time.Now()
	output, err := d.DeleteRole(input)
	if err != nil {
		return nil, fmt.Errorf("delete role: %s", err)
	}
	d.logger.ExtraVerbosef("iam.DeleteRole call took %s", time.Since(start))
	d.logger.Info("delete role done")

	return output, nil
}

func (d *IamDriver) Create_Role_DryRun(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	_, pAccount := params["principal-account"]
	_, pService := params["principal-service"]
	_, pUser := params["principal-user"]

	if !pAccount && !pService && !pUser {
		return nil, errors.New("create role: missing principal (either a user, service or account)")
	}

	d.logger.Verbose("params dry run: create role ok")
	return fakeDryRunId(cloud.Role), nil
}

type principal struct {
	AWS     interface{} `json:",omitempty"`
	Service interface{} `json:",omitempty"`
}

type policyStatement struct {
	Effect    string     `json:",omitempty"`
	Actions   []string   `json:"Action,omitempty"`
	Resource  string     `json:",omitempty"`
	Principal *principal `json:",omitempty"`
}

type policyBody struct {
	Version   string
	Statement []policyStatement
}

func (d *IamDriver) Create_Role(ctx driver.Context, params map[string]interface{}) (interface{}, error) {
	pAccount, _ := params["principal-account"]
	pService, _ := params["principal-service"]
	pUser, _ := params["principal-user"]

{'fraction_non_alphanumeric': 0.14570274179871306, 'fraction_numerical': 0.0025273099113778826, 'mean_word_length': 4.770869314910765, 'pattern_counts': {'":': 20, '<': 5, '<?xml version=': 0, '>': 13, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1651616', 'n_tokens_mistral': 20797, 'n_tokens_neox': 19383, 'n_words': 4681}