Skip to content

Commit

Permalink
feat(aws): Support for automatically tagging volumes created as a res…
Browse files Browse the repository at this point in the history
…ult of autoscaling activity (#5237)

Implementations of the `AmazonResourceTagger` interface will be used to generate one or more
`LaunchTemplateTagSpecificationRequest` that will be attached to the generated `LaunchTemplate`.

When enabled, `DefaultAmazonResourceTagger` will generate two tags:
* `spinnaker:application`
* `spinnaker:cluster`

Configuration:
```
aws:
  defaults:
    resourceTagging:
      enabled: true
      # clusterTag: spinnaker:cluster
      # applicationTag: spinnaker:application
```
  • Loading branch information
ajordens committed Feb 9, 2021
1 parent dfb06c3 commit 19426cb
Show file tree
Hide file tree
Showing 5 changed files with 182 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright 2021 Netflix, Inc.
*
* 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 com.netflix.spinnaker.clouddriver.aws.deploy;

import java.util.Collection;
import java.util.Collections;
import lombok.Data;
import org.jetbrains.annotations.NotNull;

/**
* Allows for custom tags to be set on resources created as a result of autoscaling activity
* (requires usage of launch templates).
*/
public interface AmazonResourceTagger {
@NotNull
default Collection<Tag> volumeTags(@NotNull String serverGroupName) {
return Collections.emptyList();
}

@Data(staticConstructor = "of")
class Tag {
final String key;
final String value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright 2021 Netflix, Inc.
*
* 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 com.netflix.spinnaker.clouddriver.aws.deploy;

import com.netflix.frigga.Names;
import java.util.Arrays;
import java.util.Collection;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;

/**
* Applies an application and cluster tag on ebs volumes.
*
* <p>By default tag names are of the form 'spinnaker:application' and 'spinnaker:cluster'.
*/
@Component
@ConditionalOnProperty(
name = "aws.defaults.resourceTagging.enabled",
havingValue = "true",
matchIfMissing = false)
public class DefaultAmazonResourceTagger implements AmazonResourceTagger {
private final String clusterTag;
private final String applicationTag;

@Autowired
public DefaultAmazonResourceTagger(
@Value("${aws.defaults.resourceTagging.applicationTag:spinnaker:application}")
String applicationTag,
@Value("${aws.defaults.resourceTagging.clusterTag:spinnaker:cluster}") String clusterTag) {
this.clusterTag = clusterTag;
this.applicationTag = applicationTag;
}

@NotNull
@Override
public Collection<Tag> volumeTags(@NotNull String serverGroupName) {
Names names = Names.parseName(serverGroupName);

return Arrays.asList(
Tag.of(applicationTag, names.getApp()), Tag.of(clusterTag, names.getCluster()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import com.amazonaws.services.autoscaling.model.LaunchTemplateSpecification;
import com.amazonaws.services.ec2.AmazonEC2;
import com.amazonaws.services.ec2.model.*;
import com.netflix.spinnaker.clouddriver.aws.deploy.AmazonResourceTagger;
import com.netflix.spinnaker.clouddriver.aws.deploy.AutoScalingWorker.AsgConfiguration;
import com.netflix.spinnaker.clouddriver.aws.deploy.description.ModifyServerGroupLaunchTemplateDescription;
import com.netflix.spinnaker.clouddriver.aws.deploy.userdata.LocalFileUserDataProperties;
Expand All @@ -33,17 +34,20 @@
import com.netflix.spinnaker.kork.core.RetrySupport;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;

@Slf4j
public class LaunchTemplateService {
private final AmazonEC2 ec2;
private final UserDataProviderAggregator userDataProviderAggregator;
private final LocalFileUserDataProperties localFileUserDataProperties;
private final Collection<AmazonResourceTagger> amazonResourceTaggers;
private final RetrySupport retrySupport = new RetrySupport();

/**
Expand All @@ -68,10 +72,12 @@ public class LaunchTemplateService {
public LaunchTemplateService(
AmazonEC2 ec2,
UserDataProviderAggregator userDataProviderAggregator,
LocalFileUserDataProperties localFileUserDataProperties) {
LocalFileUserDataProperties localFileUserDataProperties,
Collection<AmazonResourceTagger> amazonResourceTaggers) {
this.ec2 = ec2;
this.userDataProviderAggregator = userDataProviderAggregator;
this.localFileUserDataProperties = localFileUserDataProperties;
this.amazonResourceTaggers = amazonResourceTaggers;
}

public LaunchTemplateVersion modifyLaunchTemplate(
Expand Down Expand Up @@ -154,6 +160,12 @@ private RequestLaunchTemplateData buildLaunchTemplateData(
new LaunchTemplateIamInstanceProfileSpecificationRequest()
.withName(description.getIamRole()));

Optional<LaunchTemplateTagSpecificationRequest> tagSpecification =
tagSpecification(amazonResourceTaggers, description.getAsgName());
if (tagSpecification.isPresent()) {
request = request.withTagSpecifications(tagSpecification.get());
}

if (description.getEbsOptimized() != null) {
request.setEbsOptimized(description.getEbsOptimized());
}
Expand Down Expand Up @@ -254,6 +266,12 @@ private RequestLaunchTemplateData buildLaunchTemplateData(
new LaunchTemplatesMonitoringRequest()
.withEnabled(asgConfig.getInstanceMonitoring()));

Optional<LaunchTemplateTagSpecificationRequest> tagSpecification =
tagSpecification(amazonResourceTaggers, asgName);
if (tagSpecification.isPresent()) {
request = request.withTagSpecifications(tagSpecification.get());
}

if (asgConfig.getPlacement() != null) {
request =
request.withPlacement(
Expand Down Expand Up @@ -419,4 +437,25 @@ private LaunchTemplateEbsBlockDeviceRequest getLaunchTemplateEbsBlockDeviceReque
}
return blockDeviceRequest;
}

@NotNull
private Optional<LaunchTemplateTagSpecificationRequest> tagSpecification(
Collection<AmazonResourceTagger> amazonResourceTaggers, @NotNull String serverGroupName) {
if (amazonResourceTaggers != null && !amazonResourceTaggers.isEmpty()) {
List<Tag> volumeTags =
amazonResourceTaggers.stream()
.flatMap(t -> t.volumeTags(serverGroupName).stream())
.map(t -> new Tag(t.getKey(), t.getValue()))
.collect(Collectors.toList());

if (!volumeTags.isEmpty()) {
return Optional.of(
new LaunchTemplateTagSpecificationRequest()
.withResourceType("volume")
.withTags(volumeTags));
}
}

return Optional.empty();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package com.netflix.spinnaker.clouddriver.aws.services

import com.amazonaws.services.autoscaling.AmazonAutoScaling
import com.amazonaws.services.ec2.AmazonEC2
import com.netflix.spinnaker.clouddriver.aws.deploy.AmazonResourceTagger
import com.netflix.spinnaker.clouddriver.aws.deploy.userdata.UserDataProviderAggregator
import com.netflix.spinnaker.config.AwsConfiguration
import com.netflix.spinnaker.clouddriver.aws.deploy.AWSServerGroupNameResolver
Expand Down Expand Up @@ -52,6 +53,9 @@ class RegionScopedProviderFactory {
@Autowired
List<ClusterProvider> clusterProviders

@Autowired(required = false)
Collection<AmazonResourceTagger> amazonResourceTaggers

RegionScopedProvider forRegion(NetflixAmazonCredentials amazonCredentials, String region) {
new RegionScopedProvider(amazonCredentials, region)
}
Expand Down Expand Up @@ -115,7 +119,9 @@ class RegionScopedProviderFactory {
}

LaunchTemplateService getLaunchTemplateService() {
return new LaunchTemplateService(amazonEC2, userDataProviderAggregator, localFileUserDataProperties)
return new LaunchTemplateService(
amazonEC2, userDataProviderAggregator, localFileUserDataProperties, amazonResourceTaggers
)
}

AwsConfiguration.DeployDefaults getDeploymentDefaults() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,25 @@
*/
package com.netflix.spinnaker.clouddriver.aws.services

import com.amazonaws.services.ec2.AmazonEC2;
import com.amazonaws.services.ec2.model.Tag
import com.amazonaws.services.ec2.AmazonEC2
import com.amazonaws.services.ec2.model.LaunchTemplateTagSpecificationRequest
import com.netflix.spinnaker.clouddriver.aws.deploy.AmazonResourceTagger
import com.netflix.spinnaker.clouddriver.aws.deploy.DefaultAmazonResourceTagger;
import com.netflix.spinnaker.clouddriver.aws.deploy.userdata.LocalFileUserDataProperties;
import com.netflix.spinnaker.clouddriver.aws.deploy.userdata.UserDataProviderAggregator;
import com.netflix.spinnaker.clouddriver.aws.model.AmazonBlockDevice
import com.netflix.spinnaker.clouddriver.aws.services.LaunchTemplateService
import spock.lang.Specification
import spock.lang.Unroll

class LaunchTemplateServiceSpec extends Specification {

def launchTemplateService = new LaunchTemplateService(
Mock(AmazonEC2),
Mock(UserDataProviderAggregator),
Mock(LocalFileUserDataProperties)
Mock(LocalFileUserDataProperties),
Collections.singletonList(
new DefaultAmazonResourceTagger("spinnaker:application", "spinnaker:cluster")
)
)

void 'should match ebs encryption'() {
Expand All @@ -44,4 +49,32 @@ class LaunchTemplateServiceSpec extends Specification {
new AmazonBlockDevice(encrypted: true) | true | null
new AmazonBlockDevice(encrypted: true, kmsKeyId: "xxx") | true | "xxx"
}

@Unroll
void 'should generate volume tags'() {
expect:
launchTemplateService.tagSpecification(
amazonResourceTaggers,
"application-stack-details-v001"
) == result

where:
amazonResourceTaggers << [
null,
[],
[new AmazonResourceTagger() {}],
[new DefaultAmazonResourceTagger("spinnaker:application", "spinnaker:cluster")]
]
result << [
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.of(
new LaunchTemplateTagSpecificationRequest()
.withResourceType("volume")
.withTags([new Tag("spinnaker:application", "application"), new Tag("spinnaker:cluster", "application-stack-details")])
)
]

}
}

0 comments on commit 19426cb

Please sign in to comment.