Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(citest): Dont perform superfluous platform initializations (#1529)
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
Showing
7 changed files
with
788 additions
and
447 deletions.
There are no files selected for viewing
87 changes: 87 additions & 0 deletions
87
testing/citest/spinnaker_testing/appengine_scenario_support.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
174
testing/citest/spinnaker_testing/aws_scenario_support.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
123
testing/citest/spinnaker_testing/base_scenario_support.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)) | ||
|
Oops, something went wrong.