diff --git a/grails-app/conf/BuildConfig.groovy b/grails-app/conf/BuildConfig.groovy index 0fcdf8e8..246ed12b 100644 --- a/grails-app/conf/BuildConfig.groovy +++ b/grails-app/conf/BuildConfig.groovy @@ -131,9 +131,6 @@ grails.project.dependency.resolution = { // This fixes ivy resolution issues we had with our transitive dependency on 1.4. 'commons-codec:commons-codec:1.5', - // Call Perforce in process. Delete when user data no longer come from Perforce at deployment time. - 'com.perforce:p4java:2010.1.269249', - // Rules for AWS named objects, e.g., Names, AppVersion 'com.netflix.frigga:frigga:0.11', diff --git a/grails-app/conf/spring/resources.groovy b/grails-app/conf/spring/resources.groovy index 42b807c4..8b64a41e 100644 --- a/grails-app/conf/spring/resources.groovy +++ b/grails-app/conf/spring/resources.groovy @@ -19,9 +19,6 @@ import com.google.common.base.CaseFormat import com.netflix.asgard.CachedMapBuilder import com.netflix.asgard.Caches import com.netflix.asgard.CsiAsgAnalyzer -import com.netflix.asgard.userdata.DefaultAdvancedUserDataProvider -import com.netflix.asgard.userdata.DefaultUserDataProvider -import com.netflix.asgard.userdata.NetflixAdvancedUserDataProvider import com.netflix.asgard.NoOpAsgAnalyzer import com.netflix.asgard.Region import com.netflix.asgard.ServiceInitLoggingBeanPostProcessor @@ -34,6 +31,10 @@ import com.netflix.asgard.deployment.DeploymentActivitiesImpl import com.netflix.asgard.eureka.EurekaClientHolder import com.netflix.asgard.model.CsiScheduledAnalysisFactory import com.netflix.asgard.server.DeprecatedServerNames +import com.netflix.asgard.userdata.DefaultAdvancedUserDataProvider +import com.netflix.asgard.userdata.DefaultUserDataProvider +import com.netflix.asgard.userdata.LocalFileUserDataProvider +import com.netflix.asgard.userdata.NetflixAdvancedUserDataProvider import com.netflix.asgard.userdata.PropertiesUserDataProvider import groovy.io.FileType @@ -88,6 +89,12 @@ beans = { } } + if (application.config.plugin?.userDataProvider == 'localFileUserDataProvider') { + localFileUserDataProvider(LocalFileUserDataProvider) { bean -> + bean.lazyInit = true + } + } + if (application.config.plugin?.advancedUserDataProvider == 'netflixAdvancedUserDataProvider') { netflixAdvancedUserDataProvider(NetflixAdvancedUserDataProvider) { bean -> bean.lazyInit = true diff --git a/src/groovy/com/netflix/asgard/userdata/LocalFileUserDataProvider.groovy b/src/groovy/com/netflix/asgard/userdata/LocalFileUserDataProvider.groovy new file mode 100644 index 00000000..09bb6815 --- /dev/null +++ b/src/groovy/com/netflix/asgard/userdata/LocalFileUserDataProvider.groovy @@ -0,0 +1,131 @@ +/* + * Copyright 2014 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.asgard.userdata + +import com.netflix.asgard.ApplicationService +import com.netflix.asgard.Check +import com.netflix.asgard.ConfigService +import com.netflix.asgard.Region +import com.netflix.asgard.Relationships +import com.netflix.asgard.UserContext +import com.netflix.asgard.plugin.UserDataProvider +import com.netflix.frigga.Names +import org.springframework.beans.factory.annotation.Autowired + +class LocalFileUserDataProvider implements UserDataProvider { + + private static final INSERTION_MARKER = '\nexport EC2_REGION=' + + @Autowired + ConfigService configService + + @Autowired + ApplicationService applicationService + + String buildUserDataForVariables(UserContext userContext, String appName, + String autoScalingGroupName, String launchConfigName) { + String env = configService.getAccountName() + String rawUserData = assembleUserData(appName, autoScalingGroupName, userContext.region, env) + replaceUserDataTokens(userContext, rawUserData, appName, env, autoScalingGroupName, + launchConfigName) + } + + private String assembleUserData(String app, String groupName, Region region, String env) { + def udfRoot = System.getProperty('userDataRoot') ?: '/apps/nflx-udf' + + // If there is a matching Ruby file for Windows then it should be the entire User Data string, without any Unix + String udfRuby = getContents("${udfRoot}/custom.d/${app}-${env}.rb") + if (udfRuby) { + return udfRuby + } + + String cluster = Relationships.clusterFromGroupName(groupName) + String stack = Relationships.stackNameFromGroupName(groupName) + + // If no Ruby file then get the component Unix shell template files into string lists including custom files for + // the app and/or auto scaling group. + // If app and group names are identical, only include their UDF file once. + + // LinkedHashSet ensures correct order and no duplicates when the app, cluster, and groupName are equal. + Set udfPaths = new LinkedHashSet() + udfPaths << "${udfRoot}/udf0" + udfPaths << "${udfRoot}/udf-${env}" + udfPaths << "${udfRoot}/udf-${region}-${env}" + udfPaths << "${udfRoot}/udf1" + udfPaths << "${udfRoot}/custom.d/${app}-${env}" + udfPaths << "${udfRoot}/custom.d/${app}-${stack}-${env}" + udfPaths << "${udfRoot}/custom.d/${cluster}-${env}" + udfPaths << "${udfRoot}/custom.d/${groupName}-${env}" + udfPaths << "${udfRoot}/custom.region.d/${region}/${app}-${env}" + udfPaths << "${udfRoot}/custom.region.d/${region}/${app}-${stack}-${env}" + udfPaths << "${udfRoot}/custom.region.d/${region}/${cluster}-${env}" + udfPaths << "${udfRoot}/custom.region.d/${region}/${groupName}-${env}" + udfPaths << "${udfRoot}/udf2" + + // Concat all the Unix shell templates into one string + udfPaths.collect { String path -> getContents(path) }.join('') + } + + private String getContents(String filePath) { + try { + File file = new File(filePath) + String contents = file.getText('UTF-8') + if (contents.length() && !contents.endsWith("\n")) { contents = contents + '\n' } + return contents + } catch (IOException ignore) { + // This normal case happens if the requested file is not found. + return '' + } + } + + private String replaceUserDataTokens(UserContext userContext, String rawUserData, String app, String env, + String autoScalingGroupName, String launchConfigName) { + Check.notNull(rawUserData, String, 'rawUserData') + Check.notNull(app, String, 'app') + Check.notNull(env, String, 'env') + Check.notNull(autoScalingGroupName, String, 'autoScalingGroupName') + Check.notNull(launchConfigName, String, 'launchConfigName') + + Names names = Relationships.dissectCompoundName(autoScalingGroupName) + String stack = names.stack ?: '' + String cluster = names.cluster ?: '' + String region = userContext.region.code + String tier = applicationService.getMonitorBucket(userContext, app, names.cluster) + + // Replace the tokens & return the result + String result = rawUserData + .replace('%%app%%', app) + .replace('%%tier%%', tier) + .replace('%%env%%', env) + .replace('%%region%%', region) + .replace('%%group%%', autoScalingGroupName) + .replace('%%autogrp%%', autoScalingGroupName) + .replace('%%cluster%%', cluster) + .replace('%%stack%%', stack) + .replace('%%launchconfig%%', launchConfigName) + .replace('%%app_group%%', applicationService.getRegisteredApplication(userContext, app)?.group ?: '') + + List additionalEnvVars = Relationships.labeledEnvironmentVariables(names, + configService.userDataVarPrefix) + if (additionalEnvVars) { + String insertion = "\n${additionalEnvVars.join('\n')}" + result = result.replace(INSERTION_MARKER, "\n${insertion}${INSERTION_MARKER}") + } + result + } + +} +