mirror of
				https://github.com/optim-enterprises-bv/kubernetes.git
				synced 2025-10-31 18:28:13 +00:00 
			
		
		
		
	AWS: Support shared tag
We recognize an additional cluster tag: kubernetes.io/cluster/<clusterid> This now allows us to share resources, in particular subnets. In addition, the value is used to track ownership/lifecycle. When we create objects, we record the value as "owned". We also refactor out tags into its own file & class, as we are touching most of these functions anyway.
This commit is contained in:
		| @@ -21,6 +21,7 @@ go_library( | |||||||
|         "regions.go", |         "regions.go", | ||||||
|         "retry_handler.go", |         "retry_handler.go", | ||||||
|         "sets_ippermissions.go", |         "sets_ippermissions.go", | ||||||
|  |         "tags.go", | ||||||
|         "volumes.go", |         "volumes.go", | ||||||
|     ], |     ], | ||||||
|     tags = ["automanaged"], |     tags = ["automanaged"], | ||||||
| @@ -56,6 +57,7 @@ go_test( | |||||||
|         "device_allocator_test.go", |         "device_allocator_test.go", | ||||||
|         "regions_test.go", |         "regions_test.go", | ||||||
|         "retry_handler_test.go", |         "retry_handler_test.go", | ||||||
|  |         "tags_test.go", | ||||||
|     ], |     ], | ||||||
|     library = ":go_default_library", |     library = ":go_default_library", | ||||||
|     tags = ["automanaged"], |     tags = ["automanaged"], | ||||||
|   | |||||||
| @@ -54,10 +54,6 @@ import ( | |||||||
| // ProviderName is the name of this cloud provider. | // ProviderName is the name of this cloud provider. | ||||||
| const ProviderName = "aws" | const ProviderName = "aws" | ||||||
|  |  | ||||||
| // TagNameKubernetesCluster is the tag name we use to differentiate multiple |  | ||||||
| // logically independent clusters running in the same AZ |  | ||||||
| const TagNameKubernetesCluster = "KubernetesCluster" |  | ||||||
|  |  | ||||||
| // TagNameKubernetesService is the tag name we use to differentiate multiple | // TagNameKubernetesService is the tag name we use to differentiate multiple | ||||||
| // services. Used currently for ELBs only. | // services. Used currently for ELBs only. | ||||||
| const TagNameKubernetesService = "kubernetes.io/service-name" | const TagNameKubernetesService = "kubernetes.io/service-name" | ||||||
| @@ -359,7 +355,7 @@ type Cloud struct { | |||||||
| 	region   string | 	region   string | ||||||
| 	vpcID    string | 	vpcID    string | ||||||
|  |  | ||||||
| 	filterTags map[string]string | 	tagging awsTagging | ||||||
|  |  | ||||||
| 	// The AWS instance that we are running on | 	// The AWS instance that we are running on | ||||||
| 	// Note that we cache some state in awsInstance (mountpoints), so we must preserve the instance | 	// Note that we cache some state in awsInstance (mountpoints), so we must preserve the instance | ||||||
| @@ -388,7 +384,10 @@ type CloudConfig struct { | |||||||
| 		// Maybe if we're not running on AWS, e.g. bootstrap; for now it is not very useful | 		// Maybe if we're not running on AWS, e.g. bootstrap; for now it is not very useful | ||||||
| 		Zone string | 		Zone string | ||||||
|  |  | ||||||
|  | 		// KubernetesClusterTag is the legacy cluster id we'll use to identify our cluster resources | ||||||
| 		KubernetesClusterTag string | 		KubernetesClusterTag string | ||||||
|  | 		// KubernetesClusterTag is the cluster id we'll use to identify our cluster resources | ||||||
|  | 		KubernetesClusterID string | ||||||
|  |  | ||||||
| 		//The aws provider creates an inbound rule per load balancer on the node security | 		//The aws provider creates an inbound rule per load balancer on the node security | ||||||
| 		//group. However, this can run into the AWS security group rule limit of 50 if | 		//group. However, this can run into the AWS security group rule limit of 50 if | ||||||
| @@ -534,12 +533,12 @@ func orEmpty(s *string) string { | |||||||
| 	return aws.StringValue(s) | 	return aws.StringValue(s) | ||||||
| } | } | ||||||
|  |  | ||||||
| func newEc2Filter(name string, value string) *ec2.Filter { | func newEc2Filter(name string, values ...string) *ec2.Filter { | ||||||
| 	filter := &ec2.Filter{ | 	filter := &ec2.Filter{ | ||||||
| 		Name: aws.String(name), | 		Name: aws.String(name), | ||||||
| 		Values: []*string{ | 	} | ||||||
| 			aws.String(value), | 	for _, value := range values { | ||||||
| 		}, | 		filter.Values = append(filter.Values, aws.String(value)) | ||||||
| 	} | 	} | ||||||
| 	return filter | 	return filter | ||||||
| } | } | ||||||
| @@ -817,32 +816,20 @@ func newAWSCloud(config io.Reader, awsServices Services) (*Cloud, error) { | |||||||
| 	awsCloud.selfAWSInstance = selfAWSInstance | 	awsCloud.selfAWSInstance = selfAWSInstance | ||||||
| 	awsCloud.vpcID = selfAWSInstance.vpcID | 	awsCloud.vpcID = selfAWSInstance.vpcID | ||||||
|  |  | ||||||
| 	filterTags := map[string]string{} | 	if cfg.Global.KubernetesClusterTag != "" || cfg.Global.KubernetesClusterID != "" { | ||||||
| 	if cfg.Global.KubernetesClusterTag != "" { | 		if err := awsCloud.tagging.init(cfg.Global.KubernetesClusterTag, cfg.Global.KubernetesClusterID); err != nil { | ||||||
| 		filterTags[TagNameKubernetesCluster] = cfg.Global.KubernetesClusterTag | 			return nil, err | ||||||
|  | 		} | ||||||
| 	} else { | 	} else { | ||||||
| 		// TODO: Clean up double-API query | 		// TODO: Clean up double-API query | ||||||
| 		info, err := selfAWSInstance.describeInstance() | 		info, err := selfAWSInstance.describeInstance() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
| 		for _, tag := range info.Tags { | 		if err := awsCloud.tagging.initFromTags(info.Tags); err != nil { | ||||||
| 			if orEmpty(tag.Key) == TagNameKubernetesCluster { | 			return nil, err | ||||||
| 				filterTags[TagNameKubernetesCluster] = orEmpty(tag.Value) |  | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if filterTags[TagNameKubernetesCluster] == "" { |  | ||||||
| 		glog.Errorf("Tag %q not found; Kubernetes may behave unexpectedly.", TagNameKubernetesCluster) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	awsCloud.filterTags = filterTags |  | ||||||
| 	if len(filterTags) > 0 { |  | ||||||
| 		glog.Infof("AWS cloud filtering on tags: %v", filterTags) |  | ||||||
| 	} else { |  | ||||||
| 		glog.Infof("AWS cloud - no tag filtering") |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Register regions, in particular for ECR credentials | 	// Register regions, in particular for ECR credentials | ||||||
| 	once.Do(func() { | 	once.Do(func() { | ||||||
| @@ -990,15 +977,12 @@ func (c *Cloud) InstanceType(nodeName types.NodeName) (string, error) { | |||||||
| // Return a list of instances matching regex string. | // Return a list of instances matching regex string. | ||||||
| func (c *Cloud) getInstancesByRegex(regex string) ([]types.NodeName, error) { | func (c *Cloud) getInstancesByRegex(regex string) ([]types.NodeName, error) { | ||||||
| 	filters := []*ec2.Filter{newEc2Filter("instance-state-name", "running")} | 	filters := []*ec2.Filter{newEc2Filter("instance-state-name", "running")} | ||||||
| 	filters = c.addFilters(filters) |  | ||||||
| 	request := &ec2.DescribeInstancesInput{ |  | ||||||
| 		Filters: filters, |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	instances, err := c.ec2.DescribeInstances(request) | 	instances, err := c.describeInstances(filters) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return []types.NodeName{}, err | 		return []types.NodeName{}, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if len(instances) == 0 { | 	if len(instances) == 0 { | ||||||
| 		return []types.NodeName{}, fmt.Errorf("no instances returned") | 		return []types.NodeName{}, fmt.Errorf("no instances returned") | ||||||
| 	} | 	} | ||||||
| @@ -1050,15 +1034,12 @@ func (c *Cloud) getAllZones() (sets.String, error) { | |||||||
| 	// TODO: We could also query for subnets, I think | 	// TODO: We could also query for subnets, I think | ||||||
|  |  | ||||||
| 	filters := []*ec2.Filter{newEc2Filter("instance-state-name", "running")} | 	filters := []*ec2.Filter{newEc2Filter("instance-state-name", "running")} | ||||||
| 	filters = c.addFilters(filters) |  | ||||||
| 	request := &ec2.DescribeInstancesInput{ |  | ||||||
| 		Filters: filters, |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	instances, err := c.ec2.DescribeInstances(request) | 	instances, err := c.describeInstances(filters) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if len(instances) == 0 { | 	if len(instances) == 0 { | ||||||
| 		return nil, fmt.Errorf("no instances returned") | 		return nil, fmt.Errorf("no instances returned") | ||||||
| 	} | 	} | ||||||
| @@ -1647,17 +1628,7 @@ func (c *Cloud) CreateDisk(volumeOptions *VolumeOptions) (KubernetesVolumeID, er | |||||||
| 	volumeName := KubernetesVolumeID("aws://" + aws.StringValue(response.AvailabilityZone) + "/" + string(awsID)) | 	volumeName := KubernetesVolumeID("aws://" + aws.StringValue(response.AvailabilityZone) + "/" + string(awsID)) | ||||||
|  |  | ||||||
| 	// apply tags | 	// apply tags | ||||||
| 	tags := make(map[string]string) | 	if err := c.tagging.createTags(c.ec2, string(awsID), ResourceLifecycleOwned, volumeOptions.Tags); err != nil { | ||||||
| 	for k, v := range volumeOptions.Tags { |  | ||||||
| 		tags[k] = v |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if c.getClusterName() != "" { |  | ||||||
| 		tags[TagNameKubernetesCluster] = c.getClusterName() |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if len(tags) != 0 { |  | ||||||
| 		if err := c.createTags(string(awsID), tags); err != nil { |  | ||||||
| 		// delete the volume and hope it succeeds | 		// delete the volume and hope it succeeds | ||||||
| 		_, delerr := c.DeleteDisk(volumeName) | 		_, delerr := c.DeleteDisk(volumeName) | ||||||
| 		if delerr != nil { | 		if delerr != nil { | ||||||
| @@ -1666,7 +1637,7 @@ func (c *Cloud) CreateDisk(volumeOptions *VolumeOptions) (KubernetesVolumeID, er | |||||||
| 		} | 		} | ||||||
| 		return "", fmt.Errorf("error tagging volume %s: %v", volumeName, err) | 		return "", fmt.Errorf("error tagging volume %s: %v", volumeName, err) | ||||||
| 	} | 	} | ||||||
| 	} |  | ||||||
| 	return volumeName, nil | 	return volumeName, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -2115,36 +2086,6 @@ func (c *Cloud) removeSecurityGroupIngress(securityGroupID string, removePermiss | |||||||
| 	return true, nil | 	return true, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // Ensure that a resource has the correct tags |  | ||||||
| // If it has no tags, we assume that this was a problem caused by an error in between creation and tagging, |  | ||||||
| // and we add the tags.  If it has a different cluster's tags, that is an error. |  | ||||||
| func (c *Cloud) ensureClusterTags(resourceID string, tags []*ec2.Tag) error { |  | ||||||
| 	actualTags := make(map[string]string) |  | ||||||
| 	for _, tag := range tags { |  | ||||||
| 		actualTags[aws.StringValue(tag.Key)] = aws.StringValue(tag.Value) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	addTags := make(map[string]string) |  | ||||||
| 	for k, expected := range c.filterTags { |  | ||||||
| 		actual := actualTags[k] |  | ||||||
| 		if actual == expected { |  | ||||||
| 			continue |  | ||||||
| 		} |  | ||||||
| 		if actual == "" { |  | ||||||
| 			glog.Warningf("Resource %q was missing expected cluster tag %q.  Will add (with value %q)", resourceID, k, expected) |  | ||||||
| 			addTags[k] = expected |  | ||||||
| 		} else { |  | ||||||
| 			return fmt.Errorf("resource %q has tag belonging to another cluster: %q=%q (expected %q)", resourceID, k, actual, expected) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if err := c.createTags(resourceID, addTags); err != nil { |  | ||||||
| 		return fmt.Errorf("error adding missing tags to resource %q: %v", resourceID, err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Makes sure the security group exists. | // Makes sure the security group exists. | ||||||
| // For multi-cluster isolation, name must be globally unique, for example derived from the service UUID. | // For multi-cluster isolation, name must be globally unique, for example derived from the service UUID. | ||||||
| // Returns the security group id or error | // Returns the security group id or error | ||||||
| @@ -2175,7 +2116,9 @@ func (c *Cloud) ensureSecurityGroup(name string, description string) (string, er | |||||||
| 			if len(securityGroups) > 1 { | 			if len(securityGroups) > 1 { | ||||||
| 				glog.Warningf("Found multiple security groups with name: %q", name) | 				glog.Warningf("Found multiple security groups with name: %q", name) | ||||||
| 			} | 			} | ||||||
| 			err := c.ensureClusterTags(aws.StringValue(securityGroups[0].GroupId), securityGroups[0].Tags) | 			err := c.tagging.readRepairClusterTags( | ||||||
|  | 				c.ec2, aws.StringValue(securityGroups[0].GroupId), | ||||||
|  | 				ResourceLifecycleOwned, nil, securityGroups[0].Tags) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return "", err | 				return "", err | ||||||
| 			} | 			} | ||||||
| @@ -2212,7 +2155,7 @@ func (c *Cloud) ensureSecurityGroup(name string, description string) (string, er | |||||||
| 		return "", fmt.Errorf("created security group, but id was not returned: %s", name) | 		return "", fmt.Errorf("created security group, but id was not returned: %s", name) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	err := c.createTags(groupID, c.filterTags) | 	err := c.tagging.createTags(c.ec2, groupID, ResourceLifecycleOwned, nil) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		// If we retry, ensureClusterTags will recover from this - it | 		// If we retry, ensureClusterTags will recover from this - it | ||||||
| 		// will add the missing tags.  We could delete the security | 		// will add the missing tags.  We could delete the security | ||||||
| @@ -2223,52 +2166,6 @@ func (c *Cloud) ensureSecurityGroup(name string, description string) (string, er | |||||||
| 	return groupID, nil | 	return groupID, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // createTags calls EC2 CreateTags, but adds retry-on-failure logic |  | ||||||
| // We retry mainly because if we create an object, we cannot tag it until it is "fully created" (eventual consistency) |  | ||||||
| // The error code varies though (depending on what we are tagging), so we simply retry on all errors |  | ||||||
| func (c *Cloud) createTags(resourceID string, tags map[string]string) error { |  | ||||||
| 	if tags == nil || len(tags) == 0 { |  | ||||||
| 		return nil |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	var awsTags []*ec2.Tag |  | ||||||
| 	for k, v := range tags { |  | ||||||
| 		tag := &ec2.Tag{ |  | ||||||
| 			Key:   aws.String(k), |  | ||||||
| 			Value: aws.String(v), |  | ||||||
| 		} |  | ||||||
| 		awsTags = append(awsTags, tag) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	backoff := wait.Backoff{ |  | ||||||
| 		Duration: createTagInitialDelay, |  | ||||||
| 		Factor:   createTagFactor, |  | ||||||
| 		Steps:    createTagSteps, |  | ||||||
| 	} |  | ||||||
| 	request := &ec2.CreateTagsInput{} |  | ||||||
| 	request.Resources = []*string{&resourceID} |  | ||||||
| 	request.Tags = awsTags |  | ||||||
|  |  | ||||||
| 	var lastErr error |  | ||||||
| 	err := wait.ExponentialBackoff(backoff, func() (bool, error) { |  | ||||||
| 		_, err := c.ec2.CreateTags(request) |  | ||||||
| 		if err == nil { |  | ||||||
| 			return true, nil |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		// We could check that the error is retryable, but the error code changes based on what we are tagging |  | ||||||
| 		// SecurityGroup: InvalidGroup.NotFound |  | ||||||
| 		glog.V(2).Infof("Failed to create tags; will retry.  Error was %v", err) |  | ||||||
| 		lastErr = err |  | ||||||
| 		return false, nil |  | ||||||
| 	}) |  | ||||||
| 	if err == wait.ErrWaitTimeout { |  | ||||||
| 		// return real CreateTags error instead of timeout |  | ||||||
| 		err = lastErr |  | ||||||
| 	} |  | ||||||
| 	return err |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Finds the value for a given tag. | // Finds the value for a given tag. | ||||||
| func findTag(tags []*ec2.Tag, key string) (string, bool) { | func findTag(tags []*ec2.Tag, key string) (string, bool) { | ||||||
| 	for _, tag := range tags { | 	for _, tag := range tags { | ||||||
| @@ -2284,18 +2181,23 @@ func findTag(tags []*ec2.Tag, key string) (string, bool) { | |||||||
| // However, in future this will likely be treated as an error. | // However, in future this will likely be treated as an error. | ||||||
| func (c *Cloud) findSubnets() ([]*ec2.Subnet, error) { | func (c *Cloud) findSubnets() ([]*ec2.Subnet, error) { | ||||||
| 	request := &ec2.DescribeSubnetsInput{} | 	request := &ec2.DescribeSubnetsInput{} | ||||||
| 	vpcIDFilter := newEc2Filter("vpc-id", c.vpcID) | 	filters := []*ec2.Filter{newEc2Filter("vpc-id", c.vpcID)} | ||||||
| 	filters := []*ec2.Filter{vpcIDFilter} | 	request.Filters = c.tagging.addFilters(filters) | ||||||
| 	filters = c.addFilters(filters) |  | ||||||
| 	request.Filters = filters |  | ||||||
|  |  | ||||||
| 	subnets, err := c.ec2.DescribeSubnets(request) | 	subnets, err := c.ec2.DescribeSubnets(request) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("error describing subnets: %v", err) | 		return nil, fmt.Errorf("error describing subnets: %v", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if len(subnets) != 0 { | 	var matches []*ec2.Subnet | ||||||
| 		return subnets, nil | 	for _, subnet := range subnets { | ||||||
|  | 		if c.tagging.hasClusterTag(subnet.Tags) { | ||||||
|  | 			matches = append(matches, subnet) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if len(matches) != 0 { | ||||||
|  | 		return matches, nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Fall back to the current instance subnets, if nothing is tagged | 	// Fall back to the current instance subnets, if nothing is tagged | ||||||
| @@ -2850,7 +2752,7 @@ func findSecurityGroupForInstance(instance *ec2.Instance, taggedSecurityGroups m | |||||||
| // Return all the security groups that are tagged as being part of our cluster | // Return all the security groups that are tagged as being part of our cluster | ||||||
| func (c *Cloud) getTaggedSecurityGroups() (map[string]*ec2.SecurityGroup, error) { | func (c *Cloud) getTaggedSecurityGroups() (map[string]*ec2.SecurityGroup, error) { | ||||||
| 	request := &ec2.DescribeSecurityGroupsInput{} | 	request := &ec2.DescribeSecurityGroupsInput{} | ||||||
| 	request.Filters = c.addFilters(nil) | 	request.Filters = c.tagging.addFilters(nil) | ||||||
| 	groups, err := c.ec2.DescribeSecurityGroups(request) | 	groups, err := c.ec2.DescribeSecurityGroups(request) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("error querying security groups: %v", err) | 		return nil, fmt.Errorf("error querying security groups: %v", err) | ||||||
| @@ -2858,6 +2760,10 @@ func (c *Cloud) getTaggedSecurityGroups() (map[string]*ec2.SecurityGroup, error) | |||||||
|  |  | ||||||
| 	m := make(map[string]*ec2.SecurityGroup) | 	m := make(map[string]*ec2.SecurityGroup) | ||||||
| 	for _, group := range groups { | 	for _, group := range groups { | ||||||
|  | 		if !c.tagging.hasClusterTag(group.Tags) { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		id := aws.StringValue(group.GroupId) | 		id := aws.StringValue(group.GroupId) | ||||||
| 		if id == "" { | 		if id == "" { | ||||||
| 			glog.Warningf("Ignoring group without id: %v", group) | 			glog.Warningf("Ignoring group without id: %v", group) | ||||||
| @@ -2892,14 +2798,24 @@ func (c *Cloud) updateInstanceSecurityGroupsForLoadBalancer(lb *elb.LoadBalancer | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Get the actual list of groups that allow ingress from the load-balancer | 	// Get the actual list of groups that allow ingress from the load-balancer | ||||||
|  | 	var actualGroups []*ec2.SecurityGroup | ||||||
|  | 	{ | ||||||
| 		describeRequest := &ec2.DescribeSecurityGroupsInput{} | 		describeRequest := &ec2.DescribeSecurityGroupsInput{} | ||||||
| 	filters := []*ec2.Filter{} | 		filters := []*ec2.Filter{ | ||||||
| 	filters = append(filters, newEc2Filter("ip-permission.group-id", loadBalancerSecurityGroupID)) | 			newEc2Filter("ip-permission.group-id", loadBalancerSecurityGroupID), | ||||||
| 	describeRequest.Filters = c.addFilters(filters) | 		} | ||||||
| 	actualGroups, err := c.ec2.DescribeSecurityGroups(describeRequest) | 		describeRequest.Filters = c.tagging.addFilters(filters) | ||||||
|  | 		response, err := c.ec2.DescribeSecurityGroups(describeRequest) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return fmt.Errorf("error querying security groups for ELB: %v", err) | 			return fmt.Errorf("error querying security groups for ELB: %v", err) | ||||||
| 		} | 		} | ||||||
|  | 		for _, sg := range response { | ||||||
|  | 			if !c.tagging.hasClusterTag(sg.Tags) { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			actualGroups = append(actualGroups, sg) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	taggedSecurityGroups, err := c.getTaggedSecurityGroups() | 	taggedSecurityGroups, err := c.getTaggedSecurityGroups() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -3187,12 +3103,7 @@ func (c *Cloud) getInstancesByNodeNamesCached(nodeNames sets.String) ([]*ec2.Ins | |||||||
| 		newEc2Filter("instance-state-name", "running"), | 		newEc2Filter("instance-state-name", "running"), | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	filters = c.addFilters(filters) | 	instances, err := c.describeInstances(filters) | ||||||
| 	request := &ec2.DescribeInstancesInput{ |  | ||||||
| 		Filters: filters, |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	instances, err := c.ec2.DescribeInstances(request) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		glog.V(2).Infof("Failed to describe instances %v", nodeNames) | 		glog.V(2).Infof("Failed to describe instances %v", nodeNames) | ||||||
| 		return nil, err | 		return nil, err | ||||||
| @@ -3209,6 +3120,26 @@ func (c *Cloud) getInstancesByNodeNamesCached(nodeNames sets.String) ([]*ec2.Ins | |||||||
| 	return instances, nil | 	return instances, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (c *Cloud) describeInstances(filters []*ec2.Filter) ([]*ec2.Instance, error) { | ||||||
|  | 	filters = c.tagging.addFilters(filters) | ||||||
|  | 	request := &ec2.DescribeInstancesInput{ | ||||||
|  | 		Filters: filters, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	response, err := c.ec2.DescribeInstances(request) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var matches []*ec2.Instance | ||||||
|  | 	for _, instance := range response { | ||||||
|  | 		if c.tagging.hasClusterTag(instance.Tags) { | ||||||
|  | 			matches = append(matches, instance) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return matches, nil | ||||||
|  | } | ||||||
|  |  | ||||||
| // mapNodeNameToPrivateDNSName maps a k8s NodeName to an AWS Instance PrivateDNSName | // mapNodeNameToPrivateDNSName maps a k8s NodeName to an AWS Instance PrivateDNSName | ||||||
| // This is a simple string cast | // This is a simple string cast | ||||||
| func mapNodeNameToPrivateDNSName(nodeName types.NodeName) string { | func mapNodeNameToPrivateDNSName(nodeName types.NodeName) string { | ||||||
| @@ -3228,15 +3159,12 @@ func (c *Cloud) findInstanceByNodeName(nodeName types.NodeName) (*ec2.Instance, | |||||||
| 		newEc2Filter("private-dns-name", privateDNSName), | 		newEc2Filter("private-dns-name", privateDNSName), | ||||||
| 		newEc2Filter("instance-state-name", "running"), | 		newEc2Filter("instance-state-name", "running"), | ||||||
| 	} | 	} | ||||||
| 	filters = c.addFilters(filters) |  | ||||||
| 	request := &ec2.DescribeInstancesInput{ |  | ||||||
| 		Filters: filters, |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	instances, err := c.ec2.DescribeInstances(request) | 	instances, err := c.describeInstances(filters) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if len(instances) == 0 { | 	if len(instances) == 0 { | ||||||
| 		return nil, nil | 		return nil, nil | ||||||
| 	} | 	} | ||||||
| @@ -3268,23 +3196,3 @@ func (c *Cloud) getFullInstance(nodeName types.NodeName) (*awsInstance, *ec2.Ins | |||||||
| 	awsInstance := newAWSInstance(c.ec2, instance) | 	awsInstance := newAWSInstance(c.ec2, instance) | ||||||
| 	return awsInstance, instance, err | 	return awsInstance, instance, err | ||||||
| } | } | ||||||
|  |  | ||||||
| // Add additional filters, to match on our tags |  | ||||||
| // This lets us run multiple k8s clusters in a single EC2 AZ |  | ||||||
| func (c *Cloud) addFilters(filters []*ec2.Filter) []*ec2.Filter { |  | ||||||
| 	for k, v := range c.filterTags { |  | ||||||
| 		filters = append(filters, newEc2Filter("tag:"+k, v)) |  | ||||||
| 	} |  | ||||||
| 	if len(filters) == 0 { |  | ||||||
| 		// We can't pass a zero-length Filters to AWS (it's an error) |  | ||||||
| 		// So if we end up with no filters; just return nil |  | ||||||
| 		return nil |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return filters |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Returns the cluster name or an empty string |  | ||||||
| func (c *Cloud) getClusterName() string { |  | ||||||
| 	return c.filterTags[TagNameKubernetesCluster] |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -55,9 +55,14 @@ func (c *Cloud) ensureLoadBalancer(namespacedName types.NamespacedName, loadBala | |||||||
|  |  | ||||||
| 		createRequest.SecurityGroups = stringPointerArray(securityGroupIDs) | 		createRequest.SecurityGroups = stringPointerArray(securityGroupIDs) | ||||||
|  |  | ||||||
| 		createRequest.Tags = []*elb.Tag{ | 		tags := c.tagging.buildTags(ResourceLifecycleOwned, map[string]string{ | ||||||
| 			{Key: aws.String(TagNameKubernetesCluster), Value: aws.String(c.getClusterName())}, | 			TagNameKubernetesService: namespacedName.String(), | ||||||
| 			{Key: aws.String(TagNameKubernetesService), Value: aws.String(namespacedName.String())}, | 		}) | ||||||
|  |  | ||||||
|  | 		for k, v := range tags { | ||||||
|  | 			createRequest.Tags = append(createRequest.Tags, &elb.Tag{ | ||||||
|  | 				Key: aws.String(k), Value: aws.String(v), | ||||||
|  | 			}) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		glog.Infof("Creating load balancer for %v with name: %s", namespacedName, loadBalancerName) | 		glog.Infof("Creating load balancer for %v with name: %s", namespacedName, loadBalancerName) | ||||||
|   | |||||||
| @@ -29,14 +29,20 @@ func (c *Cloud) findRouteTable(clusterName string) (*ec2.RouteTable, error) { | |||||||
| 	// This should be unnecessary (we already filter on TagNameKubernetesCluster, | 	// This should be unnecessary (we already filter on TagNameKubernetesCluster, | ||||||
| 	// and something is broken if cluster name doesn't match, but anyway... | 	// and something is broken if cluster name doesn't match, but anyway... | ||||||
| 	// TODO: All clouds should be cluster-aware by default | 	// TODO: All clouds should be cluster-aware by default | ||||||
| 	filters := []*ec2.Filter{newEc2Filter("tag:"+TagNameKubernetesCluster, clusterName)} | 	request := &ec2.DescribeRouteTablesInput{Filters: c.tagging.addFilters(nil)} | ||||||
| 	request := &ec2.DescribeRouteTablesInput{Filters: c.addFilters(filters)} |  | ||||||
|  |  | ||||||
| 	tables, err := c.ec2.DescribeRouteTables(request) | 	response, err := c.ec2.DescribeRouteTables(request) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	var tables []*ec2.RouteTable | ||||||
|  | 	for _, table := range response { | ||||||
|  | 		if c.tagging.hasClusterTag(table.Tags) { | ||||||
|  | 			tables = append(tables, table) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	if len(tables) == 0 { | 	if len(tables) == 0 { | ||||||
| 		return nil, fmt.Errorf("unable to find route table for AWS cluster: %s", clusterName) | 		return nil, fmt.Errorf("unable to find route table for AWS cluster: %s", clusterName) | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -146,7 +146,7 @@ func NewFakeAWSServices() *FakeAWSServices { | |||||||
| 	s.instances = []*ec2.Instance{selfInstance} | 	s.instances = []*ec2.Instance{selfInstance} | ||||||
|  |  | ||||||
| 	var tag ec2.Tag | 	var tag ec2.Tag | ||||||
| 	tag.Key = aws.String(TagNameKubernetesCluster) | 	tag.Key = aws.String(TagNameKubernetesClusterLegacy) | ||||||
| 	tag.Value = aws.String(TestClusterId) | 	tag.Value = aws.String(TestClusterId) | ||||||
| 	selfInstance.Tags = []*ec2.Tag{&tag} | 	selfInstance.Tags = []*ec2.Tag{&tag} | ||||||
|  |  | ||||||
| @@ -177,24 +177,6 @@ func (s *FakeAWSServices) Metadata() (EC2Metadata, error) { | |||||||
| 	return s.metadata, nil | 	return s.metadata, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestFilterTags(t *testing.T) { |  | ||||||
| 	awsServices := NewFakeAWSServices() |  | ||||||
| 	c, err := newAWSCloud(strings.NewReader("[global]"), awsServices) |  | ||||||
| 	if err != nil { |  | ||||||
| 		t.Errorf("Error building aws cloud: %v", err) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if len(c.filterTags) != 1 { |  | ||||||
| 		t.Errorf("unexpected filter tags: %v", c.filterTags) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if c.filterTags[TagNameKubernetesCluster] != TestClusterId { |  | ||||||
| 		t.Errorf("unexpected filter tags: %v", c.filterTags) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func TestNewAWSCloud(t *testing.T) { | func TestNewAWSCloud(t *testing.T) { | ||||||
| 	tests := []struct { | 	tests := []struct { | ||||||
| 		name string | 		name string | ||||||
| @@ -279,6 +261,15 @@ func instanceMatchesFilter(instance *ec2.Instance, filter *ec2.Filter) bool { | |||||||
| 		return contains(filter.Values, *instance.State.Name) | 		return contains(filter.Values, *instance.State.Name) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if name == "tag-key" { | ||||||
|  | 		for _, instanceTag := range instance.Tags { | ||||||
|  | 			if contains(filter.Values, aws.StringValue(instanceTag.Key)) { | ||||||
|  | 				return true | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	if strings.HasPrefix(name, "tag:") { | 	if strings.HasPrefix(name, "tag:") { | ||||||
| 		tagName := name[4:] | 		tagName := name[4:] | ||||||
| 		for _, instanceTag := range instance.Tags { | 		for _, instanceTag := range instance.Tags { | ||||||
| @@ -286,7 +277,9 @@ func instanceMatchesFilter(instance *ec2.Instance, filter *ec2.Filter) bool { | |||||||
| 				return true | 				return true | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  | 		return false | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	panic("Unknown filter name: " + name) | 	panic("Unknown filter name: " + name) | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -969,13 +962,14 @@ func TestIpPermissionExistsHandlesMultipleGroupIdsWithUserIds(t *testing.T) { | |||||||
| 		t.Errorf("Should have not been considered equal since first is not in the second array of groups") | 		t.Errorf("Should have not been considered equal since first is not in the second array of groups") | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestFindInstanceByNodeNameExcludesTerminatedInstances(t *testing.T) { | func TestFindInstanceByNodeNameExcludesTerminatedInstances(t *testing.T) { | ||||||
| 	awsServices := NewFakeAWSServices() | 	awsServices := NewFakeAWSServices() | ||||||
|  |  | ||||||
| 	nodeName := types.NodeName("my-dns.internal") | 	nodeName := types.NodeName("my-dns.internal") | ||||||
|  |  | ||||||
| 	var tag ec2.Tag | 	var tag ec2.Tag | ||||||
| 	tag.Key = aws.String(TagNameKubernetesCluster) | 	tag.Key = aws.String(TagNameKubernetesClusterLegacy) | ||||||
| 	tag.Value = aws.String(TestClusterId) | 	tag.Value = aws.String(TestClusterId) | ||||||
| 	tags := []*ec2.Tag{&tag} | 	tags := []*ec2.Tag{&tag} | ||||||
|  |  | ||||||
| @@ -1019,8 +1013,8 @@ func TestFindInstancesByNodeNameCached(t *testing.T) { | |||||||
| 	nodeNameTwo := "my-dns-two.internal" | 	nodeNameTwo := "my-dns-two.internal" | ||||||
|  |  | ||||||
| 	var tag ec2.Tag | 	var tag ec2.Tag | ||||||
| 	tag.Key = aws.String(TagNameKubernetesCluster) | 	tag.Key = aws.String(TagNameKubernetesClusterPrefix + TestClusterId) | ||||||
| 	tag.Value = aws.String(TestClusterId) | 	tag.Value = aws.String("") | ||||||
| 	tags := []*ec2.Tag{&tag} | 	tags := []*ec2.Tag{&tag} | ||||||
|  |  | ||||||
| 	var runningInstance ec2.Instance | 	var runningInstance ec2.Instance | ||||||
|   | |||||||
							
								
								
									
										256
									
								
								pkg/cloudprovider/providers/aws/tags.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										256
									
								
								pkg/cloudprovider/providers/aws/tags.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,256 @@ | |||||||
|  | /* | ||||||
|  | Copyright 2017 The Kubernetes Authors. | ||||||
|  |  | ||||||
|  | Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  | you may not use this file except in compliance with the License. | ||||||
|  | You may obtain a copy of the License at | ||||||
|  |  | ||||||
|  |     http://www.apache.org/licenses/LICENSE-2.0 | ||||||
|  |  | ||||||
|  | Unless required by applicable law or agreed to in writing, software | ||||||
|  | distributed under the License is distributed on an "AS IS" BASIS, | ||||||
|  | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
|  | See the License for the specific language governing permissions and | ||||||
|  | limitations under the License. | ||||||
|  | */ | ||||||
|  |  | ||||||
|  | package aws | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  |  | ||||||
|  | 	"github.com/aws/aws-sdk-go/aws" | ||||||
|  | 	"github.com/aws/aws-sdk-go/service/ec2" | ||||||
|  | 	"github.com/golang/glog" | ||||||
|  | 	"k8s.io/apimachinery/pkg/util/wait" | ||||||
|  | 	"strings" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // TagNameKubernetesClusterPrefix is the tag name we use to differentiate multiple | ||||||
|  | // logically independent clusters running in the same AZ. | ||||||
|  | // The tag key = TagNameKubernetesClusterPrefix + clusterID | ||||||
|  | // The tag value is an ownership value | ||||||
|  | const TagNameKubernetesClusterPrefix = "kubernetes.io/cluster/" | ||||||
|  |  | ||||||
|  | // TagNameKubernetesClusterLegacy is the legacy tag name we use to differentiate multiple | ||||||
|  | // logically independent clusters running in the same AZ.  The problem with it was that it | ||||||
|  | // did not allow shared resources. | ||||||
|  | const TagNameKubernetesClusterLegacy = "KubernetesCluster" | ||||||
|  |  | ||||||
|  | type ResourceLifecycle string | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	// ResourceLifecycleOwned is the value we use when tagging resources to indicate | ||||||
|  | 	// that the resource is considered owned and managed by the cluster, | ||||||
|  | 	// and in particular that the lifecycle is tied to the lifecycle of the cluster. | ||||||
|  | 	ResourceLifecycleOwned = "owned" | ||||||
|  | 	// ResourceLifecycleShared is the value we use when tagging resources to indicate | ||||||
|  | 	// that the resource is shared between multiple clusters, and should not be destroyed | ||||||
|  | 	// if the cluster is destroyed. | ||||||
|  | 	ResourceLifecycleShared = "shared" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type awsTagging struct { | ||||||
|  | 	// ClusterID is our cluster identifier: we tag AWS resources with this value, | ||||||
|  | 	// and thus we can run two independent clusters in the same VPC or subnets. | ||||||
|  | 	// This gives us similar functionality to GCE projects. | ||||||
|  | 	ClusterID string | ||||||
|  |  | ||||||
|  | 	// usesLegacyTags is true if we are using the legacy TagNameKubernetesClusterLegacy tags | ||||||
|  | 	usesLegacyTags bool | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t *awsTagging) init(legacyClusterID string, clusterID string) error { | ||||||
|  | 	if legacyClusterID != "" { | ||||||
|  | 		if clusterID != "" && legacyClusterID != clusterID { | ||||||
|  | 			return fmt.Errorf("ClusterID tags did not match: %q vs %q", clusterID, legacyClusterID) | ||||||
|  | 		} | ||||||
|  | 		t.usesLegacyTags = true | ||||||
|  | 		clusterID = legacyClusterID | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	t.ClusterID = clusterID | ||||||
|  |  | ||||||
|  | 	if clusterID != "" { | ||||||
|  | 		glog.Infof("AWS cloud filtering on ClusterID: %v", clusterID) | ||||||
|  | 	} else { | ||||||
|  | 		glog.Infof("AWS cloud - no clusterID filtering") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Extracts a clusterID from the given tags, if one is present | ||||||
|  | // If no clusterID is found, returns "", nil | ||||||
|  | // If multiple (different) clusterIDs are found, returns an error | ||||||
|  | func (t *awsTagging) initFromTags(tags []*ec2.Tag) error { | ||||||
|  | 	legacyClusterID, newClusterID, err := findClusterIDs(tags) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if legacyClusterID == "" && newClusterID == "" { | ||||||
|  | 		glog.Errorf("Tag %q nor %q not found; Kubernetes may behave unexpectedly.", TagNameKubernetesClusterLegacy, TagNameKubernetesClusterPrefix+"...") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return t.init(legacyClusterID, newClusterID) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Extracts the legacy & new cluster ids from the given tags, if they are present | ||||||
|  | // If duplicate tags are found, returns an error | ||||||
|  | func findClusterIDs(tags []*ec2.Tag) (string, string, error) { | ||||||
|  | 	legacyClusterID := "" | ||||||
|  | 	newClusterID := "" | ||||||
|  |  | ||||||
|  | 	for _, tag := range tags { | ||||||
|  | 		tagKey := aws.StringValue(tag.Key) | ||||||
|  | 		if strings.HasPrefix(tagKey, TagNameKubernetesClusterPrefix) { | ||||||
|  | 			id := strings.TrimPrefix(tagKey, TagNameKubernetesClusterPrefix) | ||||||
|  | 			if newClusterID != "" { | ||||||
|  | 				return "", "", fmt.Errorf("Found multiple cluster tags with prefix %s (%q and %q)", TagNameKubernetesClusterPrefix, newClusterID, id) | ||||||
|  | 			} | ||||||
|  | 			newClusterID = id | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if tagKey == TagNameKubernetesClusterLegacy { | ||||||
|  | 			id := aws.StringValue(tag.Value) | ||||||
|  | 			if legacyClusterID != "" { | ||||||
|  | 				return "", "", fmt.Errorf("Found multiple %s tags (%q and %q)", TagNameKubernetesClusterLegacy, legacyClusterID, id) | ||||||
|  | 			} | ||||||
|  | 			legacyClusterID = id | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return legacyClusterID, newClusterID, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t *awsTagging) clusterTagKey() string { | ||||||
|  | 	return TagNameKubernetesClusterPrefix + t.ClusterID | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t *awsTagging) hasClusterTag(tags []*ec2.Tag) bool { | ||||||
|  | 	clusterTagKey := t.clusterTagKey() | ||||||
|  | 	for _, tag := range tags { | ||||||
|  | 		tagKey := aws.StringValue(tag.Key) | ||||||
|  | 		// For 1.6, we continue to recognize the legacy tags, for the 1.5 -> 1.6 upgrade | ||||||
|  | 		if tagKey == TagNameKubernetesClusterLegacy { | ||||||
|  | 			return aws.StringValue(tag.Value) == t.ClusterID | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if tagKey == clusterTagKey { | ||||||
|  | 			return true | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Ensure that a resource has the correct tags | ||||||
|  | // If it has no tags, we assume that this was a problem caused by an error in between creation and tagging, | ||||||
|  | // and we add the tags.  If it has a different cluster's tags, that is an error. | ||||||
|  | func (c *awsTagging) readRepairClusterTags(client EC2, resourceID string, lifecycle ResourceLifecycle, additionalTags map[string]string, observedTags []*ec2.Tag) error { | ||||||
|  | 	actualTagMap := make(map[string]string) | ||||||
|  | 	for _, tag := range observedTags { | ||||||
|  | 		actualTagMap[aws.StringValue(tag.Key)] = aws.StringValue(tag.Value) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	expectedTags := c.buildTags(lifecycle, additionalTags) | ||||||
|  |  | ||||||
|  | 	addTags := make(map[string]string) | ||||||
|  | 	for k, expected := range expectedTags { | ||||||
|  | 		actual := actualTagMap[k] | ||||||
|  | 		if actual == expected { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		if actual == "" { | ||||||
|  | 			glog.Warningf("Resource %q was missing expected cluster tag %q.  Will add (with value %q)", resourceID, k, expected) | ||||||
|  | 			addTags[k] = expected | ||||||
|  | 		} else { | ||||||
|  | 			return fmt.Errorf("resource %q has tag belonging to another cluster: %q=%q (expected %q)", resourceID, k, actual, expected) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := c.createTags(client, resourceID, lifecycle, additionalTags); err != nil { | ||||||
|  | 		return fmt.Errorf("error adding missing tags to resource %q: %v", resourceID, err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // createTags calls EC2 CreateTags, but adds retry-on-failure logic | ||||||
|  | // We retry mainly because if we create an object, we cannot tag it until it is "fully created" (eventual consistency) | ||||||
|  | // The error code varies though (depending on what we are tagging), so we simply retry on all errors | ||||||
|  | func (t *awsTagging) createTags(client EC2, resourceID string, lifecycle ResourceLifecycle, additionalTags map[string]string) error { | ||||||
|  | 	tags := t.buildTags(lifecycle, additionalTags) | ||||||
|  |  | ||||||
|  | 	if tags == nil || len(tags) == 0 { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var awsTags []*ec2.Tag | ||||||
|  | 	for k, v := range tags { | ||||||
|  | 		tag := &ec2.Tag{ | ||||||
|  | 			Key:   aws.String(k), | ||||||
|  | 			Value: aws.String(v), | ||||||
|  | 		} | ||||||
|  | 		awsTags = append(awsTags, tag) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	backoff := wait.Backoff{ | ||||||
|  | 		Duration: createTagInitialDelay, | ||||||
|  | 		Factor:   createTagFactor, | ||||||
|  | 		Steps:    createTagSteps, | ||||||
|  | 	} | ||||||
|  | 	request := &ec2.CreateTagsInput{} | ||||||
|  | 	request.Resources = []*string{&resourceID} | ||||||
|  | 	request.Tags = awsTags | ||||||
|  |  | ||||||
|  | 	var lastErr error | ||||||
|  | 	err := wait.ExponentialBackoff(backoff, func() (bool, error) { | ||||||
|  | 		_, err := client.CreateTags(request) | ||||||
|  | 		if err == nil { | ||||||
|  | 			return true, nil | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// We could check that the error is retryable, but the error code changes based on what we are tagging | ||||||
|  | 		// SecurityGroup: InvalidGroup.NotFound | ||||||
|  | 		glog.V(2).Infof("Failed to create tags; will retry.  Error was %v", err) | ||||||
|  | 		lastErr = err | ||||||
|  | 		return false, nil | ||||||
|  | 	}) | ||||||
|  | 	if err == wait.ErrWaitTimeout { | ||||||
|  | 		// return real CreateTags error instead of timeout | ||||||
|  | 		err = lastErr | ||||||
|  | 	} | ||||||
|  | 	return err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Add additional filters, to match on our tags | ||||||
|  | // This lets us run multiple k8s clusters in a single EC2 AZ | ||||||
|  | func (t *awsTagging) addFilters(filters []*ec2.Filter) []*ec2.Filter { | ||||||
|  | 	// For 1.6, we always recognize the legacy tag, for the 1.5 -> 1.6 upgrade | ||||||
|  | 	// There are no "or" filters by key, so we look for both the legacy and new key, and then we have to post-filter | ||||||
|  | 	f := newEc2Filter("tag-key", TagNameKubernetesClusterLegacy, t.clusterTagKey()) | ||||||
|  | 	filters = append(filters, f) | ||||||
|  |  | ||||||
|  | 	if len(filters) == 0 { | ||||||
|  | 		// We can't pass a zero-length Filters to AWS (it's an error) | ||||||
|  | 		// So if we end up with no filters; just return nil | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return filters | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t *awsTagging) buildTags(lifecycle ResourceLifecycle, additionalTags map[string]string) map[string]string { | ||||||
|  | 	tags := make(map[string]string) | ||||||
|  | 	for k, v := range additionalTags { | ||||||
|  | 		tags[k] = v | ||||||
|  | 	} | ||||||
|  | 	// We only create legacy tags if we are using legacy tags, i.e. if we have seen a legacy tag on our instance | ||||||
|  | 	if t.usesLegacyTags { | ||||||
|  | 		tags[TagNameKubernetesClusterLegacy] = t.ClusterID | ||||||
|  | 	} | ||||||
|  | 	tags[t.clusterTagKey()] = string(lifecycle) | ||||||
|  |  | ||||||
|  | 	return tags | ||||||
|  | } | ||||||
							
								
								
									
										111
									
								
								pkg/cloudprovider/providers/aws/tags_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								pkg/cloudprovider/providers/aws/tags_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,111 @@ | |||||||
|  | /* | ||||||
|  | Copyright 2014 The Kubernetes Authors. | ||||||
|  |  | ||||||
|  | Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  | you may not use this file except in compliance with the License. | ||||||
|  | You may obtain a copy of the License at | ||||||
|  |  | ||||||
|  |     http://www.apache.org/licenses/LICENSE-2.0 | ||||||
|  |  | ||||||
|  | Unless required by applicable law or agreed to in writing, software | ||||||
|  | distributed under the License is distributed on an "AS IS" BASIS, | ||||||
|  | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
|  | See the License for the specific language governing permissions and | ||||||
|  | limitations under the License. | ||||||
|  | */ | ||||||
|  |  | ||||||
|  | package aws | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"github.com/aws/aws-sdk-go/aws" | ||||||
|  | 	"github.com/aws/aws-sdk-go/service/ec2" | ||||||
|  | 	"strings" | ||||||
|  | 	"testing" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestFilterTags(t *testing.T) { | ||||||
|  | 	awsServices := NewFakeAWSServices() | ||||||
|  | 	c, err := newAWSCloud(strings.NewReader("[global]"), awsServices) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Errorf("Error building aws cloud: %v", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if c.tagging.ClusterID != TestClusterId { | ||||||
|  | 		t.Errorf("unexpected ClusterID: %v", c.tagging.ClusterID) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestFindClusterID(t *testing.T) { | ||||||
|  | 	grid := []struct { | ||||||
|  | 		Tags           map[string]string | ||||||
|  | 		ExpectedNew    string | ||||||
|  | 		ExpectedLegacy string | ||||||
|  | 		ExpectError    bool | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			Tags: map[string]string{}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Tags: map[string]string{ | ||||||
|  | 				TagNameKubernetesClusterLegacy: "a", | ||||||
|  | 			}, | ||||||
|  | 			ExpectedLegacy: "a", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Tags: map[string]string{ | ||||||
|  | 				TagNameKubernetesClusterPrefix + "a": "owned", | ||||||
|  | 			}, | ||||||
|  | 			ExpectedNew: "a", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Tags: map[string]string{ | ||||||
|  | 				TagNameKubernetesClusterPrefix + "a": "", | ||||||
|  | 			}, | ||||||
|  | 			ExpectedNew: "a", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Tags: map[string]string{ | ||||||
|  | 				TagNameKubernetesClusterLegacy:       "a", | ||||||
|  | 				TagNameKubernetesClusterPrefix + "a": "", | ||||||
|  | 			}, | ||||||
|  | 			ExpectedLegacy: "a", | ||||||
|  | 			ExpectedNew:    "a", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Tags: map[string]string{ | ||||||
|  | 				TagNameKubernetesClusterPrefix + "a": "", | ||||||
|  | 				TagNameKubernetesClusterPrefix + "b": "", | ||||||
|  | 			}, | ||||||
|  | 			ExpectError: true, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	for _, g := range grid { | ||||||
|  | 		var ec2Tags []*ec2.Tag | ||||||
|  | 		for k, v := range g.Tags { | ||||||
|  | 			ec2Tags = append(ec2Tags, &ec2.Tag{Key: aws.String(k), Value: aws.String(v)}) | ||||||
|  | 		} | ||||||
|  | 		actualLegacy, actualNew, err := findClusterIDs(ec2Tags) | ||||||
|  | 		if g.ExpectError { | ||||||
|  | 			if err == nil { | ||||||
|  | 				t.Errorf("expected error for tags %v", g.Tags) | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Errorf("unexpected error for tags %v: %v", g.Tags, err) | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if g.ExpectedNew != actualNew { | ||||||
|  | 				t.Errorf("unexpected new clusterid for tags %v: %s vs %s", g.Tags, g.ExpectedNew, actualNew) | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if g.ExpectedLegacy != actualLegacy { | ||||||
|  | 				t.Errorf("unexpected new clusterid for tags %v: %s vs %s", g.Tags, g.ExpectedLegacy, actualLegacy) | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user
	 Justin Santa Barbara
					Justin Santa Barbara