diff --git a/grails-app/controllers/com/netflix/asgard/AutoScalingController.groovy b/grails-app/controllers/com/netflix/asgard/AutoScalingController.groovy index 5649ad61..757f085c 100644 --- a/grails-app/controllers/com/netflix/asgard/AutoScalingController.groovy +++ b/grails-app/controllers/com/netflix/asgard/AutoScalingController.groovy @@ -22,6 +22,8 @@ import com.amazonaws.services.autoscaling.model.LaunchConfiguration import com.amazonaws.services.autoscaling.model.ScalingPolicy import com.amazonaws.services.autoscaling.model.ScheduledUpdateGroupAction import com.amazonaws.services.autoscaling.model.SuspendedProcess +import com.amazonaws.services.autoscaling.model.Tag +import com.amazonaws.services.autoscaling.model.TagDescription import com.amazonaws.services.cloudwatch.model.MetricAlarm import com.amazonaws.services.ec2.model.AvailabilityZone import com.amazonaws.services.ec2.model.Image @@ -169,6 +171,10 @@ class AutoScalingController { } as Map String clusterName = Relationships.clusterFromGroupName(name) boolean isChaosMonkeyActive = cloudReadyService.isChaosMonkeyActive(userContext.region) + + //Grab tag data and set for display + List tags = group.getTags() + def details = [ instanceCount: instanceCount, showPostponeButton: showPostponeButton, @@ -193,7 +199,8 @@ class AutoScalingController { subnetPurpose: subnetPurpose ?: null, vpcZoneIdentifier: group.VPCZoneIdentifier, isChaosMonkeyActive: isChaosMonkeyActive, - chaosMonkeyEditLink: cloudReadyService.constructChaosMonkeyEditLink(userContext.region, appName) + chaosMonkeyEditLink: cloudReadyService.constructChaosMonkeyEditLink(userContext.region, appName), + tags: tags ] withFormat { html { return details } @@ -254,6 +261,26 @@ class AutoScalingController { 'Chaos Monkey settings directly in Cloudready after ASG creation.' } } + // Auto Scaling Group Tags + List asgTags = [] + + if (params.tags) { + Map tags = [:] + + // The tags get funky when passed by the save chain + params.entrySet().findAll { + it.key.startsWith('tags.value') + }.each { + tags.put(it.key.tokenize('.')[2], it.value) + } + + if (tags.size() > 0){ + tags.each { key, value -> + Tag t = new Tag(key:key, value:value, propagateAtLaunch:params['tags.props.' + key] == 'on' ? true:false) + asgTags.add(t) + } + } + } [ applications: applicationService.getRegisteredApplications(userContext), group: group, @@ -277,7 +304,8 @@ class AutoScalingController { iamInstanceProfile: configService.defaultIamRole, spotUrl: configService.spotUrl, isChaosMonkeyActive: cloudReadyService.isChaosMonkeyActive(userContext.region), - appsWithClusterOptLevel: appsWithClusterOptLevel ?: [] + appsWithClusterOptLevel: appsWithClusterOptLevel ?: [], + tags: asgTags ] } @@ -296,6 +324,16 @@ class AutoScalingController { String subnetPurpose = params.subnetPurpose ?: null String vpcId = subnets.getVpcIdForSubnetPurpose(subnetPurpose) ?: '' + // Auto Scaling Group Tags + List asgTags = [] + + if (params.tags) { + params.tags.value.each { key, value -> + Tag t = new Tag(key:key, value:value, propagateAtLaunch:params['tags.props.' + key] == 'on' ? true:false, resourceId:groupName, resourceType:"auto-scaling-group") + asgTags.add(t) + } + } + // Auto Scaling Group Integer minSize = (params.min ?: 0) as Integer Integer desiredCapacity = (params.desiredCapacity ?: 0) as Integer @@ -313,7 +351,7 @@ class AutoScalingController { withMinSize(minSize).withDesiredCapacity(desiredCapacity). withMaxSize(maxSize).withDefaultCooldown(defaultCooldown). withHealthCheckType(healthCheckType).withHealthCheckGracePeriod(healthCheckGracePeriod). - withTerminationPolicies(terminationPolicies) + withTerminationPolicies(terminationPolicies).withTags(asgTags) // If this ASG lauches VPC instances, we must find the proper subnets and add them. if (subnetPurpose) { @@ -387,6 +425,7 @@ class AutoScalingController { addToLoadBalancerSuspended: group?.isProcessSuspended(AutoScalingProcessType.AddToLoadBalancer), manualStaticSizingNeeded: manualStaticSizingNeeded, vpcZoneIdentifier: group.VPCZoneIdentifier, + tags: group.tags, ] } @@ -429,6 +468,31 @@ class AutoScalingController { resumeProcesses << processType } } + List tags = [] + + if (params.tags) { + params.tags.value.each { key, value -> + Tag t = new Tag(key:key, value:value, propagateAtLaunch:params['tags.props.' + key] == 'on' ? true:false, resourceId:name, resourceType:"auto-scaling-group") + tags.add(t) + } + + if (tags.size() > 0){ + awsAutoScalingService.updateTags(userContext, tags, name) + } + + tags = [] + params.tags.delete.each { key, value -> + if (value == 'on'){ + Tag t = new Tag(key:key, value:params['tags.values.' + key], propagateAtLaunch:params['tags.props.' + key] == 'on' ? true:false, resourceId:name, resourceType:"auto-scaling-group") + tags.add(t) + } + } + + if (tags.size() > 0){ + awsAutoScalingService.deleteTags(userContext, tags, name) + } + } + final AutoScalingGroupData autoScalingGroupData = AutoScalingGroupData.forUpdate( name, lcName, minSize, desiredCapacity, maxSize, defaultCooldown, healthCheckType, healthCheckGracePeriod, terminationPolicies, availabilityZones diff --git a/grails-app/controllers/com/netflix/asgard/ClusterController.groovy b/grails-app/controllers/com/netflix/asgard/ClusterController.groovy index f9e00284..8dbafb1e 100644 --- a/grails-app/controllers/com/netflix/asgard/ClusterController.groovy +++ b/grails-app/controllers/com/netflix/asgard/ClusterController.groovy @@ -18,6 +18,8 @@ package com.netflix.asgard import com.amazonaws.services.autoscaling.model.AutoScalingGroup import com.amazonaws.services.autoscaling.model.LaunchConfiguration import com.amazonaws.services.autoscaling.model.ScheduledUpdateGroupAction +import com.amazonaws.services.autoscaling.model.Tag +import com.amazonaws.services.autoscaling.model.TagDescription import com.amazonaws.services.ec2.model.AvailabilityZone import com.amazonaws.services.elasticloadbalancing.model.LoadBalancerDescription import com.amazonaws.services.simpleworkflow.flow.ManualActivityCompletionClient @@ -145,6 +147,7 @@ ${lastGroup.loadBalancerNames}""" SubnetTarget.EC2).sort() Map> zonesByPurpose = subnets.groupZonesByPurpose( availabilityZones*.zoneName, SubnetTarget.EC2) + List tags = lastGroup.getTags() attributes.putAll([ cluster: cluster, runningTasks: runningTasks, @@ -161,7 +164,8 @@ ${lastGroup.loadBalancerNames}""" loadBalancersGroupedByVpcId: loadBalancers.groupBy { it.VPCId }, selectedLoadBalancers: selectedLoadBalancers, spotUrl: configService.spotUrl, - pricing: params.pricing ?: attributes.pricing + pricing: params.pricing ?: attributes.pricing, + tags:tags ]) attributes } @@ -429,13 +433,19 @@ ${loadBalancerNames}""" Group: ${lastGroup.loadBalancerNames}""" boolean ebsOptimized = params.containsKey('ebsOptimized') ? params.ebsOptimized?.toBoolean() : lastLaunchConfig.ebsOptimized + + List tags = convertTags(nextGroupName) + if (params.noOptionalDefaults != 'true') { securityGroups = securityGroups ?: lastLaunchConfig.securityGroups termPolicies = termPolicies ?: lastGroup.terminationPolicies loadBalancerNames = loadBalancerNames ?: lastGroup.loadBalancerNames vpcZoneIdentifier = vpcZoneIdentifier ?: subnets.constructNewVpcZoneIdentifierForZones( lastGroup.vpcZoneIdentifier, selectedZones) + tags = lastGroup.tags } + + log.debug """ClusterController.createNextGroup for Cluster '${cluster.name}' Load Balancers for next \ Group: ${loadBalancerNames}""" GroupCreateOptions options = new GroupCreateOptions( @@ -469,13 +479,25 @@ Group: ${loadBalancerNames}""" scheduledActions: newScheduledActions, vpcZoneIdentifier: vpcZoneIdentifier, spotPrice: spotPrice, - ebsOptimized: ebsOptimized + ebsOptimized: ebsOptimized, + tags: tags ) def operation = pushService.startGroupCreate(options) flash.message = "${operation.task.name} has been started." redirectToTask(operation.taskId) } } + + private List convertTags(String nextGroupName){ + List tags = [] + if (params.tags) { + params.tags.value.each { key, value -> + Tag t = new Tag(key:key, value:value, propagateAtLaunch:params['tags.props.' + key] == 'on' ? true:false, resourceId:nextGroupName, resourceType:"auto-scaling-group") + tags.add(t) + } + } + tags + } private int convertToIntOrUseDefault(String value, Integer defaultValue) { value?.toInteger() ?: defaultValue diff --git a/grails-app/services/com/netflix/asgard/AwsAutoScalingService.groovy b/grails-app/services/com/netflix/asgard/AwsAutoScalingService.groovy index 2a65fe20..ce7de510 100644 --- a/grails-app/services/com/netflix/asgard/AwsAutoScalingService.groovy +++ b/grails-app/services/com/netflix/asgard/AwsAutoScalingService.groovy @@ -22,10 +22,12 @@ import com.amazonaws.services.autoscaling.model.Alarm import com.amazonaws.services.autoscaling.model.AutoScalingGroup import com.amazonaws.services.autoscaling.model.BlockDeviceMapping import com.amazonaws.services.autoscaling.model.CreateAutoScalingGroupRequest +import com.amazonaws.services.autoscaling.model.CreateOrUpdateTagsRequest import com.amazonaws.services.autoscaling.model.DeleteAutoScalingGroupRequest import com.amazonaws.services.autoscaling.model.DeleteLaunchConfigurationRequest import com.amazonaws.services.autoscaling.model.DeletePolicyRequest import com.amazonaws.services.autoscaling.model.DeleteScheduledActionRequest +import com.amazonaws.services.autoscaling.model.DeleteTagsRequest import com.amazonaws.services.autoscaling.model.DescribeAutoScalingGroupsRequest import com.amazonaws.services.autoscaling.model.DescribeAutoScalingGroupsResult import com.amazonaws.services.autoscaling.model.DescribeLaunchConfigurationsRequest @@ -47,6 +49,7 @@ import com.amazonaws.services.autoscaling.model.ResumeProcessesRequest import com.amazonaws.services.autoscaling.model.ScalingPolicy import com.amazonaws.services.autoscaling.model.ScheduledUpdateGroupAction import com.amazonaws.services.autoscaling.model.SuspendProcessesRequest +import com.amazonaws.services.autoscaling.model.Tag import com.amazonaws.services.autoscaling.model.TerminateInstanceInAutoScalingGroupRequest import com.amazonaws.services.autoscaling.model.UpdateAutoScalingGroupRequest import com.amazonaws.services.cloudwatch.model.MetricAlarm @@ -906,8 +909,9 @@ class AwsAutoScalingService implements CacheInitializer, InitializingBean { void setExpirationTime(UserContext userContext, String autoScalingGroupName, DateTime expirationTime, Task existingTask = null) { - Map tagNameValuePairs = [(TagNames.EXPIRATION_TIME): Time.format(expirationTime)] - createOrUpdateAutoScalingGroupTags(userContext, autoScalingGroupName, tagNameValuePairs, existingTask) + List tags = [] + tags.add(new Tag(key:TagNames.EXPIRATION_TIME, value:Time.format(expirationTime), propagateAtLaunch:false, resourceId:autoScalingGroupName, resourceType:"auto-scaling-group")) + updateTags(userContext, tags, autoScalingGroupName, existingTask) } void postponeExpirationTime(UserContext userContext, String autoScalingGroupName, Duration extraTime, @@ -927,17 +931,15 @@ class AwsAutoScalingService implements CacheInitializer, InitializingBean { } void removeExpirationTime(UserContext userContext, String autoScalingGroupName, Task existingTask = null) { - deleteAutoScalingGroupTags(userContext, autoScalingGroupName, [TagNames.EXPIRATION_TIME], existingTask) + List tags = [] + tags.add(new Tag(key:TagNames.EXPIRATION_TIME, resourceId:autoScalingGroupName, resourceType:"auto-scaling-group")) + deleteTags(userContext, tags, autoScalingGroupName, existingTask) } void createOrUpdateAutoScalingGroupTags(UserContext userContext, String autoScalingGroupName, Map tagNameValuePairs, Task existingTask = null) { - // TODO: Re-enable this call after Amazon fixes bugs on their side and tell us it's safe again - /* - - // Hopefully Amazon will eventually change CreateOrUpdateTagsRequest to take List instead of List List tagStringsEqualDelimited = tagNameValuePairs.collect { "${it.key}=${it.value}".toString() } String suffix = tagNameValuePairs.size() == 1 ? '' : 's' @@ -945,29 +947,32 @@ class AwsAutoScalingService implements CacheInitializer, InitializingBean { taskService.runTask(userContext, msg, { Task task -> CreateOrUpdateTagsRequest request = new CreateOrUpdateTagsRequest(autoScalingGroupName: autoScalingGroupName, forceOverwriteTags: true, propagate: true, tags: tagStringsEqualDelimited) + CreateOrUpdateTagsRequest cr = new CreateOrUpdateTagsRequest() + cr.setTags(tagStringsEqualDelimited) + awsClient.by(userContext.region).createOrUpdateTags(request) }, Link.to(EntityType.autoScaling, autoScalingGroupName), existingTask) - */ - } - - void deleteAutoScalingGroupTags(UserContext userContext, String autoScalingGroupName, List tagNames, - Task existingTask = null) { - - // TODO: Re-enable this call after Amazon fixes bugs on their side and tell us it's safe again - - /* - - String suffix = tagNames.size() == 1 ? '' : 's' - String msg = "Delete tag${suffix} ${tagNames} on Auto Scaling Group on '${autoScalingGroupName}'" - taskService.runTask(userContext, msg, { Task task -> - DeleteTagsRequest request = new DeleteTagsRequest(autoScalingGroupName: autoScalingGroupName, - tagsToDelete: tagNames) - awsClient.by(userContext.region).deleteTags(request) - }, Link.to(EntityType.autoScaling, autoScalingGroupName), existingTask) - - */ - } + + } + + void updateTags(UserContext userContext, List tags, String autoScalingGroupName, Task existingTask = null){ + String msg = "Create tags on Auto Scaling Group on '${autoScalingGroupName}'" + taskService.runTask(userContext, msg, {Task task -> + CreateOrUpdateTagsRequest request = new CreateOrUpdateTagsRequest() + request.setTags(tags) + awsClient.by(userContext.region).createOrUpdateTags(request) + }, Link.to(EntityType.autoScaling, autoScalingGroupName), existingTask) + } + + void deleteTags(UserContext userContext, List tags, String autoScalingGroupName, Task existingTask = null){ + String msg = "Delete tags on Auto Scaling Group on '${autoScalingGroupName}'" + taskService.runTask(userContext, msg, {Task task -> + DeleteTagsRequest request = new DeleteTagsRequest() + request.setTags(tags) + awsClient.by(userContext.region).deleteTags(request) + }, Link.to(EntityType.autoScaling, autoScalingGroupName), existingTask) + } void deleteAutoScalingGroup(UserContext userContext, String name, AsgDeletionMode mode = AsgDeletionMode.ATTEMPT, Task existingTask = null) { diff --git a/grails-app/services/com/netflix/asgard/JokeService.groovy b/grails-app/services/com/netflix/asgard/JokeService.groovy index 1b4fad52..41238a46 100644 --- a/grails-app/services/com/netflix/asgard/JokeService.groovy +++ b/grails-app/services/com/netflix/asgard/JokeService.groovy @@ -37,7 +37,8 @@ class JokeService { 'Something sank your battleship', 'Blame the dog', 'Blame the cat', 'The code broke', "That wasn't supposed to happen", 'Not enough test coverage', "Let's blame Microsoft", "Let's blame Oracle", 'I sense a disruption in the force', "Don't blame yourself", - 'An interaction between man and machine has failed today'].asImmutable() + 'An interaction between man and machine has failed today', + 'The Baboon is LOOSE!', 'Your job... It\'s over!'].asImmutable() private final List failureImages = ImageAttributions.FAILURE_IMAGES diff --git a/grails-app/services/com/netflix/asgard/PushService.groovy b/grails-app/services/com/netflix/asgard/PushService.groovy index 1d024952..9c4c1c97 100644 --- a/grails-app/services/com/netflix/asgard/PushService.groovy +++ b/grails-app/services/com/netflix/asgard/PushService.groovy @@ -17,6 +17,7 @@ package com.netflix.asgard import com.amazonaws.services.autoscaling.model.AutoScalingGroup import com.amazonaws.services.autoscaling.model.LaunchConfiguration +import com.amazonaws.services.autoscaling.model.TagDescription import com.amazonaws.services.ec2.model.Image import com.amazonaws.services.ec2.model.SecurityGroup import com.netflix.asgard.model.InstancePriceType @@ -133,6 +134,7 @@ class PushService { if (!group) { throw new NoSuchObjectException("Auto scaling group '${name}' not found") } + List tags = group.getTags() Integer relaunchCount = group.instances.size() LaunchConfiguration lc = awsAutoScalingService.getLaunchConfiguration(userContext, group.launchConfigurationName) @@ -154,6 +156,7 @@ class PushService { appName: appName, name: name, cluster: Relationships.clusterFromGroupName(name), + tags: tags, variables: Relationships.parts(name), actionName: actionName, allTerminationPolicies: awsAutoScalingService.terminationPolicyTypes, diff --git a/grails-app/views/autoScaling/create.gsp b/grails-app/views/autoScaling/create.gsp index 4c996c61..1bac41e1 100644 --- a/grails-app/views/autoScaling/create.gsp +++ b/grails-app/views/autoScaling/create.gsp @@ -48,6 +48,7 @@ + @@ -56,8 +57,8 @@
  • ${app}
  • - - + +
    Create New Auto Scaling Group
    diff --git a/grails-app/views/autoScaling/edit.gsp b/grails-app/views/autoScaling/edit.gsp index acc68809..9f581fdd 100644 --- a/grails-app/views/autoScaling/edit.gsp +++ b/grails-app/views/autoScaling/edit.gsp @@ -91,6 +91,41 @@ + + + Tags: + + +
    + + + + + + + + + + + + + + + + + + + + + + +
    KeyValuePropagateDelete?
    ${tag.key}
    + +
    + + + + diff --git a/grails-app/views/autoScaling/show.gsp b/grails-app/views/autoScaling/show.gsp index 1f276937..3ed8973f 100644 --- a/grails-app/views/autoScaling/show.gsp +++ b/grails-app/views/autoScaling/show.gsp @@ -118,7 +118,7 @@ VPC Zone Identifier: ${vpcZoneIdentifier} - + AZ Rebalancing: ${azRebalanceStatus} diff --git a/grails-app/views/cluster/show.gsp b/grails-app/views/cluster/show.gsp index 8b1d1be3..ac87d7cf 100644 --- a/grails-app/views/cluster/show.gsp +++ b/grails-app/views/cluster/show.gsp @@ -172,6 +172,7 @@ + diff --git a/grails-app/views/common/_editTags.gsp b/grails-app/views/common/_editTags.gsp new file mode 100644 index 00000000..2c8bc461 --- /dev/null +++ b/grails-app/views/common/_editTags.gsp @@ -0,0 +1,41 @@ + + + + diff --git a/grails-app/views/common/_showTagsDetail.gsp b/grails-app/views/common/_showTagsDetail.gsp new file mode 100644 index 00000000..e604a8f4 --- /dev/null +++ b/grails-app/views/common/_showTagsDetail.gsp @@ -0,0 +1,23 @@ + + + + diff --git a/grails-app/views/push/editRolling.gsp b/grails-app/views/push/editRolling.gsp index 2e8dca1d..7f78d179 100644 --- a/grails-app/views/push/editRolling.gsp +++ b/grails-app/views/push/editRolling.gsp @@ -51,6 +51,7 @@ + diff --git a/src/groovy/com/netflix/asgard/mock/MockAmazonAutoScalingClient.groovy b/src/groovy/com/netflix/asgard/mock/MockAmazonAutoScalingClient.groovy index c037348b..86186022 100644 --- a/src/groovy/com/netflix/asgard/mock/MockAmazonAutoScalingClient.groovy +++ b/src/groovy/com/netflix/asgard/mock/MockAmazonAutoScalingClient.groovy @@ -91,7 +91,7 @@ class MockAmazonAutoScalingClient extends AmazonAutoScalingClient { withSuspendedProcesses(it.suspendedProcesses.collect { def suspendedProcess -> new SuspendedProcess().withProcessName(suspendedProcess.processName). withSuspensionReason(suspendedProcess.suspensionReason) - }) + }).withTags(it.tags) } } diff --git a/src/groovy/com/netflix/asgard/model/AutoScalingGroupBeanOptions.groovy b/src/groovy/com/netflix/asgard/model/AutoScalingGroupBeanOptions.groovy index 592e3056..06a4f032 100644 --- a/src/groovy/com/netflix/asgard/model/AutoScalingGroupBeanOptions.groovy +++ b/src/groovy/com/netflix/asgard/model/AutoScalingGroupBeanOptions.groovy @@ -97,7 +97,7 @@ import groovy.transform.Canonical private static Set copyTags(Collection tags) { if (tags == null) { return null } tags.collect { - new Tag(resourceId: it.resourceId, resourceType: it.resourceType, key: it.key, value: it.value) + new Tag(resourceId: it.resourceId, resourceType: it.resourceType, key: it.key, value: it.value, propagateAtLaunch: it.propagateAtLaunch) } as Set } diff --git a/src/groovy/com/netflix/asgard/push/GroupCreateOperation.groovy b/src/groovy/com/netflix/asgard/push/GroupCreateOperation.groovy index 83fbb331..fbbf251e 100644 --- a/src/groovy/com/netflix/asgard/push/GroupCreateOperation.groovy +++ b/src/groovy/com/netflix/asgard/push/GroupCreateOperation.groovy @@ -67,14 +67,15 @@ class GroupCreateOperation extends AbstractPushOperation { task.email = applicationService.getEmailFromApp(options.common.userContext, options.common.appName) thisOperation.task = task task.log("Group '${options.common.groupName}' will start with 0 instances") - + AutoScalingGroup groupTemplate = new AutoScalingGroup().withAutoScalingGroupName(options.common.groupName). withAvailabilityZones(options.availabilityZones).withLoadBalancerNames(options.loadBalancerNames). withMinSize(0).withDesiredCapacity(0).withMaxSize(options.maxSize). withDefaultCooldown(options.defaultCooldown).withHealthCheckType(options.healthCheckType). withHealthCheckGracePeriod(options.healthCheckGracePeriod). withTerminationPolicies(options.terminationPolicies). - withVPCZoneIdentifier(options.vpcZoneIdentifier) + withVPCZoneIdentifier(options.vpcZoneIdentifier). + withTags(options.tags) LaunchConfiguration launchConfigTemplate = new LaunchConfiguration().withImageId(options.common.imageId). withKernelId(options.kernelId).withInstanceType(options.common.instanceType). withKeyName(options.keyName).withRamdiskId(options.ramdiskId). @@ -98,7 +99,7 @@ ${groupTemplate.loadBalancerNames} and result ${result}""" if (result.succeeded()) { // Add scalingPolicies to ASG. In the future this might need to be its own operation for reuse. awsAutoScalingService.createScalingPolicies(options.common.userContext, options.scalingPolicies, task) - awsAutoScalingService.createScheduledActions(options.common.userContext, options.scheduledActions, task) + awsAutoScalingService.createScheduledActions(options.common.userContext, options.scheduledActions, task) // If the user wanted any instances then start a resize operation. if (options.minSize > 0 || options.desiredCapacity > 0) { diff --git a/src/groovy/com/netflix/asgard/push/GroupCreateOptions.groovy b/src/groovy/com/netflix/asgard/push/GroupCreateOptions.groovy index 1ba5fa21..93c57817 100644 --- a/src/groovy/com/netflix/asgard/push/GroupCreateOptions.groovy +++ b/src/groovy/com/netflix/asgard/push/GroupCreateOptions.groovy @@ -16,6 +16,8 @@ package com.netflix.asgard.push import com.amazonaws.services.autoscaling.model.ScheduledUpdateGroupAction +import com.amazonaws.services.autoscaling.model.Tag +import com.amazonaws.services.autoscaling.model.TagDescription import com.netflix.asgard.model.ScalingPolicyData import groovy.transform.Immutable @@ -37,6 +39,7 @@ import groovy.transform.Immutable Collection scheduledActions String spotPrice boolean ebsOptimized + Collection tags /** The number of instances to create at a time while inflating the auto scaling group. */ Integer batchSize diff --git a/test/unit/com/netflix/asgard/AwsAutoScalingServiceIntegrationSpec.groovy b/test/unit/com/netflix/asgard/AwsAutoScalingServiceIntegrationSpec.groovy index e25522a6..36d80b66 100644 --- a/test/unit/com/netflix/asgard/AwsAutoScalingServiceIntegrationSpec.groovy +++ b/test/unit/com/netflix/asgard/AwsAutoScalingServiceIntegrationSpec.groovy @@ -27,6 +27,7 @@ import com.amazonaws.services.autoscaling.model.ResumeProcessesRequest import com.amazonaws.services.autoscaling.model.ScalingPolicy import com.amazonaws.services.autoscaling.model.SuspendProcessesRequest import com.amazonaws.services.autoscaling.model.SuspendedProcess +import com.amazonaws.services.autoscaling.model.Tag import com.amazonaws.services.autoscaling.model.UpdateAutoScalingGroupRequest import com.amazonaws.services.cloudwatch.model.Dimension import com.amazonaws.services.cloudwatch.model.MetricAlarm @@ -139,7 +140,8 @@ class AwsAutoScalingServiceIntegrationSpec extends Specification { final AutoScalingGroup groupTemplate = new AutoScalingGroup().withAutoScalingGroupName('helloworld-example'). withAvailabilityZones([]).withLoadBalancerNames([]). - withMaxSize(0).withMinSize(0).withDefaultCooldown(0) + withMaxSize(0).withMinSize(0).withDefaultCooldown(0). + withTags([new Tag(resourceType:'auto-scaling-group', resourceId:'helloworld-example',key:'test',value:'lastTag')]) final LaunchConfiguration launchConfigTemplate = new LaunchConfiguration().withImageId('ami-deadbeef'). withInstanceType('m1.small').withKeyName('keyName').withSecurityGroups([]).withUserData(''). withEbsOptimized(false) diff --git a/test/unit/com/netflix/asgard/ClusterControllerSpec.groovy b/test/unit/com/netflix/asgard/ClusterControllerSpec.groovy index 3dc1a5aa..934200c8 100644 --- a/test/unit/com/netflix/asgard/ClusterControllerSpec.groovy +++ b/test/unit/com/netflix/asgard/ClusterControllerSpec.groovy @@ -18,6 +18,7 @@ package com.netflix.asgard import com.amazonaws.services.autoscaling.model.AutoScalingGroup import com.amazonaws.services.autoscaling.model.Instance import com.amazonaws.services.autoscaling.model.LaunchConfiguration +import com.amazonaws.services.autoscaling.model.Tag import com.netflix.asgard.model.AutoScalingGroupData import com.netflix.asgard.model.AutoScalingGroupHealthCheckType import com.netflix.asgard.model.AutoScalingGroupMixin @@ -46,12 +47,15 @@ class ClusterControllerSpec extends Specification { subnet('subnet-3', 'us-east-1e', 'internal'), subnet('subnet-4', 'us-east-1e', 'external'), ]) + + final AutoScalingGroup asg = new AutoScalingGroup(autoScalingGroupName: 'helloworld-example-v015', minSize: 3, desiredCapacity: 5, maxSize: 7, healthCheckGracePeriod: 42, defaultCooldown: 360, launchConfigurationName: 'helloworld-lc', healthCheckType: AutoScalingGroupHealthCheckType.EC2, instances: [new Instance(instanceId: 'i-6ef9f30e'), new Instance(instanceId: 'i-95fe1df6')], availabilityZones: ['us-east-1c'], loadBalancerNames: ['hello-elb'], terminationPolicies: ['hello-tp'], - vPCZoneIdentifier: 'subnet-1') + vPCZoneIdentifier: 'subnet-1', + tags: [new Tag(resourceType:'auto-scaling-group', resourceId:'helloworld-example-v015',key:'test',value:'lastTag')]) final LaunchConfiguration launchConfiguration = new LaunchConfiguration(imageId: 'lastImageId', instanceType: 'lastInstanceType', keyName: 'lastKeyName', securityGroups: ['sg-123', 'sg-456'], iamInstanceProfile: 'lastIamProfile', spotPrice: '1.23') @@ -166,6 +170,7 @@ class ClusterControllerSpec extends Specification { assert vpcZoneIdentifier == 'subnet-1' assert iamInstanceProfile == 'lastIamProfile' assert spotPrice == '1.23' + assert tags["value"] == ['lastTag'] } true }) >> { args -> @@ -223,6 +228,7 @@ class ClusterControllerSpec extends Specification { controller.awsAutoScalingService.getLaunchConfiguration(_, 'helloworld-lc') >> launchConfiguration controller.params.with() { name = 'helloworld-example' + noOptionalDefaults = 'true' selectedSecurityGroups = 'sg-789' selectedZones = 'us-east-1e' terminationPolicy = 'hello-tp2' @@ -240,6 +246,7 @@ class ClusterControllerSpec extends Specification { keyName = 'newKeyName' subnetPurpose = 'external' pricing = InstancePriceType.ON_DEMAND.name() + tags = [value:[test:'newTag']] } when: @@ -266,6 +273,7 @@ class ClusterControllerSpec extends Specification { assert vpcZoneIdentifier == 'subnet-4' assert iamInstanceProfile == 'newIamProfile' assert spotPrice == null + assert tags["value"] == ['newTag'] } true }) >> { args -> diff --git a/web-app/css/main.css b/web-app/css/main.css index 73fbd181..881d9831 100644 --- a/web-app/css/main.css +++ b/web-app/css/main.css @@ -463,7 +463,7 @@ td.checkbox { text-align: center; } .groupReplacingPush li.disabledGroup, .groupReplacingPush li.disabledGroup th, .groupReplacingPush li.disabledGroup div.buttons { background-color: #EBEBEB; background-image: none; } -.groupReplacingPush li.create { max-width: 380px; } +.groupReplacingPush li.create { max-width: 540px; } .groupReplacingPush li.create table { width: auto; } .groupReplacingPush li h2 { margin-top: 0; } .groupReplacingPush li.create h2:first-of-type { float: left; } diff --git a/web-app/js/custom.js b/web-app/js/custom.js index 6fa9ca9a..bc9d506d 100644 --- a/web-app/js/custom.js +++ b/web-app/js/custom.js @@ -1058,6 +1058,23 @@ jQuery(document).ready(function() { // Auto Scaling Group edit page var setUpGroupEditScreen = function() { + var normalForms, tagValueInputs, tagNameInputs, formSubmitInterceptor; + normalForms = jQuery('form').has('button[type="submit"]'); + + formSubmitInterceptor = function(event) { + tagValueInputs = normalForms.find('input.tagValue'); + // Dynamically name tag input + tagValueInputs.each(function() { + var jInput, name; + jInput = jQuery(this); + row = jInput.closest('tr') + name = row.find('input.tagName').val(); + row.find('input.tagValue').attr('name', 'tags.value.' + name); + row.find('input.tagProps').attr('name', 'tags.props.' + name); + }); + return true; + }; + var showAndEnableDesiredSize, jDesiredCapacityContainer = jQuery('.desiredCapacityContainer');; if (jDesiredCapacityContainer.exists()) { showAndEnableDesiredSize = function() { @@ -1066,6 +1083,88 @@ jQuery(document).ready(function() { }; jQuery(document).on('click', '.enableManualDesiredCapacityOverride', showAndEnableDesiredSize); } + + clearDisable = function(){ + if (normalForms.find('input[type="text"].error').length == 0){ + jQuery('button[type="submit"]').attr('disabled', false); + } + } + + uniqueName = function(event){ + var current = jQuery(event); + + tagNameInputs = normalForms.find('input.tagName'); + tagNameInputs.each(function() { + if (jQuery(this).val() == current.val() && jQuery(this).attr('id') != current.attr('id')) + { + alert('Tag names must be unique!'); + current.addClass('error').focus().parent().yellowFade(); + jQuery('button[type="submit"]').attr('disabled', true); + return false; + } + + current.removeClass('error'); + clearDisable(); + }); + } + + uniqueValue = function(event){ + var current = jQuery(event); + + tagValueInputs = normalForms.find('input.tagValue'); + tagValueInputs.each(function() { + if (jQuery(this).val() == current.val() && jQuery(this).attr('id') != current.attr('id')) + { + alert('Tag values must be unique!'); + current.addClass('error').focus().parent().yellowFade(); + jQuery('button[type="submit"]').attr('disabled', true); + return false; + } + + current.removeClass('error'); + clearDisable(); + }); + } + + addRow = function(){ + counter = jQuery('#tags tr').length - 1; + + var newRow = jQuery(""); + var cols = ""; + + cols += ''; + cols += ''; + cols += ''; + + cols += ''; + jQuery(newRow).append(cols); + if (counter == 9) jQuery('#addrow').attr('disabled', true).prop('value', "You've reached the limit"); + jQuery('table[id="tags"]').append(newRow); + counter++; + }; + + delRow = function(){ + jQuery(this).closest("tr").remove(); + + counter -= 1 + jQuery('#addrow').attr('disabled', false).prop('value', "Add Tag"); + + clearDisable(); + } + + jQuery(document).on('click', '#addrow', addRow); + jQuery(document).on('click', '.ibtnDel', delRow); + + normalForms.find('.tagsDel').change(function (){ + if(jQuery(this).is(':checked')){ + jQuery(this).closest("tr").css({backgroundColor: "#FF0000"}); + } + else { + jQuery(this).closest("tr").css({backgroundColor: ""}); + } + }); + + normalForms.submit(formSubmitInterceptor); }; setUpGroupEditScreen();

    Auto Scaling

    Tags: +
    + + + + + + + + + + + + + + + + + + + + + +
    KeyValuePropDelete?
    + + + +
    + +
    +
    Tags: + + + + + + + + + + + + + + + + + +
    TagValuePropagate
    ${tag.key}
    ${tag.value}
    ${tag.propagateAtLaunch}
    +
    ${entry.value}