Skip to content

Commit

Permalink
fix(citest): Dont perform superfluous platform initializations (#1529)
Browse files Browse the repository at this point in the history
This CL refactors SpinnakerTestScenario so that bindings requiring
platform interrogation are not performed until they are needed.
This allows tests to be decoupled from platforms they are not using.

This also refactors the SpinnakerTestScenario so that the platform
specific code is factored out into an encapsulated support module.
This should make it easier to add and maintain new platforms as
well as simplify what's going on.
  • Loading branch information
Eric Wiseblatt committed Apr 3, 2017
1 parent 888d4d3 commit 6075c8f
Show file tree
Hide file tree
Showing 7 changed files with 788 additions and 447 deletions.
87 changes: 87 additions & 0 deletions testing/citest/spinnaker_testing/appengine_scenario_support.py
@@ -0,0 +1,87 @@
# Copyright 2017 Google Inc. All Rights Reserved.
#
# 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.

"""Google App Engine platform and testing support for SpinnakerTestScenario."""

import citest.gcp_testing as gcp
from spinnaker_testing.base_scenario_support import BaseScenarioPlatformSupport


class AppEngineScenarioSupport(BaseScenarioPlatformSupport):
"""Provides SpinnakerScenarioSupport for Google App Engine."""

@classmethod
def add_commandline_parameters(cls, scenario_class, builder, defaults):
"""Implements BaseScenarioPlatformSupport interface.
Args:
scenario_class: [class spinnaker_testing.SpinnakerTestScenario]
builder: [citest.base.ConfigBindingsBuilder]
defaults: [dict] Default binding value overrides.
This is used to initialize the default commandline parameters.
"""
#
# Operation Parameters
#
builder.add_argument(
'--spinnaker_appengine_account',
default=defaults.get('SPINNAKER_APPENGINE_ACCOUNT', None),
help='Spinnaker account name to use for test operations against'
'App Engine. Only used when managing resources on App Engine.')

#
# Observer parameters
#
builder.add_argument(
'--appengine_credentials_path',
default=defaults.get('APPENGINE_CREDENTIALS_PATH', None),
help='A path to the JSON file with credentials to use for observing'
' tests against Google App Engine. Defaults to the value set for'
'--gce_credentials_path, which defaults to application default'
' credentials.')

def __init__(self, scenario):
"""Constructor.
Args:
scenario: [SpinnakerTestScenario] The scenario being supported.
"""
super(AppEngineScenarioSupport, self).__init__("appengine", scenario)

bindings = scenario.bindings

if not bindings.get('APPENGINE_PRIMARY_MANAGED_PROJECT_ID'):
bindings['APPENGINE_PRIMARY_MANAGED_PROJECT_ID'] = (
scenario.agent.deployed_config.get(
'providers.appengine.primaryCredentials.project', None))
# Fall back on Google project and credentials.
if not bindings['APPENGINE_PRIMARY_MANAGED_PROJECT_ID']:
bindings['APPENGINE_PRIMARY_MANAGED_PROJECT_ID'] = (
bindings['GOOGLE_PRIMARY_MANAGED_PROJECT_ID'])
bindings['APPENGINE_CREDENTIALS_PATH'] = (
bindings['GCE_CREDENTIALS_PATH'])

def _make_observer(self):
"""Implements BaseScenarioPlatformSupport interface."""
bindings = self.scenario.bindings
if not bindings.get('APPENGINE_PRIMARY_MANAGED_PROJECT_ID'):
raise ValueError('There is no "appengine_primary_managed_project_id"')

return gcp.GcpAppengineAgent.make_agent(
scopes=(gcp.APPENGINE_FULL_SCOPE
if bindings['APPENGINE_CREDENTIALS_PATH']
else None),
credentials_path=bindings['APPENGINE_CREDENTIALS_PATH'],
default_variables={
'project': bindings['APPENGINE_PRIMARY_MANAGED_PROJECT_ID']})
174 changes: 174 additions & 0 deletions testing/citest/spinnaker_testing/aws_scenario_support.py
@@ -0,0 +1,174 @@
# Copyright 2017 Google Inc. All Rights Reserved.
#
# 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.

"""Amazon Web Services platform and test support for SpinnakerTestScenario."""

import logging

from citest.base import ExecutionContext
import citest.aws_testing as aws
from spinnaker_testing.base_scenario_support import BaseScenarioPlatformSupport


class AwsScenarioSupport(BaseScenarioPlatformSupport):
"""Provides SpinnakerScenarioSupport for Amazon Web Services."""

@classmethod
def add_commandline_parameters(cls, scenario_class, builder, defaults):
"""Implements BaseScenarioPlatformSupport interface.
Args:
scenario_class: [class spinnaker_testing.SpinnakerTestScenario]
builder: [citest.base.ConfigBindingsBuilder]
defaults: [dict] Default binding value overrides.
This is used to initialize the default commandline parameters.
"""
#
# Observer parameters
#
builder.add_argument(
'--aws_profile', default=defaults.get('AWS_PROFILE', None),
help='aws command-line tool --profile parameter when observing AWS.')

#
# Operation Parameters
#
builder.add_argument(
'--aws_credentials',
dest='spinnaker_aws_account',
help='DEPRECATED. Replaced by --spinnaker_aws_account')

builder.add_argument(
'--spinnaker_aws_account',
default=defaults.get('SPINNAKER_AWS_ACCOUNT', None),
help='Spinnaker account name to use for test operations against AWS.'
' Only used when managing resources on AWS.')

builder.add_argument(
'--aws_iam_role', default=defaults.get('AWS_IAM_ROLE', None),
help='Spinnaker IAM role name for test operations.'
' Only used when managing jobs running on AWS.')

builder.add_argument(
'--test_aws_ami',
default=defaults.get(
'TEST_AWS_AMI',
'bitnami-tomcatstack-7.0.63-1-linux-ubuntu-14.04.1-x86_64-ebs'),
help='Default Amazon AMI to use when creating test instances.'
' The default image will listen on port 80.')

builder.add_argument(
'--test_aws_region',
default=defaults.get('TEST_AWS_REGION', ''),
help='The GCE region to test generated instances in (when managing'
' AWS). If not specified, then derive it fro --test_aws_zone.')

builder.add_argument(
'--test_aws_security_group_id',
default=defaults.get('TEST_AWS_SECURITY_GROUP_ID', None),
help='Default AWS SecurityGroupId when creating test resources.')

builder.add_argument(
'--test_aws_zone',
default=defaults.get('TEST_AWS_ZONE', 'us-east-1c'),
help='The AWS zone to test generated instances in (when managing AWS).'
' This implies the AWS region as well.')

builder.add_argument(
'--test_aws_vpc_id',
default=defaults.get('TEST_AWS_VPC_ID', None),
help='Default AWS VpcId to use when creating test resources.')

def _make_observer(self):
"""Implements BaseScenarioPlatformSupport interface."""
bindings = self.scenario.bindings
profile = bindings.get('AWS_PROFILE')
if not profile:
raise ValueError('An AWS Observer requires an AWS_PROFILE')

return aws.AwsAgent(profile, bindings['TEST_AWS_REGION'])

def __init__(self, scenario):
"""Constructor.
Args:
scenario: [SpinnakerTestScenario] The scenario being supported.
"""
super(AwsScenarioSupport, self).__init__("aws", scenario)
self.__aws_observer = None

bindings = scenario.bindings

if not bindings['AWS_IAM_ROLE']:
bindings['AWS_IAM_ROLE'] = scenario.agent.deployed_config.get(
'providers.aws.defaultIAMRole', None)

if not bindings['TEST_AWS_ZONE']:
bindings['TEST_AWS_ZONE'] = bindings['AWS_ZONE']

if not bindings.get('TEST_AWS_REGION', ''):
bindings['TEST_AWS_REGION'] = bindings['TEST_AWS_ZONE'][:-1]

bindings.add_lazy_initializer(
'TEST_AWS_VPC_ID', self.__lazy_binding_initializer)
bindings.add_lazy_initializer(
'TEST_AWS_SECURITY_GROUP', self.__lazy_binding_initializer)

def __lazy_binding_initializer(self, bindings, key):
normalized_key = key.upper()

if normalized_key == 'TEST_AWS_VPC_ID':
# We need to figure out a specific aws vpc id to use.
logger = logging.getLogger(__name__)
logger.info('Determine default AWS VpcId...')
vpc_list = self.observer.get_resource_list(
ExecutionContext(),
root_key='Vpcs',
aws_command='describe-vpcs',
args=['--filters', 'Name=tag:Name,Values=defaultvpc'],
region=bindings['TEST_AWS_REGION'],
aws_module='ec2', profile=self.__aws_observer.profile)
if not vpc_list:
raise ValueError('There is no vpc tagged as "defaultvpc"')

found = vpc_list[0]['VpcId']
logger.info('Using discovered default VpcId=%s', str(found))
return found

if normalized_key == 'TEST_AWS_SECURITY_GROUP':
# We need to figure out a specific security group that is compatable
# with the VpcId we are using.
logger = logging.getLogger(__name__)
logger.info('Determine default AWS SecurityGroupId...')
sg_list = self.__aws_observer.get_resource_list(
ExecutionContext(),
root_key='SecurityGroups',
aws_command='describe-security-groups', args=[],
region=bindings['TEST_AWS_REGION'],
aws_module='ec2', profile=self.__aws_observer.profile)

found = None
vpc_id = bindings['TEST_AWS_VPC_ID']
for entry in sg_list:
if entry.get('VpcId', None) == vpc_id:
found = entry['GroupId']
break
if not found:
raise ValueError('Could not find a security group for AWS_VPC_ID {0}'
.format(vpc_id))

logger.info('Using discovered default SecurityGroupId=%s', str(found))
return found

raise KeyError(key)
123 changes: 123 additions & 0 deletions testing/citest/spinnaker_testing/base_scenario_support.py
@@ -0,0 +1,123 @@
# Copyright 2017 Google Inc. All Rights Reserved.
#
# 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.

"""Interface for platform-specific support in SpinnakerTestScenario."""

import logging
import threading


class BaseScenarioPlatformSupport(object):
"""Interface for adding a specific platform to SpinnakerTestScenario."""

@property
def platform_name(self):
"""Returns the platform name bound at construction."""
return self.__platform_name

@property
def scenario(self):
"""Returns the scenario instance bound at construction."""
return self.__scenario

@property
def observer(self):
"""Returns the default observer for this platform as configured.
Raises:
This will throw an exception if the observer is not available for
whatever reason. The reason may vary depending on the platform.
"""
with self.__lock:
if self.__observer is None:
logger = logging.getLogger(__name__)
logger.info('Initializing observer for "%s"', self.__platform_name)
try:
self.__observer = self._make_observer()
except:
logger.exception('Failed to create observer for "%s"',
self.__platform_name)
raise
return self.__observer

@classmethod
def init_bindings_builder(cls, scenario_class, builder, defaults):
"""Mediates to the specific methods in this interface.
This is not intended to be overriden further. Instead, override
the remaining methods.
Args:
scenario_class: [class spinnaker_testing.SpinnakerTestScenario]
builder: [citest.base.ConfigBindingsBuilder]
defaults: [dict] Default binding value overrides.
This is used to initialize the default commandline parameters.
"""
cls.add_commandline_parameters(builder, defaults)

@classmethod
def add_commandline_parameters(cls, scenario_class, builder, defaults):
"""Adds commandline arguments to the builder.
Args:
scenario_class: [class spinnaker_testing.SpinnakerTestScenario]
builder: [citest.base.ConfigBindingsBuilder]
defaults: [dict] Default binding value overrides.
This is used to initialize the default commandline parameters.
"""
raise NotImplementedError('{0} not implemented'.format(cls))

def __init__(self, platform_name, scenario):
"""Constructor.
This ensures the local bindings for:
SPINNAKER_<platform>_ACCOUNT
SPINNAKER_<platform>_ENABLED
where <platform> is the platform_name or OS for openstack.
It will use the scenario's deployed configuration if available and needed
so that these variables will correspond to the agent's target.
The default ACCOUNT is the configured primary account.
Args:
platform_name: [string] Identifies which platform this is.
This should be the name used in the Spinnaker "provider".
scenario: [SpinnakerTestScenario] The scenario being supported.
"""
self.__lock = threading.Lock()
self.__observer = None
self.__scenario = scenario
self.__platform_name = platform_name
test_platform_key = platform_name if platform_name != 'openstack' else 'os'

bindings = scenario.bindings
agent = scenario.agent
account_key = 'spinnaker_{0}_account'.format(test_platform_key)
if not bindings.get(account_key):
bindings[account_key] = agent.deployed_config.get(
'providers.{0}.primaryCredentials.name'.format(platform_name))

enabled_key = 'spinnaker_{0}_enabled'.format(test_platform_key)
if bindings.get(enabled_key, None) is None:
bindings[enabled_key] = agent.deployed_config.get(
'providers.{0}.enabled'.format(platform_name))

def _make_observer(self):
"""Hook for specialized classes to instantiate their observer.
This method is called internally as needed when accessing the
observer property.
"""
raise NotImplementedError('{0} not implemented'.format(cls))

0 comments on commit 6075c8f

Please sign in to comment.