Skip to content

Commit

Permalink
Added an ApplicationService supporting Spinnaker.
Browse files Browse the repository at this point in the history
  • Loading branch information
ajordens committed Dec 3, 2014
1 parent e1b8e71 commit 85cf902
Show file tree
Hide file tree
Showing 13 changed files with 858 additions and 202 deletions.
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ root = true

# Change these settings to your own preference
indent_style = space
indent_size = 2
indent_size = 4

# We recommend you to keep these unchanged
end_of_line = lf
Expand Down
6 changes: 5 additions & 1 deletion grails-app/conf/BuildConfig.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ grails.project.dependency.resolution = {
grailsHome()
grailsCentral()
mavenCentral()
mavenRepo "http://dl.bintray.com/spinnaker/spinnaker"

// Optional custom repository for dependencies.
Closure internalRepo = {
Expand Down Expand Up @@ -140,7 +141,10 @@ grails.project.dependency.resolution = {
// Used for JSON parsing of AWS Simple Workflow Service metadata.
// Previously this was an indirect depencency through Grails itself, but this caused errors in some
// Grails environments.
'com.googlecode.json-simple:json-simple:1.1'
'com.googlecode.json-simple:json-simple:1.1',

// Spinnaker client is used to retrieve application metadata
'com.netflix.spinnaker.client:spinnaker-client:0.6'
) { // Exclude superfluous and dangerous transitive dependencies
excludes(
// Some libraries bring older versions of JUnit as a transitive dependency and that can interfere
Expand Down
15 changes: 15 additions & 0 deletions grails-app/conf/spring/resources.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ 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.applications.SimpleDBApplicationService
import com.netflix.asgard.applications.SpinnakerApplicationService
import com.netflix.asgard.NoOpAsgAnalyzer
import com.netflix.asgard.Region
import com.netflix.asgard.ServiceInitLoggingBeanPostProcessor
Expand Down Expand Up @@ -113,6 +115,19 @@ beans = {

restrictBrowserAuthorizationProvider(RestrictBrowserAuthorizationProvider)

if (application.config.spinnaker?.gateUrl) {
applicationService(
SpinnakerApplicationService, application.config.spinnaker.gateUrl, application.config.cloud.accountName
) { bean ->
bean.lazyInit = true
}
} else {
applicationService(SimpleDBApplicationService) { bean ->
bean.lazyInit = true
}
}


//**** Plugin behavior

xmlns lang:'http://www.springframework.org/schema/lang'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,9 @@ class ApplicationController {
String monitorBucketTypeString = params.monitorBucketType
String tags = normalizeTagDelimiter(params.tags)
MonitorBucketType bucketType = Enum.valueOf(MonitorBucketType, monitorBucketTypeString)
CreateApplicationResult result = applicationService.createRegisteredApplication(userContext, name, group,
type, desc, owner, email, bucketType, tags)
def result = applicationService.createRegisteredApplication(
userContext, name, group, type, desc, owner, email, bucketType, tags
)
flash.message = result.toString()
if (result.succeeded()) {
redirect(action: 'show', params: [id: name])
Expand Down Expand Up @@ -210,9 +211,10 @@ class ApplicationController {
String monitorBucketTypeString = params.monitorBucketType
try {
MonitorBucketType bucketType = Enum.valueOf(MonitorBucketType, monitorBucketTypeString)
applicationService.updateRegisteredApplication(userContext, name, group, type, desc, owner, email, tags,
bucketType)
flash.message = "Application '${name}' has been updated."
def result = applicationService.updateRegisteredApplication(
userContext, name, group, type, desc, owner, email, tags, bucketType
)
flash.message = result.toString()
} catch (Exception e) {
flash.message = "Could not update Application: ${e}"
}
Expand Down
202 changes: 25 additions & 177 deletions grails-app/services/com/netflix/asgard/ApplicationService.groovy
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2012 Netflix, Inc.
* 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.
Expand All @@ -15,172 +15,33 @@
*/
package com.netflix.asgard

import com.amazonaws.AmazonServiceException
import com.amazonaws.services.simpledb.model.Attribute
import com.amazonaws.services.simpledb.model.Item
import com.amazonaws.services.simpledb.model.ReplaceableAttribute
import com.netflix.asgard.cache.CacheInitializer
import com.netflix.asgard.collections.GroupedAppRegistrationSet
import com.netflix.asgard.model.MonitorBucketType
import org.joda.time.DateTime
import org.springframework.beans.factory.InitializingBean

class ApplicationService implements CacheInitializer, InitializingBean {

static transactional = false

/** The name of SimpleDB domain that stores the cloud application registry. */
String domainName

def grailsApplication // injected after construction
def awsAutoScalingService
def awsClientService
def awsEc2Service
def awsLoadBalancerService
def awsSimpleDbService
Caches caches
def configService
def fastPropertyService
def mergedInstanceGroupingService
def taskService

void afterPropertiesSet() {
domainName = configService.applicationsDomain
}

void initializeCaches() {
caches.allApplications.ensureSetUp({ retrieveApplications() })
}
interface ApplicationService {
List<AppRegistration> getRegisteredApplications(UserContext userContext)

private Collection<AppRegistration> retrieveApplications() {
List<Item> items = awsSimpleDbService.selectAll(domainName).sort { it.name.toLowerCase() }
items.collect { AppRegistration.from(it) }
}

List<AppRegistration> getRegisteredApplications(UserContext userContext) {
caches.allApplications.list().sort { it.name }
}

List<AppRegistration> getRegisteredApplicationsForLoadBalancer(UserContext userContext) {
new ArrayList<AppRegistration>(getRegisteredApplications(userContext).findAll {
Relationships.checkAppNameForLoadBalancer(it.name)
})
}

GroupedAppRegistrationSet getGroupedRegisteredApplications(UserContext ctx) {
new GroupedAppRegistrationSet(getRegisteredApplications(ctx))
}
List<AppRegistration> getRegisteredApplicationsForLoadBalancer(UserContext userContext)

AppRegistration getRegisteredApplication(UserContext userContext, String nameInput, From from = From.AWS) {
if (!nameInput) { return null }
String name = nameInput.toLowerCase()
if (from == From.CACHE) {
return caches.allApplications.get(name)
}
Item item = awsSimpleDbService.selectOne(domainName, name.toUpperCase())
AppRegistration appRegistration = AppRegistration.from(item)
caches.allApplications.put(name, appRegistration)
appRegistration
}
GroupedAppRegistrationSet getGroupedRegisteredApplications(UserContext ctx)

AppRegistration getRegisteredApplicationForLoadBalancer(UserContext userContext, String name) {
Relationships.checkAppNameForLoadBalancer(name) ? getRegisteredApplication(userContext, name) : null
}
AppRegistration getRegisteredApplication(UserContext userContext, String nameInput)

CreateApplicationResult createRegisteredApplication(UserContext userContext, String nameInput, String group,
String type, String description, String owner, String email, MonitorBucketType monitorBucketType,
String tags) {
String name = nameInput.toLowerCase()
CreateApplicationResult result = new CreateApplicationResult()
result.appName = name
if (getRegisteredApplication(userContext, name)) {
result.appCreateException = new IllegalStateException("Can't add Application ${name}. It already exists.")
return result
}
String nowEpoch = new DateTime().millis as String
Collection<ReplaceableAttribute> attributes = buildAttributesList(group, type, description, owner, email,
monitorBucketType, tags, false)
attributes << new ReplaceableAttribute('createTs', nowEpoch, false)
String creationLogMessage = "Create registered app ${name}, type ${type}, owner ${owner}, email ${email}"
taskService.runTask(userContext, creationLogMessage, { task ->
try {
awsSimpleDbService.save(domainName, name.toUpperCase(), attributes)
result.appCreated = true
} catch (AmazonServiceException e) {
result.appCreateException = e
}
}, Link.to(EntityType.application, name))
getRegisteredApplication(userContext, name)
result
}
AppRegistration getRegisteredApplication(UserContext userContext, String nameInput, From from)

private static Collection<ReplaceableAttribute> buildAttributesList(String group, String type, String description,
String owner, String email, MonitorBucketType monitorBucketType, String tags,
Boolean replaceExistingValues) {

Check.notNull(monitorBucketType, MonitorBucketType, 'monitorBucketType')
String nowEpoch = new DateTime().millis as String
Collection<ReplaceableAttribute> attributes = []
attributes << new ReplaceableAttribute('group', group ?: '', replaceExistingValues)
attributes << new ReplaceableAttribute('type', Check.notEmpty(type), replaceExistingValues)
attributes << new ReplaceableAttribute('description', Check.notEmpty(description), replaceExistingValues)
attributes << new ReplaceableAttribute('owner', Check.notEmpty(owner), replaceExistingValues)
attributes << new ReplaceableAttribute('email', Check.notEmpty(email), replaceExistingValues)
attributes << new ReplaceableAttribute('monitorBucketType', monitorBucketType.name(), replaceExistingValues)
attributes << new ReplaceableAttribute('updateTs', nowEpoch, replaceExistingValues)
if (tags) {
attributes << new ReplaceableAttribute('tags', tags, replaceExistingValues)
}
return attributes
}
AppRegistration getRegisteredApplicationForLoadBalancer(UserContext userContext, String name)

void updateRegisteredApplication(UserContext userContext, String name, String group, String type, String desc,
String owner, String email, String tags, MonitorBucketType bucketType) {
Collection<ReplaceableAttribute> attributes = buildAttributesList(group, type, desc, owner, email,
bucketType, tags, true)
taskService.runTask(userContext,
"Update registered app ${name}, type ${type}, owner ${owner}, email ${email}", { task ->
awsSimpleDbService.save(domainName, name.toUpperCase(), attributes)
if (!tags) {
awsSimpleDbService.delete(domainName, name.toUpperCase(), [new Attribute().withName('tags')])
}
}, Link.to(EntityType.application, name))
getRegisteredApplication(userContext, name)
}
ApplicationModificationResult createRegisteredApplication(UserContext userContext, String name, String group,
String type, String description, String owner,
String email, MonitorBucketType monitorBucketType,
String tags)

void deleteRegisteredApplication(UserContext userContext, String name) {
Check.notEmpty(name, "name")
validateDelete(userContext, name)
taskService.runTask(userContext, "Delete registered app ${name}", { task ->
awsSimpleDbService.delete(domainName, name.toUpperCase())
}, Link.to(EntityType.application, name))
getRegisteredApplication(userContext, name)
}
ApplicationModificationResult updateRegisteredApplication(UserContext userContext, String name, String group,
String type, String desc, String owner,
String email, MonitorBucketType bucketType,
String tags)

private void validateDelete(UserContext userContext, String name) {
List<String> objectsWithEntities = []
if (awsAutoScalingService.getAutoScalingGroupsForApp(userContext, name)) {
objectsWithEntities.add('Auto Scaling Groups')
}
if (awsLoadBalancerService.getLoadBalancersForApp(userContext, name)) {
objectsWithEntities.add('Load Balancers')
}
if (awsEc2Service.getSecurityGroupsForApp(userContext, name)) {
objectsWithEntities.add('Security Groups')
}
if (mergedInstanceGroupingService.getMergedInstances(userContext, name)) {
objectsWithEntities.add('Instances')
}
if (fastPropertyService.getFastPropertiesByAppName(userContext, name)) {
objectsWithEntities.add('Fast Properties')
}

if (objectsWithEntities) {
String referencesString = objectsWithEntities.join(', ')
String message = "${name} ineligible for delete because it still references ${referencesString}"
throw new ValidationException(message)
}
}
void deleteRegisteredApplication(UserContext userContext, String name)

/**
* Get the email address of the relevant app, or empty string if no email address can be found for the specified
Expand All @@ -189,9 +50,7 @@ class ApplicationService implements CacheInitializer, InitializingBean {
* @param appName the name of the app that has the email address
* @return the email address associated with the app, or empty string if no email address can be found
*/
String getEmailFromApp(UserContext userContext, String appName) {
getRegisteredApplication(userContext, appName)?.email ?: ''
}
String getEmailFromApp(UserContext userContext, String appName)

/**
* Provides a string to use for monitoring bucket, either provided an empty string, cluster name or app name based
Expand All @@ -202,33 +61,22 @@ class ApplicationService implements CacheInitializer, InitializingBean {
* @param clusterName value to return if the application's monitor bucket type is 'cluster'
* @return appName or clusterName or empty string, based on the application's monitorBucketType
*/
String getMonitorBucket(UserContext userContext, String appName, String clusterName) {
MonitorBucketType type = getRegisteredApplication(userContext, appName)?.monitorBucketType
type == MonitorBucketType.application ? appName : type == MonitorBucketType.cluster ? clusterName : ''
}
String getMonitorBucket(UserContext userContext, String appName, String clusterName)
}

/**
* Records the results of trying to create an Application.
* Records the results of trying to modify an Application.
*/
class CreateApplicationResult {
String appName
Boolean appCreated
Exception appCreateException
class ApplicationModificationResult {
boolean successful
String message

String toString() {
StringBuilder output = new StringBuilder()
if (appCreated) {
output.append("Application '${appName}' has been created. ")
}
if (appCreateException) {
output.append("Could not create Application '${appName}': ${appCreateException}. ")
}
output.toString()
return message
}

Boolean succeeded() {
appCreated && !appCreateException
return successful
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* 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.applications

import com.netflix.asgard.AppRegistration
import com.netflix.asgard.ApplicationService
import com.netflix.asgard.Relationships
import com.netflix.asgard.UserContext
import com.netflix.asgard.collections.GroupedAppRegistrationSet
import com.netflix.asgard.model.MonitorBucketType

abstract class AbstractApplicationService implements ApplicationService {
@Override
final List<AppRegistration> getRegisteredApplicationsForLoadBalancer(UserContext userContext) {
new ArrayList<AppRegistration>(getRegisteredApplications(userContext).findAll {
Relationships.checkAppNameForLoadBalancer(it.name)
})
}

@Override
final GroupedAppRegistrationSet getGroupedRegisteredApplications(UserContext ctx) {
new GroupedAppRegistrationSet(getRegisteredApplications(ctx))
}

@Override
final AppRegistration getRegisteredApplicationForLoadBalancer(UserContext userContext, String name) {
Relationships.checkAppNameForLoadBalancer(name) ? getRegisteredApplication(userContext, name) : null
}

@Override
final String getEmailFromApp(UserContext userContext, String appName) {
getRegisteredApplication(userContext, appName)?.email ?: ''
}

@Override
final String getMonitorBucket(UserContext userContext, String appName, String clusterName) {
MonitorBucketType type = getRegisteredApplication(userContext, appName)?.monitorBucketType
type == MonitorBucketType.application ? appName : type == MonitorBucketType.cluster ? clusterName : ''
}
}
Loading

0 comments on commit 85cf902

Please sign in to comment.