<commit_before>package create

import (
	"fmt"
	"strings"
	"sync"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/route53"
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/giantswarm/awstpr"
	"github.com/giantswarm/certificatetpr"
	"github.com/giantswarm/microerror"
	"github.com/giantswarm/micrologger"
	"github.com/giantswarm/operatorkit/tpr"
	"github.com/giantswarm/randomkeytpr"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/tools/cache"

	awsutil "github.com/giantswarm/aws-operator/client/aws"
	"github.com/giantswarm/aws-operator/resources"
	awsresources "github.com/giantswarm/aws-operator/resources/aws"
	"github.com/giantswarm/aws-operator/service/cloudconfig"
	"github.com/giantswarm/aws-operator/service/key"
)

const (
	// The format of instance's name is "[name of cluster]-[prefix ('master' or 'worker')]-[number]".
	instanceNameFormat string = "%s-%s-%d"
	// The format of prefix inside a cluster "[name of cluster]-[prefix ('master' or 'worker')]".
	instanceClusterPrefixFormat string = "%s-%s"
	// Prefixes used for machine names.
	prefixMaster  string = "master"
	prefixWorker  string = "worker"
	prefixIngress string = "ingress"
	// Suffixes used for subnets
	suffixPublic  string = "public"
	suffixPrivate string = "private"
	// Number of retries of RunInstances to wait for Roles to propagate to
	// Instance Profiles
	runInstancesRetries = 10
	// The number of seconds AWS will wait, before issuing a health check on
	// instances in an Auto Scaling Group.
	gracePeriodSeconds = 10
)

// Config represents the configuration used to create a version service.
type Config struct {
	// Dependencies.
	CertWatcher *certificatetpr.Service
	CloudConfig *cloudconfig.CloudConfig
	K8sClient   kubernetes.Interface
	KeyWatcher  *randomkeytpr.Service
	Logger      micrologger.Logger

	// Settings.
	AwsConfig     awsutil.Config
	AwsHostConfig awsutil.Config
	PubKeyFile    string
}

// DefaultConfig provides a default configuration to create a new service by
// best effort.
func DefaultConfig() Config {
	return Config{
		// Dependencies.
		CertWatcher: nil,
		CloudConfig: nil,
		K8sClient:   nil,
		KeyWatcher:  nil,
		Logger:      nil,

		// Settings.
		AwsConfig:     awsutil.Config{},
		AwsHostConfig: awsutil.Config{},
		PubKeyFile:    "",
	}
}

// New creates a new configured service.
func New(config Config) (*Service, error) {
	// Dependencies.
	if config.CertWatcher == nil {
		return nil, microerror.Maskf(invalidConfigError, "config.CertWatcher must not be empty")
	}
	if config.CloudConfig == nil {
		return nil, microerror.Maskf(invalidConfigError, "config.CloudConfig must not be empty")
	}
	if config.K8sClient == nil {
		return nil, microerror.Maskf(invalidConfigError, "config.K8sClient must not be empty")
	}
	if config.KeyWatcher == nil {
		return nil, microerror.Maskf(invalidConfigError, "config.KeyWatcher must not be empty")
	}
	if config.Logger == nil {
		return nil, microerror.Maskf(invalidConfigError, "config.Logger must not be empty")
	}

	// Settings.
	var emptyAwsConfig awsutil.Config
	if config.AwsConfig == emptyAwsConfig {
		return nil, microerror.Maskf(invalidConfigError, "config.AwsConfig must not be empty")
	}
	if config.AwsHostConfig == emptyAwsConfig {
		return nil, microerror.Maskf(invalidConfigError, "config.AwsHostConfig must not be empty")
	}
	if config.PubKeyFile == "" {
		return nil, microerror.Maskf(invalidConfigError, "config.PubKeyFile must not be empty")
	}

	var err error

	var newTPR *tpr.TPR
	{
		tprConfig := tpr.DefaultConfig()

		tprConfig.K8sClient = config.K8sClient
		tprConfig.Logger = config.Logger

		tprConfig.Description = awstpr.Description
		tprConfig.Name = awstpr.Name
		tprConfig.Version = awstpr.VersionV1

		newTPR, err = tpr.New(tprConfig)
		if err != nil {
			return nil, microerror.Mask(err)
		}
	}

	newService := &Service{
		// Dependencies.
		certWatcher: config.CertWatcher,
		cloudConfig: config.CloudConfig,
		k8sClient:   config.K8sClient,
		keyWatcher:  config.KeyWatcher,
		logger:      config.Logger,

		// Internals
		bootOnce: sync.Once{},
		tpr:      newTPR,

		// Settings.
		awsConfig:     config.AwsConfig,
		awsHostConfig: config.AwsHostConfig,
		pubKeyFile:    config.PubKeyFile,
	}

	return newService, nil
}

// Service implements the version service interface.
type Service struct {
	// Dependencies.
	certWatcher *certificatetpr.Service
	cloudConfig *cloudconfig.CloudConfig
	k8sClient   kubernetes.Interface
	keyWatcher  *randomkeytpr.Service
	logger      micrologger.Logger

	// Internals.
	bootOnce sync.Once
	tpr      *tpr.TPR

	// Settings.
	awsConfig     awsutil.Config
	awsHostConfig awsutil.Config
	pubKeyFile    string
}

type Event struct {
	Type   string
	Object *awstpr.CustomObject
}

func (s *Service) Boot() {
	s.bootOnce.Do(func() {
		err := s.tpr.CreateAndWait()
		if tpr.IsAlreadyExists(err) {
			s.logger.Log("debug", "third party resource already exists")
		} else if err != nil {
			s.logger.Log("error", fmt.Sprintf("%#v", err))
			return
		}

		s.logger.Log("debug", "starting list/watch")

		newResourceEventHandler := &cache.ResourceEventHandlerFuncs{
			AddFunc:    s.addFunc,
			DeleteFunc: s.deleteFunc,
			UpdateFunc: s.updateFunc,
		}
		newZeroObjectFactory := &tpr.ZeroObjectFactoryFuncs{
			NewObjectFunc:     func() runtime.Object { return &awstpr.CustomObject{} },
			NewObjectListFunc: func() runtime.Object { return &awstpr.List{} },
		}

		s.tpr.NewInformer(newResourceEventHandler, newZeroObjectFactory).Run(nil)
	})
}

func (s *Service) addFunc(obj interface{}) {
	customObject, ok := obj.(*awstpr.CustomObject)
	if !ok {
		s.logger.Log("error", "could not convert to awstpr.CustomObject")
		return
	}
	cluster := *customObject

	s.logger.Log("info", fmt.Sprintf("creating cluster '%s'", key.ClusterID(cluster)))

	if err := validateCluster(cluster); err != nil {
		s.logger.Log("error", "cluster spec is invalid: '%#v'", err)
		return
	}

	err := s.processCluster(cluster)
	if err != nil {
		s.logger.Log("error", fmt.Sprintf("error processing cluster '%s': '%#v'", key.ClusterID(cluster), err))
		return
	}

	s.logger.Log("info", fmt.Sprintf("cluster '%s' processed", key.ClusterID(cluster)))
}

func (s *Service) processCluster(cluster awstpr.CustomObject) error {
	// Create cluster namespace in k8s.
	if err := s.createClusterNamespace(cluster.Spec.Cluster); err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create cluster namespace: '%#v'", err))
	}

	// Create AWS guest cluster client.
	s.awsConfig.Region = cluster.Spec.AWS.Region
	clients := awsutil.NewClients(s.awsConfig)
	if err := s.awsConfig.SetAccountID(clients.IAM); err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not retrieve guest amazon account id: '%#v'", err))
	}

	// Create AWS host cluster client.
	s.awsHostConfig.Region = cluster.Spec.AWS.Region
	hostClients := awsutil.NewClients(s.awsHostConfig)
	if err := s.awsHostConfig.SetAccountID(hostClients.IAM); err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not retrieve host amazon account id: '%#v'", err))
	}

	// An EC2 Keypair is needed for legacy clusters. New clusters provide SSH keys via cloud config.
	if !key.HasClusterVersion(cluster) {
		// Create keypair.
		var keyPair resources.ReusableResource
		var keyPairCreated bool
		{
			var err error
			keyPair = &awsresources.KeyPair{
				ClusterName: key.ClusterID(cluster),
				Provider:    awsresources.NewFSKeyPairProvider(s.pubKeyFile),
				AWSEntity:   awsresources.AWSEntity{Clients: clients},
			}
			keyPairCreated, err = keyPair.CreateIfNotExists()
			if err != nil {
				return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create keypair: '%#v'", err))
			}
		}

		if keyPairCreated {
			s.logger.Log("info", fmt.Sprintf("created keypair '%s'", key.ClusterID(cluster)))
		} else {
			s.logger.Log("info", fmt.Sprintf("keypair '%s' already exists, reusing", key.ClusterID(cluster)))
		}
	}

	s.logger.Log("info", fmt.Sprintf("waiting for k8s secrets..."))
	clusterID := key.ClusterID(cluster)
	certs, err := s.certWatcher.SearchCerts(clusterID)
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not get certificates from secrets: '%#v'", err))
	}

	// Create Encryption key
	s.logger.Log("info", fmt.Sprintf("waiting for k8s keys..."))
	keys, err := s.keyWatcher.SearchKeys(clusterID)
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not get keys from secrets: '%#v'", err))
	}

	// Create KMS key.
	kmsKey := &awsresources.KMSKey{
		Name:      key.ClusterID(cluster),
		AWSEntity: awsresources.AWSEntity{Clients: clients},
	}

	kmsCreated, kmsKeyErr := kmsKey.CreateIfNotExists()
	if kmsKeyErr != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create KMS key: '%#v'", kmsKeyErr))
	}

	if kmsCreated {
		s.logger.Log("info", fmt.Sprintf("created KMS key for cluster '%s'", key.ClusterID(cluster)))
	} else {
		s.logger.Log("info", fmt.Sprintf("kms key '%s' already exists, reusing", kmsKey.Name))
	}

	// Encode TLS assets.
	tlsAssets, err := s.encodeTLSAssets(certs, clients.KMS, kmsKey.Arn())
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not encode TLS assets: '%#v'", err))
	}

	// Encode Key assets.
	clusterKeys, err := s.encodeKeyAssets(keys, clients.KMS, kmsKey.Arn())
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not encode Keys assets: '%#v'", err))
	}

	bucketName := s.bucketName(cluster)

	// Create master IAM policy.
	var masterPolicy resources.NamedResource
	var masterPolicyCreated bool
	{
		var err error
		masterPolicy = &awsresources.Policy{
			ClusterID:  key.ClusterID(cluster),
			KMSKeyArn:  kmsKey.Arn(),
			PolicyType: prefixMaster,
			S3Bucket:   bucketName,
			AWSEntity:  awsresources.AWSEntity{Clients: clients},
		}
		masterPolicyCreated, err = masterPolicy.CreateIfNotExists()
		if err != nil {
			return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create master policy: '%#v'", err))
		}
	}
	if masterPolicyCreated {
		s.logger.Log("info", fmt.Sprintf("created master policy for cluster '%s'", key.ClusterID(cluster)))
	} else {
		s.logger.Log("info", fmt.Sprintf("master policy for cluster '%s' already exists, reusing", key.ClusterID(cluster)))
	}

	// Create worker IAM policy.
	var workerPolicy resources.NamedResource
	var workerPolicyCreated bool
	{
		var err error
		workerPolicy = &awsresources.Policy{
			ClusterID:  key.ClusterID(cluster),
			KMSKeyArn:  kmsKey.Arn(),
			PolicyType: prefixWorker,
			S3Bucket:   bucketName,
			AWSEntity:  awsresources.AWSEntity{Clients: clients},
		}
		workerPolicyCreated, err = workerPolicy.CreateIfNotExists()
		if err != nil {
			return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create worker policy: '%#v'", err))
		}
	}
	if workerPolicyCreated {
		s.logger.Log("info", fmt.Sprintf("created worker policy for cluster '%s'", key.ClusterID(cluster)))
	} else {
		s.logger.Log("info", fmt.Sprintf("worker policy for cluster '%s' already exists, reusing", key.ClusterID(cluster)))
	}

	// Create S3 bucket.
	var bucket resources.ReusableResource
	var bucketCreated bool
	{
		var err error
		bucket = &awsresources.Bucket{
			Name:      bucketName,
			AWSEntity: awsresources.AWSEntity{Clients: clients},
		}
		bucketCreated, err = bucket.CreateIfNotExists()
		if err != nil {
			return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create S3 bucket: '%#v'", err))
		}
	}

	if bucketCreated {
		s.logger.Log("info", fmt.Sprintf("created bucket '%s'", bucketName))
	} else {
		s.logger.Log("info", fmt.Sprintf("bucket '%s' already exists, reusing", bucketName))
	}

	// Create VPC.
	var vpc resources.ResourceWithID
	vpc = &awsresources.VPC{
		CidrBlock: cluster.Spec.AWS.VPC.CIDR,
		Name:      key.ClusterID(cluster),
		AWSEntity: awsresources.AWSEntity{Clients: clients},
	}
	vpcCreated, err := vpc.CreateIfNotExists()
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create VPC: '%#v'", err))
	}
	if vpcCreated {
		s.logger.Log("info", fmt.Sprintf("created vpc for cluster '%s'", key.ClusterID(cluster)))
	} else {
		s.logger.Log("info", fmt.Sprintf("vpc for cluster '%s' already exists, reusing", key.ClusterID(cluster)))
	}
	vpcID, err := vpc.GetID()
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not get VPC ID: '%#v'", err))
	}

	// Create VPC peering connection.
	vpcPeeringConection := &awsresources.VPCPeeringConnection{
		VPCId:     vpcID,
		PeerVPCId: cluster.Spec.AWS.VPC.PeerID,
		AWSEntity: awsresources.AWSEntity{
			Clients:     clients,
			HostClients: hostClients,
		},
		Logger: s.logger,
	}
	vpcPeeringConnectionCreated, err := vpcPeeringConection.CreateIfNotExists()
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create vpc peering connection: '%#v'", err))
	}
	if vpcPeeringConnectionCreated {
		s.logger.Log("info", fmt.Sprintf("created vpc peering connection for cluster '%s'", key.ClusterID(cluster)))
	} else {
		s.logger.Log("info", fmt.Sprintf("vpc peering connection for cluster '%s' already exists, reusing", key.ClusterID(cluster)))
	}

	conn, err := vpcPeeringConection.FindExisting()
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not find vpc peering connection: '%#v'", err))
	}

	// Create internet gateway.
	var internetGateway resources.ResourceWithID
	internetGateway = &awsresources.InternetGateway{
		Name:  key.ClusterID(cluster),
		VpcID: vpcID,
		// Dependencies.
		Logger:    s.logger,
		AWSEntity: awsresources.AWSEntity{Clients: clients},
	}
	internetGatewayCreated, err := internetGateway.CreateIfNotExists()
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create internet gateway: '%#v'", err))
	}
	if internetGatewayCreated {
		s.logger.Log("info", fmt.Sprintf("created internet gateway for cluster '%s'", key.ClusterID(cluster)))
	} else {
		s.logger.Log("info", fmt.Sprintf("internet gateway for cluster '%s' already exists, reusing", key.ClusterID(cluster)))
	}

	// Create masters security group.
	mastersSGInput := securityGroupInput{
		Clients:   clients,
		GroupName: key.SecurityGroupName(cluster, prefixMaster),
		VPCID:     vpcID,
	}
	mastersSecurityGroup, err := s.createSecurityGroup(mastersSGInput)
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create security group '%s': '%#v'", mastersSGInput.GroupName, err))
	}
	mastersSecurityGroupID, err := mastersSecurityGroup.GetID()
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not get security group '%s' ID: '%#v'", mastersSGInput.GroupName, err))
	}

	// Create workers security group.
	workersSGInput := securityGroupInput{
		Clients:   clients,
		GroupName: key.SecurityGroupName(cluster, prefixWorker),
		VPCID:     vpcID,
	}
	workersSecurityGroup, err := s.createSecurityGroup(workersSGInput)
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create security group '%s': '%#v'", workersSGInput.GroupName, err))
	}
	workersSecurityGroupID, err := workersSecurityGroup.GetID()
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not get security group '%s' ID: '%#v'", workersSGInput.GroupName, err))
	}

	// Create ingress ELB security group.
	ingressSGInput := securityGroupInput{
		Clients:   clients,
		GroupName: key.SecurityGroupName(cluster, prefixIngress),
		VPCID:     vpcID,
	}
	ingressSecurityGroup, err := s.createSecurityGroup(ingressSGInput)
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create security group '%s': '%#v'", ingressSGInput.GroupName, err))
	}
	ingressSecurityGroupID, err := ingressSecurityGroup.GetID()
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not get security group '%s' ID: '%#v'", ingressSGInput.GroupName, err))
	}

	// In rare cases the host cluster CIDR block can be nil. Return error rather
	// than panic. TODO Fix issue #451 that tracks this.
	if conn.AccepterVpcInfo.CidrBlock == nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("vpc peering connection cidr block is nil for cluster '%s'", key.ClusterID(cluster)))
	}

	// Create rules for the security groups.
	rulesInput := rulesInput{
		Cluster:                cluster,
		MastersSecurityGroupID: mastersSecurityGroupID,
		WorkersSecurityGroupID: workersSecurityGroupID,
		IngressSecurityGroupID: ingressSecurityGroupID,
		HostClusterCIDR:        *conn.AccepterVpcInfo.CidrBlock,
	}

	if err := mastersSecurityGroup.ApplyRules(rulesInput.masterRules()); err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create rules for security group '%s': '%#v", mastersSecurityGroup.GroupName, err))
	}

	if err := workersSecurityGroup.ApplyRules(rulesInput.workerRules()); err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create rules for security group '%s': '%#v'", workersSecurityGroup.GroupName, err))
	}

	if err := ingressSecurityGroup.ApplyRules(rulesInput.ingressRules()); err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create rules for security group '%s': '%#v'", ingressSecurityGroup.GroupName, err))
	}

	// Create public route table.
	publicRouteTable := &awsresources.RouteTable{
		Name:   key.ClusterID(cluster),
		VpcID:  vpcID,
		Client: clients.EC2,
		Logger: s.logger,
	}
	publicRouteTableCreated, err := publicRouteTable.CreateIfNotExists()
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create route table: '%#v'", err))
	}
	if publicRouteTableCreated {
		s.logger.Log("info", "created public route table")
	} else {
		s.logger.Log("info", "route table already exists, reusing")
	}

	if err := publicRouteTable.MakePublic(); err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not make route table public: '%#v'", err))
	}

	// Create public subnet.
	subnetInput := SubnetInput{
		Name:       key.SubnetName(cluster, suffixPublic),
		CidrBlock:  cluster.Spec.AWS.VPC.PublicSubnetCIDR,
		Clients:    clients,
		Cluster:    cluster,
		MakePublic: true,
		RouteTable: publicRouteTable,
		VpcID:      vpcID,
	}
	publicSubnet, err := s.createSubnet(subnetInput)
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create public subnet: '%#v'", err))
	}

	publicSubnetID, err := publicSubnet.GetID()
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not get public subnet ID: '%#v'", err))
	}

	var privateRouteTable *awsresources.RouteTable
	var privateSubnet *awsresources.Subnet
	var privateSubnetID string

	// For new clusters create a NAT gateway, private route table and private subnet.
	if key.HasClusterVersion(cluster) {
		// Create NAT gateway.
		natGateway := &awsresources.NatGateway{
			Name:   key.ClusterID(cluster),
			Subnet: publicSubnet,
			// Dependencies.
			Logger:    s.logger,
			AWSEntity: awsresources.AWSEntity{Clients: clients},
		}
		natGatewayCreated, err := natGateway.CreateIfNotExists()
		if err != nil {
			return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create nat gateway: '%#v'", err))
		}
		if natGatewayCreated {
			s.logger.Log("info", fmt.Sprintf("created nat gateway for cluster '%s'", key.ClusterID(cluster)))
		} else {
			s.logger.Log("info", fmt.Sprintf("nat gateway for cluster '%s' already exists, reusing", key.ClusterID(cluster)))
		}

		natGatewayID, err := natGateway.GetID()
		if err != nil {
			return microerror.Maskf(executionFailedError, fmt.Sprintf("could not get nat gateway id: '%#v'", err))
		}

		// Create private route table.
		privateRouteTable = &awsresources.RouteTable{
			Name:   key.RouteTableName(cluster, suffixPrivate),
			VpcID:  vpcID,
			Client: clients.EC2,
			Logger: s.logger,
		}
		privateRouteTableCreated, err := privateRouteTable.CreateIfNotExists()
		if err != nil {
			return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create route table: '%#v'", err))
		}
		if privateRouteTableCreated {
			s.logger.Log("info", "created private route table")
		} else {
			s.logger.Log("info", "private route table already exists, reusing")
		}

		// Create default route for the NAT gateway in the private route table.
		natGatewayRouteCreated, err := privateRouteTable.CreateNatGatewayRoute(natGatewayID)
		if err != nil {
			return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create route for nat gateway: '%#v'", err))
		}
		if natGatewayRouteCreated {
			s.logger.Log("info", "created nat gateway route")
		} else {
			s.logger.Log("info", "nat gateway route already exists, reusing")
		}

		// Create private subnet.
		subnetInput := SubnetInput{
			Name:       key.SubnetName(cluster, suffixPrivate),
			CidrBlock:  cluster.Spec.AWS.VPC.PrivateSubnetCIDR,
			Clients:    clients,
			Cluster:    cluster,
			MakePublic: false,
			RouteTable: privateRouteTable,
			VpcID:      vpcID,
		}
		privateSubnet, err = s.createSubnet(subnetInput)
		if err != nil {
			return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create private subnet: '%#v'", err))
		}

		privateSubnetID, err = privateSubnet.GetID()
		if err != nil {
			return microerror.Maskf(executionFailedError, fmt.Sprintf("could not get private subnet ID: '%#v'", err))
		}
	}

	hostClusterRoute := &awsresources.Route{
		RouteTable:             *publicRouteTable,
		DestinationCidrBlock:   *conn.AccepterVpcInfo.CidrBlock,
		VpcPeeringConnectionID: *conn.VpcPeeringConnectionId,
		AWSEntity:              awsresources.AWSEntity{Clients: clients},
	}

	if key.HasClusterVersion(cluster) {
		// New clusters have private IPs so use the private route table.
		hostClusterRoute.RouteTable = *privateRouteTable
	} else {
		// Legacy clusters have public IPs so use the public route table.
		hostClusterRoute.RouteTable = *publicRouteTable
	}

	hostRouteCreated, err := hostClusterRoute.CreateIfNotExists()
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not add host vpc route: '%#v'", err))
	}
	if hostRouteCreated {
		s.logger.Log("info", fmt.Sprintf("created host vpc route for cluster '%s'", key.ClusterID(cluster)))
	} else {
		s.logger.Log("info", fmt.Sprintf("host vpc route for cluster '%s' already exists, reusing", key.ClusterID(cluster)))
	}

	for _, privateRouteTableName := range cluster.Spec.AWS.VPC.RouteTableNames {
		privateRouteTable := &awsresources.RouteTable{
			Name:   privateRouteTableName,
			VpcID:  cluster.Spec.AWS.VPC.PeerID,
			Client: hostClients.EC2,
			Logger: s.logger,
		}

		privateRoute := &awsresources.Route{
			RouteTable:             *privateRouteTable,
			DestinationCidrBlock:   *conn.RequesterVpcInfo.CidrBlock,
			VpcPeeringConnectionID: *conn.VpcPeeringConnectionId,
			AWSEntity:              awsresources.AWSEntity{Clients: hostClients},
		}

		privateRouteCreated, err := privateRoute.CreateIfNotExists()
		if err != nil {
			return microerror.Maskf(executionFailedError, fmt.Sprintf("could not add guest vpc route: '%#v'", err))
		}
		if privateRouteCreated {
			s.logger.Log("info", fmt.Sprintf("created guest vpc route for cluster '%s'", key.ClusterID(cluster)))
		} else {
			s.logger.Log("info", fmt.Sprintf("host vpc guest for cluster '%s' already exists, reusing", key.ClusterID(cluster)))
		}

	}

	mastersInput := runMachinesInput{
		clients:             clients,
		cluster:             cluster,
		tlsAssets:           tlsAssets,
		clusterKeys:         clusterKeys,
		clusterName:         key.ClusterID(cluster),
		bucket:              bucket,
		securityGroup:       mastersSecurityGroup,
		instanceProfileName: masterPolicy.GetName(),
		prefix:              prefixMaster,
	}

	if key.HasClusterVersion(cluster) {
		// New clusters have masters in the private subnet.
		mastersInput.subnet = privateSubnet
	} else {
		// Legacy clusters have masters in the public subnet.
		mastersInput.subnet = publicSubnet

		// An EC2 Keypair is needed for legacy clusters. New clusters provide SSH keys via cloud config.
		mastersInput.keyPairName = key.ClusterID(cluster)
	}

	// Run masters.
	anyMastersCreated, masterIDs, err := s.runMachines(mastersInput)
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not start masters: '%#v'", err))
	}

	if !validateIDs(masterIDs) {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("master nodes had invalid instance IDs: %v", masterIDs))
	}

	// Create apiserver load balancer.
	lbInput := LoadBalancerInput{
		Name:               cluster.Spec.Cluster.Kubernetes.API.Domain,
		Clients:            clients,
		Cluster:            cluster,
		IdleTimeoutSeconds: cluster.Spec.AWS.ELB.IdleTimeoutSeconds.API,
		InstanceIDs:        masterIDs,
		PortsToOpen: awsresources.PortPairs{
			{
				PortELB:      cluster.Spec.Cluster.Kubernetes.API.SecurePort,
				PortInstance: cluster.Spec.Cluster.Kubernetes.API.SecurePort,
			},
		},
		SecurityGroupID: mastersSecurityGroupID,
		SubnetID:        publicSubnetID,
	}

	apiLB, err := s.createLoadBalancer(lbInput)
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create apiserver load balancer: '%#v'", err))
	}

	// Create etcd load balancer.
	lbInput = LoadBalancerInput{
		Name:               cluster.Spec.Cluster.Etcd.Domain,
		Clients:            clients,
		Cluster:            cluster,
		IdleTimeoutSeconds: cluster.Spec.AWS.ELB.IdleTimeoutSeconds.Etcd,
		InstanceIDs:        masterIDs,
		PortsToOpen: awsresources.PortPairs{
			{
				PortELB:      httpsPort,
				PortInstance: cluster.Spec.Cluster.Etcd.Port,
			},
		},
		SecurityGroupID: mastersSecurityGroupID,
		SubnetID:        publicSubnetID,
	}

	if key.HasClusterVersion(cluster) {
		lbInput.Scheme = "internal"
	}

	etcdLB, err := s.createLoadBalancer(lbInput)
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create etcd load balancer: '%#v'", err))
	}

	// Masters were created but the master IAM policy existed from a previous
	// execution. Its likely that previous execution failed. IAM policies can't
	// be reused for EC2 instances.
	if anyMastersCreated && !masterPolicyCreated {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("cluster '%s' cannot be processed. As IAM policy for master nodes cannot be reused. Please delete this cluster.", key.ClusterID(cluster)))
	}

	// Create Ingress load balancer.
	lbInput = LoadBalancerInput{
		Name:               cluster.Spec.Cluster.Kubernetes.IngressController.Domain,
		Clients:            clients,
		Cluster:            cluster,
		IdleTimeoutSeconds: cluster.Spec.AWS.ELB.IdleTimeoutSeconds.Ingress,
		PortsToOpen: awsresources.PortPairs{
			{
				PortELB:      httpsPort,
				PortInstance: cluster.Spec.Cluster.Kubernetes.IngressController.SecurePort,
			},
			{
				PortELB:      httpPort,
				PortInstance: cluster.Spec.Cluster.Kubernetes.IngressController.InsecurePort,
			},
		},
		SecurityGroupID: ingressSecurityGroupID,
		SubnetID:        publicSubnetID,
	}

	ingressLB, err := s.createLoadBalancer(lbInput)
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create ingress load balancer: '%#v'", err))
	}

	// Create a launch configuration for the worker nodes.
	lcInput := launchConfigurationInput{
		clients:             clients,
		cluster:             cluster,
		clusterKeys:         clusterKeys,
		tlsAssets:           tlsAssets,
		bucket:              bucket,
		securityGroup:       workersSecurityGroup,
		subnet:              publicSubnet,
		instanceProfileName: workerPolicy.GetName(),
		prefix:              prefixWorker,
		ebsStorage:          true,
	}

	// For new clusters don't assign public IPs and use the private subnet.
	if key.HasClusterVersion(cluster) {
		lcInput.associatePublicIP = false
		lcInput.subnet = privateSubnet
	} else {
		lcInput.associatePublicIP = true
		lcInput.subnet = publicSubnet
	}

	// An EC2 Keypair is needed for legacy clusters. New clusters provide SSH keys via cloud config.
	if !key.HasClusterVersion(cluster) {
		lcInput.keypairName = key.ClusterID(cluster)
	}

	lcCreated, err := s.createLaunchConfiguration(lcInput)
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create launch config: '%#v'", err))
	}

	if lcCreated {
		s.logger.Log("info", fmt.Sprintf("created worker launch config"))
	} else {
		s.logger.Log("info", fmt.Sprintf("launch config %s already exists, reusing", key.ClusterID(cluster)))
	}

	workersLCName, err := launchConfigurationName(cluster, "worker", workersSecurityGroupID)
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not get launch config name: '%#v'", err))
	}

	asg := awsresources.AutoScalingGroup{
		Client:                  clients.AutoScaling,
		Name:                    key.AutoScalingGroupName(cluster, prefixWorker),
		ClusterID:               key.ClusterID(cluster),
		MinSize:                 key.WorkerCount(cluster),
		MaxSize:                 key.WorkerCount(cluster),
		AvailabilityZone:        key.AvailabilityZone(cluster),
		LaunchConfigurationName: workersLCName,
		LoadBalancerName:        ingressLB.Name,
		VPCZoneIdentifier:       publicSubnetID,
		HealthCheckGracePeriod:  gracePeriodSeconds,
	}

	// For new clusters launch the workers in the private subnet.
	if key.HasClusterVersion(cluster) {
		asg.VPCZoneIdentifier = privateSubnetID
	} else {
		asg.VPCZoneIdentifier = publicSubnetID
	}

	asgCreated, err := asg.CreateIfNotExists()
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create auto scaling group: '%#v'", err))
	}

	if asgCreated {
		s.logger.Log("info", fmt.Sprintf("created auto scaling group '%s' with size %v", asg.Name, key.WorkerCount(cluster)))
	} else {
		s.logger.Log("info", fmt.Sprintf("auto scaling group '%s' already exists, reusing", asg.Name))
	}

	// Create Record Sets for the Load Balancers.
	recordSetInputs := []recordSetInput{
		{
			Cluster:      cluster,
			Client:       clients.Route53,
			Resource:     apiLB,
			Domain:       cluster.Spec.Cluster.Kubernetes.API.Domain,
			HostedZoneID: cluster.Spec.AWS.HostedZones.API,
			Type:         route53.RRTypeA,
		},
		{
			Cluster:      cluster,
			Client:       clients.Route53,
			Resource:     etcdLB,
			Domain:       cluster.Spec.Cluster.Etcd.Domain,
			HostedZoneID: cluster.Spec.AWS.HostedZones.Etcd,
			Type:         route53.RRTypeA,
		},
		{
			Cluster:      cluster,
			Client:       clients.Route53,
			Resource:     ingressLB,
			Domain:       cluster.Spec.Cluster.Kubernetes.IngressController.Domain,
			HostedZoneID: cluster.Spec.AWS.HostedZones.Ingress,
			Type:         route53.RRTypeA,
		},
		{
			Cluster:      cluster,
			Client:       clients.Route53,
			Domain:       cluster.Spec.Cluster.Kubernetes.IngressController.WildcardDomain,
			HostedZoneID: cluster.Spec.AWS.HostedZones.Ingress,
			Value:        cluster.Spec.Cluster.Kubernetes.IngressController.Domain,
			Type:         route53.RRTypeCname,
		},
	}

	var rsErr error
	for _, input := range recordSetInputs {
		if rsErr = s.createRecordSet(input); rsErr != nil {
			return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create record set '%#v'", rsErr))
		}
	}
	if rsErr == nil {
		s.logger.Log("info", fmt.Sprintf("created DNS records for load balancers"))
	}

	return nil
}

func (s *Service) deleteFunc(obj interface{}) {
	// TODO(nhlfr): Move this to a separate operator.

	// We can receive an instance of awstpr.CustomObject or cache.DeletedFinalStateUnknown.
	// We need to assert the type properly and log an error when we cannot do that.
	// Also, the cache.DeleteFinalStateUnknown object can contain the proper CustomObject,
	// but doesn't have to.
	// https://github.com/kubernetes/client-go/blob/7ba6be594966f4bec08a57a60c855d17a9f7000a/tools/cache/delta_fifo.go#L674-L677
	var cluster awstpr.CustomObject
	clusterPtr, ok := obj.(*awstpr.CustomObject)
	if ok {
		cluster = *clusterPtr
	} else {
		deletedObj, ok := obj.(cache.DeletedFinalStateUnknown)
		if !ok {
			s.logger.Log("error", "received unknown type of third-party object")
			return
		}
		clusterPtr, ok := deletedObj.Obj.(*awstpr.CustomObject)
		if !ok {
			s.logger.Log("error", "received the proper delete request, but the type of third-party object is unknown")
			return
		}
		cluster = *clusterPtr
	}

	if err := validateCluster(cluster); err != nil {
		s.logger.Log("error", "cluster spec is invalid: '%#v'", err)
		return
	}

	if err := s.deleteClusterNamespace(cluster.Spec.Cluster); err != nil {
		s.logger.Log("error", "could not delete cluster namespace:", err)
	}

	clients := awsutil.NewClients(s.awsConfig)
	err := s.awsConfig.SetAccountID(clients.IAM)
	if err != nil {
		s.logger.Log("error", fmt.Sprintf("could not retrieve amazon account id: '%#v'", err))
		return
	}

	// Retrieve AWS host cluster client.
	s.awsHostConfig.Region = cluster.Spec.AWS.Region
	hostClients := awsutil.NewClients(s.awsHostConfig)
	if err := s.awsHostConfig.SetAccountID(hostClients.IAM); err != nil {
		s.logger.Log("error", fmt.Sprintf("could not retrieve host amazon account id: '%#v'", err))
	}

	// Delete masters.
	s.logger.Log("info", "deleting masters...")
	if err := s.deleteMachines(deleteMachinesInput{
		clients:     clients,
		clusterName: key.ClusterID(cluster),
		prefix:      prefixMaster,
	}); err != nil {
		s.logger.Log("error", fmt.Sprintf("%#v", err))
	} else {
		s.logger.Log("info", "deleted masters")
	}

	// Delete workers Auto Scaling Group.
	asg := awsresources.AutoScalingGroup{
		Client: clients.AutoScaling,
		Name:   key.AutoScalingGroupName(cluster, prefixWorker),
	}

	if err := asg.Delete(); err != nil {
		s.logger.Log("error", fmt.Sprintf("%#v", err))
	} else {
		s.logger.Log("info", "deleted workers auto scaling group")
	}

	// Delete workers launch configuration.
	lcInput := launchConfigurationInput{
		clients: clients,
		cluster: cluster,
		prefix:  "worker",
	}
	if err := s.deleteLaunchConfiguration(lcInput); err != nil {
		s.logger.Log("error", fmt.Sprintf("%#v", err))
	} else {
		s.logger.Log("info", "deleted worker launch config")
	}

	// Delete Record Sets.
	apiLBName, err := loadBalancerName(cluster.Spec.Cluster.Kubernetes.API.Domain, cluster)
	etcdLBName, err := loadBalancerName(cluster.Spec.Cluster.Etcd.Domain, cluster)
	ingressLBName, err := loadBalancerName(cluster.Spec.Cluster.Kubernetes.IngressController.Domain, cluster)
	if err != nil {
		s.logger.Log("error", fmt.Sprintf("%#v", err))
	} else {
		apiLB, err := awsresources.NewELBFromExisting(apiLBName, clients.ELB)
		etcdLB, err := awsresources.NewELBFromExisting(etcdLBName, clients.ELB)
		ingressLB, err := awsresources.NewELBFromExisting(ingressLBName, clients.ELB)
		if err != nil {
			s.logger.Log("error", fmt.Sprintf("%#v", err))
		} else {
			recordSetInputs := []recordSetInput{
				{
					Cluster:      cluster,
					Client:       clients.Route53,
					Resource:     apiLB,
					Domain:       cluster.Spec.Cluster.Kubernetes.API.Domain,
					HostedZoneID: cluster.Spec.AWS.HostedZones.API,
					Type:         route53.RRTypeA,
				},
				{
					Cluster:      cluster,
					Client:       clients.Route53,
					Resource:     etcdLB,
					Domain:       cluster.Spec.Cluster.Etcd.Domain,
					HostedZoneID: cluster.Spec.AWS.HostedZones.Etcd,
					Type:         route53.RRTypeA,
				},
				{
					Cluster:      cluster,
					Client:       clients.Route53,
					Resource:     ingressLB,
					Domain:       cluster.Spec.Cluster.Kubernetes.IngressController.Domain,
					HostedZoneID: cluster.Spec.AWS.HostedZones.Ingress,
					Type:         route53.RRTypeA,
				},
				{
					Cluster:      cluster,
					Client:       clients.Route53,
					Value:        cluster.Spec.Cluster.Kubernetes.IngressController.Domain,
					Domain:       cluster.Spec.Cluster.Kubernetes.IngressController.WildcardDomain,
					HostedZoneID: cluster.Spec.AWS.HostedZones.Ingress,
					Type:         route53.RRTypeCname,
				},
			}

			var rsErr error
			for _, input := range recordSetInputs {
				if rsErr = s.deleteRecordSet(input); rsErr != nil {
					s.logger.Log("error", fmt.Sprintf("%#v", rsErr))
				}
			}
			if rsErr == nil {
				s.logger.Log("info", "deleted record sets")
			}
		}
	}

	// Delete Load Balancers.
	loadBalancerInputs := []LoadBalancerInput{
		{
			Name:    cluster.Spec.Cluster.Kubernetes.API.Domain,
			Clients: clients,
			Cluster: cluster,
		},
		{
			Name:    cluster.Spec.Cluster.Etcd.Domain,
			Clients: clients,
			Cluster: cluster,
		},
		{
			Name:    cluster.Spec.Cluster.Kubernetes.IngressController.Domain,
			Clients: clients,
			Cluster: cluster,
		},
	}

	var elbErr error
	for _, lbInput := range loadBalancerInputs {
		if elbErr = s.deleteLoadBalancer(lbInput); elbErr != nil {
			s.logger.Log("error", fmt.Sprintf("%#v", elbErr))
		}
	}
	if elbErr == nil {
		s.logger.Log("info", "deleted ELBs")
	}

	// Delete route table.
	var publicRouteTable resources.ResourceWithID
	publicRouteTable = &awsresources.RouteTable{
		Name:   key.ClusterID(cluster),
		Client: clients.EC2,
		Logger: s.logger,
	}
	if err := publicRouteTable.Delete(); err != nil {
		s.logger.Log("error", fmt.Sprintf("could not delete route table: '%#v'", err))
	} else {
		s.logger.Log("info", "deleted route table")
	}

	// Sync VPC.
	var vpc resources.ResourceWithID
	vpc = &awsresources.VPC{
		Name:      key.ClusterID(cluster),
		AWSEntity: awsresources.AWSEntity{Clients: clients},
		Logger:    s.logger,
	}
	vpcID, err := vpc.GetID()
	if err != nil {
		s.logger.Log("error", fmt.Sprintf("%#v", err))
	}

	// Delete NAT gateway and private subnet for new clusters.
	if key.HasClusterVersion(cluster) {
		// Delete NAT gateway.
		natGateway := &awsresources.NatGateway{
			Name: key.ClusterID(cluster),
			// Dependencies.
			Logger:    s.logger,
			AWSEntity: awsresources.AWSEntity{Clients: clients},
		}
		if err := natGateway.Delete(); err != nil {
			s.logger.Log("error", fmt.Sprintf("could not delete nat gateway: '%#v'", err))
		} else {
			s.logger.Log("info", "deleted nat gateway")
		}

		// Delete private route table.
		privateRouteTable := &awsresources.RouteTable{
			Name:   key.RouteTableName(cluster, suffixPrivate),
			Client: clients.EC2,
			Logger: s.logger,
		}
		if err := privateRouteTable.Delete(); err != nil {
			s.logger.Log("error", fmt.Sprintf("could not delete private route table: '%#v'", err))
		} else {
			s.logger.Log("info", "deleted private route table")
		}

		// Delete private subnet.
		subnetInput := SubnetInput{
			Name:    key.SubnetName(cluster, suffixPrivate),
			Clients: clients,
		}
		if err := s.deleteSubnet(subnetInput); err != nil {
			s.logger.Log("error", fmt.Sprintf("could not delete private subnet: '%#v'", err))
		} else {
			s.logger.Log("info", "deleted private subnet")
		}
	}

	// Delete internet gateway.
	internetGateway := &awsresources.InternetGateway{
		Name:  key.ClusterID(cluster),
		VpcID: vpcID,
		// Dependencies.
		Logger:    s.logger,
		AWSEntity: awsresources.AWSEntity{Clients: clients},
	}
	if err := internetGateway.Delete(); err != nil {
		s.logger.Log("error", fmt.Sprintf("could not delete internet gateway: '%#v'", err))
	} else {
		s.logger.Log("info", "deleted internet gateway")
	}

	// Delete public subnet.
	subnetInput := SubnetInput{
		Name:    key.SubnetName(cluster, suffixPublic),
		Clients: clients,
	}
	if err := s.deleteSubnet(subnetInput); err != nil {
		s.logger.Log("error", fmt.Sprintf("could not delete public subnet: '%#v'", err))
	} else {
		s.logger.Log("info", "deleted public subnet")
	}

	// Before the security groups can be deleted any rules referencing other
	// groups must first be deleted.
	mastersSGRulesInput := securityGroupRulesInput{
		Clients:   clients,
		GroupName: key.SecurityGroupName(cluster, prefixMaster),
	}
	if err := s.deleteSecurityGroupRules(mastersSGRulesInput); err != nil {
		s.logger.Log("error", fmt.Sprintf("could not delete rules for security group '%s': '%#v'", mastersSGRulesInput.GroupName, err))
	}

	workersSGRulesInput := securityGroupRulesInput{
		Clients:   clients,
		GroupName: key.SecurityGroupName(cluster, prefixWorker),
	}
	if err := s.deleteSecurityGroupRules(workersSGRulesInput); err != nil {
		s.logger.Log("error", fmt.Sprintf("could not delete rules for security group '%s': '%#v'", mastersSGRulesInput.GroupName, err))
	}

	ingressSGRulesInput := securityGroupRulesInput{
		Clients:   clients,
		GroupName: key.SecurityGroupName(cluster, prefixIngress),
	}
	if err := s.deleteSecurityGroupRules(ingressSGRulesInput); err != nil {
		s.logger.Log("error", fmt.Sprintf("could not delete rules for security group '%s': '%#v'", mastersSGRulesInput.GroupName, err))
	}

	// Delete masters security group.
	mastersSGInput := securityGroupInput{
		Clients:   clients,
		GroupName: key.SecurityGroupName(cluster, prefixMaster),
	}
	if err := s.deleteSecurityGroup(mastersSGInput); err != nil {
		s.logger.Log("error", fmt.Sprintf("could not delete security group '%s': '%#v'", mastersSGInput.GroupName, err))
	}

	// Delete workers security group.
	workersSGInput := securityGroupInput{
		Clients:   clients,
		GroupName: key.SecurityGroupName(cluster, prefixWorker),
	}
	if err := s.deleteSecurityGroup(workersSGInput); err != nil {
		s.logger.Log("error", fmt.Sprintf("could not delete security group '%s': '%#v'", workersSGInput.GroupName, err))
	}

	// Delete ingress security group.
	ingressSGInput := securityGroupInput{
		Clients:   clients,
		GroupName: key.SecurityGroupName(cluster, prefixIngress),
	}
	if err := s.deleteSecurityGroup(ingressSGInput); err != nil {
		s.logger.Log("error", fmt.Sprintf("could not delete security group '%s': '%#v'", ingressSGInput.GroupName, err))
	}

	vpcPeeringConection := &awsresources.VPCPeeringConnection{
		VPCId:     vpcID,
		PeerVPCId: cluster.Spec.AWS.VPC.PeerID,
		AWSEntity: awsresources.AWSEntity{
			Clients:     clients,
			HostClients: hostClients,
		},
		Logger: s.logger,
	}
	conn, err := vpcPeeringConection.FindExisting()
	if err != nil {
		s.logger.Log("error", fmt.Sprintf("could not find vpc peering connection: '%#v'", err))
	}

	// Delete Guest VPC Routes.
	for _, privateRouteTableName := range cluster.Spec.AWS.VPC.RouteTableNames {
		privateRouteTable := &awsresources.RouteTable{
			Name:   privateRouteTableName,
			VpcID:  cluster.Spec.AWS.VPC.PeerID,
			Client: hostClients.EC2,
			Logger: s.logger,
		}

		privateRoute := &awsresources.Route{
			RouteTable:             *privateRouteTable,
			DestinationCidrBlock:   *conn.RequesterVpcInfo.CidrBlock,
			VpcPeeringConnectionID: *conn.VpcPeeringConnectionId,
			AWSEntity:              awsresources.AWSEntity{Clients: hostClients},
		}

		if err := privateRoute.Delete(); err != nil {
			s.logger.Log("error", fmt.Sprintf("could not delete vpc route: '%v'", err))
		}
	}

	// Delete VPC peering connection.
	if err := vpcPeeringConection.Delete(); err != nil {
		s.logger.Log("error", fmt.Sprintf("could not delete vpc peering connection: '%#v'", err))
	} else {
		s.logger.Log("info", "deleted vpc peering connection")
	}

	// Delete VPC.
	if err := vpc.Delete(); err != nil {
		s.logger.Log("error", fmt.Sprintf("could not delete vpc: '%#v'", err))
	} else {
		s.logger.Log("info", "deleted vpc")
	}

	// Delete S3 bucket.
	bucketName := s.bucketName(cluster)

	bucket := &awsresources.Bucket{
		AWSEntity: awsresources.AWSEntity{Clients: clients},
		Name:      bucketName,
	}

	if err := bucket.Delete(); err != nil {
		s.logger.Log("error", fmt.Sprintf("%#v", err))
	}

	s.logger.Log("info", "deleted bucket")

	// Delete master policy.
	var masterPolicy resources.NamedResource
	masterPolicy = &awsresources.Policy{
		ClusterID:  key.ClusterID(cluster),
		PolicyType: prefixMaster,
		S3Bucket:   bucketName,
		AWSEntity:  awsresources.AWSEntity{Clients: clients},
	}
	if err := masterPolicy.Delete(); err != nil {
		s.logger.Log("error", fmt.Sprintf("%#v", err))
	} else {
		s.logger.Log("info", fmt.Sprintf("deleted %s roles, policies, instance profiles", prefixMaster))
	}

	// Delete worker policy.
	var workerPolicy resources.NamedResource
	workerPolicy = &awsresources.Policy{
		ClusterID:  key.ClusterID(cluster),
		PolicyType: prefixWorker,
		S3Bucket:   bucketName,
		AWSEntity:  awsresources.AWSEntity{Clients: clients},
	}
	if err := workerPolicy.Delete(); err != nil {
		s.logger.Log("error", fmt.Sprintf("%#v", err))
	} else {
		s.logger.Log("info", fmt.Sprintf("deleted %s roles, policies, instance profiles", prefixWorker))
	}

	// Delete KMS key.
	var kmsKey resources.ArnResource
	kmsKey = &awsresources.KMSKey{
		Name:      key.ClusterID(cluster),
		AWSEntity: awsresources.AWSEntity{Clients: clients},
	}
	if err := kmsKey.Delete(); err != nil {
		s.logger.Log("error", fmt.Sprintf("%#v", err))
	} else {
		s.logger.Log("info", "deleted KMS key")
	}

	// Delete keypair.
	var keyPair resources.Resource
	keyPair = &awsresources.KeyPair{
		ClusterName: key.ClusterID(cluster),
		AWSEntity:   awsresources.AWSEntity{Clients: clients},
	}
	if err := keyPair.Delete(); err != nil {
		s.logger.Log("error", fmt.Sprintf("%#v", err))
	} else {
		s.logger.Log("info", "deleted keypair")
	}

	s.logger.Log("info", fmt.Sprintf("cluster '%s' deleted", key.ClusterID(cluster)))
}

// TODO we need to support this in operatorkit.
func (s *Service) updateFunc(oldObj, newObj interface{}) {
	oldCluster := *oldObj.(*awstpr.CustomObject)<commit_msg>Extract delete logic to processDelete (#477)<commit_after>package create

import (
	"fmt"
	"strings"
	"sync"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/route53"
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/giantswarm/awstpr"
	"github.com/giantswarm/certificatetpr"
	"github.com/giantswarm/microerror"
	"github.com/giantswarm/micrologger"
	"github.com/giantswarm/operatorkit/tpr"
	"github.com/giantswarm/randomkeytpr"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/tools/cache"

	awsutil "github.com/giantswarm/aws-operator/client/aws"
	"github.com/giantswarm/aws-operator/resources"
	awsresources "github.com/giantswarm/aws-operator/resources/aws"
	"github.com/giantswarm/aws-operator/service/cloudconfig"
	"github.com/giantswarm/aws-operator/service/key"
)

const (
	// The format of instance's name is "[name of cluster]-[prefix ('master' or 'worker')]-[number]".
	instanceNameFormat string = "%s-%s-%d"
	// The format of prefix inside a cluster "[name of cluster]-[prefix ('master' or 'worker')]".
	instanceClusterPrefixFormat string = "%s-%s"
	// Prefixes used for machine names.
	prefixMaster  string = "master"
	prefixWorker  string = "worker"
	prefixIngress string = "ingress"
	// Suffixes used for subnets
	suffixPublic  string = "public"
	suffixPrivate string = "private"
	// Number of retries of RunInstances to wait for Roles to propagate to
	// Instance Profiles
	runInstancesRetries = 10
	// The number of seconds AWS will wait, before issuing a health check on
	// instances in an Auto Scaling Group.
	gracePeriodSeconds = 10
)

// Config represents the configuration used to create a version service.
type Config struct {
	// Dependencies.
	CertWatcher *certificatetpr.Service
	CloudConfig *cloudconfig.CloudConfig
	K8sClient   kubernetes.Interface
	KeyWatcher  *randomkeytpr.Service
	Logger      micrologger.Logger

	// Settings.
	AwsConfig     awsutil.Config
	AwsHostConfig awsutil.Config
	PubKeyFile    string
}

// DefaultConfig provides a default configuration to create a new service by
// best effort.
func DefaultConfig() Config {
	return Config{
		// Dependencies.
		CertWatcher: nil,
		CloudConfig: nil,
		K8sClient:   nil,
		KeyWatcher:  nil,
		Logger:      nil,

		// Settings.
		AwsConfig:     awsutil.Config{},
		AwsHostConfig: awsutil.Config{},
		PubKeyFile:    "",
	}
}

// New creates a new configured service.
func New(config Config) (*Service, error) {
	// Dependencies.
	if config.CertWatcher == nil {
		return nil, microerror.Maskf(invalidConfigError, "config.CertWatcher must not be empty")
	}
	if config.CloudConfig == nil {
		return nil, microerror.Maskf(invalidConfigError, "config.CloudConfig must not be empty")
	}
	if config.K8sClient == nil {
		return nil, microerror.Maskf(invalidConfigError, "config.K8sClient must not be empty")
	}
	if config.KeyWatcher == nil {
		return nil, microerror.Maskf(invalidConfigError, "config.KeyWatcher must not be empty")
	}
	if config.Logger == nil {
		return nil, microerror.Maskf(invalidConfigError, "config.Logger must not be empty")
	}

	// Settings.
	var emptyAwsConfig awsutil.Config
	if config.AwsConfig == emptyAwsConfig {
		return nil, microerror.Maskf(invalidConfigError, "config.AwsConfig must not be empty")
	}
	if config.AwsHostConfig == emptyAwsConfig {
		return nil, microerror.Maskf(invalidConfigError, "config.AwsHostConfig must not be empty")
	}
	if config.PubKeyFile == "" {
		return nil, microerror.Maskf(invalidConfigError, "config.PubKeyFile must not be empty")
	}

	var err error

	var newTPR *tpr.TPR
	{
		tprConfig := tpr.DefaultConfig()

		tprConfig.K8sClient = config.K8sClient
		tprConfig.Logger = config.Logger

		tprConfig.Description = awstpr.Description
		tprConfig.Name = awstpr.Name
		tprConfig.Version = awstpr.VersionV1

		newTPR, err = tpr.New(tprConfig)
		if err != nil {
			return nil, microerror.Mask(err)
		}
	}

	newService := &Service{
		// Dependencies.
		certWatcher: config.CertWatcher,
		cloudConfig: config.CloudConfig,
		k8sClient:   config.K8sClient,
		keyWatcher:  config.KeyWatcher,
		logger:      config.Logger,

		// Internals
		bootOnce: sync.Once{},
		tpr:      newTPR,

		// Settings.
		awsConfig:     config.AwsConfig,
		awsHostConfig: config.AwsHostConfig,
		pubKeyFile:    config.PubKeyFile,
	}

	return newService, nil
}

// Service implements the version service interface.
type Service struct {
	// Dependencies.
	certWatcher *certificatetpr.Service
	cloudConfig *cloudconfig.CloudConfig
	k8sClient   kubernetes.Interface
	keyWatcher  *randomkeytpr.Service
	logger      micrologger.Logger

	// Internals.
	bootOnce sync.Once
	tpr      *tpr.TPR

	// Settings.
	awsConfig     awsutil.Config
	awsHostConfig awsutil.Config
	pubKeyFile    string
}

type Event struct {
	Type   string
	Object *awstpr.CustomObject
}

func (s *Service) Boot() {
	s.bootOnce.Do(func() {
		err := s.tpr.CreateAndWait()
		if tpr.IsAlreadyExists(err) {
			s.logger.Log("debug", "third party resource already exists")
		} else if err != nil {
			s.logger.Log("error", fmt.Sprintf("%#v", err))
			return
		}

		s.logger.Log("debug", "starting list/watch")

		newResourceEventHandler := &cache.ResourceEventHandlerFuncs{
			AddFunc:    s.addFunc,
			DeleteFunc: s.deleteFunc,
			UpdateFunc: s.updateFunc,
		}
		newZeroObjectFactory := &tpr.ZeroObjectFactoryFuncs{
			NewObjectFunc:     func() runtime.Object { return &awstpr.CustomObject{} },
			NewObjectListFunc: func() runtime.Object { return &awstpr.List{} },
		}

		s.tpr.NewInformer(newResourceEventHandler, newZeroObjectFactory).Run(nil)
	})
}

func (s *Service) addFunc(obj interface{}) {
	customObject, ok := obj.(*awstpr.CustomObject)
	if !ok {
		s.logger.Log("error", "could not convert to awstpr.CustomObject")
		return
	}
	cluster := *customObject

	s.logger.Log("info", fmt.Sprintf("creating cluster '%s'", key.ClusterID(cluster)))

	if err := validateCluster(cluster); err != nil {
		s.logger.Log("error", "cluster spec is invalid: '%#v'", err)
		return
	}

	err := s.processCluster(cluster)
	if err != nil {
		s.logger.Log("error", fmt.Sprintf("error processing cluster '%s': '%#v'", key.ClusterID(cluster), err))
		return
	}

	s.logger.Log("info", fmt.Sprintf("cluster '%s' processed", key.ClusterID(cluster)))
}

func (s *Service) deleteFunc(obj interface{}) {
	// We can receive an instance of awstpr.CustomObject or cache.DeletedFinalStateUnknown.
	// We need to assert the type properly and log an error when we cannot do that.
	// Also, the cache.DeleteFinalStateUnknown object can contain the proper CustomObject,
	// but doesn't have to.
	// https://github.com/kubernetes/client-go/blob/7ba6be594966f4bec08a57a60c855d17a9f7000a/tools/cache/delta_fifo.go#L674-L677
	var cluster awstpr.CustomObject
	clusterPtr, ok := obj.(*awstpr.CustomObject)
	if ok {
		cluster = *clusterPtr
	} else {
		deletedObj, ok := obj.(cache.DeletedFinalStateUnknown)
		if !ok {
			s.logger.Log("error", "received unknown type of third-party object")
			return
		}
		clusterPtr, ok := deletedObj.Obj.(*awstpr.CustomObject)
		if !ok {
			s.logger.Log("error", "received the proper delete request, but the type of third-party object is unknown")
			return
		}
		cluster = *clusterPtr
	}

	s.logger.Log("info", fmt.Sprintf("deleting cluster '%s'", key.ClusterID(cluster)))

	err := s.processDelete(cluster)
	if err != nil {
		s.logger.Log("error", fmt.Sprintf("error deleting cluster '%s': '%#v'", key.ClusterID(cluster), err))
		return
	}

	s.logger.Log("info", fmt.Sprintf("cluster '%s' deleted", key.ClusterID(cluster)))
}

func (s *Service) processCluster(cluster awstpr.CustomObject) error {
	// Create cluster namespace in k8s.
	if err := s.createClusterNamespace(cluster.Spec.Cluster); err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create cluster namespace: '%#v'", err))
	}

	// Create AWS guest cluster client.
	s.awsConfig.Region = cluster.Spec.AWS.Region
	clients := awsutil.NewClients(s.awsConfig)
	if err := s.awsConfig.SetAccountID(clients.IAM); err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not retrieve guest amazon account id: '%#v'", err))
	}

	// Create AWS host cluster client.
	s.awsHostConfig.Region = cluster.Spec.AWS.Region
	hostClients := awsutil.NewClients(s.awsHostConfig)
	if err := s.awsHostConfig.SetAccountID(hostClients.IAM); err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not retrieve host amazon account id: '%#v'", err))
	}

	// An EC2 Keypair is needed for legacy clusters. New clusters provide SSH keys via cloud config.
	if !key.HasClusterVersion(cluster) {
		// Create keypair.
		var keyPair resources.ReusableResource
		var keyPairCreated bool
		{
			var err error
			keyPair = &awsresources.KeyPair{
				ClusterName: key.ClusterID(cluster),
				Provider:    awsresources.NewFSKeyPairProvider(s.pubKeyFile),
				AWSEntity:   awsresources.AWSEntity{Clients: clients},
			}
			keyPairCreated, err = keyPair.CreateIfNotExists()
			if err != nil {
				return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create keypair: '%#v'", err))
			}
		}

		if keyPairCreated {
			s.logger.Log("info", fmt.Sprintf("created keypair '%s'", key.ClusterID(cluster)))
		} else {
			s.logger.Log("info", fmt.Sprintf("keypair '%s' already exists, reusing", key.ClusterID(cluster)))
		}
	}

	s.logger.Log("info", fmt.Sprintf("waiting for k8s secrets..."))
	clusterID := key.ClusterID(cluster)
	certs, err := s.certWatcher.SearchCerts(clusterID)
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not get certificates from secrets: '%#v'", err))
	}

	// Create Encryption key
	s.logger.Log("info", fmt.Sprintf("waiting for k8s keys..."))
	keys, err := s.keyWatcher.SearchKeys(clusterID)
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not get keys from secrets: '%#v'", err))
	}

	// Create KMS key.
	kmsKey := &awsresources.KMSKey{
		Name:      key.ClusterID(cluster),
		AWSEntity: awsresources.AWSEntity{Clients: clients},
	}

	kmsCreated, kmsKeyErr := kmsKey.CreateIfNotExists()
	if kmsKeyErr != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create KMS key: '%#v'", kmsKeyErr))
	}

	if kmsCreated {
		s.logger.Log("info", fmt.Sprintf("created KMS key for cluster '%s'", key.ClusterID(cluster)))
	} else {
		s.logger.Log("info", fmt.Sprintf("kms key '%s' already exists, reusing", kmsKey.Name))
	}

	// Encode TLS assets.
	tlsAssets, err := s.encodeTLSAssets(certs, clients.KMS, kmsKey.Arn())
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not encode TLS assets: '%#v'", err))
	}

	// Encode Key assets.
	clusterKeys, err := s.encodeKeyAssets(keys, clients.KMS, kmsKey.Arn())
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not encode Keys assets: '%#v'", err))
	}

	bucketName := s.bucketName(cluster)

	// Create master IAM policy.
	var masterPolicy resources.NamedResource
	var masterPolicyCreated bool
	{
		var err error
		masterPolicy = &awsresources.Policy{
			ClusterID:  key.ClusterID(cluster),
			KMSKeyArn:  kmsKey.Arn(),
			PolicyType: prefixMaster,
			S3Bucket:   bucketName,
			AWSEntity:  awsresources.AWSEntity{Clients: clients},
		}
		masterPolicyCreated, err = masterPolicy.CreateIfNotExists()
		if err != nil {
			return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create master policy: '%#v'", err))
		}
	}
	if masterPolicyCreated {
		s.logger.Log("info", fmt.Sprintf("created master policy for cluster '%s'", key.ClusterID(cluster)))
	} else {
		s.logger.Log("info", fmt.Sprintf("master policy for cluster '%s' already exists, reusing", key.ClusterID(cluster)))
	}

	// Create worker IAM policy.
	var workerPolicy resources.NamedResource
	var workerPolicyCreated bool
	{
		var err error
		workerPolicy = &awsresources.Policy{
			ClusterID:  key.ClusterID(cluster),
			KMSKeyArn:  kmsKey.Arn(),
			PolicyType: prefixWorker,
			S3Bucket:   bucketName,
			AWSEntity:  awsresources.AWSEntity{Clients: clients},
		}
		workerPolicyCreated, err = workerPolicy.CreateIfNotExists()
		if err != nil {
			return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create worker policy: '%#v'", err))
		}
	}
	if workerPolicyCreated {
		s.logger.Log("info", fmt.Sprintf("created worker policy for cluster '%s'", key.ClusterID(cluster)))
	} else {
		s.logger.Log("info", fmt.Sprintf("worker policy for cluster '%s' already exists, reusing", key.ClusterID(cluster)))
	}

	// Create S3 bucket.
	var bucket resources.ReusableResource
	var bucketCreated bool
	{
		var err error
		bucket = &awsresources.Bucket{
			Name:      bucketName,
			AWSEntity: awsresources.AWSEntity{Clients: clients},
		}
		bucketCreated, err = bucket.CreateIfNotExists()
		if err != nil {
			return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create S3 bucket: '%#v'", err))
		}
	}

	if bucketCreated {
		s.logger.Log("info", fmt.Sprintf("created bucket '%s'", bucketName))
	} else {
		s.logger.Log("info", fmt.Sprintf("bucket '%s' already exists, reusing", bucketName))
	}

	// Create VPC.
	var vpc resources.ResourceWithID
	vpc = &awsresources.VPC{
		CidrBlock: cluster.Spec.AWS.VPC.CIDR,
		Name:      key.ClusterID(cluster),
		AWSEntity: awsresources.AWSEntity{Clients: clients},
	}
	vpcCreated, err := vpc.CreateIfNotExists()
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create VPC: '%#v'", err))
	}
	if vpcCreated {
		s.logger.Log("info", fmt.Sprintf("created vpc for cluster '%s'", key.ClusterID(cluster)))
	} else {
		s.logger.Log("info", fmt.Sprintf("vpc for cluster '%s' already exists, reusing", key.ClusterID(cluster)))
	}
	vpcID, err := vpc.GetID()
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not get VPC ID: '%#v'", err))
	}

	// Create VPC peering connection.
	vpcPeeringConection := &awsresources.VPCPeeringConnection{
		VPCId:     vpcID,
		PeerVPCId: cluster.Spec.AWS.VPC.PeerID,
		AWSEntity: awsresources.AWSEntity{
			Clients:     clients,
			HostClients: hostClients,
		},
		Logger: s.logger,
	}
	vpcPeeringConnectionCreated, err := vpcPeeringConection.CreateIfNotExists()
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create vpc peering connection: '%#v'", err))
	}
	if vpcPeeringConnectionCreated {
		s.logger.Log("info", fmt.Sprintf("created vpc peering connection for cluster '%s'", key.ClusterID(cluster)))
	} else {
		s.logger.Log("info", fmt.Sprintf("vpc peering connection for cluster '%s' already exists, reusing", key.ClusterID(cluster)))
	}

	conn, err := vpcPeeringConection.FindExisting()
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not find vpc peering connection: '%#v'", err))
	}

	// Create internet gateway.
	var internetGateway resources.ResourceWithID
	internetGateway = &awsresources.InternetGateway{
		Name:  key.ClusterID(cluster),
		VpcID: vpcID,
		// Dependencies.
		Logger:    s.logger,
		AWSEntity: awsresources.AWSEntity{Clients: clients},
	}
	internetGatewayCreated, err := internetGateway.CreateIfNotExists()
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create internet gateway: '%#v'", err))
	}
	if internetGatewayCreated {
		s.logger.Log("info", fmt.Sprintf("created internet gateway for cluster '%s'", key.ClusterID(cluster)))
	} else {
		s.logger.Log("info", fmt.Sprintf("internet gateway for cluster '%s' already exists, reusing", key.ClusterID(cluster)))
	}

	// Create masters security group.
	mastersSGInput := securityGroupInput{
		Clients:   clients,
		GroupName: key.SecurityGroupName(cluster, prefixMaster),
		VPCID:     vpcID,
	}
	mastersSecurityGroup, err := s.createSecurityGroup(mastersSGInput)
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create security group '%s': '%#v'", mastersSGInput.GroupName, err))
	}
	mastersSecurityGroupID, err := mastersSecurityGroup.GetID()
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not get security group '%s' ID: '%#v'", mastersSGInput.GroupName, err))
	}

	// Create workers security group.
	workersSGInput := securityGroupInput{
		Clients:   clients,
		GroupName: key.SecurityGroupName(cluster, prefixWorker),
		VPCID:     vpcID,
	}
	workersSecurityGroup, err := s.createSecurityGroup(workersSGInput)
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create security group '%s': '%#v'", workersSGInput.GroupName, err))
	}
	workersSecurityGroupID, err := workersSecurityGroup.GetID()
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not get security group '%s' ID: '%#v'", workersSGInput.GroupName, err))
	}

	// Create ingress ELB security group.
	ingressSGInput := securityGroupInput{
		Clients:   clients,
		GroupName: key.SecurityGroupName(cluster, prefixIngress),
		VPCID:     vpcID,
	}
	ingressSecurityGroup, err := s.createSecurityGroup(ingressSGInput)
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create security group '%s': '%#v'", ingressSGInput.GroupName, err))
	}
	ingressSecurityGroupID, err := ingressSecurityGroup.GetID()
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not get security group '%s' ID: '%#v'", ingressSGInput.GroupName, err))
	}

	// In rare cases the host cluster CIDR block can be nil. Return error rather
	// than panic. TODO Fix issue #451 that tracks this.
	if conn.AccepterVpcInfo.CidrBlock == nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("vpc peering connection cidr block is nil for cluster '%s'", key.ClusterID(cluster)))
	}

	// Create rules for the security groups.
	rulesInput := rulesInput{
		Cluster:                cluster,
		MastersSecurityGroupID: mastersSecurityGroupID,
		WorkersSecurityGroupID: workersSecurityGroupID,
		IngressSecurityGroupID: ingressSecurityGroupID,
		HostClusterCIDR:        *conn.AccepterVpcInfo.CidrBlock,
	}

	if err := mastersSecurityGroup.ApplyRules(rulesInput.masterRules()); err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create rules for security group '%s': '%#v", mastersSecurityGroup.GroupName, err))
	}

	if err := workersSecurityGroup.ApplyRules(rulesInput.workerRules()); err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create rules for security group '%s': '%#v'", workersSecurityGroup.GroupName, err))
	}

	if err := ingressSecurityGroup.ApplyRules(rulesInput.ingressRules()); err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create rules for security group '%s': '%#v'", ingressSecurityGroup.GroupName, err))
	}

	// Create public route table.
	publicRouteTable := &awsresources.RouteTable{
		Name:   key.ClusterID(cluster),
		VpcID:  vpcID,
		Client: clients.EC2,
		Logger: s.logger,
	}
	publicRouteTableCreated, err := publicRouteTable.CreateIfNotExists()
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create route table: '%#v'", err))
	}
	if publicRouteTableCreated {
		s.logger.Log("info", "created public route table")
	} else {
		s.logger.Log("info", "route table already exists, reusing")
	}

	if err := publicRouteTable.MakePublic(); err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not make route table public: '%#v'", err))
	}

	// Create public subnet.
	subnetInput := SubnetInput{
		Name:       key.SubnetName(cluster, suffixPublic),
		CidrBlock:  cluster.Spec.AWS.VPC.PublicSubnetCIDR,
		Clients:    clients,
		Cluster:    cluster,
		MakePublic: true,
		RouteTable: publicRouteTable,
		VpcID:      vpcID,
	}
	publicSubnet, err := s.createSubnet(subnetInput)
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create public subnet: '%#v'", err))
	}

	publicSubnetID, err := publicSubnet.GetID()
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not get public subnet ID: '%#v'", err))
	}

	var privateRouteTable *awsresources.RouteTable
	var privateSubnet *awsresources.Subnet
	var privateSubnetID string

	// For new clusters create a NAT gateway, private route table and private subnet.
	if key.HasClusterVersion(cluster) {
		// Create NAT gateway.
		natGateway := &awsresources.NatGateway{
			Name:   key.ClusterID(cluster),
			Subnet: publicSubnet,
			// Dependencies.
			Logger:    s.logger,
			AWSEntity: awsresources.AWSEntity{Clients: clients},
		}
		natGatewayCreated, err := natGateway.CreateIfNotExists()
		if err != nil {
			return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create nat gateway: '%#v'", err))
		}
		if natGatewayCreated {
			s.logger.Log("info", fmt.Sprintf("created nat gateway for cluster '%s'", key.ClusterID(cluster)))
		} else {
			s.logger.Log("info", fmt.Sprintf("nat gateway for cluster '%s' already exists, reusing", key.ClusterID(cluster)))
		}

		natGatewayID, err := natGateway.GetID()
		if err != nil {
			return microerror.Maskf(executionFailedError, fmt.Sprintf("could not get nat gateway id: '%#v'", err))
		}

		// Create private route table.
		privateRouteTable = &awsresources.RouteTable{
			Name:   key.RouteTableName(cluster, suffixPrivate),
			VpcID:  vpcID,
			Client: clients.EC2,
			Logger: s.logger,
		}
		privateRouteTableCreated, err := privateRouteTable.CreateIfNotExists()
		if err != nil {
			return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create route table: '%#v'", err))
		}
		if privateRouteTableCreated {
			s.logger.Log("info", "created private route table")
		} else {
			s.logger.Log("info", "private route table already exists, reusing")
		}

		// Create default route for the NAT gateway in the private route table.
		natGatewayRouteCreated, err := privateRouteTable.CreateNatGatewayRoute(natGatewayID)
		if err != nil {
			return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create route for nat gateway: '%#v'", err))
		}
		if natGatewayRouteCreated {
			s.logger.Log("info", "created nat gateway route")
		} else {
			s.logger.Log("info", "nat gateway route already exists, reusing")
		}

		// Create private subnet.
		subnetInput := SubnetInput{
			Name:       key.SubnetName(cluster, suffixPrivate),
			CidrBlock:  cluster.Spec.AWS.VPC.PrivateSubnetCIDR,
			Clients:    clients,
			Cluster:    cluster,
			MakePublic: false,
			RouteTable: privateRouteTable,
			VpcID:      vpcID,
		}
		privateSubnet, err = s.createSubnet(subnetInput)
		if err != nil {
			return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create private subnet: '%#v'", err))
		}

		privateSubnetID, err = privateSubnet.GetID()
		if err != nil {
			return microerror.Maskf(executionFailedError, fmt.Sprintf("could not get private subnet ID: '%#v'", err))
		}
	}

	hostClusterRoute := &awsresources.Route{
		RouteTable:             *publicRouteTable,
		DestinationCidrBlock:   *conn.AccepterVpcInfo.CidrBlock,
		VpcPeeringConnectionID: *conn.VpcPeeringConnectionId,
		AWSEntity:              awsresources.AWSEntity{Clients: clients},
	}

	if key.HasClusterVersion(cluster) {
		// New clusters have private IPs so use the private route table.
		hostClusterRoute.RouteTable = *privateRouteTable
	} else {
		// Legacy clusters have public IPs so use the public route table.
		hostClusterRoute.RouteTable = *publicRouteTable
	}

	hostRouteCreated, err := hostClusterRoute.CreateIfNotExists()
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not add host vpc route: '%#v'", err))
	}
	if hostRouteCreated {
		s.logger.Log("info", fmt.Sprintf("created host vpc route for cluster '%s'", key.ClusterID(cluster)))
	} else {
		s.logger.Log("info", fmt.Sprintf("host vpc route for cluster '%s' already exists, reusing", key.ClusterID(cluster)))
	}

	for _, privateRouteTableName := range cluster.Spec.AWS.VPC.RouteTableNames {
		privateRouteTable := &awsresources.RouteTable{
			Name:   privateRouteTableName,
			VpcID:  cluster.Spec.AWS.VPC.PeerID,
			Client: hostClients.EC2,
			Logger: s.logger,
		}

		privateRoute := &awsresources.Route{
			RouteTable:             *privateRouteTable,
			DestinationCidrBlock:   *conn.RequesterVpcInfo.CidrBlock,
			VpcPeeringConnectionID: *conn.VpcPeeringConnectionId,
			AWSEntity:              awsresources.AWSEntity{Clients: hostClients},
		}

		privateRouteCreated, err := privateRoute.CreateIfNotExists()
		if err != nil {
			return microerror.Maskf(executionFailedError, fmt.Sprintf("could not add guest vpc route: '%#v'", err))
		}
		if privateRouteCreated {
			s.logger.Log("info", fmt.Sprintf("created guest vpc route for cluster '%s'", key.ClusterID(cluster)))
		} else {
			s.logger.Log("info", fmt.Sprintf("host vpc guest for cluster '%s' already exists, reusing", key.ClusterID(cluster)))
		}

	}

	mastersInput := runMachinesInput{
		clients:             clients,
		cluster:             cluster,
		tlsAssets:           tlsAssets,
		clusterKeys:         clusterKeys,
		clusterName:         key.ClusterID(cluster),
		bucket:              bucket,
		securityGroup:       mastersSecurityGroup,
		instanceProfileName: masterPolicy.GetName(),
		prefix:              prefixMaster,
	}

	if key.HasClusterVersion(cluster) {
		// New clusters have masters in the private subnet.
		mastersInput.subnet = privateSubnet
	} else {
		// Legacy clusters have masters in the public subnet.
		mastersInput.subnet = publicSubnet

		// An EC2 Keypair is needed for legacy clusters. New clusters provide SSH keys via cloud config.
		mastersInput.keyPairName = key.ClusterID(cluster)
	}

	// Run masters.
	anyMastersCreated, masterIDs, err := s.runMachines(mastersInput)
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not start masters: '%#v'", err))
	}

	if !validateIDs(masterIDs) {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("master nodes had invalid instance IDs: %v", masterIDs))
	}

	// Create apiserver load balancer.
	lbInput := LoadBalancerInput{
		Name:               cluster.Spec.Cluster.Kubernetes.API.Domain,
		Clients:            clients,
		Cluster:            cluster,
		IdleTimeoutSeconds: cluster.Spec.AWS.ELB.IdleTimeoutSeconds.API,
		InstanceIDs:        masterIDs,
		PortsToOpen: awsresources.PortPairs{
			{
				PortELB:      cluster.Spec.Cluster.Kubernetes.API.SecurePort,
				PortInstance: cluster.Spec.Cluster.Kubernetes.API.SecurePort,
			},
		},
		SecurityGroupID: mastersSecurityGroupID,
		SubnetID:        publicSubnetID,
	}

	apiLB, err := s.createLoadBalancer(lbInput)
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create apiserver load balancer: '%#v'", err))
	}

	// Create etcd load balancer.
	lbInput = LoadBalancerInput{
		Name:               cluster.Spec.Cluster.Etcd.Domain,
		Clients:            clients,
		Cluster:            cluster,
		IdleTimeoutSeconds: cluster.Spec.AWS.ELB.IdleTimeoutSeconds.Etcd,
		InstanceIDs:        masterIDs,
		PortsToOpen: awsresources.PortPairs{
			{
				PortELB:      httpsPort,
				PortInstance: cluster.Spec.Cluster.Etcd.Port,
			},
		},
		SecurityGroupID: mastersSecurityGroupID,
		SubnetID:        publicSubnetID,
	}

	if key.HasClusterVersion(cluster) {
		lbInput.Scheme = "internal"
	}

	etcdLB, err := s.createLoadBalancer(lbInput)
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create etcd load balancer: '%#v'", err))
	}

	// Masters were created but the master IAM policy existed from a previous
	// execution. Its likely that previous execution failed. IAM policies can't
	// be reused for EC2 instances.
	if anyMastersCreated && !masterPolicyCreated {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("cluster '%s' cannot be processed. As IAM policy for master nodes cannot be reused. Please delete this cluster.", key.ClusterID(cluster)))
	}

	// Create Ingress load balancer.
	lbInput = LoadBalancerInput{
		Name:               cluster.Spec.Cluster.Kubernetes.IngressController.Domain,
		Clients:            clients,
		Cluster:            cluster,
		IdleTimeoutSeconds: cluster.Spec.AWS.ELB.IdleTimeoutSeconds.Ingress,
		PortsToOpen: awsresources.PortPairs{
			{
				PortELB:      httpsPort,
				PortInstance: cluster.Spec.Cluster.Kubernetes.IngressController.SecurePort,
			},
			{
				PortELB:      httpPort,
				PortInstance: cluster.Spec.Cluster.Kubernetes.IngressController.InsecurePort,
			},
		},
		SecurityGroupID: ingressSecurityGroupID,
		SubnetID:        publicSubnetID,
	}

	ingressLB, err := s.createLoadBalancer(lbInput)
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create ingress load balancer: '%#v'", err))
	}

	// Create a launch configuration for the worker nodes.
	lcInput := launchConfigurationInput{
		clients:             clients,
		cluster:             cluster,
		clusterKeys:         clusterKeys,
		tlsAssets:           tlsAssets,
		bucket:              bucket,
		securityGroup:       workersSecurityGroup,
		subnet:              publicSubnet,
		instanceProfileName: workerPolicy.GetName(),
		prefix:              prefixWorker,
		ebsStorage:          true,
	}

	// For new clusters don't assign public IPs and use the private subnet.
	if key.HasClusterVersion(cluster) {
		lcInput.associatePublicIP = false
		lcInput.subnet = privateSubnet
	} else {
		lcInput.associatePublicIP = true
		lcInput.subnet = publicSubnet
	}

	// An EC2 Keypair is needed for legacy clusters. New clusters provide SSH keys via cloud config.
	if !key.HasClusterVersion(cluster) {
		lcInput.keypairName = key.ClusterID(cluster)
	}

	lcCreated, err := s.createLaunchConfiguration(lcInput)
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create launch config: '%#v'", err))
	}

	if lcCreated {
		s.logger.Log("info", fmt.Sprintf("created worker launch config"))
	} else {
		s.logger.Log("info", fmt.Sprintf("launch config %s already exists, reusing", key.ClusterID(cluster)))
	}

	workersLCName, err := launchConfigurationName(cluster, "worker", workersSecurityGroupID)
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not get launch config name: '%#v'", err))
	}

	asg := awsresources.AutoScalingGroup{
		Client:                  clients.AutoScaling,
		Name:                    key.AutoScalingGroupName(cluster, prefixWorker),
		ClusterID:               key.ClusterID(cluster),
		MinSize:                 key.WorkerCount(cluster),
		MaxSize:                 key.WorkerCount(cluster),
		AvailabilityZone:        key.AvailabilityZone(cluster),
		LaunchConfigurationName: workersLCName,
		LoadBalancerName:        ingressLB.Name,
		VPCZoneIdentifier:       publicSubnetID,
		HealthCheckGracePeriod:  gracePeriodSeconds,
	}

	// For new clusters launch the workers in the private subnet.
	if key.HasClusterVersion(cluster) {
		asg.VPCZoneIdentifier = privateSubnetID
	} else {
		asg.VPCZoneIdentifier = publicSubnetID
	}

	asgCreated, err := asg.CreateIfNotExists()
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create auto scaling group: '%#v'", err))
	}

	if asgCreated {
		s.logger.Log("info", fmt.Sprintf("created auto scaling group '%s' with size %v", asg.Name, key.WorkerCount(cluster)))
	} else {
		s.logger.Log("info", fmt.Sprintf("auto scaling group '%s' already exists, reusing", asg.Name))
	}

	// Create Record Sets for the Load Balancers.
	recordSetInputs := []recordSetInput{
		{
			Cluster:      cluster,
			Client:       clients.Route53,
			Resource:     apiLB,
			Domain:       cluster.Spec.Cluster.Kubernetes.API.Domain,
			HostedZoneID: cluster.Spec.AWS.HostedZones.API,
			Type:         route53.RRTypeA,
		},
		{
			Cluster:      cluster,
			Client:       clients.Route53,
			Resource:     etcdLB,
			Domain:       cluster.Spec.Cluster.Etcd.Domain,
			HostedZoneID: cluster.Spec.AWS.HostedZones.Etcd,
			Type:         route53.RRTypeA,
		},
		{
			Cluster:      cluster,
			Client:       clients.Route53,
			Resource:     ingressLB,
			Domain:       cluster.Spec.Cluster.Kubernetes.IngressController.Domain,
			HostedZoneID: cluster.Spec.AWS.HostedZones.Ingress,
			Type:         route53.RRTypeA,
		},
		{
			Cluster:      cluster,
			Client:       clients.Route53,
			Domain:       cluster.Spec.Cluster.Kubernetes.IngressController.WildcardDomain,
			HostedZoneID: cluster.Spec.AWS.HostedZones.Ingress,
			Value:        cluster.Spec.Cluster.Kubernetes.IngressController.Domain,
			Type:         route53.RRTypeCname,
		},
	}

	var rsErr error
	for _, input := range recordSetInputs {
		if rsErr = s.createRecordSet(input); rsErr != nil {
			return microerror.Maskf(executionFailedError, fmt.Sprintf("could not create record set '%#v'", rsErr))
		}
	}
	if rsErr == nil {
		s.logger.Log("info", fmt.Sprintf("created DNS records for load balancers"))
	}

	return nil
}

func (s *Service) processDelete(cluster awstpr.CustomObject) error {
	if err := validateCluster(cluster); err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("cluster spec is invalid: '%#v'", err))
	}

	if err := s.deleteClusterNamespace(cluster.Spec.Cluster); err != nil {
		s.logger.Log("error", "could not delete cluster namespace:", err)
	}

	clients := awsutil.NewClients(s.awsConfig)
	err := s.awsConfig.SetAccountID(clients.IAM)
	if err != nil {
		return microerror.Maskf(executionFailedError, fmt.Sprintf("could not retrieve amazon account id: '%#v'", err))
	}

	// Retrieve AWS host cluster client.
	s.awsHostConfig.Region = cluster.Spec.AWS.Region
	hostClients := awsutil.NewClients(s.awsHostConfig)
	if err := s.awsHostConfig.SetAccountID(hostClients.IAM); err != nil {
		s.logger.Log("error", fmt.Sprintf("could not retrieve host amazon account id: '%#v'", err))
	}

	// Delete masters.
	s.logger.Log("info", "deleting masters...")
	if err := s.deleteMachines(deleteMachinesInput{
		clients:     clients,
		clusterName: key.ClusterID(cluster),
		prefix:      prefixMaster,
	}); err != nil {
		s.logger.Log("error", fmt.Sprintf("%#v", err))
	} else {
		s.logger.Log("info", "deleted masters")
	}

	// Delete workers Auto Scaling Group.
	asg := awsresources.AutoScalingGroup{
		Client: clients.AutoScaling,
		Name:   key.AutoScalingGroupName(cluster, prefixWorker),
	}

	if err := asg.Delete(); err != nil {
		s.logger.Log("error", fmt.Sprintf("%#v", err))
	} else {
		s.logger.Log("info", "deleted workers auto scaling group")
	}

	// Delete workers launch configuration.
	lcInput := launchConfigurationInput{
		clients: clients,
		cluster: cluster,
		prefix:  "worker",
	}
	if err := s.deleteLaunchConfiguration(lcInput); err != nil {
		s.logger.Log("error", fmt.Sprintf("%#v", err))
	} else {
		s.logger.Log("info", "deleted worker launch config")
	}

	// Delete Record Sets.
	apiLBName, err := loadBalancerName(cluster.Spec.Cluster.Kubernetes.API.Domain, cluster)
	etcdLBName, err := loadBalancerName(cluster.Spec.Cluster.Etcd.Domain, cluster)
	ingressLBName, err := loadBalancerName(cluster.Spec.Cluster.Kubernetes.IngressController.Domain, cluster)
	if err != nil {
		s.logger.Log("error", fmt.Sprintf("%#v", err))
	} else {
		apiLB, err := awsresources.NewELBFromExisting(apiLBName, clients.ELB)
		etcdLB, err := awsresources.NewELBFromExisting(etcdLBName, clients.ELB)
		ingressLB, err := awsresources.NewELBFromExisting(ingressLBName, clients.ELB)
		if err != nil {
			s.logger.Log("error", fmt.Sprintf("%#v", err))
		} else {
			recordSetInputs := []recordSetInput{
				{
					Cluster:      cluster,
					Client:       clients.Route53,
					Resource:     apiLB,
					Domain:       cluster.Spec.Cluster.Kubernetes.API.Domain,
					HostedZoneID: cluster.Spec.AWS.HostedZones.API,
					Type:         route53.RRTypeA,
				},
				{
					Cluster:      cluster,
					Client:       clients.Route53,
					Resource:     etcdLB,
					Domain:       cluster.Spec.Cluster.Etcd.Domain,
					HostedZoneID: cluster.Spec.AWS.HostedZones.Etcd,
					Type:         route53.RRTypeA,
				},
				{
					Cluster:      cluster,
					Client:       clients.Route53,
					Resource:     ingressLB,
					Domain:       cluster.Spec.Cluster.Kubernetes.IngressController.Domain,
					HostedZoneID: cluster.Spec.AWS.HostedZones.Ingress,
					Type:         route53.RRTypeA,
				},
				{
					Cluster:      cluster,
					Client:       clients.Route53,
					Value:        cluster.Spec.Cluster.Kubernetes.IngressController.Domain,
					Domain:       cluster.Spec.Cluster.Kubernetes.IngressController.WildcardDomain,
					HostedZoneID: cluster.Spec.AWS.HostedZones.Ingress,
					Type:         route53.RRTypeCname,
				},
			}

			var rsErr error
			for _, input := range recordSetInputs {
				if rsErr = s.deleteRecordSet(input); rsErr != nil {
					s.logger.Log("error", fmt.Sprintf("%#v", rsErr))
				}
			}
			if rsErr == nil {
				s.logger.Log("info", "deleted record sets")
			}
		}
	}

	// Delete Load Balancers.
	loadBalancerInputs := []LoadBalancerInput{
		{
			Name:    cluster.Spec.Cluster.Kubernetes.API.Domain,
			Clients: clients,
			Cluster: cluster,
		},
		{
			Name:    cluster.Spec.Cluster.Etcd.Domain,
			Clients: clients,
			Cluster: cluster,
		},
		{
			Name:    cluster.Spec.Cluster.Kubernetes.IngressController.Domain,
			Clients: clients,
			Cluster: cluster,
		},
	}

	var elbErr error
	for _, lbInput := range loadBalancerInputs {
		if elbErr = s.deleteLoadBalancer(lbInput); elbErr != nil {
			s.logger.Log("error", fmt.Sprintf("%#v", elbErr))
		}
	}
	if elbErr == nil {
		s.logger.Log("info", "deleted ELBs")
	}

	// Delete route table.
	var publicRouteTable resources.ResourceWithID
	publicRouteTable = &awsresources.RouteTable{
		Name:   key.ClusterID(cluster),
		Client: clients.EC2,
		Logger: s.logger,
	}
	if err := publicRouteTable.Delete(); err != nil {
		s.logger.Log("error", fmt.Sprintf("could not delete route table: '%#v'", err))
	} else {
		s.logger.Log("info", "deleted route table")
	}

	// Sync VPC.
	var vpc resources.ResourceWithID
	vpc = &awsresources.VPC{
		Name:      key.ClusterID(cluster),
		AWSEntity: awsresources.AWSEntity{Clients: clients},
		Logger:    s.logger,
	}
	vpcID, err := vpc.GetID()
	if err != nil {
		s.logger.Log("error", fmt.Sprintf("%#v", err))
	}

	// Delete NAT gateway and private subnet for new clusters.
	if key.HasClusterVersion(cluster) {
		// Delete NAT gateway.
		natGateway := &awsresources.NatGateway{
			Name: key.ClusterID(cluster),
			// Dependencies.
			Logger:    s.logger,
			AWSEntity: awsresources.AWSEntity{Clients: clients},
		}
		if err := natGateway.Delete(); err != nil {
			s.logger.Log("error", fmt.Sprintf("could not delete nat gateway: '%#v'", err))
		} else {
			s.logger.Log("info", "deleted nat gateway")
		}

		// Delete private route table.
		privateRouteTable := &awsresources.RouteTable{
			Name:   key.RouteTableName(cluster, suffixPrivate),
			Client: clients.EC2,
			Logger: s.logger,
		}
		if err := privateRouteTable.Delete(); err != nil {
			s.logger.Log("error", fmt.Sprintf("could not delete private route table: '%#v'", err))
		} else {
			s.logger.Log("info", "deleted private route table")
		}

		// Delete private subnet.
		subnetInput := SubnetInput{
			Name:    key.SubnetName(cluster, suffixPrivate),
			Clients: clients,
		}
		if err := s.deleteSubnet(subnetInput); err != nil {
			s.logger.Log("error", fmt.Sprintf("could not delete private subnet: '%#v'", err))
		} else {
			s.logger.Log("info", "deleted private subnet")
		}
	}

	// Delete internet gateway.
	internetGateway := &awsresources.InternetGateway{
		Name:  key.ClusterID(cluster),
		VpcID: vpcID,
		// Dependencies.
		Logger:    s.logger,
		AWSEntity: awsresources.AWSEntity{Clients: clients},
	}
	if err := internetGateway.Delete(); err != nil {
		s.logger.Log("error", fmt.Sprintf("could not delete internet gateway: '%#v'", err))
	} else {
		s.logger.Log("info", "deleted internet gateway")
	}

	// Delete public subnet.
	subnetInput := SubnetInput{
		Name:    key.SubnetName(cluster, suffixPublic),
		Clients: clients,
	}
	if err := s.deleteSubnet(subnetInput); err != nil {
		s.logger.Log("error", fmt.Sprintf("could not delete public subnet: '%#v'", err))
	} else {
		s.logger.Log("info", "deleted public subnet")
	}

	// Before the security groups can be deleted any rules referencing other
	// groups must first be deleted.
	mastersSGRulesInput := securityGroupRulesInput{
		Clients:   clients,
		GroupName: key.SecurityGroupName(cluster, prefixMaster),
	}
	if err := s.deleteSecurityGroupRules(mastersSGRulesInput); err != nil {
		s.logger.Log("error", fmt.Sprintf("could not delete rules for security group '%s': '%#v'", mastersSGRulesInput.GroupName, err))
	}

	workersSGRulesInput := securityGroupRulesInput{
		Clients:   clients,
		GroupName: key.SecurityGroupName(cluster, prefixWorker),
	}
	if err := s.deleteSecurityGroupRules(workersSGRulesInput); err != nil {
		s.logger.Log("error", fmt.Sprintf("could not delete rules for security group '%s': '%#v'", mastersSGRulesInput.GroupName, err))
	}

	ingressSGRulesInput := securityGroupRulesInput{
		Clients:   clients,
		GroupName: key.SecurityGroupName(cluster, prefixIngress),
	}
	if err := s.deleteSecurityGroupRules(ingressSGRulesInput); err != nil {
		s.logger.Log("error", fmt.Sprintf("could not delete rules for security group '%s': '%#v'", mastersSGRulesInput.GroupName, err))
	}

	// Delete masters security group.
	mastersSGInput := securityGroupInput{
		Clients:   clients,
		GroupName: key.SecurityGroupName(cluster, prefixMaster),
	}
	if err := s.deleteSecurityGroup(mastersSGInput); err != nil {
		s.logger.Log("error", fmt.Sprintf("could not delete security group '%s': '%#v'", mastersSGInput.GroupName, err))
	}

	// Delete workers security group.
	workersSGInput := securityGroupInput{
		Clients:   clients,
		GroupName: key.SecurityGroupName(cluster, prefixWorker),
	}
	if err := s.deleteSecurityGroup(workersSGInput); err != nil {
		s.logger.Log("error", fmt.Sprintf("could not delete security group '%s': '%#v'", workersSGInput.GroupName, err))
	}

	// Delete ingress security group.
	ingressSGInput := securityGroupInput{
		Clients:   clients,
		GroupName: key.SecurityGroupName(cluster, prefixIngress),
	}
	if err := s.deleteSecurityGroup(ingressSGInput); err != nil {
		s.logger.Log("error", fmt.Sprintf("could not delete security group '%s': '%#v'", ingressSGInput.GroupName, err))
	}

	vpcPeeringConection := &awsresources.VPCPeeringConnection{
		VPCId:     vpcID,
		PeerVPCId: cluster.Spec.AWS.VPC.PeerID,
		AWSEntity: awsresources.AWSEntity{
			Clients:     clients,
			HostClients: hostClients,
		},
		Logger: s.logger,
	}
	conn, err := vpcPeeringConection.FindExisting()
	if err != nil {
		s.logger.Log("error", fmt.Sprintf("could not find vpc peering connection: '%#v'", err))
	}

	// Delete Guest VPC Routes.
	for _, privateRouteTableName := range cluster.Spec.AWS.VPC.RouteTableNames {
		privateRouteTable := &awsresources.RouteTable{
			Name:   privateRouteTableName,
			VpcID:  cluster.Spec.AWS.VPC.PeerID,
			Client: hostClients.EC2,
			Logger: s.logger,
		}

		privateRoute := &awsresources.Route{
			RouteTable:             *privateRouteTable,
			DestinationCidrBlock:   *conn.RequesterVpcInfo.CidrBlock,
			VpcPeeringConnectionID: *conn.VpcPeeringConnectionId,
			AWSEntity:              awsresources.AWSEntity{Clients: hostClients},
		}

		if err := privateRoute.Delete(); err != nil {
			s.logger.Log("error", fmt.Sprintf("could not delete vpc route: '%v'", err))
		}
	}

	// Delete VPC peering connection.
	if err := vpcPeeringConection.Delete(); err != nil {
		s.logger.Log("error", fmt.Sprintf("could not delete vpc peering connection: '%#v'", err))
	} else {
		s.logger.Log("info", "deleted vpc peering connection")
	}

	// Delete VPC.
	if err := vpc.Delete(); err != nil {
		s.logger.Log("error", fmt.Sprintf("could not delete vpc: '%#v'", err))
	} else {
		s.logger.Log("info", "deleted vpc")
	}

	// Delete S3 bucket.
	bucketName := s.bucketName(cluster)

	bucket := &awsresources.Bucket{
		AWSEntity: awsresources.AWSEntity{Clients: clients},
		Name:      bucketName,
	}

	if err := bucket.Delete(); err != nil {
		s.logger.Log("error", fmt.Sprintf("%#v", err))
	}

	s.logger.Log("info", "deleted bucket")

	// Delete master policy.
	var masterPolicy resources.NamedResource
	masterPolicy = &awsresources.Policy{
		ClusterID:  key.ClusterID(cluster),
		PolicyType: prefixMaster,
		S3Bucket:   bucketName,
		AWSEntity:  awsresources.AWSEntity{Clients: clients},
	}
	if err := masterPolicy.Delete(); err != nil {
		s.logger.Log("error", fmt.Sprintf("%#v", err))
	} else {
		s.logger.Log("info", fmt.Sprintf("deleted %s roles, policies, instance profiles", prefixMaster))
	}

	// Delete worker policy.
	var workerPolicy resources.NamedResource
	workerPolicy = &awsresources.Policy{
		ClusterID:  key.ClusterID(cluster),
		PolicyType: prefixWorker,
		S3Bucket:   bucketName,
		AWSEntity:  awsresources.AWSEntity{Clients: clients},
	}
	if err := workerPolicy.Delete(); err != nil {
		s.logger.Log("error", fmt.Sprintf("%#v", err))
	} else {
		s.logger.Log("info", fmt.Sprintf("deleted %s roles, policies, instance profiles", prefixWorker))
	}

	// Delete KMS key.
	var kmsKey resources.ArnResource
	kmsKey = &awsresources.KMSKey{
		Name:      key.ClusterID(cluster),
		AWSEntity: awsresources.AWSEntity{Clients: clients},
	}
	if err := kmsKey.Delete(); err != nil {
		s.logger.Log("error", fmt.Sprintf("%#v", err))
	} else {
		s.logger.Log("info", "deleted KMS key")
	}

	// Delete keypair.
	var keyPair resources.Resource
	keyPair = &awsresources.KeyPair{
		ClusterName: key.ClusterID(cluster),
		AWSEntity:   awsresources.AWSEntity{Clients: clients},
	}
	if err := keyPair.Delete(); err != nil {
		s.logger.Log("error", fmt.Sprintf("%#v", err))
	} else {
		s.logger.Log("info", "deleted keypair")
	}

	return nil
}

// TODO we need to support this in operatorkit.
func (s *Service) updateFunc(oldObj, newObj interface{}) {
	oldCluster := *oldObj.(*awstpr.CustomObject)
{'fraction_non_alphanumeric': 0.12651927684600306, 'fraction_numerical': 0.002341537791330865, 'mean_word_length': 4.011242700431152, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, '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': '6927825', 'n_tokens_mistral': 32101, 'n_tokens_neox': 29888, 'n_words': 6992}