import { module } from 'angular' ; import { chain , clone , cloneDeep , extend , find , flatten , has , intersection , keys , map , partition , some , xor , } from 'lodash' ; import { $q } from 'ngimport' ; import type { Application , CacheInitializerService , IAccountDetails , IDeploymentStrategy , IRegion , ISecurityGroup , IServerGroupCommand , IServerGroupCommandBackingData , IServerGroupCommandBackingDataFiltered , IServerGroupCommandDirty , IServerGroupCommandResult , IServerGroupCommandViewState , ISubnet , LoadBalancerReader , SecurityGroupReader , ServerGroupCommandRegistry , } from '@spinnaker/core' ; import { AccountService , CACHE_INITIALIZER_SERVICE , LOAD_BALANCER_READ_SERVICE , NameUtils , SECURITY_GROUP_READER , SERVER_GROUP_COMMAND_REGISTRY_PROVIDER , setMatchingResourceSummary , SubnetReader , } from '@spinnaker/core' ; import { AWSProviderSettings } from '../../aws.settings' ; import { AutoScalingProcessService } from '../details/scalingProcesses/AutoScalingProcessService' ; import type { IAmazonLoadBalancerSourceData , IApplicationLoadBalancerSourceData , IKeyPair , IScalingProcess , } from '../../domain' ; import { AMAZON_INSTANCE_AWSINSTANCETYPE_SERVICE } from '../../instance/awsInstanceType.service' ; import { KeyPairsReader } from '../../keyPairs' ; export type IBlockDeviceMappingSource = 'source' | 'ami' | 'default' ; export interface IAmazonServerGroupCommandDirty extends IServerGroupCommandDirty { targetGroups ? : string [ ] ; } export interface IAmazonServerGroupCommandResult extends IServerGroupCommandResult { dirty : IAmazonServerGroupCommandDirty ; } export interface IAmazonServerGroupCommandBackingDataFiltered extends IServerGroupCommandBackingDataFiltered { keyPairs : string [ ] ; targetGroups : string [ ] ; } export interface IAmazonServerGroupCommandBackingData extends IServerGroupCommandBackingData { appLoadBalancers : IAmazonLoadBalancerSourceData [ ] ; filtered : IAmazonServerGroupCommandBackingDataFiltered ; keyPairs : IKeyPair [ ] ; targetGroups : string [ ] ; scalingProcesses : IScalingProcess [ ] ; } export interface IAmazonServerGroupCommandViewState extends IServerGroupCommandViewState { isNew : boolean ; dirty : IAmazonServerGroupCommandDirty ; spelTargetGroups : string [ ] ; spelLoadBalancers : string [ ] ; useSimpleInstanceTypeSelector : boolean ; } export interface IAmazonInstanceTypeOverride { instanceType : string ; weightedCapacity ? : string ; priority ? : number ; } /**
 * We model server group commands in two subtly different ways.
 * It's unclear what the intention is, but we should converge on a single model eventually.
 *
 * This model is stored in a Aws Deploy Stage under `clusters[]`.
 * It is also sent across the wire during a deploy task from infrastructure view
 * (i.e., "clone server group", "create server group")
 */ export interface IAmazonServerGroupDeployConfiguration extends IAmazonServerGroupCommand_Internal { account : string ; availabilityZones : { [ region ] : string [ ] ; } ; } interface IAmazonServerGroupCommand_Internal extends IAmazonServerGroupCommand { availabilityZones : any ; } export interface IAmazonServerGroupCommand extends IServerGroupCommand { associateIPv6Address ? : boolean ; associatePublicIpAddress : boolean ; backingData : IAmazonServerGroupCommandBackingData ; base64UserData : string ; copySourceCustomBlockDeviceMappings : boolean ; ebsOptimized : boolean ; healthCheckGracePeriod : number ; instanceMonitoring : boolean ; keyPair : string ; legacyUdf ? : boolean ; requireIMDSv2 ? : boolean ; spotPrice : string ; targetHealthyDeployPercentage : number ; useAmiBlockDeviceMappings : boolean ; targetGroups : string [ ] ; setLaunchTemplate ? : boolean ; unlimitedCpuCredits ? : boolean ; capacityRebalance ? : boolean ; ramdiskId ? : string ; viewState : IAmazonServerGroupCommandViewState ; source : { account : string ; region : string ; asgName : string ; } ; onDemandAllocationStrategy ? : string ; onDemandBaseCapacity ? : number ; onDemandPercentageAboveBaseCapacity ? : number ; spotAllocationStrategy ? : string ; spotInstancePools ? : number ; launchTemplateOverridesForInstanceType ? : IAmazonInstanceTypeOverride [ ] ; getBlockDeviceMappingsSource : ( command : IServerGroupCommand ) => IBlockDeviceMappingSource ; selectBlockDeviceMappingsSource : ( command : IServerGroupCommand , selection : string ) => void ; usePreferredZonesChanged : ( command : IServerGroupCommand ) => IAmazonServerGroupCommandResult ; regionIsDeprecated : ( command : IServerGroupCommand ) => boolean ; launchTemplateOverridesChanged : ( command : IServerGroupCommand ) => void ; } export class AwsServerGroupConfigurationService { private enabledMetrics = [ 'GroupMinSize' , 'GroupMaxSize' , 'GroupDesiredCapacity' , 'GroupInServiceInstances' , 'GroupPendingInstances' , 'GroupStandbyInstances' , 'GroupTerminatingInstances' , 'GroupTotalInstances' , ] ; private healthCheckTypes = [ 'EC2' , 'ELB' ] ; private terminationPolicies = [ 'OldestInstance' , 'NewestInstance' , 'OldestLaunchConfiguration' , 'ClosestToNextInstanceHour' , 'Default' , ] ; public static $inject = [ 'securityGroupReader' , 'awsInstanceTypeService' , 'cacheInitializer' , 'loadBalancerReader' , 'serverGroupCommandRegistry' , ] ; constructor ( private securityGroupReader , private awsInstanceTypeService , private cacheInitializer , private loadBalancerReader , private serverGroupCommandRegistry ) { } public configureUpdateCommand ( command ) { command . backingData = { enabledMetrics : clone ( this . enabledMetrics ) , healthCheckTypes : clone ( this . healthCheckTypes ) , terminationPolicies : clone ( this . terminationPolicies ) , } as IAmazonServerGroupCommandBackingData ; } public configureCommand ( application , cmd ) { this . applyOverrides ( 'beforeConfiguration' , cmd ) ; cmd . toggleSuspendedProcess = ( command , process ) : void => { command . suspendedProcesses = command . suspendedProcesses || [ ] ; const processIndex = command . suspendedProcesses . indexOf ( process ) ; if ( processIndex === - 1 ) { command . suspendedProcesses = command . suspendedProcesses . concat ( process ) ; } else { command . suspendedProcesses = command . suspendedProcesses . filter ( ( p ) => p !== process ) ; } } ; cmd . processIsSuspended = ( command , process ) : boolean => command . suspendedProcesses . includes ( process ) ; cmd . onStrategyChange = ( command , strategy ) : void => { if ( AWSProviderSettings . serverGroups ?. enableLaunchTemplates ) { command . setLaunchTemplate = strategy . key === 'rollingpush' ? true : undefined ; } if ( strategy . key !== '' && strategy . key !== 'custom' ) { command . suspendedProcesses = ( command . suspendedProcesses || [ ] ) . filter ( ( p ) => p !== 'AddToLoadBalancer' ) ; } } ; cmd . getBlockDeviceMappingsSource = ( command ) : IBlockDeviceMappingSource => { if ( command . copySourceCustomBlockDeviceMappings ) { return 'source' ; } else if ( command . useAmiBlockDeviceMappings ) { return 'ami' ; } return 'default' ; } ; cmd . selectBlockDeviceMappingsSource = ( command , selection ) : void => { if ( selection === 'source' ) { command . copySourceCustomBlockDeviceMappings = true ; command . useAmiBlockDeviceMappings = false ; } else if ( selection === 'ami' ) { command . copySourceCustomBlockDeviceMappings = false ; command . useAmiBlockDeviceMappings = true ; } else { command . copySourceCustomBlockDeviceMappings = false ; command . useAmiBlockDeviceMappings = false ; } } ; cmd . regionIsDeprecated = ( command ) : boolean => { return ( has ( command , 'backingData.filtered.regions' ) && command . backingData . filtered . regions . some ( ( region ) => region . name === command . region && region . deprecated ) ) ; } ; return $q . all ( [ AccountService . getCredentialsKeyedByAccount ( 'aws' ) , this . securityGroupReader . getAllSecurityGroups ( ) , SubnetReader . listSubnets ( ) , AccountService . getPreferredZonesByAccount ( 'aws' ) , KeyPairsReader . listKeyPairs ( ) , this . awsInstanceTypeService . getAllTypesByRegion ( ) , $q . when ( clone ( this . enabledMetrics ) ) , $q . when ( clone ( this . healthCheckTypes ) ) , $q . when ( clone ( this . terminationPolicies ) ) , ] ) . then ( ( [ credentialsKeyedByAccount , securityGroups , subnets , preferredZones , keyPairs , instanceTypes , enabledMetrics , healthCheckTypes , terminationPolicies , ] ) => { const backingData = { credentialsKeyedByAccount , securityGroups , subnets , preferredZones , keyPairs , instanceTypes , enabledMetrics , healthCheckTypes , terminationPolicies , } ; let securityGroupReloader = $q . when ( ) ; backingData . accounts = keys ( backingData . credentialsKeyedByAccount ) ; backingData . filtered = { } as IAmazonServerGroupCommandBackingDataFiltered ; backingData . scalingProcesses = AutoScalingProcessService . listProcesses ( ) ; backingData . appLoadBalancers = application . getDataSource ( 'loadBalancers' ) . data ; backingData . managedResources = application . getDataSource ( 'managedResources' ) ?. data ?. resources ; cmd . backingData = backingData as IAmazonServerGroupCommandBackingData ; this . configureVpcId ( cmd ) ; backingData . filtered . securityGroups = this . getRegionalSecurityGroups ( cmd ) ; if ( cmd . viewState . disableImageSelection ) { this . configureInstanceTypes ( cmd ) ; } if ( cmd . securityGroups && cmd . securityGroups . length ) { const regionalSecurityGroupIds = map ( this . getRegionalSecurityGroups ( cmd ) , 'id' ) ; if ( intersection ( cmd . securityGroups , regionalSecurityGroupIds ) . length < cmd . securityGroups . length ) { securityGroupReloader = this . refreshSecurityGroups ( cmd , true ) ; } } return securityGroupReloader . then ( ( ) => { this . applyOverrides ( 'afterConfiguration' , cmd ) ; this . attachEventHandlers ( cmd ) ; } ) ; } ) ; } public applyOverrides ( phase , command ) { this . serverGroupCommandRegistry . getCommandOverrides ( 'aws' ) . forEach ( ( override ) => { if ( override [ phase ] ) { override [ phase ] ( command ) ; } } ) ; } public configureKeyPairs ( command ) { const result = { dirty : { } } ; if ( command . credentials && command . region ) { const isDefault = some < any > ( command . backingData . credentialsKeyedByAccount , ( c ) => c . defaultKeyPair && command . keyPair && command . keyPair . indexOf ( c . defaultKeyPair . replace ( '{{region}}' , '' ) ) === 0 ) ; const filtered = chain ( command . backingData . keyPairs ) . filter ( { account : command . credentials , region : command . region } ) . map ( 'keyName' ) . value ( ) ; if ( command . keyPair && filtered . length && ! filtered . includes ( command . keyPair ) ) { const acct = command . backingData . credentialsKeyedByAccount [ command . credentials ] || ( { regions : [ ] , defaultKeyPair : null , } as IAccountDetails ) ; if ( acct . defaultKeyPair ) { const defaultKeyPair = acct . defaultKeyPair . replace ( '{{region}}' , command . region ) ; if ( isDefault && filtered . includes ( defaultKeyPair ) ) { command . keyPair = defaultKeyPair ; } else { command . keyPair = null ; result . dirty . keyPair = true ; } } else { command . keyPair = null ; result . dirty . keyPair = true ; } } command . backingData . filtered . keyPairs = filtered ; } else { command . backingData . filtered . keyPairs = [ ] ; } return result ; } public configureInstanceTypes ( command ) { const result = { dirty : { } } ; if ( command . region && ( command . virtualizationType || command . viewState . disableImageSelection ) ) { let filtered = this . awsInstanceTypeService . getAvailableTypesForRegions ( command . backingData . instanceTypes , [ command . region , ] ) ; if ( command . virtualizationType ) { filtered = this . awsInstanceTypeService . filterInstanceTypes ( filtered , command . virtualizationType , ! ! command . vpcId ) ; } if ( command . instanceType && ! filtered . includes ( command . instanceType ) ) { result . dirty . instanceType = command . instanceType ; command . instanceType = null ; } command . backingData . filtered . instanceTypes = filtered ; } else { command . backingData . filtered . instanceTypes = [ ] ; } extend ( command . viewState . dirty , result . dirty ) ; return result ; } public configureImages ( command ) { const result = { dirty : { } } ; if ( ! command . amiName ) { command . virtualizationType = null ; } if ( command . viewState . disableImageSelection ) { return result ; } if ( command . amiName && ! command . region ) { result . dirty . amiName = true ; command . amiName = null ; } return result ; } public configureAvailabilityZones ( command ) { command . backingData . filtered . availabilityZones = find < IRegion > ( command . backingData . credentialsKeyedByAccount [ command . credentials ] . regions , { name : command . region } ) . availabilityZones ; } public configureSubnetPurposes ( command ) { const result = { dirty : { } } ; const filteredData = command . backingData . filtered ; if ( command . region === null ) { return result ; } filteredData . subnetPurposes = chain ( command . backingData . subnets ) . filter ( { account : command . credentials , region : command . region } ) . reject ( { target : 'elb' } ) . reject ( { purpose : null } ) . uniqBy ( 'purpose' ) . value ( ) ; if ( ! chain ( filteredData . subnetPurposes ) . some ( { purpose : command . subnetType } ) . value ( ) ) { command . subnetType = null ; result . dirty . subnetType = true ; } return result ; } public getRegionalSecurityGroups ( command ) { const newSecurityGroups = command . backingData . securityGroups [ command . credentials ] || { aws : { } } ; return chain ( newSecurityGroups . aws [ command . region ] ) . filter ( { vpcId : command . vpcId || null } ) . sortBy ( 'name' ) . value ( ) ; } public configureSecurityGroupOptions ( command ) { const result = { dirty : { } } ; const currentOptions = command . backingData . filtered . securityGroups ; const newRegionalSecurityGroups = this . getRegionalSecurityGroups ( command ) ; const isExpression = typeof command . securityGroups === 'string' && ( command . securityGroups as string ) . includes ( '${' ) ; if ( currentOptions && command . securityGroups && ! isExpression ) { const currentGroupNames = command . securityGroups . map ( ( groupId ) => { const match = find ( currentOptions , { id : groupId } ) ; return match ? match . name : groupId ; } ) ; const matchedGroups = command . securityGroups . map ( ( groupId ) => { const securityGroup = find ( currentOptions , { id : groupId } ) || find ( currentOptions , { name : groupId } ) ; return securityGroup ? securityGroup . name : null ; } ) . map ( ( groupName ) => find ( newRegionalSecurityGroups , { name : groupName } ) ) . filter ( ( group ) => group ) ; const matchedGroupNames = map ( matchedGroups , 'name' ) ; const removed = xor ( currentGroupNames , matchedGroupNames ) ; command . securityGroups = map ( matchedGroups , 'id' ) ; if ( removed . length ) { result . dirty . securityGroups = removed ; } } command . backingData . filtered . securityGroups = newRegionalSecurityGroups . sort ( ( a , b ) => { return a . name . localeCompare ( b . name ) ; } ) ; return result ; } public refreshSecurityGroups ( command , skipCommandReconfiguration ? ) { return this . cacheInitializer . refreshCache ( 'securityGroups' ) . then ( ( ) => { return this . securityGroupReader . getAllSecurityGroups ( ) . then ( ( securityGroups ) => { command . backingData . securityGroups = securityGroups ; if ( ! skipCommandReconfiguration ) { this . configureSecurityGroupOptions ( command ) ; } } ) ; } ) ; } private getLoadBalancerMap ( command ) { if ( command . backingData . loadBalancers ) { return chain ( command . backingData . loadBalancers ) . map ( 'accounts' ) . flattenDeep ( ) . filter ( { name : command . credentials } ) . map ( 'regions' ) . flattenDeep ( ) . filter ( { name : command . region } ) . map < IAmazonLoadBalancerSourceData > ( 'loadBalancers' ) . flattenDeep < IAmazonLoadBalancerSourceData > ( ) . value ( ) ; } const appLoadBalancers = command . backingData . appLoadBalancers || [ ] ; return appLoadBalancers . filter ( ( lb ) => lb . region === command . region && lb . account === command . credentials ) ; } public getLoadBalancerNames ( command ) { const loadBalancers = this . getLoadBalancerMap ( command ) . filter ( ( lb ) => ( ! lb . loadBalancerType || lb . loadBalancerType === 'classic' ) && lb . vpcId === command . vpcId ) ; return loadBalancers . map ( ( lb ) => lb . name ) . sort ( ) ; } public getVpcLoadBalancerNames ( command ) { const loadBalancersForVpc = this . getLoadBalancerMap ( command ) . filter ( ( lb ) => ( ! lb . loadBalancerType || lb . loadBalancerType === 'classic' ) && lb . vpcId ) ; return loadBalancersForVpc . map ( ( lb ) => lb . name ) . sort ( ) ; } public getTargetGroupNames ( command ) { const loadBalancersV2 = this . getLoadBalancerMap ( command ) . filter ( ( lb ) => lb . loadBalancerType !== 'classic' ) as IApplicationLoadBalancerSourceData [ ] ; const instanceTargetGroups = flatten ( loadBalancersV2 . map < any > ( ( lb ) => lb . targetGroups . filter ( ( tg ) => tg . targetType === 'instance' ) ) ) ; return instanceTargetGroups . map ( ( tg ) => tg . name ) . sort ( ) ; } private getValidMatches ( all , current ) { const spel = current . filter ( ( v ) => v . includes ( '${' ) ) ; const matched = intersection ( all , current ) ; const [ valid , invalid ] = partition ( current , ( c ) => matched . includes ( c ) || spel . includes ( c ) ) ; return { valid , invalid , spel } ; } public configureLoadBalancerOptions ( command ) { const result = { dirty : { } } ; const currentLoadBalancers = ( command . loadBalancers || [ ] ) . concat ( command . vpcLoadBalancers || [ ] ) ; const currentTargetGroups = command . targetGroups || [ ] ; const newLoadBalancers = this . getLoadBalancerNames ( command ) ; const vpcLoadBalancers = this . getVpcLoadBalancerNames ( command ) ; const allTargetGroups = this . getTargetGroupNames ( command ) ; if ( currentLoadBalancers && command . loadBalancers ) { const allValidLoadBalancers = command . vpcId ? newLoadBalancers : newLoadBalancers . concat ( vpcLoadBalancers ) ; const { valid , invalid , spel } = this . getValidMatches ( allValidLoadBalancers , currentLoadBalancers ) ; command . loadBalancers = intersection ( newLoadBalancers . concat ( spel ) , valid ) ; if ( ! command . vpcId ) { command . vpcLoadBalancers = intersection ( vpcLoadBalancers , valid ) ; } else { delete command . vpcLoadBalancers ; } if ( invalid . length ) { result . dirty . loadBalancers = invalid ; } command . viewState . spelLoadBalancers = spel || [ ] ; } if ( currentTargetGroups && command . targetGroups && ! currentTargetGroups . includes ( '${' ) ) { const { valid , invalid , spel } = this . getValidMatches ( allTargetGroups , currentTargetGroups ) ; command . targetGroups = valid ; if ( invalid . length ) { result . dirty . targetGroups = invalid ; } command . viewState . spelTargetGroups = spel || [ ] ; } command . backingData . filtered . loadBalancers = newLoadBalancers ; command . backingData . filtered . vpcLoadBalancers = vpcLoadBalancers ; command . backingData . filtered . targetGroups = allTargetGroups ; return result ; } public refreshLoadBalancers ( command , skipCommandReconfiguration ? ) { return this . loadBalancerReader . listLoadBalancers ( 'aws' ) . then ( ( loadBalancers ) => { command . backingData . loadBalancers = loadBalancers ; if ( ! skipCommandReconfiguration ) { this . configureLoadBalancerOptions ( command ) ; } } ) ; } public configureVpcId ( command ) { const result = { dirty : { } } ; if ( ! command . subnetType ) { command . vpcId = null ; result . dirty . vpcId = true ; } else { const subnet = find < ISubnet > ( command . backingData . subnets , { purpose : command . subnetType , account : command . credentials , region : command . region , } ) ; command . vpcId = subnet ? subnet . vpcId : null ; } extend ( result . dirty , this . configureInstanceTypes ( command ) . dirty ) ; return result ; } public attachEventHandlers ( cmd ) { cmd . usePreferredZonesChanged = ( command ) : IAmazonServerGroupCommandResult => { const currentZoneCount = command . availabilityZones ? command . availabilityZones . length : 0 ; const result = { dirty : { } } ; const preferredZonesForAccount = command . backingData . preferredZones [ command . credentials ] ; if ( preferredZonesForAccount && preferredZonesForAccount [ command . region ] && command . viewState . usePreferredZones ) { command . availabilityZones = cloneDeep ( preferredZonesForAccount [ command . region ] . sort ( ) ) ; } else { command . availabilityZones = intersection ( command . availabilityZones , command . backingData . filtered . availabilityZones ) ; const newZoneCount = command . availabilityZones ? command . availabilityZones . length : 0 ; if ( currentZoneCount !== newZoneCount ) { result . dirty . availabilityZones = true ; } } return result ; } ; cmd . subnetChanged = ( command ) : IServerGroupCommandResult => { const result = this . configureVpcId ( command ) ; extend ( result . dirty , this . configureSecurityGroupOptions ( command ) . dirty ) ; extend ( result . dirty , this . configureLoadBalancerOptions ( command ) . dirty ) ; command . viewState . dirty = command . viewState . dirty || { } ; extend ( command . viewState . dirty , result . dirty ) ; return result ; } ; cmd . regionChanged = ( command ) : IServerGroupCommandResult => { const result = { dirty : { } } ; const filteredData = command . backingData . filtered ; extend ( result . dirty , this . configureSubnetPurposes ( command ) . dirty ) ; if ( command . region ) { extend ( result . dirty , command . subnetChanged ( command ) . dirty ) ; extend ( result . dirty , this . configureInstanceTypes ( command ) . dirty ) ; this . configureAvailabilityZones ( command ) ; extend ( result . dirty , command . usePreferredZonesChanged ( command ) . dirty ) ; extend ( result . dirty , this . configureImages ( command ) . dirty ) ; extend ( result . dirty , this . configureKeyPairs ( command ) . dirty ) ; } else { filteredData . regionalAvailabilityZones = null ; } setMatchingResourceSummary ( command ) ; return result ; } ; cmd . clusterChanged = ( command ) : void => { command . moniker = NameUtils . getMoniker ( command . application , command . stack , command . freeFormDetails ) ; setMatchingResourceSummary ( command ) ; } ; cmd . credentialsChanged = ( command ) : IServerGroupCommandResult => { const result = { dirty : { } } ; const backingData = command . backingData ; if ( command . credentials ) { const regionsForAccount = backingData . credentialsKeyedByAccount [ command . credentials ] || ( { regions : [ ] , defaultKeyPair : null } as IAccountDetails ) ; backingData . filtered . regions = regionsForAccount . regions ; if ( ! some ( backingData . filtered . regions , { name : command . region } ) ) { command . region = null ; result . dirty . region = true ; } else { extend ( result . dirty , command . regionChanged ( command ) . dirty ) ; } } else { command . region = null ; } setMatchingResourceSummary ( command ) ; return result ; } ; cmd . imageChanged = ( command ) : IServerGroupCommandResult => this . configureInstanceTypes ( command ) ; cmd . instanceTypeChanged = ( command ) : void => { command . ebsOptimized = this . awsInstanceTypeService . isEbsOptimized ( command . instanceType ) ; } ; cmd . launchTemplateOverridesChanged = ( command ) : void => { command . ebsOptimized = command . launchTemplateOverridesForInstanceType ?. every ( ( o ) => this . awsInstanceTypeService . isEbsOptimized ( o . instanceType ) ) ; } ; this . applyOverrides ( 'attachEventHandlers' , cmd ) ; } } export const AWS_SERVER_GROUP_CONFIGURATION_SERVICE = 'spinnaker.amazon.serverGroup.configure.service' ; module ( AWS_SERVER_GROUP_CONFIGURATION_SERVICE , [ SECURITY_GROUP_READER , AMAZON_INSTANCE_AWSINSTANCETYPE_SERVICE , LOAD_BALANCER_READ_SERVICE , CACHE_INITIALIZER_SERVICE , SERVER_GROUP_COMMAND_REGISTRY_PROVIDER , ] ) . service ( 'awsServerGroupConfigurationService' , AwsServerGroupConfigurationService ) ;