def _validator_execute(self, validator_class, validator_args, suppressors): validator = validator_class() if any( suppressor.suppress_validator(validator) for suppressor in (suppressors or [])): LOGGER.debug("Suppressing validator %s", validator_class.__name__) return [] LOGGER.debug("Executing validator %s", validator_class.__name__) try: return validator.execute(**validator_args) except Exception as e: return [ ValidationResult(str(e), FailureLevel.ERROR, validator.type) ]
def _validate_config(self, validator_suppressors, validation_failure_level): """Validate the configuration, throwing an exception for failures above a given failure level.""" try: validation_failures = self.config.validate(validator_suppressors) except ValidationError as e: # syntactic failure data = str(sorted(e.messages.items()) if isinstance(e.messages, dict) else e) validation_failures = [ValidationResult(data, FailureLevel.ERROR, validator_type="ImageSchemaValidator")] raise ConfigValidationError("Invalid image configuration.", validation_failures=validation_failures) for failure in validation_failures: if failure.level.value >= FailureLevel(validation_failure_level).value: raise BadRequestImageBuilderActionError( message="Configuration is invalid", validation_failures=validation_failures ) return validation_failures
def _load_config(self, cluster_config: dict) -> BaseClusterConfig: """Load the config and catch / translate any errors that occur during loading.""" try: return ClusterSchema(cluster_name=self.name).load(cluster_config) except ValidationError as e: # syntactic failure data = str( sorted(e.messages.items()) if isinstance(e.messages, dict ) else e) validation_failures = [ ValidationResult(data, FailureLevel.ERROR, validator_type="ConfigSchemaValidator") ] raise ConfigValidationError( "Invalid cluster configuration.", validation_failures=validation_failures)
def validate( self, validator_suppressor_list: List[ValidatorSuppressor] = None ) -> List[ValidationResult]: """Execute registered validators.""" # Cleanup failures and validators self._validation_failures.clear() # Call validators for nested resources for attr, value in self.__dict__.items(): if isinstance(value, Resource): # Validate nested Resources self._validation_failures.extend( value.validate(validator_suppressor_list)) if isinstance(value, list) and value: # Validate nested lists of Resources for item in self.__getattribute__(attr): if isinstance(item, Resource): self._validation_failures.extend( item.validate(validator_suppressor_list)) # Update validators to be executed according to current status of the model and order by priority self._validators.clear() self._register_validators() for validator_class, validator_args in self._validators: validator = validator_class() if validator_suppressor_list and any( suppressor.suppress_validator(validator) for suppressor in validator_suppressor_list): LOGGER.debug("Suppressing validator %s", validator_class.__name__) continue LOGGER.debug("Executing validator %s", validator_class.__name__) try: validation_failures = validator.execute(**validator_args) self._validation_failures.extend(validation_failures) except Exception as e: self._validation_failures.append( ValidationResult(str(e), FailureLevel.ERROR, validator.type)) return self._validation_failures
class TestBuildImage: url = "/v3/images/custom" method = "POST" config = ( "Build:\n InstanceType: c5.xlarge\n ParentImage: arn:aws:imagebuilder:us-east-1:aws:image/amazon-" "linux-2-x86/x.x.x" "\n\nDevSettings:\n Cookbook:\n ChefCookbook: https://github.com/aws/aws-par" "allelcluster-cookbook/tarball/26ab8423b84de1a098bc26e8ff1768e930fc7707\n NodePackage: https://git" "hub.com/aws/aws-parallelcluster-node/tarball/875ef93986a86ea3267835a813d38eaa05e575f3\n AwsBatchC" "liPackage: https://github.com/aws/aws-parallelcluster/tarball/d5c2a1ec267a865cff3cf350af30d44e68f0" "ef18") def _send_test_request(self, client, dryrun=None, suppress_validators=None, rollback_on_failure=None): build_image_request_content = { "imageConfiguration": self.config, "imageId": "imageid" } query_string = [("validationFailureLevel", ValidationLevel.INFO), ("region", "eu-west-1")] if dryrun is not None: query_string.append(("dryrun", dryrun)) if rollback_on_failure is not None: query_string.append(("rollbackOnFailure", rollback_on_failure)) if suppress_validators: query_string.extend([("suppressValidators", validator) for validator in suppress_validators]) headers = { "Accept": "application/json", "Content-Type": "application/json" } return client.open( self.url, method=self.method, data=json.dumps(build_image_request_content), headers=headers, query_string=query_string, content_type="application/json", ) @pytest.mark.parametrize( "suppress_validators, suppressed_validation_errors, rollback_on_failure", [ (None, None, None), (["type:type1", "type:type2"], [ ValidationResult("suppressed failure", FailureLevel.INFO, "type1") ], None), (None, None, False), ], ids=[ "test with no validation errors", "test with suppressed validators", "rollback on failure" ], ) def test_build_image_success(self, client, mocker, suppress_validators, suppressed_validation_errors, rollback_on_failure): mocked_call = mocker.patch( "pcluster.models.imagebuilder.ImageBuilder.create", auto_spec=True, return_value=suppressed_validation_errors, ) mocker.patch( "pcluster.aws.cfn.CfnClient.describe_stack", return_value=_create_stack( "image1", CloudFormationStackStatus.CREATE_IN_PROGRESS), ) mocker.patch("pcluster.aws.cfn.CfnClient.describe_stack_resource", return_value=None) expected_response = { "image": { "cloudformationStackArn": "arn:image1", "cloudformationStackStatus": "CREATE_IN_PROGRESS", "imageBuildStatus": "BUILD_IN_PROGRESS", "imageId": "image1", "region": "eu-west-1", "version": "3.0.0", } } if suppressed_validation_errors: expected_response["validationMessages"] = [{ "level": "INFO", "type": "type1", "message": "suppressed failure" }] response = self._send_test_request( client, dryrun=False, suppress_validators=suppress_validators, rollback_on_failure=rollback_on_failure) with soft_assertions(): assert_that(response.status_code).is_equal_to(202) assert_that(response.get_json()).is_equal_to(expected_response) mocked_call.assert_called_with( disable_rollback=not rollback_on_failure if rollback_on_failure is not None else True, validator_suppressors=mocker.ANY, validation_failure_level=FailureLevel[ValidationLevel.INFO], ) mocked_call.assert_called_once() if suppress_validators: _, kwargs = mocked_call.call_args assert_that(kwargs["validator_suppressors"].pop( )._validators_to_suppress).is_equal_to({"type1", "type2"}) mocked_call.assert_called_once() @pytest.mark.parametrize( "validation_errors, error_code, expected_response", [ pytest.param( None, 412, { "message": "Request would have succeeded, but DryRun flag is set." }, id="test success"), pytest.param( BadRequestImageBuilderActionError("test validation error", [ ValidationResult("test failure", FailureLevel.ERROR, "dummy validator") ]), 400, { "configurationValidationErrors": [{ "level": "ERROR", "type": "dummy validator", "message": "test failure" }], "message": "test validation error", }, id="test validation failure", ), pytest.param(ConflictImageBuilderActionError("test error"), 409, {"message": "test error"}, id="test conflict error"), ], ) def test_dryrun(self, client, mocker, validation_errors, error_code, expected_response): if validation_errors: mocker.patch( "pcluster.models.imagebuilder.ImageBuilder.validate_create_request", side_effect=validation_errors) else: mocker.patch( "pcluster.models.imagebuilder.ImageBuilder.validate_create_request", return_value=None) response = self._send_test_request(client, dryrun=True) with soft_assertions(): assert_that(response.status_code).is_equal_to(error_code) assert_that(response.get_json()).is_equal_to(expected_response) @pytest.mark.parametrize( "error, error_code", [ (LimitExceededImageError("test error"), 429), (LimitExceededStackError("test error"), 429), (LimitExceededImageBuilderActionError("test error"), 429), (BadRequestImageError("test error"), 400), (BadRequestStackError("test error"), 400), (BadRequestImageBuilderActionError("test error", []), 400), ( BadRequestImageBuilderActionError("test error", [ ValidationResult("message", FailureLevel.WARNING, "type") ]), 400, ), (ConflictImageBuilderActionError("test error"), 409), ], ) def test_that_errors_are_converted(self, client, mocker, error, error_code): mocker.patch("pcluster.models.imagebuilder.ImageBuilder.create", side_effect=error) expected_error = {"message": "test error"} if isinstance(error, (BadRequestImageError, BadRequestStackError)): expected_error[ "message"] = "Bad Request: " + expected_error["message"] if isinstance(error, BadRequestImageBuilderActionError ) and error.validation_failures: expected_error["configurationValidationErrors"] = [{ "level": "WARNING", "message": "message", "type": "type" }] response = self._send_test_request(client, dryrun=False) with soft_assertions(): assert_that(response.status_code).is_equal_to(error_code) assert_that(response.get_json()).is_equal_to(expected_error) def test_parse_config_error(self, client, mocker): mocker.patch("pcluster.aws.ec2.Ec2Client.image_exists", return_value=False) mocker.patch("pcluster.aws.cfn.CfnClient.stack_exists", return_value=False) mocker.patch("marshmallow.Schema.load", side_effect=ValidationError(message={"Error": "error"})) response = self._send_test_request(client) assert_that(response.status_code).is_equal_to(400) assert_that(response.get_json()["message"]).matches( "Invalid image configuration.")