diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..7ff164421 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,1229 @@ +[*] +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = false +max_line_length = 120 +tab_width = 4 +ij_continuation_indent_size = 8 +ij_formatter_off_tag = @formatter:off +ij_formatter_on_tag = @formatter:on +ij_formatter_tags_enabled = true +ij_smart_tabs = false +ij_visual_guides = +ij_wrap_on_typing = false + +[*.css] +ij_css_align_closing_brace_with_properties = false +ij_css_blank_lines_around_nested_selector = 1 +ij_css_blank_lines_between_blocks = 1 +ij_css_block_comment_add_space = false +ij_css_brace_placement = end_of_line +ij_css_enforce_quotes_on_format = false +ij_css_hex_color_long_format = false +ij_css_hex_color_lower_case = false +ij_css_hex_color_short_format = false +ij_css_hex_color_upper_case = false +ij_css_keep_blank_lines_in_code = 2 +ij_css_keep_indents_on_empty_lines = false +ij_css_keep_single_line_blocks = false +ij_css_properties_order = font, font-family, font-size, font-weight, font-style, font-variant, font-size-adjust, font-stretch, line-height, position, z-index, top, right, bottom, left, display, visibility, float, clear, overflow, overflow-x, overflow-y, clip, zoom, align-content, align-items, align-self, flex, flex-flow, flex-basis, flex-direction, flex-grow, flex-shrink, flex-wrap, justify-content, order, box-sizing, width, min-width, max-width, height, min-height, max-height, margin, margin-top, margin-right, margin-bottom, margin-left, padding, padding-top, padding-right, padding-bottom, padding-left, table-layout, empty-cells, caption-side, border-spacing, border-collapse, list-style, list-style-position, list-style-type, list-style-image, content, quotes, counter-reset, counter-increment, resize, cursor, user-select, nav-index, nav-up, nav-right, nav-down, nav-left, transition, transition-delay, transition-timing-function, transition-duration, transition-property, transform, transform-origin, animation, animation-name, animation-duration, animation-play-state, animation-timing-function, animation-delay, animation-iteration-count, animation-direction, text-align, text-align-last, vertical-align, white-space, text-decoration, text-emphasis, text-emphasis-color, text-emphasis-style, text-emphasis-position, text-indent, text-justify, letter-spacing, word-spacing, text-outline, text-transform, text-wrap, text-overflow, text-overflow-ellipsis, text-overflow-mode, word-wrap, word-break, tab-size, hyphens, pointer-events, opacity, color, border, border-width, border-style, border-color, border-top, border-top-width, border-top-style, border-top-color, border-right, border-right-width, border-right-style, border-right-color, border-bottom, border-bottom-width, border-bottom-style, border-bottom-color, border-left, border-left-width, border-left-style, border-left-color, border-radius, border-top-left-radius, border-top-right-radius, border-bottom-right-radius, border-bottom-left-radius, border-image, border-image-source, border-image-slice, border-image-width, border-image-outset, border-image-repeat, outline, outline-width, outline-style, outline-color, outline-offset, background, background-color, background-image, background-repeat, background-attachment, background-position, background-position-x, background-position-y, background-clip, background-origin, background-size, box-decoration-break, box-shadow, text-shadow +ij_css_space_after_colon = true +ij_css_space_before_opening_brace = true +ij_css_use_double_quotes = true +ij_css_value_alignment = do_not_align + +[*.dcl] +ij_declarative_keep_indents_on_empty_lines = false + +[*.java] +ij_continuation_indent_size = 4 +ij_java_align_consecutive_assignments = false +ij_java_align_consecutive_variable_declarations = false +ij_java_align_group_field_declarations = false +ij_java_align_multiline_annotation_parameters = false +ij_java_align_multiline_array_initializer_expression = false +ij_java_align_multiline_assignment = false +ij_java_align_multiline_binary_operation = false +ij_java_align_multiline_chained_methods = false +ij_java_align_multiline_deconstruction_list_components = true +ij_java_align_multiline_extends_list = false +ij_java_align_multiline_for = true +ij_java_align_multiline_method_parentheses = false +ij_java_align_multiline_parameters = true +ij_java_align_multiline_parameters_in_calls = false +ij_java_align_multiline_parenthesized_expression = false +ij_java_align_multiline_records = true +ij_java_align_multiline_resources = true +ij_java_align_multiline_ternary_operation = false +ij_java_align_multiline_text_blocks = false +ij_java_align_multiline_throws_list = false +ij_java_align_subsequent_simple_methods = false +ij_java_align_throws_keyword = false +ij_java_align_types_in_multi_catch = true +ij_java_annotation_parameter_wrap = off +ij_java_array_initializer_new_line_after_left_brace = false +ij_java_array_initializer_right_brace_on_new_line = false +ij_java_array_initializer_wrap = off +ij_java_assert_statement_colon_on_next_line = false +ij_java_assert_statement_wrap = off +ij_java_assignment_wrap = off +ij_java_binary_operation_sign_on_next_line = false +ij_java_binary_operation_wrap = off +ij_java_blank_lines_after_anonymous_class_header = 0 +ij_java_blank_lines_after_class_header = 0 +ij_java_blank_lines_after_imports = 1 +ij_java_blank_lines_after_package = 1 +ij_java_blank_lines_around_class = 1 +ij_java_blank_lines_around_field = 0 +ij_java_blank_lines_around_field_in_interface = 0 +ij_java_blank_lines_around_initializer = 1 +ij_java_blank_lines_around_method = 1 +ij_java_blank_lines_around_method_in_interface = 1 +ij_java_blank_lines_before_class_end = 0 +ij_java_blank_lines_before_imports = 1 +ij_java_blank_lines_before_method_body = 0 +ij_java_blank_lines_before_package = 0 +ij_java_block_brace_style = end_of_line +ij_java_block_comment_add_space = false +ij_java_block_comment_at_first_column = true +ij_java_builder_methods = +ij_java_call_parameters_new_line_after_left_paren = false +ij_java_call_parameters_right_paren_on_new_line = false +ij_java_call_parameters_wrap = off +ij_java_case_statement_on_separate_line = true +ij_java_catch_on_new_line = false +ij_java_class_annotation_wrap = split_into_lines +ij_java_class_brace_style = end_of_line +ij_java_class_count_to_use_import_on_demand = 999 +ij_java_class_names_in_javadoc = 1 +ij_java_deconstruction_list_wrap = normal +ij_java_do_not_indent_top_level_class_members = false +ij_java_do_not_wrap_after_single_annotation = false +ij_java_do_not_wrap_after_single_annotation_in_parameter = false +ij_java_do_while_brace_force = always +ij_java_doc_add_blank_line_after_description = true +ij_java_doc_add_blank_line_after_param_comments = false +ij_java_doc_add_blank_line_after_return = false +ij_java_doc_add_p_tag_on_empty_lines = true +ij_java_doc_align_exception_comments = true +ij_java_doc_align_param_comments = true +ij_java_doc_do_not_wrap_if_one_line = false +ij_java_doc_enable_formatting = true +ij_java_doc_enable_leading_asterisks = true +ij_java_doc_indent_on_continuation = false +ij_java_doc_keep_empty_lines = true +ij_java_doc_keep_empty_parameter_tag = true +ij_java_doc_keep_empty_return_tag = true +ij_java_doc_keep_empty_throws_tag = true +ij_java_doc_keep_invalid_tags = true +ij_java_doc_param_description_on_new_line = false +ij_java_doc_preserve_line_breaks = false +ij_java_doc_use_throws_not_exception_tag = true +ij_java_else_on_new_line = false +ij_java_entity_dd_prefix = +ij_java_entity_dd_suffix = EJB +ij_java_entity_eb_prefix = +ij_java_entity_eb_suffix = Bean +ij_java_entity_hi_prefix = +ij_java_entity_hi_suffix = Home +ij_java_entity_lhi_prefix = Local +ij_java_entity_lhi_suffix = Home +ij_java_entity_li_prefix = Local +ij_java_entity_li_suffix = +ij_java_entity_pk_class = java.lang.String +ij_java_entity_ri_prefix = +ij_java_entity_ri_suffix = +ij_java_entity_vo_prefix = +ij_java_entity_vo_suffix = VO +ij_java_enum_constants_wrap = off +ij_java_enum_field_annotation_wrap = off +ij_java_extends_keyword_wrap = off +ij_java_extends_list_wrap = off +ij_java_field_annotation_wrap = split_into_lines +ij_java_field_name_prefix = +ij_java_field_name_suffix = +ij_java_filter_class_prefix = +ij_java_filter_class_suffix = +ij_java_filter_dd_prefix = +ij_java_filter_dd_suffix = +ij_java_finally_on_new_line = false +ij_java_for_brace_force = always +ij_java_for_statement_new_line_after_left_paren = false +ij_java_for_statement_right_paren_on_new_line = false +ij_java_for_statement_wrap = off +ij_java_generate_final_locals = false +ij_java_generate_final_parameters = false +ij_java_generate_use_type_annotation_before_type = true +ij_java_if_brace_force = always +ij_java_imports_layout = *, |, javax.**, java.**, |, $* +ij_java_indent_case_from_switch = true +ij_java_insert_inner_class_imports = false +ij_java_insert_override_annotation = true +ij_java_keep_blank_lines_before_right_brace = 2 +ij_java_keep_blank_lines_between_package_declaration_and_header = 2 +ij_java_keep_blank_lines_in_code = 2 +ij_java_keep_blank_lines_in_declarations = 2 +ij_java_keep_builder_methods_indents = false +ij_java_keep_control_statement_in_one_line = true +ij_java_keep_first_column_comment = true +ij_java_keep_indents_on_empty_lines = false +ij_java_keep_line_breaks = true +ij_java_keep_multiple_expressions_in_one_line = false +ij_java_keep_simple_blocks_in_one_line = true +ij_java_keep_simple_classes_in_one_line = true +ij_java_keep_simple_lambdas_in_one_line = false +ij_java_keep_simple_methods_in_one_line = true +ij_java_label_indent_absolute = false +ij_java_label_indent_size = 0 +ij_java_lambda_brace_style = end_of_line +ij_java_layout_static_imports_separately = true +ij_java_line_comment_add_space = false +ij_java_line_comment_add_space_on_reformat = false +ij_java_line_comment_at_first_column = true +ij_java_listener_class_prefix = +ij_java_listener_class_suffix = +ij_java_local_variable_name_prefix = +ij_java_local_variable_name_suffix = +ij_java_message_dd_prefix = +ij_java_message_dd_suffix = EJB +ij_java_message_eb_prefix = +ij_java_message_eb_suffix = Bean +ij_java_method_annotation_wrap = split_into_lines +ij_java_method_brace_style = end_of_line +ij_java_method_call_chain_wrap = off +ij_java_method_parameters_new_line_after_left_paren = false +ij_java_method_parameters_right_paren_on_new_line = false +ij_java_method_parameters_wrap = off +ij_java_modifier_list_wrap = false +ij_java_multi_catch_types_wrap = normal +ij_java_names_count_to_use_import_on_demand = 999 +ij_java_new_line_after_lparen_in_annotation = false +ij_java_new_line_after_lparen_in_deconstruction_pattern = true +ij_java_new_line_after_lparen_in_record_header = false +ij_java_new_line_when_body_is_presented = false +ij_java_packages_to_use_import_on_demand = java.awt.*, javax.swing.* +ij_java_parameter_annotation_wrap = off +ij_java_parameter_name_prefix = +ij_java_parameter_name_suffix = +ij_java_parentheses_expression_new_line_after_left_paren = false +ij_java_parentheses_expression_right_paren_on_new_line = false +ij_java_place_assignment_sign_on_next_line = false +ij_java_prefer_longer_names = true +ij_java_prefer_parameters_wrap = false +ij_java_record_components_wrap = normal +ij_java_repeat_annotations = +ij_java_repeat_synchronized = true +ij_java_replace_instanceof_and_cast = false +ij_java_replace_null_check = true +ij_java_replace_sum_lambda_with_method_ref = true +ij_java_resource_list_new_line_after_left_paren = false +ij_java_resource_list_right_paren_on_new_line = false +ij_java_resource_list_wrap = off +ij_java_rparen_on_new_line_in_annotation = false +ij_java_rparen_on_new_line_in_deconstruction_pattern = true +ij_java_rparen_on_new_line_in_record_header = false +ij_java_servlet_class_prefix = +ij_java_servlet_class_suffix = +ij_java_servlet_dd_prefix = +ij_java_servlet_dd_suffix = +ij_java_session_dd_prefix = +ij_java_session_dd_suffix = EJB +ij_java_session_eb_prefix = +ij_java_session_eb_suffix = Bean +ij_java_session_hi_prefix = +ij_java_session_hi_suffix = Home +ij_java_session_lhi_prefix = Local +ij_java_session_lhi_suffix = Home +ij_java_session_li_prefix = Local +ij_java_session_li_suffix = +ij_java_session_ri_prefix = +ij_java_session_ri_suffix = +ij_java_session_si_prefix = +ij_java_session_si_suffix = Service +ij_java_space_after_closing_angle_bracket_in_type_argument = false +ij_java_space_after_colon = true +ij_java_space_after_comma = true +ij_java_space_after_comma_in_type_arguments = true +ij_java_space_after_for_semicolon = true +ij_java_space_after_quest = true +ij_java_space_after_type_cast = true +ij_java_space_before_annotation_array_initializer_left_brace = false +ij_java_space_before_annotation_parameter_list = false +ij_java_space_before_array_initializer_left_brace = false +ij_java_space_before_catch_keyword = true +ij_java_space_before_catch_left_brace = true +ij_java_space_before_catch_parentheses = true +ij_java_space_before_class_left_brace = true +ij_java_space_before_colon = true +ij_java_space_before_colon_in_foreach = true +ij_java_space_before_comma = false +ij_java_space_before_deconstruction_list = false +ij_java_space_before_do_left_brace = true +ij_java_space_before_else_keyword = true +ij_java_space_before_else_left_brace = true +ij_java_space_before_finally_keyword = true +ij_java_space_before_finally_left_brace = true +ij_java_space_before_for_left_brace = true +ij_java_space_before_for_parentheses = true +ij_java_space_before_for_semicolon = false +ij_java_space_before_if_left_brace = true +ij_java_space_before_if_parentheses = true +ij_java_space_before_method_call_parentheses = false +ij_java_space_before_method_left_brace = true +ij_java_space_before_method_parentheses = false +ij_java_space_before_opening_angle_bracket_in_type_parameter = false +ij_java_space_before_quest = true +ij_java_space_before_switch_left_brace = true +ij_java_space_before_switch_parentheses = true +ij_java_space_before_synchronized_left_brace = true +ij_java_space_before_synchronized_parentheses = true +ij_java_space_before_try_left_brace = true +ij_java_space_before_try_parentheses = true +ij_java_space_before_type_parameter_list = false +ij_java_space_before_while_keyword = true +ij_java_space_before_while_left_brace = true +ij_java_space_before_while_parentheses = true +ij_java_space_inside_one_line_enum_braces = false +ij_java_space_within_empty_array_initializer_braces = false +ij_java_space_within_empty_method_call_parentheses = false +ij_java_space_within_empty_method_parentheses = false +ij_java_spaces_around_additive_operators = true +ij_java_spaces_around_annotation_eq = true +ij_java_spaces_around_assignment_operators = true +ij_java_spaces_around_bitwise_operators = true +ij_java_spaces_around_equality_operators = true +ij_java_spaces_around_lambda_arrow = true +ij_java_spaces_around_logical_operators = true +ij_java_spaces_around_method_ref_dbl_colon = false +ij_java_spaces_around_multiplicative_operators = true +ij_java_spaces_around_relational_operators = true +ij_java_spaces_around_shift_operators = true +ij_java_spaces_around_type_bounds_in_type_parameters = true +ij_java_spaces_around_unary_operator = false +ij_java_spaces_inside_block_braces_when_body_is_present = false +ij_java_spaces_within_angle_brackets = false +ij_java_spaces_within_annotation_parentheses = false +ij_java_spaces_within_array_initializer_braces = false +ij_java_spaces_within_braces = false +ij_java_spaces_within_brackets = false +ij_java_spaces_within_cast_parentheses = false +ij_java_spaces_within_catch_parentheses = false +ij_java_spaces_within_deconstruction_list = false +ij_java_spaces_within_for_parentheses = false +ij_java_spaces_within_if_parentheses = false +ij_java_spaces_within_method_call_parentheses = false +ij_java_spaces_within_method_parentheses = false +ij_java_spaces_within_parentheses = false +ij_java_spaces_within_record_header = false +ij_java_spaces_within_switch_parentheses = false +ij_java_spaces_within_synchronized_parentheses = false +ij_java_spaces_within_try_parentheses = false +ij_java_spaces_within_while_parentheses = false +ij_java_special_else_if_treatment = true +ij_java_static_field_name_prefix = +ij_java_static_field_name_suffix = +ij_java_subclass_name_prefix = +ij_java_subclass_name_suffix = Impl +ij_java_switch_expressions_wrap = normal +ij_java_ternary_operation_signs_on_next_line = false +ij_java_ternary_operation_wrap = off +ij_java_test_name_prefix = +ij_java_test_name_suffix = Test +ij_java_throws_keyword_wrap = off +ij_java_throws_list_wrap = off +ij_java_use_external_annotations = false +ij_java_use_fq_class_names = false +ij_java_use_relative_indents = false +ij_java_use_single_class_imports = true +ij_java_variable_annotation_wrap = off +ij_java_visibility = public +ij_java_while_brace_force = always +ij_java_while_on_new_line = false +ij_java_wrap_comments = false +ij_java_wrap_first_method_in_call_chain = false +ij_java_wrap_long_lines = false +ij_java_wrap_semicolon_after_call_chain = false + +[*.less] +indent_size = 2 +ij_less_align_closing_brace_with_properties = false +ij_less_blank_lines_around_nested_selector = 1 +ij_less_blank_lines_between_blocks = 1 +ij_less_block_comment_add_space = false +ij_less_brace_placement = 0 +ij_less_enforce_quotes_on_format = false +ij_less_hex_color_long_format = false +ij_less_hex_color_lower_case = false +ij_less_hex_color_short_format = false +ij_less_hex_color_upper_case = false +ij_less_keep_blank_lines_in_code = 2 +ij_less_keep_indents_on_empty_lines = false +ij_less_keep_single_line_blocks = false +ij_less_line_comment_add_space = false +ij_less_line_comment_at_first_column = false +ij_less_properties_order = font, font-family, font-size, font-weight, font-style, font-variant, font-size-adjust, font-stretch, line-height, position, z-index, top, right, bottom, left, display, visibility, float, clear, overflow, overflow-x, overflow-y, clip, zoom, align-content, align-items, align-self, flex, flex-flow, flex-basis, flex-direction, flex-grow, flex-shrink, flex-wrap, justify-content, order, box-sizing, width, min-width, max-width, height, min-height, max-height, margin, margin-top, margin-right, margin-bottom, margin-left, padding, padding-top, padding-right, padding-bottom, padding-left, table-layout, empty-cells, caption-side, border-spacing, border-collapse, list-style, list-style-position, list-style-type, list-style-image, content, quotes, counter-reset, counter-increment, resize, cursor, user-select, nav-index, nav-up, nav-right, nav-down, nav-left, transition, transition-delay, transition-timing-function, transition-duration, transition-property, transform, transform-origin, animation, animation-name, animation-duration, animation-play-state, animation-timing-function, animation-delay, animation-iteration-count, animation-direction, text-align, text-align-last, vertical-align, white-space, text-decoration, text-emphasis, text-emphasis-color, text-emphasis-style, text-emphasis-position, text-indent, text-justify, letter-spacing, word-spacing, text-outline, text-transform, text-wrap, text-overflow, text-overflow-ellipsis, text-overflow-mode, word-wrap, word-break, tab-size, hyphens, pointer-events, opacity, color, border, border-width, border-style, border-color, border-top, border-top-width, border-top-style, border-top-color, border-right, border-right-width, border-right-style, border-right-color, border-bottom, border-bottom-width, border-bottom-style, border-bottom-color, border-left, border-left-width, border-left-style, border-left-color, border-radius, border-top-left-radius, border-top-right-radius, border-bottom-right-radius, border-bottom-left-radius, border-image, border-image-source, border-image-slice, border-image-width, border-image-outset, border-image-repeat, outline, outline-width, outline-style, outline-color, outline-offset, background, background-color, background-image, background-repeat, background-attachment, background-position, background-position-x, background-position-y, background-clip, background-origin, background-size, box-decoration-break, box-shadow, text-shadow +ij_less_space_after_colon = true +ij_less_space_before_opening_brace = true +ij_less_use_double_quotes = true +ij_less_value_alignment = 0 + +[*.proto] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 4 +ij_protobuf_keep_blank_lines_in_code = 2 +ij_protobuf_keep_indents_on_empty_lines = false +ij_protobuf_keep_line_breaks = true +ij_protobuf_space_after_comma = true +ij_protobuf_space_before_comma = false +ij_protobuf_spaces_around_assignment_operators = true +ij_protobuf_spaces_within_braces = false +ij_protobuf_spaces_within_brackets = false + +[*.sass] +indent_size = 2 +ij_sass_align_closing_brace_with_properties = false +ij_sass_blank_lines_around_nested_selector = 1 +ij_sass_blank_lines_between_blocks = 1 +ij_sass_brace_placement = 0 +ij_sass_enforce_quotes_on_format = false +ij_sass_hex_color_long_format = false +ij_sass_hex_color_lower_case = false +ij_sass_hex_color_short_format = false +ij_sass_hex_color_upper_case = false +ij_sass_keep_blank_lines_in_code = 2 +ij_sass_keep_indents_on_empty_lines = false +ij_sass_keep_single_line_blocks = false +ij_sass_line_comment_add_space = false +ij_sass_line_comment_at_first_column = false +ij_sass_properties_order = font, font-family, font-size, font-weight, font-style, font-variant, font-size-adjust, font-stretch, line-height, position, z-index, top, right, bottom, left, display, visibility, float, clear, overflow, overflow-x, overflow-y, clip, zoom, align-content, align-items, align-self, flex, flex-flow, flex-basis, flex-direction, flex-grow, flex-shrink, flex-wrap, justify-content, order, box-sizing, width, min-width, max-width, height, min-height, max-height, margin, margin-top, margin-right, margin-bottom, margin-left, padding, padding-top, padding-right, padding-bottom, padding-left, table-layout, empty-cells, caption-side, border-spacing, border-collapse, list-style, list-style-position, list-style-type, list-style-image, content, quotes, counter-reset, counter-increment, resize, cursor, user-select, nav-index, nav-up, nav-right, nav-down, nav-left, transition, transition-delay, transition-timing-function, transition-duration, transition-property, transform, transform-origin, animation, animation-name, animation-duration, animation-play-state, animation-timing-function, animation-delay, animation-iteration-count, animation-direction, text-align, text-align-last, vertical-align, white-space, text-decoration, text-emphasis, text-emphasis-color, text-emphasis-style, text-emphasis-position, text-indent, text-justify, letter-spacing, word-spacing, text-outline, text-transform, text-wrap, text-overflow, text-overflow-ellipsis, text-overflow-mode, word-wrap, word-break, tab-size, hyphens, pointer-events, opacity, color, border, border-width, border-style, border-color, border-top, border-top-width, border-top-style, border-top-color, border-right, border-right-width, border-right-style, border-right-color, border-bottom, border-bottom-width, border-bottom-style, border-bottom-color, border-left, border-left-width, border-left-style, border-left-color, border-radius, border-top-left-radius, border-top-right-radius, border-bottom-right-radius, border-bottom-left-radius, border-image, border-image-source, border-image-slice, border-image-width, border-image-outset, border-image-repeat, outline, outline-width, outline-style, outline-color, outline-offset, background, background-color, background-image, background-repeat, background-attachment, background-position, background-position-x, background-position-y, background-clip, background-origin, background-size, box-decoration-break, box-shadow, text-shadow +ij_sass_space_after_colon = true +ij_sass_space_before_opening_brace = true +ij_sass_use_double_quotes = true +ij_sass_value_alignment = 0 + +[*.scss] +indent_size = 2 +ij_scss_align_closing_brace_with_properties = false +ij_scss_blank_lines_around_nested_selector = 1 +ij_scss_blank_lines_between_blocks = 1 +ij_scss_block_comment_add_space = false +ij_scss_brace_placement = 0 +ij_scss_enforce_quotes_on_format = false +ij_scss_hex_color_long_format = false +ij_scss_hex_color_lower_case = false +ij_scss_hex_color_short_format = false +ij_scss_hex_color_upper_case = false +ij_scss_keep_blank_lines_in_code = 2 +ij_scss_keep_indents_on_empty_lines = false +ij_scss_keep_single_line_blocks = false +ij_scss_line_comment_add_space = false +ij_scss_line_comment_at_first_column = false +ij_scss_properties_order = font, font-family, font-size, font-weight, font-style, font-variant, font-size-adjust, font-stretch, line-height, position, z-index, top, right, bottom, left, display, visibility, float, clear, overflow, overflow-x, overflow-y, clip, zoom, align-content, align-items, align-self, flex, flex-flow, flex-basis, flex-direction, flex-grow, flex-shrink, flex-wrap, justify-content, order, box-sizing, width, min-width, max-width, height, min-height, max-height, margin, margin-top, margin-right, margin-bottom, margin-left, padding, padding-top, padding-right, padding-bottom, padding-left, table-layout, empty-cells, caption-side, border-spacing, border-collapse, list-style, list-style-position, list-style-type, list-style-image, content, quotes, counter-reset, counter-increment, resize, cursor, user-select, nav-index, nav-up, nav-right, nav-down, nav-left, transition, transition-delay, transition-timing-function, transition-duration, transition-property, transform, transform-origin, animation, animation-name, animation-duration, animation-play-state, animation-timing-function, animation-delay, animation-iteration-count, animation-direction, text-align, text-align-last, vertical-align, white-space, text-decoration, text-emphasis, text-emphasis-color, text-emphasis-style, text-emphasis-position, text-indent, text-justify, letter-spacing, word-spacing, text-outline, text-transform, text-wrap, text-overflow, text-overflow-ellipsis, text-overflow-mode, word-wrap, word-break, tab-size, hyphens, pointer-events, opacity, color, border, border-width, border-style, border-color, border-top, border-top-width, border-top-style, border-top-color, border-right, border-right-width, border-right-style, border-right-color, border-bottom, border-bottom-width, border-bottom-style, border-bottom-color, border-left, border-left-width, border-left-style, border-left-color, border-radius, border-top-left-radius, border-top-right-radius, border-bottom-right-radius, border-bottom-left-radius, border-image, border-image-source, border-image-slice, border-image-width, border-image-outset, border-image-repeat, outline, outline-width, outline-style, outline-color, outline-offset, background, background-color, background-image, background-repeat, background-attachment, background-position, background-position-x, background-position-y, background-clip, background-origin, background-size, box-decoration-break, box-shadow, text-shadow +ij_scss_space_after_colon = true +ij_scss_space_before_opening_brace = true +ij_scss_use_double_quotes = true +ij_scss_value_alignment = 0 + +[*.vue] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 4 +ij_vue_indent_children_of_top_level = template +ij_vue_interpolation_new_line_after_start_delimiter = true +ij_vue_interpolation_new_line_before_end_delimiter = true +ij_vue_interpolation_wrap = off +ij_vue_keep_indents_on_empty_lines = false +ij_vue_spaces_within_interpolation_expressions = true + +[.editorconfig] +ij_editorconfig_align_group_field_declarations = false +ij_editorconfig_space_after_colon = false +ij_editorconfig_space_after_comma = true +ij_editorconfig_space_before_colon = false +ij_editorconfig_space_before_comma = false +ij_editorconfig_spaces_around_assignment_operators = true + +[Config] +ij_dependencyconfig_dependencyalignment = 0 +ij_dependencyconfig_dependencyalignmentscope = 1 +ij_dependencyconfig_keep_blank_lines_in_code = 2 +ij_dependencyconfig_keep_indents_on_empty_lines = false +ij_dependencyconfig_space_after_comma = true +ij_dependencyconfig_space_before_comma = false +ij_dependencyconfig_spaces_around_assignment_operators = true +ij_dependencyconfig_spaces_within_braces = false +ij_dependencyconfig_spaces_within_parentheses = false + +[definition.yaml] +indent_size = 2 +ij_yaml_align_values_properties = do_not_align +ij_yaml_autoinsert_sequence_marker = true +ij_yaml_block_mapping_on_new_line = false +ij_yaml_indent_sequence_value = true +ij_yaml_keep_indents_on_empty_lines = false +ij_yaml_keep_line_breaks = true +ij_yaml_line_comment_add_space = false +ij_yaml_line_comment_add_space_on_reformat = false +ij_yaml_line_comment_at_first_column = true +ij_yaml_sequence_on_new_line = false +ij_yaml_space_before_colon = false +ij_yaml_spaces_within_braces = true +ij_yaml_spaces_within_brackets = true + +[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.pom,*.rng,*.tld,*.wadl,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}] +ij_xml_align_attributes = true +ij_xml_align_text = false +ij_xml_attribute_wrap = normal +ij_xml_block_comment_add_space = false +ij_xml_block_comment_at_first_column = true +ij_xml_keep_blank_lines = 2 +ij_xml_keep_indents_on_empty_lines = false +ij_xml_keep_line_breaks = true +ij_xml_keep_line_breaks_in_text = true +ij_xml_keep_whitespaces = false +ij_xml_keep_whitespaces_around_cdata = preserve +ij_xml_keep_whitespaces_inside_cdata = false +ij_xml_line_comment_at_first_column = true +ij_xml_space_after_tag_name = false +ij_xml_space_around_equals_in_attribute = false +ij_xml_space_inside_empty_tag = false +ij_xml_text_wrap = normal + +[{*.ats,*.cts,*.mts,*.ts}] +ij_continuation_indent_size = 4 +ij_typescript_align_imports = false +ij_typescript_align_multiline_array_initializer_expression = false +ij_typescript_align_multiline_binary_operation = false +ij_typescript_align_multiline_chained_methods = false +ij_typescript_align_multiline_extends_list = false +ij_typescript_align_multiline_for = true +ij_typescript_align_multiline_parameters = true +ij_typescript_align_multiline_parameters_in_calls = false +ij_typescript_align_multiline_ternary_operation = false +ij_typescript_align_object_properties = 0 +ij_typescript_align_union_types = false +ij_typescript_align_var_statements = 0 +ij_typescript_array_initializer_new_line_after_left_brace = false +ij_typescript_array_initializer_right_brace_on_new_line = false +ij_typescript_array_initializer_wrap = off +ij_typescript_assignment_wrap = off +ij_typescript_binary_operation_sign_on_next_line = false +ij_typescript_binary_operation_wrap = off +ij_typescript_blacklist_imports = rxjs/Rx, node_modules/**, **/node_modules/**, @angular/material, @angular/material/typings/** +ij_typescript_blank_lines_after_imports = 1 +ij_typescript_blank_lines_around_class = 1 +ij_typescript_blank_lines_around_field = 0 +ij_typescript_blank_lines_around_field_in_interface = 0 +ij_typescript_blank_lines_around_function = 1 +ij_typescript_blank_lines_around_method = 1 +ij_typescript_blank_lines_around_method_in_interface = 1 +ij_typescript_block_brace_style = end_of_line +ij_typescript_block_comment_add_space = false +ij_typescript_block_comment_at_first_column = true +ij_typescript_call_parameters_new_line_after_left_paren = false +ij_typescript_call_parameters_right_paren_on_new_line = false +ij_typescript_call_parameters_wrap = off +ij_typescript_catch_on_new_line = false +ij_typescript_chained_call_dot_on_new_line = true +ij_typescript_class_brace_style = end_of_line +ij_typescript_comma_on_new_line = false +ij_typescript_do_while_brace_force = never +ij_typescript_else_on_new_line = false +ij_typescript_enforce_trailing_comma = keep +ij_typescript_enum_constants_wrap = on_every_item +ij_typescript_extends_keyword_wrap = off +ij_typescript_extends_list_wrap = off +ij_typescript_field_prefix = _ +ij_typescript_file_name_style = relaxed +ij_typescript_finally_on_new_line = false +ij_typescript_for_brace_force = never +ij_typescript_for_statement_new_line_after_left_paren = false +ij_typescript_for_statement_right_paren_on_new_line = false +ij_typescript_for_statement_wrap = off +ij_typescript_force_quote_style = false +ij_typescript_force_semicolon_style = false +ij_typescript_function_expression_brace_style = end_of_line +ij_typescript_if_brace_force = never +ij_typescript_import_merge_members = global +ij_typescript_import_prefer_absolute_path = global +ij_typescript_import_sort_members = true +ij_typescript_import_sort_module_name = false +ij_typescript_import_use_node_resolution = true +ij_typescript_imports_wrap = on_every_item +ij_typescript_indent_case_from_switch = true +ij_typescript_indent_chained_calls = true +ij_typescript_indent_package_children = 0 +ij_typescript_jsdoc_include_types = false +ij_typescript_jsx_attribute_value = braces +ij_typescript_keep_blank_lines_in_code = 2 +ij_typescript_keep_first_column_comment = true +ij_typescript_keep_indents_on_empty_lines = false +ij_typescript_keep_line_breaks = true +ij_typescript_keep_simple_blocks_in_one_line = false +ij_typescript_keep_simple_methods_in_one_line = false +ij_typescript_line_comment_add_space = true +ij_typescript_line_comment_at_first_column = false +ij_typescript_method_brace_style = end_of_line +ij_typescript_method_call_chain_wrap = off +ij_typescript_method_parameters_new_line_after_left_paren = false +ij_typescript_method_parameters_right_paren_on_new_line = false +ij_typescript_method_parameters_wrap = off +ij_typescript_object_literal_wrap = on_every_item +ij_typescript_object_types_wrap = on_every_item +ij_typescript_parentheses_expression_new_line_after_left_paren = false +ij_typescript_parentheses_expression_right_paren_on_new_line = false +ij_typescript_place_assignment_sign_on_next_line = false +ij_typescript_prefer_as_type_cast = false +ij_typescript_prefer_explicit_types_function_expression_returns = false +ij_typescript_prefer_explicit_types_function_returns = false +ij_typescript_prefer_explicit_types_vars_fields = false +ij_typescript_prefer_parameters_wrap = false +ij_typescript_property_prefix = +ij_typescript_reformat_c_style_comments = false +ij_typescript_space_after_colon = true +ij_typescript_space_after_comma = true +ij_typescript_space_after_dots_in_rest_parameter = false +ij_typescript_space_after_generator_mult = true +ij_typescript_space_after_property_colon = true +ij_typescript_space_after_quest = true +ij_typescript_space_after_type_colon = true +ij_typescript_space_after_unary_not = false +ij_typescript_space_before_async_arrow_lparen = true +ij_typescript_space_before_catch_keyword = true +ij_typescript_space_before_catch_left_brace = true +ij_typescript_space_before_catch_parentheses = true +ij_typescript_space_before_class_lbrace = true +ij_typescript_space_before_class_left_brace = true +ij_typescript_space_before_colon = true +ij_typescript_space_before_comma = false +ij_typescript_space_before_do_left_brace = true +ij_typescript_space_before_else_keyword = true +ij_typescript_space_before_else_left_brace = true +ij_typescript_space_before_finally_keyword = true +ij_typescript_space_before_finally_left_brace = true +ij_typescript_space_before_for_left_brace = true +ij_typescript_space_before_for_parentheses = true +ij_typescript_space_before_for_semicolon = false +ij_typescript_space_before_function_left_parenth = true +ij_typescript_space_before_generator_mult = false +ij_typescript_space_before_if_left_brace = true +ij_typescript_space_before_if_parentheses = true +ij_typescript_space_before_method_call_parentheses = false +ij_typescript_space_before_method_left_brace = true +ij_typescript_space_before_method_parentheses = false +ij_typescript_space_before_property_colon = false +ij_typescript_space_before_quest = true +ij_typescript_space_before_switch_left_brace = true +ij_typescript_space_before_switch_parentheses = true +ij_typescript_space_before_try_left_brace = true +ij_typescript_space_before_type_colon = false +ij_typescript_space_before_unary_not = false +ij_typescript_space_before_while_keyword = true +ij_typescript_space_before_while_left_brace = true +ij_typescript_space_before_while_parentheses = true +ij_typescript_spaces_around_additive_operators = true +ij_typescript_spaces_around_arrow_function_operator = true +ij_typescript_spaces_around_assignment_operators = true +ij_typescript_spaces_around_bitwise_operators = true +ij_typescript_spaces_around_equality_operators = true +ij_typescript_spaces_around_logical_operators = true +ij_typescript_spaces_around_multiplicative_operators = true +ij_typescript_spaces_around_relational_operators = true +ij_typescript_spaces_around_shift_operators = true +ij_typescript_spaces_around_unary_operator = false +ij_typescript_spaces_within_array_initializer_brackets = false +ij_typescript_spaces_within_brackets = false +ij_typescript_spaces_within_catch_parentheses = false +ij_typescript_spaces_within_for_parentheses = false +ij_typescript_spaces_within_if_parentheses = false +ij_typescript_spaces_within_imports = false +ij_typescript_spaces_within_interpolation_expressions = false +ij_typescript_spaces_within_method_call_parentheses = false +ij_typescript_spaces_within_method_parentheses = false +ij_typescript_spaces_within_object_literal_braces = false +ij_typescript_spaces_within_object_type_braces = true +ij_typescript_spaces_within_parentheses = false +ij_typescript_spaces_within_switch_parentheses = false +ij_typescript_spaces_within_type_assertion = false +ij_typescript_spaces_within_union_types = true +ij_typescript_spaces_within_while_parentheses = false +ij_typescript_special_else_if_treatment = true +ij_typescript_ternary_operation_signs_on_next_line = false +ij_typescript_ternary_operation_wrap = off +ij_typescript_union_types_wrap = on_every_item +ij_typescript_use_chained_calls_group_indents = false +ij_typescript_use_double_quotes = true +ij_typescript_use_explicit_js_extension = auto +ij_typescript_use_import_type = auto +ij_typescript_use_path_mapping = always +ij_typescript_use_public_modifier = false +ij_typescript_use_semicolon_after_statement = true +ij_typescript_var_declaration_wrap = normal +ij_typescript_while_brace_force = never +ij_typescript_while_on_new_line = false +ij_typescript_wrap_comments = false + +[{*.bash,*.sh,*.zsh}] +indent_size = 2 +tab_width = 2 +ij_shell_binary_ops_start_line = false +ij_shell_keep_column_alignment_padding = false +ij_shell_minify_program = false +ij_shell_redirect_followed_by_space = false +ij_shell_switch_cases_indented = false +ij_shell_use_unix_line_separator = true + +[{*.cjs,*.js}] +ij_continuation_indent_size = 4 +ij_javascript_align_imports = false +ij_javascript_align_multiline_array_initializer_expression = false +ij_javascript_align_multiline_binary_operation = false +ij_javascript_align_multiline_chained_methods = false +ij_javascript_align_multiline_extends_list = false +ij_javascript_align_multiline_for = true +ij_javascript_align_multiline_parameters = true +ij_javascript_align_multiline_parameters_in_calls = false +ij_javascript_align_multiline_ternary_operation = false +ij_javascript_align_object_properties = 0 +ij_javascript_align_union_types = false +ij_javascript_align_var_statements = 0 +ij_javascript_array_initializer_new_line_after_left_brace = false +ij_javascript_array_initializer_right_brace_on_new_line = false +ij_javascript_array_initializer_wrap = off +ij_javascript_assignment_wrap = off +ij_javascript_binary_operation_sign_on_next_line = false +ij_javascript_binary_operation_wrap = off +ij_javascript_blacklist_imports = rxjs/Rx, node_modules/**, **/node_modules/**, @angular/material, @angular/material/typings/** +ij_javascript_blank_lines_after_imports = 1 +ij_javascript_blank_lines_around_class = 1 +ij_javascript_blank_lines_around_field = 0 +ij_javascript_blank_lines_around_function = 1 +ij_javascript_blank_lines_around_method = 1 +ij_javascript_block_brace_style = end_of_line +ij_javascript_block_comment_add_space = false +ij_javascript_block_comment_at_first_column = true +ij_javascript_call_parameters_new_line_after_left_paren = false +ij_javascript_call_parameters_right_paren_on_new_line = false +ij_javascript_call_parameters_wrap = off +ij_javascript_catch_on_new_line = false +ij_javascript_chained_call_dot_on_new_line = true +ij_javascript_class_brace_style = end_of_line +ij_javascript_comma_on_new_line = false +ij_javascript_do_while_brace_force = never +ij_javascript_else_on_new_line = false +ij_javascript_enforce_trailing_comma = keep +ij_javascript_extends_keyword_wrap = off +ij_javascript_extends_list_wrap = off +ij_javascript_field_prefix = _ +ij_javascript_file_name_style = relaxed +ij_javascript_finally_on_new_line = false +ij_javascript_for_brace_force = never +ij_javascript_for_statement_new_line_after_left_paren = false +ij_javascript_for_statement_right_paren_on_new_line = false +ij_javascript_for_statement_wrap = off +ij_javascript_force_quote_style = false +ij_javascript_force_semicolon_style = false +ij_javascript_function_expression_brace_style = end_of_line +ij_javascript_if_brace_force = never +ij_javascript_import_merge_members = global +ij_javascript_import_prefer_absolute_path = global +ij_javascript_import_sort_members = true +ij_javascript_import_sort_module_name = false +ij_javascript_import_use_node_resolution = true +ij_javascript_imports_wrap = on_every_item +ij_javascript_indent_case_from_switch = true +ij_javascript_indent_chained_calls = true +ij_javascript_indent_package_children = 0 +ij_javascript_jsx_attribute_value = braces +ij_javascript_keep_blank_lines_in_code = 2 +ij_javascript_keep_first_column_comment = true +ij_javascript_keep_indents_on_empty_lines = false +ij_javascript_keep_line_breaks = true +ij_javascript_keep_simple_blocks_in_one_line = false +ij_javascript_keep_simple_methods_in_one_line = false +ij_javascript_line_comment_add_space = true +ij_javascript_line_comment_at_first_column = false +ij_javascript_method_brace_style = end_of_line +ij_javascript_method_call_chain_wrap = off +ij_javascript_method_parameters_new_line_after_left_paren = false +ij_javascript_method_parameters_right_paren_on_new_line = false +ij_javascript_method_parameters_wrap = off +ij_javascript_object_literal_wrap = on_every_item +ij_javascript_object_types_wrap = on_every_item +ij_javascript_parentheses_expression_new_line_after_left_paren = false +ij_javascript_parentheses_expression_right_paren_on_new_line = false +ij_javascript_place_assignment_sign_on_next_line = false +ij_javascript_prefer_as_type_cast = false +ij_javascript_prefer_explicit_types_function_expression_returns = false +ij_javascript_prefer_explicit_types_function_returns = false +ij_javascript_prefer_explicit_types_vars_fields = false +ij_javascript_prefer_parameters_wrap = false +ij_javascript_property_prefix = +ij_javascript_reformat_c_style_comments = false +ij_javascript_space_after_colon = true +ij_javascript_space_after_comma = true +ij_javascript_space_after_dots_in_rest_parameter = false +ij_javascript_space_after_generator_mult = true +ij_javascript_space_after_property_colon = true +ij_javascript_space_after_quest = true +ij_javascript_space_after_type_colon = true +ij_javascript_space_after_unary_not = false +ij_javascript_space_before_async_arrow_lparen = true +ij_javascript_space_before_catch_keyword = true +ij_javascript_space_before_catch_left_brace = true +ij_javascript_space_before_catch_parentheses = true +ij_javascript_space_before_class_lbrace = true +ij_javascript_space_before_class_left_brace = true +ij_javascript_space_before_colon = true +ij_javascript_space_before_comma = false +ij_javascript_space_before_do_left_brace = true +ij_javascript_space_before_else_keyword = true +ij_javascript_space_before_else_left_brace = true +ij_javascript_space_before_finally_keyword = true +ij_javascript_space_before_finally_left_brace = true +ij_javascript_space_before_for_left_brace = true +ij_javascript_space_before_for_parentheses = true +ij_javascript_space_before_for_semicolon = false +ij_javascript_space_before_function_left_parenth = true +ij_javascript_space_before_generator_mult = false +ij_javascript_space_before_if_left_brace = true +ij_javascript_space_before_if_parentheses = true +ij_javascript_space_before_method_call_parentheses = false +ij_javascript_space_before_method_left_brace = true +ij_javascript_space_before_method_parentheses = false +ij_javascript_space_before_property_colon = false +ij_javascript_space_before_quest = true +ij_javascript_space_before_switch_left_brace = true +ij_javascript_space_before_switch_parentheses = true +ij_javascript_space_before_try_left_brace = true +ij_javascript_space_before_type_colon = false +ij_javascript_space_before_unary_not = false +ij_javascript_space_before_while_keyword = true +ij_javascript_space_before_while_left_brace = true +ij_javascript_space_before_while_parentheses = true +ij_javascript_spaces_around_additive_operators = true +ij_javascript_spaces_around_arrow_function_operator = true +ij_javascript_spaces_around_assignment_operators = true +ij_javascript_spaces_around_bitwise_operators = true +ij_javascript_spaces_around_equality_operators = true +ij_javascript_spaces_around_logical_operators = true +ij_javascript_spaces_around_multiplicative_operators = true +ij_javascript_spaces_around_relational_operators = true +ij_javascript_spaces_around_shift_operators = true +ij_javascript_spaces_around_unary_operator = false +ij_javascript_spaces_within_array_initializer_brackets = false +ij_javascript_spaces_within_brackets = false +ij_javascript_spaces_within_catch_parentheses = false +ij_javascript_spaces_within_for_parentheses = false +ij_javascript_spaces_within_if_parentheses = false +ij_javascript_spaces_within_imports = false +ij_javascript_spaces_within_interpolation_expressions = false +ij_javascript_spaces_within_method_call_parentheses = false +ij_javascript_spaces_within_method_parentheses = false +ij_javascript_spaces_within_object_literal_braces = false +ij_javascript_spaces_within_object_type_braces = true +ij_javascript_spaces_within_parentheses = false +ij_javascript_spaces_within_switch_parentheses = false +ij_javascript_spaces_within_type_assertion = false +ij_javascript_spaces_within_union_types = true +ij_javascript_spaces_within_while_parentheses = false +ij_javascript_special_else_if_treatment = true +ij_javascript_ternary_operation_signs_on_next_line = false +ij_javascript_ternary_operation_wrap = off +ij_javascript_union_types_wrap = on_every_item +ij_javascript_use_chained_calls_group_indents = false +ij_javascript_use_double_quotes = true +ij_javascript_use_explicit_js_extension = auto +ij_javascript_use_import_type = auto +ij_javascript_use_path_mapping = always +ij_javascript_use_public_modifier = false +ij_javascript_use_semicolon_after_statement = true +ij_javascript_var_declaration_wrap = normal +ij_javascript_while_brace_force = never +ij_javascript_while_on_new_line = false +ij_javascript_wrap_comments = false + +[{*.ft,*.vm,*.vsl}] +ij_vtl_keep_indents_on_empty_lines = false + +[{*.gant,*.groovy,*.gy}] +ij_groovy_align_group_field_declarations = false +ij_groovy_align_multiline_array_initializer_expression = false +ij_groovy_align_multiline_assignment = false +ij_groovy_align_multiline_binary_operation = false +ij_groovy_align_multiline_chained_methods = false +ij_groovy_align_multiline_extends_list = false +ij_groovy_align_multiline_for = true +ij_groovy_align_multiline_list_or_map = true +ij_groovy_align_multiline_method_parentheses = false +ij_groovy_align_multiline_parameters = true +ij_groovy_align_multiline_parameters_in_calls = false +ij_groovy_align_multiline_resources = true +ij_groovy_align_multiline_ternary_operation = false +ij_groovy_align_multiline_throws_list = false +ij_groovy_align_named_args_in_map = true +ij_groovy_align_throws_keyword = false +ij_groovy_array_initializer_new_line_after_left_brace = false +ij_groovy_array_initializer_right_brace_on_new_line = false +ij_groovy_array_initializer_wrap = off +ij_groovy_assert_statement_wrap = off +ij_groovy_assignment_wrap = off +ij_groovy_binary_operation_wrap = off +ij_groovy_blank_lines_after_class_header = 0 +ij_groovy_blank_lines_after_imports = 1 +ij_groovy_blank_lines_after_package = 1 +ij_groovy_blank_lines_around_class = 1 +ij_groovy_blank_lines_around_field = 0 +ij_groovy_blank_lines_around_field_in_interface = 0 +ij_groovy_blank_lines_around_method = 1 +ij_groovy_blank_lines_around_method_in_interface = 1 +ij_groovy_blank_lines_before_imports = 1 +ij_groovy_blank_lines_before_method_body = 0 +ij_groovy_blank_lines_before_package = 0 +ij_groovy_block_brace_style = end_of_line +ij_groovy_block_comment_add_space = false +ij_groovy_block_comment_at_first_column = true +ij_groovy_call_parameters_new_line_after_left_paren = false +ij_groovy_call_parameters_right_paren_on_new_line = false +ij_groovy_call_parameters_wrap = off +ij_groovy_catch_on_new_line = false +ij_groovy_class_annotation_wrap = split_into_lines +ij_groovy_class_brace_style = end_of_line +ij_groovy_class_count_to_use_import_on_demand = 5 +ij_groovy_do_while_brace_force = never +ij_groovy_else_on_new_line = false +ij_groovy_enable_groovydoc_formatting = true +ij_groovy_enum_constants_wrap = off +ij_groovy_extends_keyword_wrap = off +ij_groovy_extends_list_wrap = off +ij_groovy_field_annotation_wrap = split_into_lines +ij_groovy_finally_on_new_line = false +ij_groovy_for_brace_force = never +ij_groovy_for_statement_new_line_after_left_paren = false +ij_groovy_for_statement_right_paren_on_new_line = false +ij_groovy_for_statement_wrap = off +ij_groovy_ginq_general_clause_wrap_policy = 2 +ij_groovy_ginq_having_wrap_policy = 1 +ij_groovy_ginq_indent_having_clause = true +ij_groovy_ginq_indent_on_clause = true +ij_groovy_ginq_on_wrap_policy = 1 +ij_groovy_ginq_space_after_keyword = true +ij_groovy_if_brace_force = never +ij_groovy_import_annotation_wrap = 2 +ij_groovy_imports_layout = *, |, javax.**, java.**, |, $* +ij_groovy_indent_case_from_switch = true +ij_groovy_indent_label_blocks = true +ij_groovy_insert_inner_class_imports = false +ij_groovy_keep_blank_lines_before_right_brace = 2 +ij_groovy_keep_blank_lines_in_code = 2 +ij_groovy_keep_blank_lines_in_declarations = 2 +ij_groovy_keep_control_statement_in_one_line = true +ij_groovy_keep_first_column_comment = true +ij_groovy_keep_indents_on_empty_lines = false +ij_groovy_keep_line_breaks = true +ij_groovy_keep_multiple_expressions_in_one_line = false +ij_groovy_keep_simple_blocks_in_one_line = false +ij_groovy_keep_simple_classes_in_one_line = true +ij_groovy_keep_simple_lambdas_in_one_line = true +ij_groovy_keep_simple_methods_in_one_line = true +ij_groovy_label_indent_absolute = false +ij_groovy_label_indent_size = 0 +ij_groovy_lambda_brace_style = end_of_line +ij_groovy_layout_static_imports_separately = true +ij_groovy_line_comment_add_space = false +ij_groovy_line_comment_add_space_on_reformat = false +ij_groovy_line_comment_at_first_column = true +ij_groovy_method_annotation_wrap = split_into_lines +ij_groovy_method_brace_style = end_of_line +ij_groovy_method_call_chain_wrap = off +ij_groovy_method_parameters_new_line_after_left_paren = false +ij_groovy_method_parameters_right_paren_on_new_line = false +ij_groovy_method_parameters_wrap = off +ij_groovy_modifier_list_wrap = false +ij_groovy_names_count_to_use_import_on_demand = 3 +ij_groovy_packages_to_use_import_on_demand = java.awt.*, javax.swing.* +ij_groovy_parameter_annotation_wrap = off +ij_groovy_parentheses_expression_new_line_after_left_paren = false +ij_groovy_parentheses_expression_right_paren_on_new_line = false +ij_groovy_prefer_parameters_wrap = false +ij_groovy_resource_list_new_line_after_left_paren = false +ij_groovy_resource_list_right_paren_on_new_line = false +ij_groovy_resource_list_wrap = off +ij_groovy_space_after_assert_separator = true +ij_groovy_space_after_colon = true +ij_groovy_space_after_comma = true +ij_groovy_space_after_comma_in_type_arguments = true +ij_groovy_space_after_for_semicolon = true +ij_groovy_space_after_quest = true +ij_groovy_space_after_type_cast = true +ij_groovy_space_before_annotation_parameter_list = false +ij_groovy_space_before_array_initializer_left_brace = false +ij_groovy_space_before_assert_separator = false +ij_groovy_space_before_catch_keyword = true +ij_groovy_space_before_catch_left_brace = true +ij_groovy_space_before_catch_parentheses = true +ij_groovy_space_before_class_left_brace = true +ij_groovy_space_before_closure_left_brace = true +ij_groovy_space_before_colon = true +ij_groovy_space_before_comma = false +ij_groovy_space_before_do_left_brace = true +ij_groovy_space_before_else_keyword = true +ij_groovy_space_before_else_left_brace = true +ij_groovy_space_before_finally_keyword = true +ij_groovy_space_before_finally_left_brace = true +ij_groovy_space_before_for_left_brace = true +ij_groovy_space_before_for_parentheses = true +ij_groovy_space_before_for_semicolon = false +ij_groovy_space_before_if_left_brace = true +ij_groovy_space_before_if_parentheses = true +ij_groovy_space_before_method_call_parentheses = false +ij_groovy_space_before_method_left_brace = true +ij_groovy_space_before_method_parentheses = false +ij_groovy_space_before_quest = true +ij_groovy_space_before_record_parentheses = false +ij_groovy_space_before_switch_left_brace = true +ij_groovy_space_before_switch_parentheses = true +ij_groovy_space_before_synchronized_left_brace = true +ij_groovy_space_before_synchronized_parentheses = true +ij_groovy_space_before_try_left_brace = true +ij_groovy_space_before_try_parentheses = true +ij_groovy_space_before_while_keyword = true +ij_groovy_space_before_while_left_brace = true +ij_groovy_space_before_while_parentheses = true +ij_groovy_space_in_named_argument = true +ij_groovy_space_in_named_argument_before_colon = false +ij_groovy_space_within_empty_array_initializer_braces = false +ij_groovy_space_within_empty_method_call_parentheses = false +ij_groovy_spaces_around_additive_operators = true +ij_groovy_spaces_around_assignment_operators = true +ij_groovy_spaces_around_bitwise_operators = true +ij_groovy_spaces_around_equality_operators = true +ij_groovy_spaces_around_lambda_arrow = true +ij_groovy_spaces_around_logical_operators = true +ij_groovy_spaces_around_multiplicative_operators = true +ij_groovy_spaces_around_regex_operators = true +ij_groovy_spaces_around_relational_operators = true +ij_groovy_spaces_around_shift_operators = true +ij_groovy_spaces_within_annotation_parentheses = false +ij_groovy_spaces_within_array_initializer_braces = false +ij_groovy_spaces_within_braces = true +ij_groovy_spaces_within_brackets = false +ij_groovy_spaces_within_cast_parentheses = false +ij_groovy_spaces_within_catch_parentheses = false +ij_groovy_spaces_within_for_parentheses = false +ij_groovy_spaces_within_gstring_injection_braces = false +ij_groovy_spaces_within_if_parentheses = false +ij_groovy_spaces_within_list_or_map = false +ij_groovy_spaces_within_method_call_parentheses = false +ij_groovy_spaces_within_method_parentheses = false +ij_groovy_spaces_within_parentheses = false +ij_groovy_spaces_within_switch_parentheses = false +ij_groovy_spaces_within_synchronized_parentheses = false +ij_groovy_spaces_within_try_parentheses = false +ij_groovy_spaces_within_tuple_expression = false +ij_groovy_spaces_within_while_parentheses = false +ij_groovy_special_else_if_treatment = true +ij_groovy_ternary_operation_wrap = off +ij_groovy_throws_keyword_wrap = off +ij_groovy_throws_list_wrap = off +ij_groovy_use_flying_geese_braces = false +ij_groovy_use_fq_class_names = false +ij_groovy_use_fq_class_names_in_javadoc = true +ij_groovy_use_relative_indents = false +ij_groovy_use_single_class_imports = true +ij_groovy_variable_annotation_wrap = off +ij_groovy_while_brace_force = never +ij_groovy_while_on_new_line = false +ij_groovy_wrap_chain_calls_after_dot = false +ij_groovy_wrap_long_lines = false + +[{*.har,*.jsb2,*.jsb3,*.json,*.jsonc,*.postman_collection,*.postman_collection.json,*.postman_environment,*.postman_environment.json,.babelrc,.eslintrc,.prettierrc,.stylelintrc,.ws-context,jest.config}] +indent_size = 2 +ij_json_array_wrapping = split_into_lines +ij_json_keep_blank_lines_in_code = 0 +ij_json_keep_indents_on_empty_lines = false +ij_json_keep_line_breaks = true +ij_json_keep_trailing_comma = false +ij_json_object_wrapping = split_into_lines +ij_json_property_alignment = do_not_align +ij_json_space_after_colon = true +ij_json_space_after_comma = true +ij_json_space_before_colon = false +ij_json_space_before_comma = false +ij_json_spaces_within_braces = false +ij_json_spaces_within_brackets = false +ij_json_wrap_long_lines = false + +[{*.htm,*.html,*.sht,*.shtm,*.shtml}] +ij_html_add_new_line_before_tags = body, div, p, form, h1, h2, h3 +ij_html_align_attributes = true +ij_html_align_text = false +ij_html_attribute_wrap = normal +ij_html_block_comment_add_space = false +ij_html_block_comment_at_first_column = true +ij_html_do_not_align_children_of_min_lines = 0 +ij_html_do_not_break_if_inline_tags = title, h1, h2, h3, h4, h5, h6, p +ij_html_do_not_indent_children_of_tags = html, body, thead, tbody, tfoot +ij_html_enforce_quotes = false +ij_html_inline_tags = a, abbr, acronym, b, basefont, bdo, big, br, cite, cite, code, dfn, em, font, i, img, input, kbd, label, q, s, samp, select, small, span, strike, strong, sub, sup, textarea, tt, u, var +ij_html_keep_blank_lines = 2 +ij_html_keep_indents_on_empty_lines = false +ij_html_keep_line_breaks = true +ij_html_keep_line_breaks_in_text = true +ij_html_keep_whitespaces = false +ij_html_keep_whitespaces_inside = span, pre, textarea +ij_html_line_comment_at_first_column = true +ij_html_new_line_after_last_attribute = never +ij_html_new_line_before_first_attribute = never +ij_html_quote_style = double +ij_html_remove_new_line_before_tags = br +ij_html_space_after_tag_name = false +ij_html_space_around_equality_in_attribute = false +ij_html_space_inside_empty_tag = false +ij_html_text_wrap = normal + +[{*.http,*.rest}] +indent_size = 0 +ij_continuation_indent_size = 4 +ij_http-request_call_parameters_wrap = normal +ij_http-request_method_parameters_wrap = split_into_lines +ij_http-request_space_before_comma = true +ij_http-request_spaces_around_assignment_operators = true + +[{*.jsf,*.jsp,*.jspf,*.tag,*.tagf,*.xjsp}] +ij_jsp_jsp_prefer_comma_separated_import_list = false +ij_jsp_keep_indents_on_empty_lines = false + +[{*.jspx,*.tagx}] +ij_jspx_keep_indents_on_empty_lines = false + +[{*.kt,*.kts}] +ij_kotlin_align_in_columns_case_branch = false +ij_kotlin_align_multiline_binary_operation = false +ij_kotlin_align_multiline_extends_list = false +ij_kotlin_align_multiline_method_parentheses = false +ij_kotlin_align_multiline_parameters = true +ij_kotlin_align_multiline_parameters_in_calls = false +ij_kotlin_allow_trailing_comma = false +ij_kotlin_allow_trailing_comma_on_call_site = false +ij_kotlin_assignment_wrap = normal +ij_kotlin_blank_lines_after_class_header = 0 +ij_kotlin_blank_lines_around_block_when_branches = 0 +ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1 +ij_kotlin_block_comment_add_space = false +ij_kotlin_block_comment_at_first_column = true +ij_kotlin_call_parameters_new_line_after_left_paren = true +ij_kotlin_call_parameters_right_paren_on_new_line = true +ij_kotlin_call_parameters_wrap = on_every_item +ij_kotlin_catch_on_new_line = false +ij_kotlin_class_annotation_wrap = split_into_lines +ij_kotlin_continuation_indent_for_chained_calls = false +ij_kotlin_continuation_indent_for_expression_bodies = false +ij_kotlin_continuation_indent_in_argument_lists = false +ij_kotlin_continuation_indent_in_elvis = false +ij_kotlin_continuation_indent_in_if_conditions = false +ij_kotlin_continuation_indent_in_parameter_lists = false +ij_kotlin_continuation_indent_in_supertype_lists = false +ij_kotlin_else_on_new_line = false +ij_kotlin_enum_constants_wrap = off +ij_kotlin_extends_list_wrap = normal +ij_kotlin_field_annotation_wrap = split_into_lines +ij_kotlin_finally_on_new_line = false +ij_kotlin_if_rparen_on_new_line = true +ij_kotlin_import_nested_classes = false +ij_kotlin_imports_layout = *, java.**, javax.**, kotlin.**, ^ +ij_kotlin_indent_before_arrow_on_new_line = true +ij_kotlin_insert_whitespaces_in_simple_one_line_method = true +ij_kotlin_keep_blank_lines_before_right_brace = 2 +ij_kotlin_keep_blank_lines_in_code = 2 +ij_kotlin_keep_blank_lines_in_declarations = 2 +ij_kotlin_keep_first_column_comment = true +ij_kotlin_keep_indents_on_empty_lines = false +ij_kotlin_keep_line_breaks = true +ij_kotlin_lbrace_on_next_line = false +ij_kotlin_line_break_after_multiline_when_entry = true +ij_kotlin_line_comment_add_space = false +ij_kotlin_line_comment_add_space_on_reformat = false +ij_kotlin_line_comment_at_first_column = true +ij_kotlin_method_annotation_wrap = split_into_lines +ij_kotlin_method_call_chain_wrap = normal +ij_kotlin_method_parameters_new_line_after_left_paren = true +ij_kotlin_method_parameters_right_paren_on_new_line = true +ij_kotlin_method_parameters_wrap = on_every_item +ij_kotlin_name_count_to_use_star_import = 5 +ij_kotlin_name_count_to_use_star_import_for_members = 3 +ij_kotlin_packages_to_use_import_on_demand = java.util.*, kotlinx.android.synthetic.**, io.ktor.** +ij_kotlin_parameter_annotation_wrap = off +ij_kotlin_space_after_comma = true +ij_kotlin_space_after_extend_colon = true +ij_kotlin_space_after_type_colon = true +ij_kotlin_space_before_catch_parentheses = true +ij_kotlin_space_before_comma = false +ij_kotlin_space_before_extend_colon = true +ij_kotlin_space_before_for_parentheses = true +ij_kotlin_space_before_if_parentheses = true +ij_kotlin_space_before_lambda_arrow = true +ij_kotlin_space_before_type_colon = false +ij_kotlin_space_before_when_parentheses = true +ij_kotlin_space_before_while_parentheses = true +ij_kotlin_spaces_around_additive_operators = true +ij_kotlin_spaces_around_assignment_operators = true +ij_kotlin_spaces_around_equality_operators = true +ij_kotlin_spaces_around_function_type_arrow = true +ij_kotlin_spaces_around_logical_operators = true +ij_kotlin_spaces_around_multiplicative_operators = true +ij_kotlin_spaces_around_range = false +ij_kotlin_spaces_around_relational_operators = true +ij_kotlin_spaces_around_unary_operator = false +ij_kotlin_spaces_around_when_arrow = true +ij_kotlin_variable_annotation_wrap = off +ij_kotlin_while_on_new_line = false +ij_kotlin_wrap_elvis_expressions = 1 +ij_kotlin_wrap_expression_body_functions = 1 +ij_kotlin_wrap_first_method_in_call_chain = false + +[{*.markdown,*.md}] +ij_markdown_force_one_space_after_blockquote_symbol = true +ij_markdown_force_one_space_after_header_symbol = true +ij_markdown_force_one_space_after_list_bullet = true +ij_markdown_force_one_space_between_words = true +ij_markdown_format_tables = true +ij_markdown_insert_quote_arrows_on_wrap = true +ij_markdown_keep_indents_on_empty_lines = false +ij_markdown_keep_line_breaks_inside_text_blocks = true +ij_markdown_max_lines_around_block_elements = 1 +ij_markdown_max_lines_around_header = 1 +ij_markdown_max_lines_between_paragraphs = 1 +ij_markdown_min_lines_around_block_elements = 1 +ij_markdown_min_lines_around_header = 1 +ij_markdown_min_lines_between_paragraphs = 1 +ij_markdown_wrap_text_if_long = true +ij_markdown_wrap_text_inside_blockquotes = true + +[{*.pb,*.textproto,*.txtpb}] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 4 +ij_prototext_keep_blank_lines_in_code = 2 +ij_prototext_keep_indents_on_empty_lines = false +ij_prototext_keep_line_breaks = true +ij_prototext_space_after_colon = true +ij_prototext_space_after_comma = true +ij_prototext_space_before_colon = false +ij_prototext_space_before_comma = false +ij_prototext_spaces_within_braces = true +ij_prototext_spaces_within_brackets = false + +[{*.properties,spring.handlers,spring.schemas}] +ij_properties_align_group_field_declarations = false +ij_properties_keep_blank_lines = false +ij_properties_key_value_delimiter = equals +ij_properties_spaces_around_key_value_delimiter = false + +[{*.qute.htm,*.qute.html,*.qute.json,*.qute.txt,*.qute.yaml,*.qute.yml}] +ij_qute_keep_indents_on_empty_lines = false + +[{*.toml,Cargo.lock,Cargo.toml.orig,Gopkg.lock,Pipfile,poetry.lock}] +ij_toml_keep_indents_on_empty_lines = false diff --git a/.github/workflows/maven-verify.yml b/.github/workflows/maven-verify.yml index 51747b92a..e6b549c04 100644 --- a/.github/workflows/maven-verify.yml +++ b/.github/workflows/maven-verify.yml @@ -21,9 +21,9 @@ jobs: - name: Setup Python uses: actions/setup-python@v2.2.2 with: - python-version: 3.7 + python-version: 3.13 - name: Install Python packages - run: pip install pre-commit cloudformation-cli cloudformation-cli-java-plugin + run: pip install pre-commit cloudformation-cli cloudformation-cli-java-plugin setuptools - name: Run pre-commit run: pre-commit run --all-files - name: Verify AWS::RDS::Test::Common diff --git a/aws-rds-cfn-common/pom.xml b/aws-rds-cfn-common/pom.xml index 426326ad1..a7ef1fa4b 100644 --- a/aws-rds-cfn-common/pom.xml +++ b/aws-rds-cfn-common/pom.xml @@ -28,12 +28,18 @@ software.amazon.awssdk utils - 2.25.12 + 2.30.38 software.amazon.awssdk rds - 2.25.56 + 2.30.38 + + + + software.amazon.awssdk + aws-query-protocol + 2.30.38 software.amazon.cloudformation @@ -53,7 +59,7 @@ org.projectlombok lombok - 1.18.22 + 1.18.30 provided @@ -98,6 +104,27 @@ 1.0 test + + + software.amazon.awssdk + cloudformation + 2.30.38 + + + software.amazon.awssdk + cloudwatch + 2.30.38 + + + software.amazon.awssdk + cloudwatchevents + 2.30.38 + + + software.amazon.awssdk + cloudwatchlogs + 2.30.38 + @@ -221,17 +248,17 @@ - PACKAGE + BUNDLE BRANCH COVEREDRATIO - 0.8 + 0.7 INSTRUCTION COVEREDRATIO - 0.8 + 0.0 diff --git a/aws-rds-cfn-common/src/main/java/software/amazon/rds/common/error/ErrorStatus.java b/aws-rds-cfn-common/src/main/java/software/amazon/rds/common/error/ErrorStatus.java index 831eafc4d..32f1e5b62 100644 --- a/aws-rds-cfn-common/src/main/java/software/amazon/rds/common/error/ErrorStatus.java +++ b/aws-rds-cfn-common/src/main/java/software/amazon/rds/common/error/ErrorStatus.java @@ -20,7 +20,11 @@ static ErrorStatus ignore(final OperationStatus status) { } static ErrorStatus retry(final int callbackDelay) { - return new RetryErrorStatus(OperationStatus.IN_PROGRESS, callbackDelay); + return retry(callbackDelay, null); + } + + static ErrorStatus retry(final int callbackDelay, final HandlerErrorCode handlerErrorCode) { + return new RetryErrorStatus(OperationStatus.IN_PROGRESS, callbackDelay, handlerErrorCode); } static ErrorStatus conditional(Function condition) { diff --git a/aws-rds-cfn-common/src/main/java/software/amazon/rds/common/error/RetryErrorStatus.java b/aws-rds-cfn-common/src/main/java/software/amazon/rds/common/error/RetryErrorStatus.java index a3e692d11..e33d1fb5d 100644 --- a/aws-rds-cfn-common/src/main/java/software/amazon/rds/common/error/RetryErrorStatus.java +++ b/aws-rds-cfn-common/src/main/java/software/amazon/rds/common/error/RetryErrorStatus.java @@ -1,6 +1,7 @@ package software.amazon.rds.common.error; import lombok.Getter; +import software.amazon.cloudformation.proxy.HandlerErrorCode; import software.amazon.cloudformation.proxy.OperationStatus; public class RetryErrorStatus implements ErrorStatus { @@ -10,8 +11,12 @@ public class RetryErrorStatus implements ErrorStatus { @Getter private final int callbackDelay; - public RetryErrorStatus(final OperationStatus status, int callbackDelay) { + @Getter + HandlerErrorCode handlerErrorCode; + + public RetryErrorStatus(final OperationStatus status, int callbackDelay, final HandlerErrorCode handlerErrorCode) { this.status = status; this.callbackDelay = callbackDelay; + this.handlerErrorCode = handlerErrorCode; } } diff --git a/aws-rds-cfn-common/src/main/java/software/amazon/rds/common/handler/Commons.java b/aws-rds-cfn-common/src/main/java/software/amazon/rds/common/handler/Commons.java index 0e727561b..17adc218a 100644 --- a/aws-rds-cfn-common/src/main/java/software/amazon/rds/common/handler/Commons.java +++ b/aws-rds-cfn-common/src/main/java/software/amazon/rds/common/handler/Commons.java @@ -12,6 +12,7 @@ import software.amazon.awssdk.core.exception.SdkServiceException; import software.amazon.awssdk.services.rds.model.KmsKeyNotAccessibleException; import software.amazon.cloudformation.proxy.HandlerErrorCode; +import software.amazon.cloudformation.proxy.OperationStatus; import software.amazon.cloudformation.proxy.ProgressEvent; import software.amazon.cloudformation.resource.ResourceTypeSchema; import software.amazon.rds.common.error.ErrorRuleSet; @@ -54,6 +55,14 @@ public final class Commons { SdkClientException.class) .build(); + public static final ErrorRuleSet ACCESS_DENIED_RULE_SET = ErrorRuleSet.extend(ErrorRuleSet.EMPTY_RULE_SET) + .withErrorCodes(ErrorStatus.failWith(HandlerErrorCode.AccessDenied), + ErrorCode.AccessDenied, + ErrorCode.AccessDeniedException, + ErrorCode.NotAuthorized, + ErrorCode.UnauthorizedOperation) + .build(); + private Commons() { } @@ -85,7 +94,17 @@ public static ProgressEvent handleException( } } else if (errorStatus instanceof RetryErrorStatus) { RetryErrorStatus retryErrorStatus = (RetryErrorStatus) errorStatus; - return ProgressEvent.defaultInProgressHandler(context, retryErrorStatus.getCallbackDelay(), model); + if (retryErrorStatus.getHandlerErrorCode() == null) { + return ProgressEvent.defaultInProgressHandler(context, retryErrorStatus.getCallbackDelay(), model); + } else { + return ProgressEvent.builder() + .callbackContext(context) + .resourceModel(model) + .errorCode(retryErrorStatus.getHandlerErrorCode()) + .callbackDelaySeconds(retryErrorStatus.getCallbackDelay()) + .status(OperationStatus.IN_PROGRESS) + .build(); + } } else if (errorStatus instanceof HandlerErrorStatus) { final HandlerErrorStatus handlerErrorStatus = (HandlerErrorStatus) errorStatus; // We need to set model and context to null in case of AlreadyExists errors diff --git a/aws-rds-cfn-common/src/main/java/software/amazon/rds/common/handler/Vpc.java b/aws-rds-cfn-common/src/main/java/software/amazon/rds/common/handler/Vpc.java new file mode 100644 index 000000000..abd65283e --- /dev/null +++ b/aws-rds-cfn-common/src/main/java/software/amazon/rds/common/handler/Vpc.java @@ -0,0 +1,28 @@ +package software.amazon.rds.common.handler; + +import com.amazonaws.util.CollectionUtils; + +import java.util.List; + +public final class Vpc { + + public Vpc() { + } + + /** + * Is very important that we never reset to default VPC when both previous and desired security group is null, + * or we would be potentially doing a modification that is not clearly intended from the model. + */ + public static boolean shouldSetDefaultVpcId( + final List previousResourceStateVPCSecurityGroups, + final List desiredResourceStateVPCSecurityGroups + ) { + if (!CollectionUtils.isNullOrEmpty(previousResourceStateVPCSecurityGroups) && CollectionUtils.isNullOrEmpty(desiredResourceStateVPCSecurityGroups)) { + // The only condition when we should update the default VPC is when the model is unsetting the securityGroup, and never in any other condition. + // For example, when a customer import an existing resource (DBInstance or DBCluster), we should never change the VPC security groups regardless + // of what the model says, because is not intuitive from the model definition that we are changing to a default VPC + return true; + } + return false; + } +} diff --git a/aws-rds-cfn-common/src/main/java/software/amazon/rds/common/request/ValidatedRequest.java b/aws-rds-cfn-common/src/main/java/software/amazon/rds/common/request/ValidatedRequest.java index b71931d11..0a97450cd 100644 --- a/aws-rds-cfn-common/src/main/java/software/amazon/rds/common/request/ValidatedRequest.java +++ b/aws-rds-cfn-common/src/main/java/software/amazon/rds/common/request/ValidatedRequest.java @@ -23,6 +23,8 @@ public ValidatedRequest(final ResourceHandlerRequest base) { base.getUpdatePolicy(), base.getCreationPolicy(), base.getRegion(), - base.getStackId()); + base.getStackId(), + base.getMaxResults() + ); } } diff --git a/aws-rds-cfn-common/src/main/java/software/amazon/rds/common/util/ArnHelper.java b/aws-rds-cfn-common/src/main/java/software/amazon/rds/common/util/ArnHelper.java new file mode 100644 index 000000000..006485a18 --- /dev/null +++ b/aws-rds-cfn-common/src/main/java/software/amazon/rds/common/util/ArnHelper.java @@ -0,0 +1,67 @@ +package software.amazon.rds.common.util; + +import com.amazonaws.arn.Arn; +import com.amazonaws.arn.ArnResource; +import com.amazonaws.util.StringUtils; + + +public final class ArnHelper { + private static String RDS_SERVICE = "rds"; + public enum ResourceType { + DB_INSTANCE_SNAPSHOT("snapshot"), + DB_CLUSTER_SNAPSHOT("cluster-snapshot"); + + private String value; + + ResourceType(String value) { + this.value = value; + } + + public static ResourceType fromString(final String resourceString) { + if (!StringUtils.isNullOrEmpty(resourceString)) { + for (final ResourceType type : ResourceType.values()) { + if (type.value.equals(resourceString)) { + return type; + } + } + } + return null; + } + } + + public static boolean isValidArn(final String arn) { + if (StringUtils.isNullOrEmpty(arn)) { + return false; + } + try { + Arn.fromString(arn); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } + + public static String getRegionFromArn(final String arn) { + return Arn.fromString(arn).getRegion(); + } + + public static String getResourceNameFromArn(final String arn) { + return Arn.fromString(arn).getResource().getResource(); + } + + public static String getAccountIdFromArn(final String arn) { + return Arn.fromString(arn).getAccountId(); + } + + public static ResourceType getResourceType(String potentialArn) { + if (isValidArn(potentialArn)) { + final Arn arn = Arn.fromString(potentialArn); + if (!RDS_SERVICE.equalsIgnoreCase(arn.getService())) { + return null; + } + final ArnResource resource = arn.getResource(); + return ResourceType.fromString(resource.getResourceType()); + } + return null; + } +} diff --git a/aws-rds-cfn-common/src/main/java/software/amazon/rds/common/util/IdempotencyHelper.java b/aws-rds-cfn-common/src/main/java/software/amazon/rds/common/util/IdempotencyHelper.java new file mode 100644 index 000000000..53dc17ee8 --- /dev/null +++ b/aws-rds-cfn-common/src/main/java/software/amazon/rds/common/util/IdempotencyHelper.java @@ -0,0 +1,87 @@ +package software.amazon.rds.common.util; + +import java.util.function.Function; +import java.util.function.UnaryOperator; + +import com.google.common.annotations.VisibleForTesting; +import software.amazon.cloudformation.exceptions.CfnAlreadyExistsException; +import software.amazon.cloudformation.exceptions.CfnNotFoundException; +import software.amazon.cloudformation.proxy.HandlerErrorCode; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.rds.common.logging.RequestLogger; + +/** + * Utility class containing functions to handle non-idempotent APIs. + */ +public final class IdempotencyHelper { + + private static boolean bypass = false; + + @VisibleForTesting + public static void setBypass(boolean bypass) { + IdempotencyHelper.bypass = bypass; + } + + /** + * Wraps a non-idempotent create operation to prevent spurious "already exists" errors. + * + * @param checkExistenceFunction a function that checks if the resource exists and returns the resource if it does, + * or if it does not, then null or a CfnNotFoundException. + * @param createFunction the non-idempotent create operation + * @param resourceTypeName the resource type name (used to form the error message) + * @param resourceIdentifier the resource identifier (used to form the error message) + */ + public static ProgressEvent safeCreate( + final Function checkExistenceFunction, + final UnaryOperator> createFunction, + final String resourceTypeName, + final String resourceIdentifier, + final ProgressEvent progress, + final RequestLogger requestLogger + ) { + // The approach recommended by CloudFormation is as follows: + // - First, check whether the requested resource already exists. If it does, then fail immediately. Otherwise, + // return IN_PROGRESS with a non-zero callback delay. This forces the handler to return back to CFN, which + // will persist the status so that the pre-existence check is not repeated in case the next step fails. + // - Then, perform the create operation. If it returns an AlreadyExists error, then ignore it. + + if (bypass) { + return createFunction.apply(progress); + } + + final var preExistenceCheckDone = progress.getCallbackContext().getPreExistenceCheckDone(); + if (preExistenceCheckDone == null || !preExistenceCheckDone) { + try { + final var existingResource = checkExistenceFunction.apply(progress.getResourceModel()); + if (existingResource != null) { + requestLogger.log("Resource already exists"); + throw new CfnAlreadyExistsException(resourceTypeName, resourceIdentifier); + } + } catch (final CfnNotFoundException ignored) { + requestLogger.log("CfnNotFoundException thrown during pre-existence check (all good)"); + } + + progress.getCallbackContext().setPreExistenceCheckDone(true); + + // !!!: The callbackDelaySeconds of 1 is important here. (See canContinueProgress in ProgressEvent) + return ProgressEvent.defaultInProgressHandler(progress.getCallbackContext(), 1, + progress.getResourceModel()); + } else { + final var result = createFunction.apply(progress); + if (result.isFailed() && result.getErrorCode() == HandlerErrorCode.AlreadyExists) { + requestLogger.log("Ignoring AlreadyExists error from create operation"); + return ProgressEvent.defaultInProgressHandler(progress.getCallbackContext(), 0, + progress.getResourceModel()); + } else { + return result; + } + } + } + + public interface PreExistenceContext { + Boolean getPreExistenceCheckDone(); + + void setPreExistenceCheckDone(Boolean preExistenceCheckDone); + } + +} diff --git a/aws-rds-cfn-common/src/main/java/software/amazon/rds/common/util/WaiterHelper.java b/aws-rds-cfn-common/src/main/java/software/amazon/rds/common/util/WaiterHelper.java new file mode 100644 index 000000000..12e0f73c1 --- /dev/null +++ b/aws-rds-cfn-common/src/main/java/software/amazon/rds/common/util/WaiterHelper.java @@ -0,0 +1,35 @@ +package software.amazon.rds.common.util; + +import software.amazon.cloudformation.proxy.ProgressEvent; + +public class WaiterHelper { + /** + * A function which introduces artificial delay during any ProgressEvent, usually used in Aurora codepaths, for example + * there might be asynchronous workflows running to update resources after the resource has become available and which + * we don't have a valid status to stabilise on + * + * This function will wait for callbackDelay, return the progress event and allow the handler + * to be reinvoke itself and keep retrying until the maxTimeSeconds is breached + * + * @param evt ProgressEvent of the handler + * @param maxSeconds The max total time we will wait + * @param pollSeconds Wait time before the next invocation of the handler, until we reach maxSeconds + * @return ProgressEvent + * @param The generic resource model + * @param The generic callback + */ + public static ProgressEvent delay(final ProgressEvent evt, final int maxSeconds, final int pollSeconds) { + final CallbackT callbackContext = evt.getCallbackContext(); + if (callbackContext.getWaitTime() <= maxSeconds) { + callbackContext.setWaitTime(callbackContext.getWaitTime() + pollSeconds); + return ProgressEvent.defaultInProgressHandler(callbackContext, pollSeconds, evt.getResourceModel()); + } else { + return ProgressEvent.progress(evt.getResourceModel(), callbackContext); + } + } + + public interface DelayContext { + int getWaitTime(); + void setWaitTime(int waitTime); + } +} diff --git a/aws-rds-cfn-common/src/main/java/software/amazon/rds/common/validation/ValidationAccessException.java b/aws-rds-cfn-common/src/main/java/software/amazon/rds/common/validation/ValidationAccessException.java new file mode 100644 index 000000000..da6257e08 --- /dev/null +++ b/aws-rds-cfn-common/src/main/java/software/amazon/rds/common/validation/ValidationAccessException.java @@ -0,0 +1,6 @@ +package software.amazon.rds.common.validation; + +@SuppressWarnings("serial") +public class ValidationAccessException extends Exception { + public ValidationAccessException(final String message) { super(message); } +} diff --git a/aws-rds-cfn-common/src/main/java/software/amazon/rds/common/validation/ValidationUtils.java b/aws-rds-cfn-common/src/main/java/software/amazon/rds/common/validation/ValidationUtils.java new file mode 100644 index 000000000..e43c585d1 --- /dev/null +++ b/aws-rds-cfn-common/src/main/java/software/amazon/rds/common/validation/ValidationUtils.java @@ -0,0 +1,30 @@ +package software.amazon.rds.common.validation; + +import com.google.common.collect.ImmutableMap; +import software.amazon.cloudformation.proxy.HandlerErrorCode; +import software.amazon.rds.common.error.ErrorStatus; +import software.amazon.rds.common.error.HandlerErrorStatus; +import software.amazon.rds.common.handler.Commons; +import software.amazon.rds.common.logging.RequestLogger; + +import java.util.function.Supplier; + +public class ValidationUtils { + private final static String MISSING_PERMISSION = "MissingPermission"; + public static T fetchResourceForValidation(Supplier supplier, String requiredPermission) throws ValidationAccessException { + try { + return supplier.get(); + } catch (Exception ex) { + final ErrorStatus error = Commons.ACCESS_DENIED_RULE_SET.handle(ex); + if (error instanceof HandlerErrorStatus && + ((HandlerErrorStatus)error).getHandlerErrorCode() == HandlerErrorCode.AccessDenied ){ + throw new ValidationAccessException(requiredPermission); + } + throw ex; + } + } + + public static void emitMetric(final RequestLogger logger, final String validationMetric, ValidationAccessException ex) { + logger.log(validationMetric, ImmutableMap.of(MISSING_PERMISSION, ex.getMessage())); + } +} diff --git a/aws-rds-cfn-common/src/test/java/software/amazon/rds/common/handler/VpcTest.java b/aws-rds-cfn-common/src/test/java/software/amazon/rds/common/handler/VpcTest.java new file mode 100644 index 000000000..8d15edee7 --- /dev/null +++ b/aws-rds-cfn-common/src/test/java/software/amazon/rds/common/handler/VpcTest.java @@ -0,0 +1,76 @@ +package software.amazon.rds.common.handler; + +import org.junit.jupiter.api.Test; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class VpcTest { + + + @Test + void shouldSetDefaultVpcId_previousNonEmpty_desiredNull_returnsTrue() { + List previous = Arrays.asList("sg-123", "sg-456"); + List desired = null; + assertTrue(Vpc.shouldSetDefaultVpcId(previous, desired)); + } + + @Test + void shouldSetDefaultVpcId_previousNonEmpty_desiredEmpty_returnsTrue() { + List previous = Arrays.asList("sg-123"); + List desired = Collections.emptyList(); + assertTrue(Vpc.shouldSetDefaultVpcId(previous, desired)); + } + + @Test + void shouldSetDefaultVpcId_previousNull_desiredNull_returnsFalse() { + List previous = null; + List desired = null; + assertFalse(Vpc.shouldSetDefaultVpcId(previous, desired)); + } + + @Test + void shouldSetDefaultVpcId_previousEmpty_desiredNull_returnsFalse() { + List previous = Collections.emptyList(); + List desired = null; + assertFalse(Vpc.shouldSetDefaultVpcId(previous, desired)); + } + + @Test + void shouldSetDefaultVpcId_previousNull_desiredEmpty_returnsFalse() { + List previous = null; + List desired = Collections.emptyList(); + assertFalse(Vpc.shouldSetDefaultVpcId(previous, desired)); + } + + @Test + void shouldSetDefaultVpcId_previousEmpty_desiredEmpty_returnsFalse() { + List previous = Collections.emptyList(); + List desired = Collections.emptyList(); + assertFalse(Vpc.shouldSetDefaultVpcId(previous, desired)); + } + + @Test + void shouldSetDefaultVpcId_previousNonEmpty_desiredNonEmpty_returnsFalse() { + List previous = Arrays.asList("sg-123"); + List desired = Arrays.asList("sg-789"); + assertFalse(Vpc.shouldSetDefaultVpcId(previous, desired)); + } + + @Test + void shouldSetDefaultVpcId_previousNull_desiredNonEmpty_returnsFalse() { + List previous = null; + List desired = Arrays.asList("sg-789"); + assertFalse(Vpc.shouldSetDefaultVpcId(previous, desired)); + } + + @Test + void shouldSetDefaultVpcId_previousEmpty_desiredNonEmpty_returnsFalse() { + List previous = Collections.emptyList(); + List desired = Arrays.asList("sg-789"); + assertFalse(Vpc.shouldSetDefaultVpcId(previous, desired)); + } +} diff --git a/aws-rds-cfn-common/src/test/java/software/amazon/rds/common/util/ArnHelperTests.java b/aws-rds-cfn-common/src/test/java/software/amazon/rds/common/util/ArnHelperTests.java new file mode 100644 index 000000000..b8293cf0c --- /dev/null +++ b/aws-rds-cfn-common/src/test/java/software/amazon/rds/common/util/ArnHelperTests.java @@ -0,0 +1,47 @@ +package software.amazon.rds.common.util; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +public class ArnHelperTests { + @Test + public void isValidArn_returnsFalseWhenNull() { + assertThat(ArnHelper.isValidArn(null)).isFalse(); + } + + @Test + public void isValidArn_returnsFalseWhenInvalid() { + assertThat(ArnHelper.isValidArn("invalid")).isFalse(); + } + + @Test + public void isValidArn_returnsTrueWhenValid() { + assertThat(ArnHelper.isValidArn("arn:aws:rds:us-east-1:1234567890:cluster-snapshot:mysnapshot")).isTrue(); + } + + @Test + public void getResourceType_returnsClusterSnapshot() { + assertThat(ArnHelper.getResourceType("arn:aws:rds:us-east-1:1234567890:cluster-snapshot:mysnapshot")).isEqualTo(ArnHelper.ResourceType.DB_CLUSTER_SNAPSHOT); + } + + @Test + public void getResourceType_returnsNullForNonRdsService() { + assertThat(ArnHelper.getResourceType("arn:aws:someservice:us-east-1:1234567890:cluster-snapshot:mysnapshot")).isNull(); + } + + @Test + public void getResourceType_returnsInstanceSnapshot() { + assertThat(ArnHelper.getResourceType("arn:aws:rds:us-east-1:1234567890:snapshot:mysnapshot")).isEqualTo(ArnHelper.ResourceType.DB_INSTANCE_SNAPSHOT); + } + + @Test + public void getRegionFromArn_returnsRegion() { + assertThat(ArnHelper.getRegionFromArn("arn:aws:rds:us-east-1:1234567890:snapshot:mysnapshot")).isEqualTo("us-east-1"); + } + + @Test + public void getResourceName_returnsResourceName() { + assertThat(ArnHelper.getResourceNameFromArn("arn:aws:rds:us-east-1:1234567890:snapshot:mysnapshot")).isEqualTo("mysnapshot"); + } +} diff --git a/aws-rds-cfn-common/src/test/java/software/amazon/rds/common/util/IdempotencyHelperTest.java b/aws-rds-cfn-common/src/test/java/software/amazon/rds/common/util/IdempotencyHelperTest.java new file mode 100644 index 000000000..288fd75fd --- /dev/null +++ b/aws-rds-cfn-common/src/test/java/software/amazon/rds/common/util/IdempotencyHelperTest.java @@ -0,0 +1,121 @@ +package software.amazon.rds.common.util; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mockito; +import software.amazon.cloudformation.exceptions.CfnAlreadyExistsException; +import software.amazon.cloudformation.exceptions.CfnNotFoundException; +import software.amazon.cloudformation.proxy.HandlerErrorCode; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.rds.common.logging.RequestLogger; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; + +class IdempotencyHelperTest { + private static final String MODEL = "blah"; + + private RequestLogger mockRequestLogger = Mockito.mock(RequestLogger.class); + private Context context = new Context(); + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void safeCreate_happy(boolean existenceCheckException) { + AtomicBoolean checkedExistence = new AtomicBoolean(false); + AtomicBoolean created = new AtomicBoolean(false); + + final var afterExistenceCheck = IdempotencyHelper.safeCreate( + model -> { + checkedExistence.set(true); + Assertions.assertThat(model).isSameAs(MODEL); + if (existenceCheckException) { + throw new CfnNotFoundException(new RuntimeException()); + } else { + return null; + } + }, + null, + "resourceTypeName", + "resourceIdentifier", + ProgressEvent.progress(MODEL, context), + mockRequestLogger + ); + + Assertions.assertThat(checkedExistence).isTrue(); + Assertions.assertThat(afterExistenceCheck.isInProgressCallbackDelay()).isTrue(); + Assertions.assertThat(afterExistenceCheck.getCallbackContext().getPreExistenceCheckDone()).isTrue(); + + final var afterCreate = IdempotencyHelper.safeCreate( + null, + p -> { + created.set(true); + return ProgressEvent.defaultInProgressHandler(p.getCallbackContext(), 0, p.getResourceModel()); + }, + "resourceTypeName", + "resourceIdentifier", + ProgressEvent.progress(MODEL, context), + mockRequestLogger + ); + + Assertions.assertThat(created).isTrue(); + Assertions.assertThat(afterCreate.canContinueProgress()).isTrue(); + Assertions.assertThat(afterCreate.getResourceModel()).isSameAs(MODEL); + Assertions.assertThat(afterCreate.getCallbackContext()).isSameAs(context); + } + + @Test + void safeCreate_pre_already_exists() { + Assertions.assertThatThrownBy(() -> { + IdempotencyHelper.safeCreate( + Function.identity(), + null, + "resourceTypeName", + "resourceIdentifier", + ProgressEvent.progress(MODEL, context), + mockRequestLogger + ); + }).isInstanceOf(CfnAlreadyExistsException.class); + } + + @Test + void safeCreate_failed_create() { + context.setPreExistenceCheckDone(true); + + final var afterCreate = IdempotencyHelper.safeCreate( + null, + p -> ProgressEvent.defaultFailureHandler(new RuntimeException(), HandlerErrorCode.InternalFailure), + "resourceTypeName", + "resourceIdentifier", + ProgressEvent.progress(MODEL, context), + mockRequestLogger + ); + + Assertions.assertThat(afterCreate.isFailed()).isTrue(); + Assertions.assertThat(afterCreate.getErrorCode()).isEqualTo(HandlerErrorCode.InternalFailure); + } + + @Test + void safeCreate_create_already_exists_after_pre_existence_check() { + context.setPreExistenceCheckDone(true); + + final var afterCreate = IdempotencyHelper.safeCreate( + null, + p -> ProgressEvent.defaultFailureHandler(new RuntimeException(), HandlerErrorCode.AlreadyExists), + "resourceTypeName", + "resourceIdentifier", + ProgressEvent.progress(MODEL, context), + mockRequestLogger + ); + + Assertions.assertThat(afterCreate.canContinueProgress()).isTrue(); + Assertions.assertThat(afterCreate.getResourceModel()).isSameAs(MODEL); + Assertions.assertThat(afterCreate.getCallbackContext()).isSameAs(context); + } + + @lombok.Data + private static class Context implements IdempotencyHelper.PreExistenceContext { + private Boolean preExistenceCheckDone; + } +} diff --git a/aws-rds-cfn-test-common/pom.xml b/aws-rds-cfn-test-common/pom.xml index 4dd596625..f442918ef 100644 --- a/aws-rds-cfn-test-common/pom.xml +++ b/aws-rds-cfn-test-common/pom.xml @@ -167,7 +167,7 @@ - PACKAGE + BUNDLE BRANCH @@ -177,7 +177,7 @@ INSTRUCTION COVEREDRATIO - 0.8 + 0.0 diff --git a/aws-rds-cfn-test-common/src/main/java/software/amazon/rds/test/common/core/AbstractTestBase.java b/aws-rds-cfn-test-common/src/main/java/software/amazon/rds/test/common/core/AbstractTestBase.java index 1908f5e4f..df4e7c401 100644 --- a/aws-rds-cfn-test-common/src/main/java/software/amazon/rds/test/common/core/AbstractTestBase.java +++ b/aws-rds-cfn-test-common/src/main/java/software/amazon/rds/test/common/core/AbstractTestBase.java @@ -9,6 +9,7 @@ import software.amazon.awssdk.awscore.AwsResponse; import software.amazon.awssdk.awscore.exception.AwsErrorDetails; import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.cloudformation.exceptions.BaseHandlerException; import software.amazon.cloudformation.proxy.HandlerErrorCode; import software.amazon.cloudformation.proxy.OperationStatus; import software.amazon.cloudformation.proxy.ProgressEvent; @@ -32,13 +33,17 @@ protected String newStackId() { } protected Consumer> expectInProgress(int pause) { + return expectInProgress(pause, null); + } + + protected Consumer> expectInProgress(int pause, final HandlerErrorCode errorCode) { return (response) -> { Assertions.assertThat(response).isNotNull(); Assertions.assertThat(response.getStatus()).isEqualTo(OperationStatus.IN_PROGRESS); Assertions.assertThat(response.getCallbackDelaySeconds()).isEqualTo(pause); Assertions.assertThat(response.getResourceModels()).isNull(); Assertions.assertThat(response.getMessage()).isNull(); - Assertions.assertThat(response.getErrorCode()).isNull(); + Assertions.assertThat(response.getErrorCode()).isEqualTo(errorCode); }; } @@ -109,7 +114,12 @@ protected ProgressEvent test_handleRequest_base( builder.clientRequestToken(newClientRequestToken()); builder.stackId(newStackId()); - final ProgressEvent response = invokeHandleRequest(builder.build(), context); + ProgressEvent response; + try { + response = invokeHandleRequest(builder.build(), context); + } catch (final BaseHandlerException e) { + response = ProgressEvent.defaultFailureHandler(e, e.getErrorCode()); + } expect.accept(response); return response; @@ -164,4 +174,39 @@ protected void test ); expectation.verify(); } + + @ExcludeFromJacocoGeneratedReport + protected void test_handleRequest_throttle( + final MethodCallExpectation expectation, + final ContextT context, + final Supplier desiredStateSupplier, + final Object requestException, + final int callbackDelay + ) { + test_handleRequest_throttle(expectation, context, null, desiredStateSupplier, requestException, callbackDelay); + } + + @ExcludeFromJacocoGeneratedReport + protected void test_handleRequest_throttle( + final MethodCallExpectation expectation, + final ContextT context, + final Supplier previousStateSupplier, + final Supplier desiredStateSupplier, + final Object requestException, + final int callbackDelay + ) { + final Exception exception = requestException instanceof Exception ? (Exception) requestException : newAwsServiceException(requestException); + + expectation.setup() + .thenThrow(exception); + + test_handleRequest_base( + context, + null, + previousStateSupplier, + desiredStateSupplier, + expectInProgress(callbackDelay, HandlerErrorCode.Throttling) + ); + expectation.verify(); + } } diff --git a/aws-rds-customdbengineversion/aws-rds-customdbengineversion.json b/aws-rds-customdbengineversion/aws-rds-customdbengineversion.json index 8017a2c16..202e20f6c 100644 --- a/aws-rds-customdbengineversion/aws-rds-customdbengineversion.json +++ b/aws-rds-customdbengineversion/aws-rds-customdbengineversion.json @@ -120,7 +120,7 @@ "propertyTransform": { "/properties/Engine": "$lowercase(Engine)", "/properties/EngineVersion": "$lowercase(EngineVersion)", - "/properties/KMSKeyId": "$join([\"arn:(aws)[-]{0,1}[a-z]{0,2}[-]{0,1}[a-z]{0,3}:kms:[a-z]{2}[-]{1}[a-z]{3,10}[-]{0,1}[a-z]{0,10}[-]{1}[1-3]{1}:[0-9]{12}[:]{1}key\\/\", KMSKeyId])" + "/properties/KMSKeyId": "$join([\"arn:.+?:kms:.+?:.+?:key\\/\", KMSKeyId])" }, "required": [ "Engine", diff --git a/aws-rds-customdbengineversion/docs/README.md b/aws-rds-customdbengineversion/docs/README.md deleted file mode 100644 index e316cd925..000000000 --- a/aws-rds-customdbengineversion/docs/README.md +++ /dev/null @@ -1,213 +0,0 @@ -# AWS::RDS::CustomDBEngineVersion - -The AWS::RDS::CustomDBEngineVersion resource creates an Amazon RDS custom DB engine version. - -## Syntax - -To declare this entity in your AWS CloudFormation template, use the following syntax: - -### JSON - -
-{
-    "Type" : "AWS::RDS::CustomDBEngineVersion",
-    "Properties" : {
-        "DatabaseInstallationFilesS3BucketName" : String,
-        "DatabaseInstallationFilesS3Prefix" : String,
-        "Description" : String,
-        "Engine" : String,
-        "EngineVersion" : String,
-        "KMSKeyId" : String,
-        "Manifest" : String,
-        "SourceCustomDbEngineVersionIdentifier" : String,
-        "UseAwsProvidedLatestImage" : Boolean,
-        "ImageId" : String,
-        "Status" : String,
-        "Tags" : [ Tag, ... ]
-    }
-}
-
- -### YAML - -
-Type: AWS::RDS::CustomDBEngineVersion
-Properties:
-    DatabaseInstallationFilesS3BucketName: String
-    DatabaseInstallationFilesS3Prefix: String
-    Description: String
-    Engine: String
-    EngineVersion: String
-    KMSKeyId: String
-    Manifest: String
-    SourceCustomDbEngineVersionIdentifier: String
-    UseAwsProvidedLatestImage: Boolean
-    ImageId: String
-    Status: String
-    Tags: 
-      - Tag
-
- -## Properties - -#### DatabaseInstallationFilesS3BucketName - -The name of an Amazon S3 bucket that contains database installation files for your CEV. For example, a valid bucket name is `my-custom-installation-files`. - -_Required_: No - -_Type_: String - -_Minimum Length_: 3 - -_Maximum Length_: 63 - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### DatabaseInstallationFilesS3Prefix - -The Amazon S3 directory that contains the database installation files for your CEV. For example, a valid bucket name is `123456789012/cev1`. If this setting isn't specified, no prefix is assumed. - -_Required_: No - -_Type_: String - -_Minimum Length_: 1 - -_Maximum Length_: 255 - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### Description - -An optional description of your CEV. - -_Required_: No - -_Type_: String - -_Minimum Length_: 1 - -_Maximum Length_: 1000 - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### Engine - -The database engine to use for your custom engine version (CEV). The only supported value is `custom-oracle-ee`. - -_Required_: Yes - -_Type_: String - -_Minimum Length_: 1 - -_Maximum Length_: 35 - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### EngineVersion - -The name of your CEV. The name format is 19.customized_string . For example, a valid name is 19.my_cev1. This setting is required for RDS Custom for Oracle, but optional for Amazon RDS. The combination of Engine and EngineVersion is unique per customer per Region. - -_Required_: Yes - -_Type_: String - -_Minimum Length_: 1 - -_Maximum Length_: 60 - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### KMSKeyId - -The AWS KMS key identifier for an encrypted CEV. A symmetric KMS key is required for RDS Custom, but optional for Amazon RDS. - -_Required_: No - -_Type_: String - -_Minimum Length_: 1 - -_Maximum Length_: 2048 - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### Manifest - -The CEV manifest, which is a JSON document that describes the installation .zip files stored in Amazon S3. Specify the name/value pairs in a file or a quoted string. RDS Custom applies the patches in the order in which they are listed. - -_Required_: No - -_Type_: String - -_Minimum Length_: 1 - -_Maximum Length_: 51000 - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### SourceCustomDbEngineVersionIdentifier - -The identifier of the source custom engine version. - -_Required_: No - -_Type_: String - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### UseAwsProvidedLatestImage - -A value that indicates whether AWS provided latest image is applied automatically to the Custom Engine Version. By default, AWS provided latest image is applied automatically. This value is only applied on create. - -_Required_: No - -_Type_: Boolean - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### ImageId - -The identifier of Amazon Machine Image (AMI) used for CEV. - -_Required_: No - -_Type_: String - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### Status - -The availability status to be assigned to the CEV. - -_Required_: No - -_Type_: String - -_Allowed Values_: available | inactive | inactive-except-restore - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### Tags - -An array of key-value pairs to apply to this resource. - -_Required_: No - -_Type_: List of Tag - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -## Return Values - -### Fn::GetAtt - -The `Fn::GetAtt` intrinsic function returns a value for a specified attribute of this type. The following are the available attributes and sample return values. - -For more information about using the `Fn::GetAtt` intrinsic function, see [Fn::GetAtt](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html). - -#### DBEngineVersionArn - -The ARN of the custom engine version. diff --git a/aws-rds-customdbengineversion/docs/tag.md b/aws-rds-customdbengineversion/docs/tag.md deleted file mode 100644 index c89794a01..000000000 --- a/aws-rds-customdbengineversion/docs/tag.md +++ /dev/null @@ -1,51 +0,0 @@ -# AWS::RDS::CustomDBEngineVersion Tag - -A key-value pair to associate with a resource. - -## Syntax - -To declare this entity in your AWS CloudFormation template, use the following syntax: - -### JSON - -
-{
-    "Key" : String,
-    "Value" : String
-}
-
- -### YAML - -
-Key: String
-Value: String
-
- -## Properties - -#### Key - -The key name of the tag. You can specify a value that is 1 to 128 Unicode characters in length and cannot be prefixed with aws:. You can use any of the following characters: the set of Unicode letters, digits, whitespace, _, ., /, =, +, and -. - -_Required_: Yes - -_Type_: String - -_Minimum Length_: 1 - -_Maximum Length_: 128 - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### Value - -The value for the tag. You can specify a value that is 0 to 256 Unicode characters in length and cannot be prefixed with aws:. You can use any of the following characters: the set of Unicode letters, digits, whitespace, _, ., /, =, +, and -. - -_Required_: No - -_Type_: String - -_Maximum Length_: 256 - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) diff --git a/aws-rds-customdbengineversion/pom.xml b/aws-rds-customdbengineversion/pom.xml index bff562778..26de1008c 100644 --- a/aws-rds-customdbengineversion/pom.xml +++ b/aws-rds-customdbengineversion/pom.xml @@ -20,11 +20,6 @@ - - software.amazon.awssdk - rds - 2.25.56 - software.amazon.rds.common aws-rds-cfn-common @@ -37,12 +32,6 @@ 1.0 test - - - software.amazon.cloudformation - aws-cloudformation-rpdk-java-plugin - [2.0.0,3.0.0) - org.projectlombok @@ -50,12 +39,6 @@ 1.18.22 provided - - - software.amazon.awssdk - aws-query-protocol - 2.20.138 - org.assertj @@ -168,10 +151,27 @@ 0.8.8 - **/BaseConfiguration* - **/BaseHandler* - **/HandlerWrapper* - **/ResourceModel* + + **/software/amazon/rds/customdbengineversion/BaseConfiguration.class + **/software/amazon/rds/customdbengineversion/BaseHandler.class + **/software/amazon/rds/customdbengineversion/BaseHandlerStd.class + **/software/amazon/rds/customdbengineversion/CallbackContext.class + **/software/amazon/rds/customdbengineversion/ClientProvider.class + **/software/amazon/rds/customdbengineversion/Configuration.class + **/software/amazon/rds/customdbengineversion/CreateHandler.class + **/software/amazon/rds/customdbengineversion/CustomDBEngineVersionStatus.class + **/software/amazon/rds/customdbengineversion/DeleteHandler.class + **/software/amazon/rds/customdbengineversion/HandlerWrapper.class + **/software/amazon/rds/customdbengineversion/HandlerWrapperExecutable.class + **/software/amazon/rds/customdbengineversion/ListHandler.class + **/software/amazon/rds/customdbengineversion/ReadHandler.class + **/software/amazon/rds/customdbengineversion/ResourceModel.class + **/software/amazon/rds/customdbengineversion/StatusOption.class + **/software/amazon/rds/customdbengineversion/Tag.class + **/software/amazon/rds/customdbengineversion/Translator.class + **/software/amazon/rds/customdbengineversion/TypeConfigurationModel.class + **/software/amazon/rds/customdbengineversion/UpdateHandler.class + @@ -195,7 +195,7 @@ - PACKAGE + BUNDLE BRANCH @@ -205,7 +205,7 @@ INSTRUCTION COVEREDRATIO - 0.8 + 0.0 diff --git a/aws-rds-customdbengineversion/src/main/java/software/amazon/rds/customdbengineversion/BaseHandlerStd.java b/aws-rds-customdbengineversion/src/main/java/software/amazon/rds/customdbengineversion/BaseHandlerStd.java index a41303db1..542b617f4 100644 --- a/aws-rds-customdbengineversion/src/main/java/software/amazon/rds/customdbengineversion/BaseHandlerStd.java +++ b/aws-rds-customdbengineversion/src/main/java/software/amazon/rds/customdbengineversion/BaseHandlerStd.java @@ -16,6 +16,7 @@ import software.amazon.awssdk.services.rds.model.InvalidS3BucketException; import software.amazon.awssdk.services.rds.model.KmsKeyNotAccessibleException; import software.amazon.awssdk.utils.StringUtils; +import software.amazon.cloudformation.exceptions.CfnNotFoundException; import software.amazon.cloudformation.exceptions.CfnNotStabilizedException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.HandlerErrorCode; @@ -109,7 +110,8 @@ public final ProgressEvent handleRequest( proxy, request, callbackContext != null ? callbackContext : new CallbackContext(), - new LoggingProxyClient<>(requestLogger, proxy.newProxy(new ClientProvider()::getClient)) + new LoggingProxyClient<>(requestLogger, proxy.newProxy(new ClientProvider()::getClient)), + requestLogger )); } @@ -163,15 +165,19 @@ private void resourceStabilizationTime(final CallbackContext callbackContext) { protected DBEngineVersion fetchDBEngineVersion(final ResourceModel model, final ProxyClient proxyClient) { - DescribeDbEngineVersionsResponse response = proxyClient.injectCredentialsAndInvokeV2( + try { + DescribeDbEngineVersionsResponse response = proxyClient.injectCredentialsAndInvokeV2( Translator.describeDbEngineVersionsRequest(model), proxyClient.client()::describeDBEngineVersions); - final Optional engineVersion = response - .dbEngineVersions().stream().findFirst(); + if (!response.hasDbEngineVersions() || response.dbEngineVersions().isEmpty()) { + throw new CfnNotFoundException(ResourceModel.TYPE_NAME, model.getEngineVersion()); + } - return engineVersion.orElseThrow(() -> CustomDbEngineVersionNotFoundException.builder().message( - "CustomDBEngineVersion " + model.getEngineVersion() + " not found").build()); + return response.dbEngineVersions().get(0); + } catch (CustomDbEngineVersionNotFoundException e) { + throw new CfnNotFoundException(e); + } } diff --git a/aws-rds-customdbengineversion/src/main/java/software/amazon/rds/customdbengineversion/CallbackContext.java b/aws-rds-customdbengineversion/src/main/java/software/amazon/rds/customdbengineversion/CallbackContext.java index e22b90fae..bb8f7da1b 100644 --- a/aws-rds-customdbengineversion/src/main/java/software/amazon/rds/customdbengineversion/CallbackContext.java +++ b/aws-rds-customdbengineversion/src/main/java/software/amazon/rds/customdbengineversion/CallbackContext.java @@ -3,6 +3,7 @@ import software.amazon.cloudformation.proxy.StdCallbackContext; import software.amazon.rds.common.handler.TaggingContext; import software.amazon.rds.common.handler.TimestampContext; +import software.amazon.rds.common.util.IdempotencyHelper; import java.time.Duration; import java.time.Instant; @@ -13,7 +14,9 @@ @lombok.Setter @lombok.ToString @lombok.EqualsAndHashCode(callSuper = true) -public class CallbackContext extends StdCallbackContext implements TaggingContext.Provider, TimestampContext.Provider { +public class CallbackContext extends StdCallbackContext implements TaggingContext.Provider, TimestampContext.Provider, + IdempotencyHelper.PreExistenceContext { + private Boolean preExistenceCheckDone; private boolean modified; private TaggingContext taggingContext; private Map timestamps; diff --git a/aws-rds-customdbengineversion/src/main/java/software/amazon/rds/customdbengineversion/CreateHandler.java b/aws-rds-customdbengineversion/src/main/java/software/amazon/rds/customdbengineversion/CreateHandler.java index 648a2a17e..cd203aa44 100644 --- a/aws-rds-customdbengineversion/src/main/java/software/amazon/rds/customdbengineversion/CreateHandler.java +++ b/aws-rds-customdbengineversion/src/main/java/software/amazon/rds/customdbengineversion/CreateHandler.java @@ -10,6 +10,7 @@ import software.amazon.rds.common.handler.Commons; import software.amazon.rds.common.handler.HandlerConfig; import software.amazon.rds.common.handler.Tagging; +import software.amazon.rds.common.util.IdempotencyHelper; import software.amazon.rds.common.util.IdentifierFactory; public class CreateHandler extends BaseHandlerStd { @@ -48,7 +49,14 @@ protected ProgressEvent handleRequest( .build(); return ProgressEvent.progress(model, callbackContext) - .then(progress -> safeCreateCustomEngineVersion(proxy, proxyClient, progress, allTags)) + .then(progress -> IdempotencyHelper.safeCreate( + m -> fetchDBEngineVersion(m, proxyClient), + p -> safeCreateCustomEngineVersion(proxy, proxyClient, p, allTags), + ResourceModel.TYPE_NAME, + model.getEngineVersion(), + progress, + requestLogger + )) .then(progress -> { if (shouldModifyEngineVersionAfterCreate(progress)) { return Commons.execOnce( diff --git a/aws-rds-customdbengineversion/src/main/java/software/amazon/rds/customdbengineversion/DeleteHandler.java b/aws-rds-customdbengineversion/src/main/java/software/amazon/rds/customdbengineversion/DeleteHandler.java index b5e416755..62c72dfe7 100644 --- a/aws-rds-customdbengineversion/src/main/java/software/amazon/rds/customdbengineversion/DeleteHandler.java +++ b/aws-rds-customdbengineversion/src/main/java/software/amazon/rds/customdbengineversion/DeleteHandler.java @@ -3,9 +3,8 @@ import java.util.function.Function; import software.amazon.awssdk.services.rds.RdsClient; -import software.amazon.awssdk.services.rds.model.CustomDbEngineVersionNotFoundException; +import software.amazon.cloudformation.exceptions.CfnNotFoundException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; -import software.amazon.cloudformation.proxy.Logger; import software.amazon.cloudformation.proxy.ProgressEvent; import software.amazon.cloudformation.proxy.ProxyClient; import software.amazon.cloudformation.proxy.ResourceHandlerRequest; @@ -67,7 +66,7 @@ protected boolean isDeleted(final ResourceModel model, try { fetchDBEngineVersion(model, proxyClient); return false; - } catch (CustomDbEngineVersionNotFoundException e) { + } catch (CfnNotFoundException e) { return true; } } diff --git a/aws-rds-customdbengineversion/src/test/java/software/amazon/rds/customdbengineversion/CreateHandlerTest.java b/aws-rds-customdbengineversion/src/test/java/software/amazon/rds/customdbengineversion/CreateHandlerTest.java index ddd31a482..48c84dbaf 100644 --- a/aws-rds-customdbengineversion/src/test/java/software/amazon/rds/customdbengineversion/CreateHandlerTest.java +++ b/aws-rds-customdbengineversion/src/test/java/software/amazon/rds/customdbengineversion/CreateHandlerTest.java @@ -43,6 +43,7 @@ import software.amazon.rds.common.error.ErrorCode; import software.amazon.rds.common.handler.HandlerConfig; import software.amazon.rds.common.handler.Tagging; +import software.amazon.rds.common.util.IdempotencyHelper; import software.amazon.rds.test.common.core.HandlerName; @ExtendWith(MockitoExtension.class) @@ -73,6 +74,7 @@ public void setup() { rdsClient = mock(RdsClient.class); proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); rdsProxy = mockProxy(proxy, rdsClient); + IdempotencyHelper.setBypass(true); } @AfterEach diff --git a/aws-rds-dbcluster/aws-rds-dbcluster.json b/aws-rds-dbcluster/aws-rds-dbcluster.json index 5d83851f6..786c48d02 100644 --- a/aws-rds-dbcluster/aws-rds-dbcluster.json +++ b/aws-rds-dbcluster/aws-rds-dbcluster.json @@ -44,10 +44,18 @@ "minimum": 1, "type": "integer" }, + "ClusterScalabilityType": { + "type": "string", + "description": "The scalability type for the DB cluster." + }, "CopyTagsToSnapshot": { "description": "A value that indicates whether to copy all tags from the DB cluster to snapshots of the DB cluster. The default is not to copy them.", "type": "boolean" }, + "DatabaseInsightsMode": { + "description": "A value that indicates the mode of Database Insights to enable for the DB cluster", + "type": "string" + }, "DatabaseName": { "description": "The name of your database. If you don't provide a name, then Amazon RDS won't create a database in this DB cluster. For naming constraints, see Naming Constraints in the Amazon RDS User Guide.", "type": "string" @@ -334,6 +342,9 @@ "MaxCapacity": { "description": "The maximum number of Aurora capacity units (ACUs) for a DB instance in an Aurora Serverless v2 cluster. You can specify ACU values in half-step increments, such as 40, 40.5, 41, and so on. The largest value that you can use is 128.", "type": "number" + }, + "SecondsUntilAutoPause": { + "type": "integer" } } }, @@ -410,13 +421,13 @@ "/properties/DBClusterIdentifier": "$lowercase(DBClusterIdentifier)", "/properties/DBClusterParameterGroupName": "$lowercase(DBClusterParameterGroupName)", "/properties/DBSubnetGroupName": "$lowercase(DBSubnetGroupName)", - "/properties/EnableHttpEndpoint": "$lowercase($string(EngineMode)) = 'serverless' ? EnableHttpEndpoint : ($lowercase($string(Engine)) = 'aurora-postgresql' ? EnableHttpEndpoint : false )", + "/properties/EnableHttpEndpoint": "$lowercase($string(EngineMode)) = 'serverless' ? EnableHttpEndpoint : ($lowercase($string(Engine)) in ['aurora-postgresql', 'aurora-mysql'] ? EnableHttpEndpoint : false )", "/properties/Engine": "$lowercase(Engine)", "/properties/EngineVersion": "$join([$string(EngineVersion), \".*\"])", - "/properties/KmsKeyId": "$join([\"arn:(aws)[-]{0,1}[a-z]{0,2}[-]{0,1}[a-z]{0,3}:kms:[a-z]{2}[-]{1}[a-z]{3,10}[-]{0,1}[a-z]{0,10}[-]{1}[1-3]{1}:[0-9]{12}[:]{1}key\\/\", KmsKeyId])", - "/properties/MasterUserSecret/KmsKeyId": "$join([\"arn:(aws)[-]{0,1}[a-z]{0,2}[-]{0,1}[a-z]{0,3}:kms:[a-z]{2}[-]{1}[a-z]{3,10}[-]{0,1}[a-z]{0,10}[-]{1}[1-3]{1}:[0-9]{12}[:]{1}key\\/\", MasterUserSecret.KmsKeyId])", + "/properties/KmsKeyId": "$join([\"arn:.+?:kms:.+?:.+?:key\\/\", KmsKeyId])", + "/properties/MasterUserSecret/KmsKeyId": "$join([\"arn:.+?:kms:.+?:.+?:key\\/\", MasterUserSecret.KmsKeyId])", "/properties/NetworkType": "$lowercase(NetworkType)", - "/properties/PerformanceInsightsKmsKeyId": "$join([\"arn:(aws)[-]{0,1}[a-z]{0,2}[-]{0,1}[a-z]{0,3}:kms:[a-z]{2}[-]{1}[a-z]{3,10}[-]{0,1}[a-z]{0,10}[-]{1}[1-3]{1}:[0-9]{12}[:]{1}key\\/\", PerformanceInsightsKmsKeyId])", + "/properties/PerformanceInsightsKmsKeyId": "$join([\"arn:.+?:kms:.+?:.+?:key\\/\", PerformanceInsightsKmsKeyId])", "/properties/PreferredMaintenanceWindow": "$lowercase(PreferredMaintenanceWindow)", "/properties/SnapshotIdentifier": "$lowercase(SnapshotIdentifier)", "/properties/SourceDBClusterIdentifier": "$lowercase(SourceDBClusterIdentifier)", @@ -428,12 +439,14 @@ "/properties/Endpoint", "/properties/Endpoint/Address", "/properties/Endpoint/Port", + "/properties/ReadEndpoint", "/properties/ReadEndpoint/Address", "/properties/MasterUserSecret/SecretArn", "/properties/StorageThroughput" ], "createOnlyProperties": [ "/properties/AvailabilityZones", + "/properties/ClusterScalabilityType", "/properties/DBClusterIdentifier", "/properties/DBSubnetGroupName", "/properties/DBSystemId", @@ -458,6 +471,7 @@ "/properties/DBClusterIdentifier" ], "writeOnlyProperties": [ + "/properties/ClusterScalabilityType", "/properties/DBInstanceParameterGroupName", "/properties/MasterUserPassword", "/properties/RestoreToTime", @@ -482,6 +496,7 @@ "rds:ModifyDBCluster", "rds:RestoreDBClusterFromSnapshot", "rds:RestoreDBClusterToPointInTime", + "rds:DescribeDBClusterSnapshots", "secretsmanager:CreateSecret", "secretsmanager:TagResource" ], diff --git a/aws-rds-dbcluster/docs/README.md b/aws-rds-dbcluster/docs/README.md deleted file mode 100644 index 73a537359..000000000 --- a/aws-rds-dbcluster/docs/README.md +++ /dev/null @@ -1,780 +0,0 @@ -# AWS::RDS::DBCluster - -The AWS::RDS::DBCluster resource creates an Amazon Aurora DB cluster. - -## Syntax - -To declare this entity in your AWS CloudFormation template, use the following syntax: - -### JSON - -
-{
-    "Type" : "AWS::RDS::DBCluster",
-    "Properties" : {
-        "ReadEndpoint" : ReadEndpoint,
-        "AllocatedStorage" : Integer,
-        "AssociatedRoles" : [ DBClusterRole, ... ],
-        "AvailabilityZones" : [ String, ... ],
-        "AutoMinorVersionUpgrade" : Boolean,
-        "BacktrackWindow" : Integer,
-        "BackupRetentionPeriod" : Integer,
-        "CopyTagsToSnapshot" : Boolean,
-        "DatabaseName" : String,
-        "DBClusterInstanceClass" : String,
-        "DBInstanceParameterGroupName" : String,
-        "DBSystemId" : String,
-        "GlobalClusterIdentifier" : String,
-        "DBClusterIdentifier" : String,
-        "DBClusterParameterGroupName" : String,
-        "DBSubnetGroupName" : String,
-        "DeletionProtection" : Boolean,
-        "Domain" : String,
-        "DomainIAMRoleName" : String,
-        "EnableCloudwatchLogsExports" : [ String, ... ],
-        "EnableGlobalWriteForwarding" : Boolean,
-        "EnableHttpEndpoint" : Boolean,
-        "EnableIAMDatabaseAuthentication" : Boolean,
-        "EnableLocalWriteForwarding" : Boolean,
-        "Engine" : String,
-        "EngineLifecycleSupport" : String,
-        "EngineMode" : String,
-        "EngineVersion" : String,
-        "ManageMasterUserPassword" : Boolean,
-        "Iops" : Integer,
-        "KmsKeyId" : String,
-        "MasterUsername" : String,
-        "MasterUserPassword" : String,
-        "MasterUserSecret" : MasterUserSecret,
-        "MonitoringInterval" : Integer,
-        "MonitoringRoleArn" : String,
-        "NetworkType" : String,
-        "PerformanceInsightsEnabled" : Boolean,
-        "PerformanceInsightsKmsKeyId" : String,
-        "PerformanceInsightsRetentionPeriod" : Integer,
-        "Port" : Integer,
-        "PreferredBackupWindow" : String,
-        "PreferredMaintenanceWindow" : String,
-        "PubliclyAccessible" : Boolean,
-        "ReplicationSourceIdentifier" : String,
-        "RestoreToTime" : String,
-        "RestoreType" : String,
-        "ServerlessV2ScalingConfiguration" : ServerlessV2ScalingConfiguration,
-        "ScalingConfiguration" : ScalingConfiguration,
-        "SnapshotIdentifier" : String,
-        "SourceDBClusterIdentifier" : String,
-        "SourceRegion" : String,
-        "StorageEncrypted" : Boolean,
-        "StorageType" : String,
-        "Tags" : [ Tag, ... ],
-        "UseLatestRestorableTime" : Boolean,
-        "VpcSecurityGroupIds" : [ String, ... ]
-    }
-}
-
- -### YAML - -
-Type: AWS::RDS::DBCluster
-Properties:
-    ReadEndpoint: ReadEndpoint
-    AllocatedStorage: Integer
-    AssociatedRoles: 
-      - DBClusterRole
-    AvailabilityZones: 
-      - String
-    AutoMinorVersionUpgrade: Boolean
-    BacktrackWindow: Integer
-    BackupRetentionPeriod: Integer
-    CopyTagsToSnapshot: Boolean
-    DatabaseName: String
-    DBClusterInstanceClass: String
-    DBInstanceParameterGroupName: String
-    DBSystemId: String
-    GlobalClusterIdentifier: String
-    DBClusterIdentifier: String
-    DBClusterParameterGroupName: String
-    DBSubnetGroupName: String
-    DeletionProtection: Boolean
-    Domain: String
-    DomainIAMRoleName: String
-    EnableCloudwatchLogsExports: 
-      - String
-    EnableGlobalWriteForwarding: Boolean
-    EnableHttpEndpoint: Boolean
-    EnableIAMDatabaseAuthentication: Boolean
-    EnableLocalWriteForwarding: Boolean
-    Engine: String
-    EngineLifecycleSupport: String
-    EngineMode: String
-    EngineVersion: String
-    ManageMasterUserPassword: Boolean
-    Iops: Integer
-    KmsKeyId: String
-    MasterUsername: String
-    MasterUserPassword: String
-    MasterUserSecret: MasterUserSecret
-    MonitoringInterval: Integer
-    MonitoringRoleArn: String
-    NetworkType: String
-    PerformanceInsightsEnabled: Boolean
-    PerformanceInsightsKmsKeyId: String
-    PerformanceInsightsRetentionPeriod: Integer
-    Port: Integer
-    PreferredBackupWindow: String
-    PreferredMaintenanceWindow: String
-    PubliclyAccessible: Boolean
-    ReplicationSourceIdentifier: String
-    RestoreToTime: String
-    RestoreType: String
-    ServerlessV2ScalingConfiguration: ServerlessV2ScalingConfiguration
-    ScalingConfiguration: ScalingConfiguration
-    SnapshotIdentifier: String
-    SourceDBClusterIdentifier: String
-    SourceRegion: String
-    StorageEncrypted: Boolean
-    StorageType: String
-    Tags: 
-      - Tag
-    UseLatestRestorableTime: Boolean
-    VpcSecurityGroupIds: 
-      - String
-
- -## Properties - -#### ReadEndpoint - -_Required_: No - -_Type_: ReadEndpoint - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### AllocatedStorage - -The amount of storage in gibibytes (GiB) to allocate to each DB instance in the Multi-AZ DB cluster. - -_Required_: No - -_Type_: Integer - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### AssociatedRoles - -Provides a list of the AWS Identity and Access Management (IAM) roles that are associated with the DB cluster. IAM roles that are associated with a DB cluster grant permission for the DB cluster to access other AWS services on your behalf. - -_Required_: No - -_Type_: List of DBClusterRole - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### AvailabilityZones - -A list of Availability Zones (AZs) where instances in the DB cluster can be created. For information on AWS Regions and Availability Zones, see Choosing the Regions and Availability Zones in the Amazon Aurora User Guide. - -_Required_: No - -_Type_: List of String - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### AutoMinorVersionUpgrade - -A value that indicates whether minor engine upgrades are applied automatically to the DB cluster during the maintenance window. By default, minor engine upgrades are applied automatically. - -_Required_: No - -_Type_: Boolean - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### BacktrackWindow - -The target backtrack window, in seconds. To disable backtracking, set this value to 0. - -_Required_: No - -_Type_: Integer - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### BackupRetentionPeriod - -The number of days for which automated backups are retained. - -_Required_: No - -_Type_: Integer - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### CopyTagsToSnapshot - -A value that indicates whether to copy all tags from the DB cluster to snapshots of the DB cluster. The default is not to copy them. - -_Required_: No - -_Type_: Boolean - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### DatabaseName - -The name of your database. If you don't provide a name, then Amazon RDS won't create a database in this DB cluster. For naming constraints, see Naming Constraints in the Amazon RDS User Guide. - -_Required_: No - -_Type_: String - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### DBClusterInstanceClass - -The compute and memory capacity of each DB instance in the Multi-AZ DB cluster, for example db.m6g.xlarge. - -_Required_: No - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### DBInstanceParameterGroupName - -The name of the DB parameter group to apply to all instances of the DB cluster. - -_Required_: No - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### DBSystemId - -Reserved for future use. - -_Required_: No - -_Type_: String - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### GlobalClusterIdentifier - -If you are configuring an Aurora global database cluster and want your Aurora DB cluster to be a secondary member in the global database cluster, specify the global cluster ID of the global database cluster. To define the primary database cluster of the global cluster, use the AWS::RDS::GlobalCluster resource. - -If you aren't configuring a global database cluster, don't specify this property. - -_Required_: No - -_Type_: String - -_Maximum Length_: 63 - -_Pattern_: ^$|^[a-zA-Z]{1}(?:-?[a-zA-Z0-9]){0,62}$ - -_Update requires_: [Some interruptions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-some-interrupt) - -#### DBClusterIdentifier - -The DB cluster identifier. This parameter is stored as a lowercase string. - -_Required_: No - -_Type_: String - -_Minimum Length_: 1 - -_Maximum Length_: 63 - -_Pattern_: ^[a-zA-Z]{1}(?:-?[a-zA-Z0-9]){0,62}$ - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### DBClusterParameterGroupName - -The name of the DB cluster parameter group to associate with this DB cluster. - -_Required_: No - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### DBSubnetGroupName - -A DB subnet group that you want to associate with this DB cluster. - -_Required_: No - -_Type_: String - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### DeletionProtection - -A value that indicates whether the DB cluster has deletion protection enabled. The database can't be deleted when deletion protection is enabled. By default, deletion protection is disabled. - -_Required_: No - -_Type_: Boolean - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### Domain - -The Active Directory directory ID to create the DB cluster in. - -_Required_: No - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### DomainIAMRoleName - -Specify the name of the IAM role to be used when making API calls to the Directory Service. - -_Required_: No - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### EnableCloudwatchLogsExports - -The list of log types that need to be enabled for exporting to CloudWatch Logs. The values in the list depend on the DB engine being used. For more information, see Publishing Database Logs to Amazon CloudWatch Logs in the Amazon Aurora User Guide. - -_Required_: No - -_Type_: List of String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### EnableGlobalWriteForwarding - -Specifies whether to enable this DB cluster to forward write operations to the primary cluster of a global cluster (Aurora global database). By default, write operations are not allowed on Aurora DB clusters that are secondary clusters in an Aurora global database. - -_Required_: No - -_Type_: Boolean - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### EnableHttpEndpoint - -A value that indicates whether to enable the HTTP endpoint for DB cluster. By default, the HTTP endpoint is disabled. - -_Required_: No - -_Type_: Boolean - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### EnableIAMDatabaseAuthentication - -A value that indicates whether to enable mapping of AWS Identity and Access Management (IAM) accounts to database accounts. By default, mapping is disabled. - -_Required_: No - -_Type_: Boolean - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### EnableLocalWriteForwarding - -Specifies whether read replicas can forward write operations to the writer DB instance in the DB cluster. By default, write operations aren't allowed on reader DB instances. - -_Required_: No - -_Type_: Boolean - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### Engine - -The name of the database engine to be used for this DB cluster. Valid Values: aurora (for MySQL 5.6-compatible Aurora), aurora-mysql (for MySQL 5.7-compatible Aurora), and aurora-postgresql - -_Required_: No - -_Type_: String - -_Update requires_: [Some interruptions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-some-interrupt) - -#### EngineLifecycleSupport - -The life cycle type of the DB cluster. You can use this setting to enroll your DB cluster into Amazon RDS Extended Support. - -_Required_: No - -_Type_: String - -_Update requires_: [Some interruptions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-some-interrupt) - -#### EngineMode - -The DB engine mode of the DB cluster, either provisioned, serverless, parallelquery, global, or multimaster. - -_Required_: No - -_Type_: String - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### EngineVersion - -The version number of the database engine to use. - -_Required_: No - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### ManageMasterUserPassword - -A value that indicates whether to manage the master user password with AWS Secrets Manager. - -_Required_: No - -_Type_: Boolean - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### Iops - -The amount of Provisioned IOPS (input/output operations per second) to be initially allocated for each DB instance in the Multi-AZ DB cluster. - -_Required_: No - -_Type_: Integer - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### KmsKeyId - -The Amazon Resource Name (ARN) of the AWS Key Management Service master key that is used to encrypt the database instances in the DB cluster, such as arn:aws:kms:us-east-1:012345678910:key/abcd1234-a123-456a-a12b-a123b4cd56ef. If you enable the StorageEncrypted property but don't specify this property, the default master key is used. If you specify this property, you must set the StorageEncrypted property to true. - -_Required_: No - -_Type_: String - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### MasterUsername - -The name of the master user for the DB cluster. You must specify MasterUsername, unless you specify SnapshotIdentifier. In that case, don't specify MasterUsername. - -_Required_: No - -_Type_: String - -_Minimum Length_: 1 - -_Pattern_: ^[a-zA-Z]{1}[a-zA-Z0-9_]*$ - -_Update requires_: [Some interruptions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-some-interrupt) - -#### MasterUserPassword - -The master password for the DB instance. - -_Required_: No - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### MasterUserSecret - -_Required_: No - -_Type_: MasterUserSecret - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### MonitoringInterval - -The interval, in seconds, between points when Enhanced Monitoring metrics are collected for the DB cluster. To turn off collecting Enhanced Monitoring metrics, specify 0. The default is 0. - -_Required_: No - -_Type_: Integer - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### MonitoringRoleArn - -The Amazon Resource Name (ARN) for the IAM role that permits RDS to send Enhanced Monitoring metrics to Amazon CloudWatch Logs. - -_Required_: No - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### NetworkType - -The network type of the DB cluster. - -_Required_: No - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### PerformanceInsightsEnabled - -A value that indicates whether to turn on Performance Insights for the DB cluster. - -_Required_: No - -_Type_: Boolean - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### PerformanceInsightsKmsKeyId - -The Amazon Web Services KMS key identifier for encryption of Performance Insights data. - -_Required_: No - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### PerformanceInsightsRetentionPeriod - -The amount of time, in days, to retain Performance Insights data. - -_Required_: No - -_Type_: Integer - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### Port - -The port number on which the instances in the DB cluster accept connections. Default: 3306 if engine is set as aurora or 5432 if set to aurora-postgresql. - -_Required_: No - -_Type_: Integer - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### PreferredBackupWindow - -The daily time range during which automated backups are created if automated backups are enabled using the BackupRetentionPeriod parameter. The default is a 30-minute window selected at random from an 8-hour block of time for each AWS Region. To see the time blocks available, see Adjusting the Preferred DB Cluster Maintenance Window in the Amazon Aurora User Guide. - -_Required_: No - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### PreferredMaintenanceWindow - -The weekly time range during which system maintenance can occur, in Universal Coordinated Time (UTC). The default is a 30-minute window selected at random from an 8-hour block of time for each AWS Region, occurring on a random day of the week. To see the time blocks available, see Adjusting the Preferred DB Cluster Maintenance Window in the Amazon Aurora User Guide. - -_Required_: No - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### PubliclyAccessible - -A value that indicates whether the DB cluster is publicly accessible. - -_Required_: No - -_Type_: Boolean - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### ReplicationSourceIdentifier - -The Amazon Resource Name (ARN) of the source DB instance or DB cluster if this DB cluster is created as a Read Replica. - -_Required_: No - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### RestoreToTime - -The date and time to restore the DB cluster to. Value must be a time in Universal Coordinated Time (UTC) format. An example: 2015-03-07T23:45:00Z - -_Required_: No - -_Type_: String - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### RestoreType - -The type of restore to be performed. You can specify one of the following values: -full-copy - The new DB cluster is restored as a full copy of the source DB cluster. -copy-on-write - The new DB cluster is restored as a clone of the source DB cluster. - -_Required_: No - -_Type_: String - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### ServerlessV2ScalingConfiguration - -Contains the scaling configuration of an Aurora Serverless v2 DB cluster. - -_Required_: No - -_Type_: ServerlessV2ScalingConfiguration - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### ScalingConfiguration - -The ScalingConfiguration property type specifies the scaling configuration of an Aurora Serverless DB cluster. - -_Required_: No - -_Type_: ScalingConfiguration - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### SnapshotIdentifier - -The identifier for the DB snapshot or DB cluster snapshot to restore from. -You can use either the name or the Amazon Resource Name (ARN) to specify a DB cluster snapshot. However, you can use only the ARN to specify a DB snapshot. -After you restore a DB cluster with a SnapshotIdentifier property, you must specify the same SnapshotIdentifier property for any future updates to the DB cluster. When you specify this property for an update, the DB cluster is not restored from the snapshot again, and the data in the database is not changed. However, if you don't specify the SnapshotIdentifier property, an empty DB cluster is created, and the original DB cluster is deleted. If you specify a property that is different from the previous snapshot restore property, the DB cluster is restored from the specified SnapshotIdentifier property, and the original DB cluster is deleted. - -_Required_: No - -_Type_: String - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### SourceDBClusterIdentifier - -The identifier of the source DB cluster from which to restore. - -_Required_: No - -_Type_: String - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### SourceRegion - -The AWS Region which contains the source DB cluster when replicating a DB cluster. For example, us-east-1. - -_Required_: No - -_Type_: String - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### StorageEncrypted - -Indicates whether the DB instance is encrypted. -If you specify the DBClusterIdentifier, SnapshotIdentifier, or SourceDBInstanceIdentifier property, don't specify this property. The value is inherited from the cluster, snapshot, or source DB instance. - -_Required_: No - -_Type_: Boolean - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### StorageType - -Specifies the storage type to be associated with the DB cluster. - -_Required_: No - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### Tags - -An array of key-value pairs to apply to this resource. - -_Required_: No - -_Type_: List of Tag - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### UseLatestRestorableTime - -A value that indicates whether to restore the DB cluster to the latest restorable backup time. By default, the DB cluster is not restored to the latest restorable backup time. - -_Required_: No - -_Type_: Boolean - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### VpcSecurityGroupIds - -A list of EC2 VPC security groups to associate with this DB cluster. - -_Required_: No - -_Type_: List of String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -## Return Values - -### Ref - -When you pass the logical ID of this resource to the intrinsic `Ref` function, Ref returns the DBClusterIdentifier. - -### Fn::GetAtt - -The `Fn::GetAtt` intrinsic function returns a value for a specified attribute of this type. The following are the available attributes and sample return values. - -For more information about using the `Fn::GetAtt` intrinsic function, see [Fn::GetAtt](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html). - -#### DBClusterArn - -The Amazon Resource Name (ARN) for the DB cluster. - -#### DBClusterResourceId - -The AWS Region-unique, immutable identifier for the DB cluster. - -#### Endpoint - -Returns the Endpoint value. - -#### Address - -Returns the Address value. - -#### Port - -Returns the Port value. - -#### Port - -Returns the Port value. - -#### Address - -Returns the Address value. - -#### SecretArn - -Returns the SecretArn value. - -#### StorageThroughput - -Specifies the storage throughput value for the DB cluster. This setting applies only to the gp3 storage type. diff --git a/aws-rds-dbcluster/docs/dbclusterrole.md b/aws-rds-dbcluster/docs/dbclusterrole.md deleted file mode 100644 index a3075bfd5..000000000 --- a/aws-rds-dbcluster/docs/dbclusterrole.md +++ /dev/null @@ -1,45 +0,0 @@ -# AWS::RDS::DBCluster DBClusterRole - -Describes an AWS Identity and Access Management (IAM) role that is associated with a DB cluster. - -## Syntax - -To declare this entity in your AWS CloudFormation template, use the following syntax: - -### JSON - -
-{
-    "FeatureName" : String,
-    "RoleArn" : String
-}
-
- -### YAML - -
-FeatureName: String
-RoleArn: String
-
- -## Properties - -#### FeatureName - -The name of the feature associated with the AWS Identity and Access Management (IAM) role. For the list of supported feature names, see DBEngineVersion in the Amazon RDS API Reference. - -_Required_: No - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### RoleArn - -The Amazon Resource Name (ARN) of the IAM role that is associated with the DB cluster. - -_Required_: Yes - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) diff --git a/aws-rds-dbcluster/docs/endpoint.md b/aws-rds-dbcluster/docs/endpoint.md deleted file mode 100644 index 1bf621527..000000000 --- a/aws-rds-dbcluster/docs/endpoint.md +++ /dev/null @@ -1,19 +0,0 @@ -# AWS::RDS::DBCluster Endpoint - -## Syntax - -To declare this entity in your AWS CloudFormation template, use the following syntax: - -### JSON - -
-{
-}
-
- -### YAML - -
-
- -## Properties diff --git a/aws-rds-dbcluster/docs/masterusersecret.md b/aws-rds-dbcluster/docs/masterusersecret.md deleted file mode 100644 index 65e6b4338..000000000 --- a/aws-rds-dbcluster/docs/masterusersecret.md +++ /dev/null @@ -1,31 +0,0 @@ -# AWS::RDS::DBCluster MasterUserSecret - -## Syntax - -To declare this entity in your AWS CloudFormation template, use the following syntax: - -### JSON - -
-{
-    "KmsKeyId" : String
-}
-
- -### YAML - -
-KmsKeyId: String
-
- -## Properties - -#### KmsKeyId - -The AWS KMS key identifier that is used to encrypt the secret. - -_Required_: No - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) diff --git a/aws-rds-dbcluster/docs/readendpoint.md b/aws-rds-dbcluster/docs/readendpoint.md deleted file mode 100644 index 66f44f013..000000000 --- a/aws-rds-dbcluster/docs/readendpoint.md +++ /dev/null @@ -1,19 +0,0 @@ -# AWS::RDS::DBCluster ReadEndpoint - -## Syntax - -To declare this entity in your AWS CloudFormation template, use the following syntax: - -### JSON - -
-{
-}
-
- -### YAML - -
-
- -## Properties diff --git a/aws-rds-dbcluster/docs/scalingconfiguration.md b/aws-rds-dbcluster/docs/scalingconfiguration.md deleted file mode 100644 index 90a80033f..000000000 --- a/aws-rds-dbcluster/docs/scalingconfiguration.md +++ /dev/null @@ -1,104 +0,0 @@ -# AWS::RDS::DBCluster ScalingConfiguration - -The ScalingConfiguration property type specifies the scaling configuration of an Aurora Serverless DB cluster. - -## Syntax - -To declare this entity in your AWS CloudFormation template, use the following syntax: - -### JSON - -
-{
-    "AutoPause" : Boolean,
-    "MaxCapacity" : Integer,
-    "MinCapacity" : Integer,
-    "SecondsBeforeTimeout" : Integer,
-    "SecondsUntilAutoPause" : Integer,
-    "TimeoutAction" : String
-}
-
- -### YAML - -
-AutoPause: Boolean
-MaxCapacity: Integer
-MinCapacity: Integer
-SecondsBeforeTimeout: Integer
-SecondsUntilAutoPause: Integer
-TimeoutAction: String
-
- -## Properties - -#### AutoPause - -A value that indicates whether to allow or disallow automatic pause for an Aurora DB cluster in serverless DB engine mode. A DB cluster can be paused only when it's idle (it has no connections). - -_Required_: No - -_Type_: Boolean - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### MaxCapacity - -The maximum capacity for an Aurora DB cluster in serverless DB engine mode. -For Aurora MySQL, valid capacity values are 1, 2, 4, 8, 16, 32, 64, 128, and 256. -For Aurora PostgreSQL, valid capacity values are 2, 4, 8, 16, 32, 64, 192, and 384. -The maximum capacity must be greater than or equal to the minimum capacity. - -_Required_: No - -_Type_: Integer - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### MinCapacity - -The minimum capacity for an Aurora DB cluster in serverless DB engine mode. -For Aurora MySQL, valid capacity values are 1, 2, 4, 8, 16, 32, 64, 128, and 256. -For Aurora PostgreSQL, valid capacity values are 2, 4, 8, 16, 32, 64, 192, and 384. -The minimum capacity must be less than or equal to the maximum capacity. - -_Required_: No - -_Type_: Integer - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### SecondsBeforeTimeout - -The amount of time, in seconds, that Aurora Serverless v1 tries to find a scaling point to perform seamless scaling before enforcing the timeout action. -The default is 300. - -_Required_: No - -_Type_: Integer - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### SecondsUntilAutoPause - -The time, in seconds, before an Aurora DB cluster in serverless mode is paused. - -_Required_: No - -_Type_: Integer - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### TimeoutAction - -The action to take when the timeout is reached, either ForceApplyCapacityChange or RollbackCapacityChange. -ForceApplyCapacityChange sets the capacity to the specified value as soon as possible. -RollbackCapacityChange, the default, ignores the capacity change if a scaling point isn't found in the timeout period. - -For more information, see Autoscaling for Aurora Serverless v1 in the Amazon Aurora User Guide. - -_Required_: No - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) diff --git a/aws-rds-dbcluster/docs/serverlessv2scalingconfiguration.md b/aws-rds-dbcluster/docs/serverlessv2scalingconfiguration.md deleted file mode 100644 index c3a383eb9..000000000 --- a/aws-rds-dbcluster/docs/serverlessv2scalingconfiguration.md +++ /dev/null @@ -1,45 +0,0 @@ -# AWS::RDS::DBCluster ServerlessV2ScalingConfiguration - -Contains the scaling configuration of an Aurora Serverless v2 DB cluster. - -## Syntax - -To declare this entity in your AWS CloudFormation template, use the following syntax: - -### JSON - -
-{
-    "MinCapacity" : Double,
-    "MaxCapacity" : Double
-}
-
- -### YAML - -
-MinCapacity: Double
-MaxCapacity: Double
-
- -## Properties - -#### MinCapacity - -The minimum number of Aurora capacity units (ACUs) for a DB instance in an Aurora Serverless v2 cluster. You can specify ACU values in half-step increments, such as 8, 8.5, 9, and so on. The smallest value that you can use is 0.5. - -_Required_: No - -_Type_: Double - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### MaxCapacity - -The maximum number of Aurora capacity units (ACUs) for a DB instance in an Aurora Serverless v2 cluster. You can specify ACU values in half-step increments, such as 40, 40.5, 41, and so on. The largest value that you can use is 128. - -_Required_: No - -_Type_: Double - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) diff --git a/aws-rds-dbcluster/docs/tag.md b/aws-rds-dbcluster/docs/tag.md deleted file mode 100644 index 00ef00022..000000000 --- a/aws-rds-dbcluster/docs/tag.md +++ /dev/null @@ -1,51 +0,0 @@ -# AWS::RDS::DBCluster Tag - -A key-value pair to associate with a resource. - -## Syntax - -To declare this entity in your AWS CloudFormation template, use the following syntax: - -### JSON - -
-{
-    "Key" : String,
-    "Value" : String
-}
-
- -### YAML - -
-Key: String
-Value: String
-
- -## Properties - -#### Key - -The key name of the tag. You can specify a value that is 1 to 128 Unicode characters in length and cannot be prefixed with aws:. You can use any of the following characters: the set of Unicode letters, digits, whitespace, _, ., /, =, +, and -. - -_Required_: Yes - -_Type_: String - -_Minimum Length_: 1 - -_Maximum Length_: 128 - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### Value - -The value for the tag. You can specify a value that is 0 to 256 Unicode characters in length and cannot be prefixed with aws:. You can use any of the following characters: the set of Unicode letters, digits, whitespace, _, ., /, =, +, and -. - -_Required_: No - -_Type_: String - -_Maximum Length_: 256 - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) diff --git a/aws-rds-dbcluster/pom.xml b/aws-rds-dbcluster/pom.xml index 0778a2f2e..61391d5f0 100644 --- a/aws-rds-dbcluster/pom.xml +++ b/aws-rds-dbcluster/pom.xml @@ -1,8 +1,8 @@ + xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 software.amazon.rds.dbcluster @@ -27,27 +27,10 @@ - - software.amazon.awssdk - rds - 2.25.56 - software.amazon.awssdk ec2 - 2.22.12 - - - - software.amazon.awssdk - aws-query-protocol - 2.20.138 - - - - software.amazon.cloudformation - aws-cloudformation-rpdk-java-plugin - [2.0.0, 3.0.0) + 2.30.38 @@ -97,6 +80,11 @@ 1.0 test + + software.amazon.cloudformation + aws-cloudformation-rpdk-java-plugin + [2.0.0,3.0.0) + @@ -181,11 +169,36 @@ 0.8.8 - **/BaseConfiguration* - **/BaseHandler* - **/HandlerWrapper* - **/ResourceModel* - **/ModelAdapter* + + **/software/amazon/rds/dbcluster/BaseConfiguration.class + **/software/amazon/rds/dbcluster/BaseHandler.class + **/software/amazon/rds/dbcluster/BaseHandlerStd.class + **/software/amazon/rds/dbcluster/CallbackContext.class + **/software/amazon/rds/dbcluster/Configuration.class + **/software/amazon/rds/dbcluster/CreateHandler.class + **/software/amazon/rds/dbcluster/DBClusterRole.class + **/software/amazon/rds/dbcluster/DBClusterStatus.class + **/software/amazon/rds/dbcluster/DeleteHandler.class + **/software/amazon/rds/dbcluster/Ec2ClientProvider.class + **/software/amazon/rds/dbcluster/Endpoint.class + **/software/amazon/rds/dbcluster/EngineMode.class + **/software/amazon/rds/dbcluster/HandlerWrapper.class + **/software/amazon/rds/dbcluster/HandlerWrapperExecutable.class + **/software/amazon/rds/dbcluster/ListHandler.class + **/software/amazon/rds/dbcluster/MasterUserSecret.class + **/software/amazon/rds/dbcluster/ModelAdapter.class + **/software/amazon/rds/dbcluster/RdsClientProvider.class + **/software/amazon/rds/dbcluster/ReadEndpoint.class + **/software/amazon/rds/dbcluster/ReadHandler.class + **/software/amazon/rds/dbcluster/ResourceModel.class + **/software/amazon/rds/dbcluster/ScalingConfiguration.class + **/software/amazon/rds/dbcluster/ServerlessV2ScalingConfiguration.class + **/software/amazon/rds/dbcluster/Tag.class + **/software/amazon/rds/dbcluster/Translator.class + **/software/amazon/rds/dbcluster/TypeConfigurationModel.class + **/software/amazon/rds/dbcluster/UpdateHandler.class + + **/software/amazon/rds/dbcluster/util/ImmutabilityHelper.class @@ -209,7 +222,7 @@ - PACKAGE + BUNDLE BRANCH @@ -219,7 +232,7 @@ INSTRUCTION COVEREDRATIO - 0.8 + 0.0 diff --git a/aws-rds-dbcluster/resource-role.yaml b/aws-rds-dbcluster/resource-role.yaml index 2685b0e53..fa85da916 100644 --- a/aws-rds-dbcluster/resource-role.yaml +++ b/aws-rds-dbcluster/resource-role.yaml @@ -40,6 +40,7 @@ Resources: - "rds:CreateDBInstance" - "rds:DeleteDBCluster" - "rds:DeleteDBInstance" + - "rds:DescribeDBClusterSnapshots" - "rds:DescribeDBClusters" - "rds:DescribeDBSubnetGroups" - "rds:DescribeEvents" diff --git a/aws-rds-dbcluster/src/main/java/software/amazon/rds/dbcluster/BaseHandlerStd.java b/aws-rds-dbcluster/src/main/java/software/amazon/rds/dbcluster/BaseHandlerStd.java index 3a09b76f8..f70717d79 100644 --- a/aws-rds-dbcluster/src/main/java/software/amazon/rds/dbcluster/BaseHandlerStd.java +++ b/aws-rds-dbcluster/src/main/java/software/amazon/rds/dbcluster/BaseHandlerStd.java @@ -13,6 +13,7 @@ import java.util.Optional; import java.util.Set; import java.util.function.Predicate; +import java.util.stream.Collectors; import com.google.common.collect.ImmutableMap; import org.apache.commons.collections.CollectionUtils; @@ -27,6 +28,7 @@ import software.amazon.awssdk.services.rds.RdsClient; import software.amazon.awssdk.services.rds.model.ClusterPendingModifiedValues; import software.amazon.awssdk.services.rds.model.DBCluster; +import software.amazon.awssdk.services.rds.model.DBClusterSnapshot; import software.amazon.awssdk.services.rds.model.DBSubnetGroup; import software.amazon.awssdk.services.rds.model.DbClusterAlreadyExistsException; import software.amazon.awssdk.services.rds.model.DbClusterNotFoundException; @@ -38,6 +40,7 @@ import software.amazon.awssdk.services.rds.model.DbInstanceNotFoundException; import software.amazon.awssdk.services.rds.model.DbSubnetGroupDoesNotCoverEnoughAZsException; import software.amazon.awssdk.services.rds.model.DbSubnetGroupNotFoundException; +import software.amazon.awssdk.services.rds.model.DescribeDbClusterSnapshotsResponse; import software.amazon.awssdk.services.rds.model.DescribeDbClustersResponse; import software.amazon.awssdk.services.rds.model.DescribeDbSubnetGroupsResponse; import software.amazon.awssdk.services.rds.model.DescribeGlobalClustersResponse; @@ -63,8 +66,10 @@ import software.amazon.awssdk.services.rds.model.StorageTypeNotSupportedException; import software.amazon.awssdk.services.rds.model.Tag; import software.amazon.awssdk.services.rds.model.WriteForwardingStatus; +import software.amazon.awssdk.services.rds.paginators.DescribeDBClustersIterable; import software.amazon.awssdk.utils.StringUtils; import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; +import software.amazon.cloudformation.exceptions.CfnNotFoundException; import software.amazon.cloudformation.exceptions.CfnNotStabilizedException; import software.amazon.cloudformation.exceptions.ResourceNotFoundException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; @@ -83,6 +88,7 @@ import software.amazon.rds.common.handler.Events; import software.amazon.rds.common.handler.HandlerConfig; import software.amazon.rds.common.handler.Tagging; +import software.amazon.rds.common.handler.Vpc; import software.amazon.rds.common.logging.LoggingProxyClient; import software.amazon.rds.common.logging.RequestLogger; import software.amazon.rds.common.printer.FilteredJsonPrinter; @@ -90,10 +96,12 @@ import software.amazon.rds.common.request.RequestValidationException; import software.amazon.rds.common.request.ValidatedRequest; import software.amazon.rds.common.request.Validations; +import software.amazon.rds.common.util.ArnHelper; public abstract class BaseHandlerStd extends BaseHandler { public static final String RESOURCE_IDENTIFIER = "dbcluster"; public static final String ENGINE_AURORA_POSTGRESQL = "aurora-postgresql"; + public static final String ENGINE_AURORA_MYSQL = "aurora-mysql"; private static final String MASTER_USER_SECRET_ACTIVE = "active"; protected static final String DB_CLUSTER_REQUEST_STARTED_AT = "dbcluster-request-started-at"; protected static final String DB_CLUSTER_REQUEST_IN_PROGRESS_AT = "dbcluster-request-in-progress-at"; @@ -132,6 +140,7 @@ public abstract class BaseHandlerStd extends BaseHandler { .withErrorClasses(ErrorStatus.failWith(HandlerErrorCode.AlreadyExists), DbClusterAlreadyExistsException.class) .withErrorClasses(ErrorStatus.failWith(HandlerErrorCode.NotFound), + CfnNotFoundException.class, // FIXME: handle BaseHandlerException in ErrorRuleSet DbClusterNotFoundException.class, DbClusterSnapshotNotFoundException.class, DbClusterParameterGroupNotFoundException.class, @@ -260,18 +269,20 @@ protected DBCluster fetchDBCluster( final ProxyClient proxyClient, final ResourceModel model ) { - final DescribeDbClustersResponse response = proxyClient.injectCredentialsAndInvokeV2( - Translator.describeDbClustersRequest(model), - proxyClient.client()::describeDBClusters - ); + try { + final DescribeDbClustersResponse response = proxyClient.injectCredentialsAndInvokeV2( + Translator.describeDbClustersRequest(model), + proxyClient.client()::describeDBClusters + ); - if (response.dbClusters().isEmpty()) { - throw DbClusterNotFoundException.builder() - .message(String.format("No clusters of identifier %s returned from describe call", model.getDBClusterIdentifier())) - .build(); - } + if (!response.hasDbClusters() || response.dbClusters().isEmpty()) { + throw new CfnNotFoundException(ResourceModel.TYPE_NAME, model.getDBClusterIdentifier()); + } - return response.dbClusters().get(0); + return response.dbClusters().get(0); + } catch (DbClusterNotFoundException e) { + throw new CfnNotFoundException(e); + } } protected GlobalCluster fetchGlobalCluster( @@ -296,6 +307,65 @@ protected DBSubnetGroup fetchDBSubnetGroup( return response.dbSubnetGroups().get(0); } + protected DBCluster fetchSourceDBCluster( + final String awsAccountId, + final ProxyClient proxyClient, + final ResourceModel model + ) { + // RDS API does not accept cross-account cluster identifiers when using --db-cluster-identifier option + // therefore resorting to this workaround + if (isCrossAccountSourceDBCluster(awsAccountId, model.getSourceDBClusterIdentifier())) { + final DescribeDBClustersIterable response = proxyClient.injectCredentialsAndInvokeIterableV2( + Translator.describeSourceDbClustersCrossAccountRequest(model), + proxyClient.client()::describeDBClustersPaginator + ); + + final List matchingClusters = response.dbClusters() + .stream() + .filter(c -> model.getSourceDBClusterIdentifier().equalsIgnoreCase(Optional.ofNullable(c.dbClusterArn()).orElse(""))) + .collect(Collectors.toList()); + if (matchingClusters.isEmpty()) { + throw DbClusterNotFoundException.builder() + .message(String.format("SourceDbCluster %s doesn't refer to an existing DB cluster", model.getSourceDBClusterIdentifier())) + .build(); + } + return matchingClusters.get(0); + } + final DescribeDbClustersResponse response = proxyClient.injectCredentialsAndInvokeV2( + Translator.describeSourceDbClustersRequest(model), + proxyClient.client()::describeDBClusters + ); + if (response.dbClusters().isEmpty()) { + throw DbClusterNotFoundException.builder() + .message(String.format("SourceDbCluster %s doesn't refer to an existing DB cluster", model.getSourceDBClusterIdentifier())) + .build(); + } + return response.dbClusters().get(0); + } + + private boolean isCrossAccountSourceDBCluster(String awsCustomer, String sourceDBCluster) { + if (ArnHelper.isValidArn(sourceDBCluster)) { + return !StringUtils.equals(awsCustomer, ArnHelper.getAccountIdFromArn(sourceDBCluster)); + } + return false; + } + + protected DBClusterSnapshot fetchDBClusterSnapshot( + final ProxyClient proxyClient, + final ResourceModel model + ) { + final DescribeDbClusterSnapshotsResponse response = proxyClient.injectCredentialsAndInvokeV2( + Translator.describeDbClusterSnapshotRequest(model), + proxyClient.client()::describeDBClusterSnapshots + ); + if (response.dbClusterSnapshots().isEmpty()) { + throw DbClusterSnapshotNotFoundException.builder() + .message(String.format("SnapshotIdentifier %s doesn't refer to an existing DB cluster snapshot", model.getSnapshotIdentifier())) + .build(); + } + return response.dbClusterSnapshots().get(0); + } + protected SecurityGroup fetchSecurityGroup( final ProxyClient ec2ProxyClient, final String vpcId, @@ -408,7 +478,7 @@ protected boolean isDBClusterDeleted( ) { try { fetchDBCluster(proxyClient, model); - } catch (DbClusterNotFoundException e) { + } catch (CfnNotFoundException e) { return true; } return false; @@ -715,21 +785,11 @@ protected ProgressEvent setDefaultVpcSecurityGro protected boolean shouldSetDefaultVpcSecurityGroupIds(final ResourceModel previousState, final ResourceModel desiredState) { - if (previousState != null) { - final List previousVpcIds = CollectionUtils.isEmpty(previousState.getVpcSecurityGroupIds()) ? - Collections.emptyList() : previousState.getVpcSecurityGroupIds(); - final List desiredVpcIds = CollectionUtils.isEmpty(desiredState.getVpcSecurityGroupIds()) ? - Collections.emptyList() : desiredState.getVpcSecurityGroupIds(); - - if (CollectionUtils.isEqualCollection(previousVpcIds, desiredVpcIds)) { - return false; - } - } - return CollectionUtils.isEmpty(desiredState.getVpcSecurityGroupIds()); + return Vpc.shouldSetDefaultVpcId(previousState.getVpcSecurityGroupIds(), desiredState.getVpcSecurityGroupIds()); } protected boolean shouldUpdateHttpEndpointV2(final ResourceModel previousState, final ResourceModel desiredState) { - return ENGINE_AURORA_POSTGRESQL.equalsIgnoreCase(desiredState.getEngine()) && + return (ENGINE_AURORA_POSTGRESQL.equalsIgnoreCase(desiredState.getEngine()) || ENGINE_AURORA_MYSQL.equalsIgnoreCase(desiredState.getEngine())) && !EngineMode.Serverless.equals(EngineMode.fromString(desiredState.getEngineMode())) && ObjectUtils.notEqual(previousState.getEnableHttpEndpoint(), desiredState.getEnableHttpEndpoint()); } diff --git a/aws-rds-dbcluster/src/main/java/software/amazon/rds/dbcluster/CallbackContext.java b/aws-rds-dbcluster/src/main/java/software/amazon/rds/dbcluster/CallbackContext.java index da28d3685..3b8ae041b 100644 --- a/aws-rds-dbcluster/src/main/java/software/amazon/rds/dbcluster/CallbackContext.java +++ b/aws-rds-dbcluster/src/main/java/software/amazon/rds/dbcluster/CallbackContext.java @@ -5,19 +5,24 @@ import java.util.HashMap; import java.util.Map; +import software.amazon.awssdk.services.rds.model.ClusterScalabilityType; import software.amazon.cloudformation.proxy.StdCallbackContext; import software.amazon.rds.common.handler.ProbingContext; import software.amazon.rds.common.handler.TaggingContext; import software.amazon.rds.common.handler.TimestampContext; +import software.amazon.rds.common.util.IdempotencyHelper; +import software.amazon.rds.common.util.WaiterHelper; @lombok.Getter @lombok.Setter @lombok.ToString @lombok.EqualsAndHashCode(callSuper = true) -public class CallbackContext extends StdCallbackContext implements TaggingContext.Provider, ProbingContext.Provider, TimestampContext.Provider { +public class CallbackContext extends StdCallbackContext implements TaggingContext.Provider, ProbingContext.Provider, TimestampContext.Provider, IdempotencyHelper.PreExistenceContext, WaiterHelper.DelayContext { + private Boolean preExistenceCheckDone; private boolean modified; private boolean rebooted; private boolean deleting; + private ClusterScalabilityType clusterScalabilityType; private Map timestamps; private Map timeDelta; @@ -25,12 +30,17 @@ public class CallbackContext extends StdCallbackContext implements TaggingContex private TaggingContext taggingContext; private ProbingContext probingContext; + // wait time is used for delaying in Aurora Serverless V2 due to async workflows modifying properties + // which may occur after the DBCluster is available + private int waitTime; + public CallbackContext() { super(); this.taggingContext = new TaggingContext(); this.probingContext = new ProbingContext(); this.timestamps = new HashMap<>(); this.timeDelta = new HashMap<>(); + this.waitTime = 0; } @Override diff --git a/aws-rds-dbcluster/src/main/java/software/amazon/rds/dbcluster/CreateHandler.java b/aws-rds-dbcluster/src/main/java/software/amazon/rds/dbcluster/CreateHandler.java index 9ca4b8a0e..c401a10c6 100644 --- a/aws-rds-dbcluster/src/main/java/software/amazon/rds/dbcluster/CreateHandler.java +++ b/aws-rds-dbcluster/src/main/java/software/amazon/rds/dbcluster/CreateHandler.java @@ -8,8 +8,15 @@ import com.amazonaws.util.StringUtils; import software.amazon.awssdk.services.ec2.Ec2Client; import software.amazon.awssdk.services.rds.RdsClient; +import software.amazon.awssdk.services.rds.model.ClusterScalabilityType; +import software.amazon.awssdk.services.rds.model.DBCluster; +import software.amazon.awssdk.services.rds.model.DBClusterSnapshot; +import software.amazon.awssdk.services.rds.model.ModifyDbClusterRequest; +import software.amazon.awssdk.services.rds.model.RestoreDbClusterFromSnapshotRequest; +import software.amazon.awssdk.services.rds.model.RestoreDbClusterToPointInTimeRequest; import software.amazon.awssdk.services.rds.model.SourceType; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.CallChain; import software.amazon.cloudformation.proxy.ProgressEvent; import software.amazon.cloudformation.proxy.ProxyClient; import software.amazon.cloudformation.proxy.ResourceHandlerRequest; @@ -20,10 +27,20 @@ import software.amazon.rds.common.request.RequestValidationException; import software.amazon.rds.common.request.ValidatedRequest; import software.amazon.rds.common.request.Validations; +import software.amazon.rds.common.util.ArnHelper; +import software.amazon.rds.common.util.IdempotencyHelper; import software.amazon.rds.common.util.IdentifierFactory; +import software.amazon.rds.common.validation.ValidationAccessException; +import software.amazon.rds.common.validation.ValidationUtils; +import software.amazon.rds.dbcluster.util.ResourceModelHelper; +import software.amazon.rds.dbcluster.validators.ClusterScalabilityTypeValidator; public class CreateHandler extends BaseHandlerStd { + public final static String DB_CLUSTER_VALIDATION_MISSING_PERMISSIONS_METRIC = "DBClusterValidationMissingPermissions"; + public final static String LIMITLESS_ENGINE_VERSION_SUFFIX = "limitless"; + public final static String ENGINE_VERSION_SEPERATOR = "-"; + private static final IdentifierFactory dbClusterIdentifierFactory = new IdentifierFactory( STACK_NAME, RESOURCE_IDENTIFIER, @@ -44,6 +61,7 @@ public CreateHandler(final HandlerConfig config) { protected void validateRequest(final ResourceHandlerRequest request) throws RequestValidationException { super.validateRequest(request); Validations.validateTimestamp(request.getDesiredResourceState().getRestoreToTime()); + ClusterScalabilityTypeValidator.validateRequest(request.getDesiredResourceState()); } @Override @@ -70,15 +88,27 @@ protected ProgressEvent handleRequest( .resourceTags(new HashSet<>(Translator.translateTagsToSdk(request.getDesiredResourceState().getTags()))) .build(); + if(ResourceModelHelper.isRestoreFromSnapshot(model)) { + ClusterScalabilityType clusterScalabilityType = getClusterScalabilityTypeFromSnapshot(rdsProxyClient, model); + callbackContext.setClusterScalabilityType(clusterScalabilityType); + } + if(ResourceModelHelper.isRestoreToPointInTime(model)) { + ClusterScalabilityType clusterScalabilityType = getClusterScalabilityTypeFromSourceDBCluster(extractAwsAccountId(request), rdsProxyClient, model); + callbackContext.setClusterScalabilityType(clusterScalabilityType); + } + return ProgressEvent.progress(model, callbackContext) - .then(progress -> { - if (isRestoreToPointInTime(model)) { - return Tagging.createWithTaggingFallback(proxy, rdsProxyClient, this::restoreDbClusterToPointInTime, progress, allTags); - } else if (isRestoreFromSnapshot(model)) { - return Tagging.createWithTaggingFallback(proxy, rdsProxyClient, this::restoreDbClusterFromSnapshot, progress, allTags); - } - return Tagging.createWithTaggingFallback(proxy, rdsProxyClient, this::createDbCluster, progress, allTags); - }) + .then(progress -> + IdempotencyHelper.safeCreate( + m -> fetchDBCluster(rdsProxyClient, m), + p -> { + if (ResourceModelHelper.isRestoreToPointInTime(model)) { + return Tagging.createWithTaggingFallback(proxy, rdsProxyClient, this::restoreDbClusterToPointInTime, p, allTags); + } else if (ResourceModelHelper.isRestoreFromSnapshot(model)) { + return Tagging.createWithTaggingFallback(proxy, rdsProxyClient, this::restoreDbClusterFromSnapshot, p, allTags); + } + return Tagging.createWithTaggingFallback(proxy, rdsProxyClient, this::createDbCluster, p, allTags); + }, ResourceModel.TYPE_NAME, model.getDBClusterIdentifier(), progress, requestLogger)) .then(progress -> Commons.execOnce(progress, () -> { final Tagging.TagSet extraTags = Tagging.TagSet.builder() .stackTags(allTags.getStackTags()) @@ -87,14 +117,14 @@ protected ProgressEvent handleRequest( return updateTags(proxy, rdsProxyClient, progress, Tagging.TagSet.emptySet(), extraTags); }, CallbackContext::isAddTagsComplete, CallbackContext::setAddTagsComplete)) .then(progress -> { - if (shouldUpdateAfterCreate(progress.getResourceModel())) { + if (ResourceModelHelper.shouldUpdateAfterCreate(progress.getResourceModel())) { return Commons.execOnce( progress, () -> { progress.getCallbackContext().timestampOnce(RESOURCE_UPDATED_AT, Instant.now()); return modifyDBCluster(proxy, rdsProxyClient, progress) .then(p -> { - if (shouldEnableHttpEndpointV2AfterCreate(progress.getResourceModel())) { + if (ResourceModelHelper.shouldEnableHttpEndpointV2AfterCreate(progress.getResourceModel())) { return enableHttpEndpointV2(proxy, rdsProxyClient, progress); } return p; @@ -159,9 +189,14 @@ private ProgressEvent restoreDbClusterToPointInT final ProgressEvent progress, final Tagging.TagSet tagSet ) { - return proxy.initiate("rds::restore-dbcluster-to-point-in-time", proxyClient, progress.getResourceModel(), progress.getCallbackContext()) - .translateToServiceRequest(model -> Translator.restoreDbClusterToPointInTimeRequest(model, tagSet)) - .backoffDelay(config.getBackoff()) + CallChain.RequestMaker requestMaker = proxy.initiate("rds::restore-dbcluster-to-point-in-time", proxyClient, progress.getResourceModel(), progress.getCallbackContext()); + CallChain.Caller caller = null; + if(progress.getCallbackContext().getClusterScalabilityType().equals(ClusterScalabilityType.LIMITLESS)) { + caller = requestMaker.translateToServiceRequest(model -> Translator.restoreLimitlessDbClusterToPointInTimeRequest(model, tagSet)); + } else { + caller = requestMaker.translateToServiceRequest(model -> Translator.restoreDbClusterToPointInTimeRequest(model, tagSet)); + } + return caller.backoffDelay(config.getBackoff()) .makeServiceCall((dbClusterRequest, proxyInvocation) -> proxyInvocation.injectCredentialsAndInvokeV2( dbClusterRequest, proxyInvocation.client()::restoreDBClusterToPointInTime @@ -184,9 +219,15 @@ private ProgressEvent restoreDbClusterFromSnapsh final ProgressEvent progress, final Tagging.TagSet tagSet ) { - return proxy.initiate("rds::restore-dbcluster-from-snapshot", proxyClient, progress.getResourceModel(), progress.getCallbackContext()) - .translateToServiceRequest(model -> Translator.restoreDbClusterFromSnapshotRequest(model, tagSet)) - .backoffDelay(config.getBackoff()) + CallChain.RequestMaker requestMaker = proxy.initiate("rds::restore-dbcluster-from-snapshot", proxyClient, progress.getResourceModel(), progress.getCallbackContext()); + CallChain.Caller caller = null; + if(progress.getCallbackContext().getClusterScalabilityType().equals(ClusterScalabilityType.LIMITLESS)) { + caller = requestMaker.translateToServiceRequest(model -> Translator.restoreLimitlessDbClusterFromSnapshotRequest(model, tagSet)); + } else { + caller = requestMaker.translateToServiceRequest(model -> Translator.restoreDbClusterFromSnapshotRequest(model, tagSet)); + } + + return caller.backoffDelay(config.getBackoff()) .makeServiceCall((dbClusterRequest, proxyInvocation) -> proxyInvocation.injectCredentialsAndInvokeV2( dbClusterRequest, proxyInvocation.client()::restoreDBClusterFromSnapshot @@ -208,9 +249,18 @@ protected ProgressEvent modifyDBCluster( final ProxyClient proxyClient, final ProgressEvent progress ) { - return proxy.initiate("rds::modify-dbcluster", proxyClient, progress.getResourceModel(), progress.getCallbackContext()) - .translateToServiceRequest(Translator::modifyDbClusterAfterCreateRequest) - .backoffDelay(config.getBackoff()) + ClusterScalabilityType clusterScalabilityType = progress.getCallbackContext().getClusterScalabilityType(); + CallChain.RequestMaker callContext = proxy.initiate("rds::modify-dbcluster", proxyClient, progress.getResourceModel(), progress.getCallbackContext()); + CallChain.Caller caller = null; + + if (clusterScalabilityType.equals(ClusterScalabilityType.LIMITLESS)) { + caller = callContext.translateToServiceRequest(Translator::modifyLimitlessDbClusterAfterCreateRequest); + } + else { + caller = callContext.translateToServiceRequest(Translator::modifyDbClusterAfterCreateRequest); + } + + return caller.backoffDelay(config.getBackoff()) .makeServiceCall((dbClusterModifyRequest, proxyInvocation) -> proxyInvocation.injectCredentialsAndInvokeV2( dbClusterModifyRequest, proxyInvocation.client()::modifyDBCluster @@ -227,19 +277,49 @@ protected ProgressEvent modifyDBCluster( .progress(); } - private boolean isRestoreToPointInTime(final ResourceModel model) { - return StringUtils.hasValue(model.getSourceDBClusterIdentifier()); + private String extractAwsAccountId(ValidatedRequest request) { + if (request != null && StringUtils.hasValue(request.getAwsAccountId())) { + return request.getAwsAccountId(); + } + return null; } - private boolean isRestoreFromSnapshot(final ResourceModel model) { - return StringUtils.hasValue(model.getSnapshotIdentifier()); + + protected ClusterScalabilityType getClusterScalabilityTypeFromSnapshot(final ProxyClient rdsProxyClient, final ResourceModel resourceModel) { + // Source SnapshotIdentifier might belong to either DBClusterSnapshot or DBSnapshot. + // Instance snapshot must use ARN format. If the format is not an ARN, treat this as a cluster snapshot + try { + if (ArnHelper.isValidArn(resourceModel.getSnapshotIdentifier()) + && (ArnHelper.getResourceType(resourceModel.getSnapshotIdentifier()) == ArnHelper.ResourceType.DB_INSTANCE_SNAPSHOT)) { + return ClusterScalabilityType.STANDARD; + } + else { + final DBClusterSnapshot dbClusterSnapshot = ValidationUtils.fetchResourceForValidation(() -> + fetchDBClusterSnapshot(rdsProxyClient, resourceModel), "DescribeDBClusterSnapshots"); + return getClusterScalabilityTypeFromEngineVersion(dbClusterSnapshot.engineVersion()); + } + } + catch (ValidationAccessException e) { + ValidationUtils.emitMetric(requestLogger, DB_CLUSTER_VALIDATION_MISSING_PERMISSIONS_METRIC, e); + return ClusterScalabilityType.STANDARD; + } } - private boolean shouldUpdateAfterCreate(final ResourceModel model) { - return isRestoreFromSnapshot(model) || isRestoreToPointInTime(model); + protected ClusterScalabilityType getClusterScalabilityTypeFromEngineVersion(final String snapshotEngineVersion) { + if (StringUtils.isNullOrEmpty(snapshotEngineVersion)) { + return ClusterScalabilityType.STANDARD; + } + // we are using the engine version suffix until clusterScalabilityType is returned as part of describe snapshot API + String[] snapshotEngineVersionParts = snapshotEngineVersion.split(ENGINE_VERSION_SEPERATOR); + if(snapshotEngineVersionParts.length > 1 && snapshotEngineVersionParts[1].equals(LIMITLESS_ENGINE_VERSION_SUFFIX)) { + return ClusterScalabilityType.LIMITLESS; + } + + return ClusterScalabilityType.STANDARD; } - private boolean shouldEnableHttpEndpointV2AfterCreate(final ResourceModel model) { - return BooleanUtils.isTrue(model.getEnableHttpEndpoint()) && !EngineMode.Serverless.equals(EngineMode.fromString(model.getEngineMode())) ; + protected ClusterScalabilityType getClusterScalabilityTypeFromSourceDBCluster(final String AwsAccountId, final ProxyClient rdsProxyClient, final ResourceModel resourceModel) { + DBCluster cluster = fetchSourceDBCluster(AwsAccountId, rdsProxyClient, resourceModel); + return cluster.clusterScalabilityType() != null ? cluster.clusterScalabilityType() : ClusterScalabilityType.STANDARD; } } diff --git a/aws-rds-dbcluster/src/main/java/software/amazon/rds/dbcluster/ModelAdapter.java b/aws-rds-dbcluster/src/main/java/software/amazon/rds/dbcluster/ModelAdapter.java index 33224e045..3fc572fb5 100644 --- a/aws-rds-dbcluster/src/main/java/software/amazon/rds/dbcluster/ModelAdapter.java +++ b/aws-rds-dbcluster/src/main/java/software/amazon/rds/dbcluster/ModelAdapter.java @@ -14,6 +14,7 @@ public class ModelAdapter { private static final int DEFAULT_MAX_CAPACITY = 16; private static final int DEFAULT_MIN_CAPACITY = 2; private static final int DEFAULT_SECONDS_UNTIL_AUTO_PAUSE = 300; + private static final int DEFAULT_SECONDS_UNTIL_AUTO_PAUSE_V2 = 300; private static final int DEFAULT_PORT = 3306; private static final String ENGINE_AURORA = "aurora"; @@ -52,6 +53,16 @@ public static ResourceModel setDefaults(final ResourceModel resourceModel) { resourceModel.setScalingConfiguration(scalingConfiguration == null ? defaultScalingConfiguration : scalingConfiguration); } + final var serverlessV2ScalingConfiguration = resourceModel.getServerlessV2ScalingConfiguration(); + final var isServerlessV2 = serverlessV2ScalingConfiguration != null; + final var isAutoPause = isServerlessV2 && serverlessV2ScalingConfiguration.getMinCapacity() != null + && serverlessV2ScalingConfiguration.getMinCapacity() == 0; + if (isAutoPause) { + if (serverlessV2ScalingConfiguration.getSecondsUntilAutoPause() == null) { + serverlessV2ScalingConfiguration.setSecondsUntilAutoPause(DEFAULT_SECONDS_UNTIL_AUTO_PAUSE_V2); + } + } + final EngineMode engineMode = EngineMode.fromString(resourceModel.getEngineMode()); resourceModel.setPort(port != null ? port : getDefaultPortForEngine(resourceModel.getEngine(), engineMode)); diff --git a/aws-rds-dbcluster/src/main/java/software/amazon/rds/dbcluster/Translator.java b/aws-rds-dbcluster/src/main/java/software/amazon/rds/dbcluster/Translator.java index a634a1ecf..12d6fafd7 100644 --- a/aws-rds-dbcluster/src/main/java/software/amazon/rds/dbcluster/Translator.java +++ b/aws-rds-dbcluster/src/main/java/software/amazon/rds/dbcluster/Translator.java @@ -24,6 +24,7 @@ import software.amazon.awssdk.services.rds.model.CloudwatchLogsExportConfiguration; import software.amazon.awssdk.services.rds.model.CreateDbClusterRequest; import software.amazon.awssdk.services.rds.model.DeleteDbClusterRequest; +import software.amazon.awssdk.services.rds.model.DescribeDbClusterSnapshotsRequest; import software.amazon.awssdk.services.rds.model.DescribeDbClustersRequest; import software.amazon.awssdk.services.rds.model.DescribeDbInstancesRequest; import software.amazon.awssdk.services.rds.model.DescribeDbSubnetGroupsRequest; @@ -55,7 +56,9 @@ static CreateDbClusterRequest createDbClusterRequest( .availabilityZones(model.getAvailabilityZones()) .backtrackWindow(castToLong(model.getBacktrackWindow())) .backupRetentionPeriod(model.getBackupRetentionPeriod()) + .clusterScalabilityType(model.getClusterScalabilityType()) .copyTagsToSnapshot(model.getCopyTagsToSnapshot()) + .databaseInsightsMode(model.getDatabaseInsightsMode()) .databaseName(model.getDatabaseName()) .dbClusterIdentifier(model.getDBClusterIdentifier()) .dbClusterInstanceClass(model.getDBClusterInstanceClass()) @@ -173,6 +176,36 @@ static RestoreDbClusterFromSnapshotRequest restoreDbClusterFromSnapshotRequest( .build(); } + static RestoreDbClusterToPointInTimeRequest restoreLimitlessDbClusterToPointInTimeRequest( + final ResourceModel model, + final Tagging.TagSet tagSet + ) { + RestoreDbClusterToPointInTimeRequest request = restoreDbClusterToPointInTimeRequest(model, tagSet); + // Restore API for limitless clusters accepts PIEM params + return request.toBuilder() + .monitoringRoleArn(model.getMonitoringRoleArn()) + .monitoringInterval(model.getMonitoringInterval()) + .enablePerformanceInsights(model.getPerformanceInsightsEnabled()) + .performanceInsightsRetentionPeriod(model.getPerformanceInsightsRetentionPeriod()) + .performanceInsightsKMSKeyId(model.getPerformanceInsightsKmsKeyId()) + .build(); + } + + static RestoreDbClusterFromSnapshotRequest restoreLimitlessDbClusterFromSnapshotRequest( + final ResourceModel model, + final Tagging.TagSet tagSet + ) { + RestoreDbClusterFromSnapshotRequest request = restoreDbClusterFromSnapshotRequest(model, tagSet); + // Restore API for limitless clusters accepts PIEM params + return request.toBuilder() + .monitoringRoleArn(model.getMonitoringRoleArn()) + .monitoringInterval(model.getMonitoringInterval()) + .enablePerformanceInsights(model.getPerformanceInsightsEnabled()) + .performanceInsightsRetentionPeriod(model.getPerformanceInsightsRetentionPeriod()) + .performanceInsightsKMSKeyId(model.getPerformanceInsightsKmsKeyId()) + .build(); + } + static Long castToLong(Object object) { return object == null ? null : Long.parseLong(String.valueOf(object)); } @@ -214,6 +247,7 @@ static ModifyDbClusterRequest modifyDbClusterAfterCreateRequest(final ResourceMo .backupRetentionPeriod(desiredModel.getBackupRetentionPeriod()) .cloudwatchLogsExportConfiguration(config) .copyTagsToSnapshot(desiredModel.getCopyTagsToSnapshot()) + .databaseInsightsMode(desiredModel.getDatabaseInsightsMode()) .dbClusterIdentifier(desiredModel.getDBClusterIdentifier()) .dbClusterInstanceClass(desiredModel.getDBClusterInstanceClass()) .dbClusterParameterGroupName(desiredModel.getDBClusterParameterGroupName()) @@ -251,6 +285,22 @@ static ModifyDbClusterRequest modifyDbClusterAfterCreateRequest(final ResourceMo return builder.build(); } + // params that are acked by Restore but + // not allowed in ModifyDBCluster for limitless usecase should not be passed + // https://code.amazon.com/packages/RDSCoralService/blobs/3edd6c5f4e19bd529e30199c7803905b6dc937e5/--/main/java/amazon/rds/admin/service/KermitClusterValidatorImpl.java#L181 + static ModifyDbClusterRequest modifyLimitlessDbClusterAfterCreateRequest(final ResourceModel desiredModel) { + ModifyDbClusterRequest modifyDbClusterRequest = modifyDbClusterAfterCreateRequest(desiredModel); + return modifyDbClusterRequest.toBuilder() + .storageType(null) + .port(null) + .vpcSecurityGroupIds((Collection) null) + .engineVersion(null) + .enablePerformanceInsights(null) + .performanceInsightsKMSKeyId(null) + .cloudwatchLogsExportConfiguration((CloudwatchLogsExportConfiguration) null) + .build(); + } + static ModifyDbClusterRequest modifyDbClusterRequest( final ResourceModel previousModel, final ResourceModel desiredModel, @@ -267,6 +317,7 @@ static ModifyDbClusterRequest modifyDbClusterRequest( .backupRetentionPeriod(desiredModel.getBackupRetentionPeriod()) .cloudwatchLogsExportConfiguration(config) .copyTagsToSnapshot(desiredModel.getCopyTagsToSnapshot()) + .databaseInsightsMode(desiredModel.getDatabaseInsightsMode()) .dbClusterIdentifier(desiredModel.getDBClusterIdentifier()) .dbClusterInstanceClass(desiredModel.getDBClusterInstanceClass()) .dbClusterParameterGroupName(desiredModel.getDBClusterParameterGroupName()) @@ -407,6 +458,23 @@ static DescribeDbClustersRequest describeDbClustersRequest( .build(); } + static DescribeDbClustersRequest describeSourceDbClustersRequest( + final ResourceModel model + ) { + return DescribeDbClustersRequest.builder() + .dbClusterIdentifier(model.getSourceDBClusterIdentifier()) + .build(); + } + + static DescribeDbClustersRequest describeSourceDbClustersCrossAccountRequest( + final ResourceModel model + ) { + return DescribeDbClustersRequest.builder() + .includeShared(true) + .build(); + } + + static EnableHttpEndpointRequest enableHttpEndpointRequest( final String clusterArn ) { @@ -415,6 +483,16 @@ static EnableHttpEndpointRequest enableHttpEndpointRequest( .build(); } + static DescribeDbClusterSnapshotsRequest describeDbClusterSnapshotRequest( + final ResourceModel model + ) { + return DescribeDbClusterSnapshotsRequest.builder() + .dbClusterSnapshotIdentifier(model.getSnapshotIdentifier()) + .includeShared(true) + .includePublic(true) + .build(); + } + static DisableHttpEndpointRequest disableHttpEndpointRequest( final String clusterArn ) { @@ -467,6 +545,7 @@ static software.amazon.awssdk.services.rds.model.ServerlessV2ScalingConfiguratio return software.amazon.awssdk.services.rds.model.ServerlessV2ScalingConfiguration.builder() .maxCapacity(serverlessV2ScalingConfiguration.getMaxCapacity()) .minCapacity(serverlessV2ScalingConfiguration.getMinCapacity()) + .secondsUntilAutoPause(serverlessV2ScalingConfiguration.getSecondsUntilAutoPause()) .build(); } @@ -495,6 +574,7 @@ static ServerlessV2ScalingConfiguration translateServerlessV2ScalingConfiguratio return ServerlessV2ScalingConfiguration.builder() .maxCapacity(serverlessV2ScalingConfiguration.maxCapacity()) .minCapacity(serverlessV2ScalingConfiguration.minCapacity()) + .secondsUntilAutoPause(serverlessV2ScalingConfiguration.secondsUntilAutoPause()) .build(); } @@ -553,6 +633,7 @@ public static ResourceModel translateDbClusterFromSdk( .backtrackWindow(Translator.castToInt(dbCluster.backtrackWindow())) .backupRetentionPeriod(dbCluster.backupRetentionPeriod()) .copyTagsToSnapshot(dbCluster.copyTagsToSnapshot()) + .databaseInsightsMode(dbCluster.databaseInsightsModeAsString()) .databaseName(dbCluster.databaseName()) .dBClusterArn(dbCluster.dbClusterArn()) .dBClusterIdentifier(dbCluster.dbClusterIdentifier()) diff --git a/aws-rds-dbcluster/src/main/java/software/amazon/rds/dbcluster/UpdateHandler.java b/aws-rds-dbcluster/src/main/java/software/amazon/rds/dbcluster/UpdateHandler.java index 5370f3c9c..fa5d0bfed 100644 --- a/aws-rds-dbcluster/src/main/java/software/amazon/rds/dbcluster/UpdateHandler.java +++ b/aws-rds-dbcluster/src/main/java/software/amazon/rds/dbcluster/UpdateHandler.java @@ -23,9 +23,13 @@ import software.amazon.rds.common.handler.Probing; import software.amazon.rds.common.handler.Tagging; import software.amazon.rds.common.request.ValidatedRequest; +import software.amazon.rds.common.util.WaiterHelper; import software.amazon.rds.dbcluster.util.ImmutabilityHelper; +import software.amazon.rds.dbcluster.util.ResourceModelHelper; public class UpdateHandler extends BaseHandlerStd { + static final int AURORA_SERVERLESS_V2_MAX_WAIT_SECONDS = 300; + static final int AURORA_SERVERLESS_V2_POLL_SECONDS = 10; public UpdateHandler() { this(DB_CLUSTER_HANDLER_CONFIG_36H); @@ -68,19 +72,14 @@ protected ProgressEvent handleRequest( "Resource is immutable" ); } + + if (!Objects.equals(request.getDesiredResourceState().getEngineLifecycleSupport(), + request.getPreviousResourceState().getEngineLifecycleSupport()) && + !request.getRollback()) { + throw new CfnInvalidRequestException("EngineLifecycleSupport cannot be modified."); + } + return ProgressEvent.progress(desiredResourceState, callbackContext) - .then(progress -> { - try { - if(!Objects.equals(request.getDesiredResourceState().getEngineLifecycleSupport(), - request.getPreviousResourceState().getEngineLifecycleSupport()) && - !request.getRollback()) { - throw new CfnInvalidRequestException("EngineLifecycleSupport cannot be modified."); - } - } catch (CfnInvalidRequestException e) { - return Commons.handleException(progress, e, DEFAULT_DB_CLUSTER_ERROR_RULE_SET, requestLogger); - } - return progress; - }) .then(progress -> { if (shouldRemoveFromGlobalCluster(request.getPreviousResourceState(), request.getDesiredResourceState())) { progress.getCallbackContext().timestampOnce(RESOURCE_UPDATED_AT, Instant.now()); @@ -129,7 +128,16 @@ protected ProgressEvent handleRequest( requestLogger )) .then(progress -> updateTags(proxy, rdsProxyClient, progress, previousTags, desiredTags)) - .then(progress -> { + .then(progress -> { + // Required delay for Aurora Serverless V2, because when scaling capacity, it kicks off an async + // workflow which could occur after the DBCluster has completed CFN update + // This delay attempts to force the async workflow to complete before the DBCluster update returns + if (ResourceModelHelper.hasServerlessV2ScalingConfigurationChanged(previousResourceState, desiredResourceState)) { + return WaiterHelper.delay(progress, AURORA_SERVERLESS_V2_MAX_WAIT_SECONDS, AURORA_SERVERLESS_V2_POLL_SECONDS); + } + return progress; + }) + .then(progress -> { desiredResourceState.setTags(Translator.translateTagsFromSdk(Tagging.translateTagsToSdk(desiredTags))); return Commons.reportResourceDrift( desiredResourceState, diff --git a/aws-rds-dbcluster/src/main/java/software/amazon/rds/dbcluster/util/ResourceModelHelper.java b/aws-rds-dbcluster/src/main/java/software/amazon/rds/dbcluster/util/ResourceModelHelper.java new file mode 100644 index 000000000..e35b1fb40 --- /dev/null +++ b/aws-rds-dbcluster/src/main/java/software/amazon/rds/dbcluster/util/ResourceModelHelper.java @@ -0,0 +1,31 @@ +package software.amazon.rds.dbcluster.util; + +import com.amazonaws.util.StringUtils; +import org.apache.commons.lang3.BooleanUtils; +import software.amazon.rds.dbcluster.EngineMode; +import software.amazon.rds.dbcluster.ResourceModel; + +import static software.amazon.rds.common.util.DifferenceUtils.diff; + +public class ResourceModelHelper { + + public static boolean isRestoreToPointInTime(final ResourceModel model) { + return StringUtils.hasValue(model.getSourceDBClusterIdentifier()); + } + + public static boolean isRestoreFromSnapshot(final ResourceModel model) { + return StringUtils.hasValue(model.getSnapshotIdentifier()); + } + + public static boolean shouldUpdateAfterCreate(final ResourceModel model) { + return isRestoreFromSnapshot(model) || isRestoreToPointInTime(model); + } + + public static boolean shouldEnableHttpEndpointV2AfterCreate(final ResourceModel model) { + return BooleanUtils.isTrue(model.getEnableHttpEndpoint()) && !EngineMode.Serverless.equals(EngineMode.fromString(model.getEngineMode())); + } + + public static boolean hasServerlessV2ScalingConfigurationChanged(final ResourceModel previousModel, final ResourceModel desiredModel) { + return diff(previousModel.getServerlessV2ScalingConfiguration(), desiredModel.getServerlessV2ScalingConfiguration()) != null; + } +} diff --git a/aws-rds-dbcluster/src/main/java/software/amazon/rds/dbcluster/validators/ClusterScalabilityTypeValidator.java b/aws-rds-dbcluster/src/main/java/software/amazon/rds/dbcluster/validators/ClusterScalabilityTypeValidator.java new file mode 100644 index 000000000..ab24ed908 --- /dev/null +++ b/aws-rds-dbcluster/src/main/java/software/amazon/rds/dbcluster/validators/ClusterScalabilityTypeValidator.java @@ -0,0 +1,18 @@ +package software.amazon.rds.dbcluster.validators; + +import software.amazon.rds.common.request.RequestValidationException; +import software.amazon.rds.dbcluster.ResourceModel; +import software.amazon.rds.dbcluster.util.ResourceModelHelper; + +public class ClusterScalabilityTypeValidator { + public static void validateRequest(final ResourceModel model) throws RequestValidationException { + if (ResourceModelHelper.isRestoreToPointInTime(model) && model.getClusterScalabilityType() != null) { + throw new RequestValidationException("The ClusterScalabilityType parameter is not allowed when creating a cluster from a point-in-time restore. This value is automatically inherited from the source DB cluster."); + } + + if (ResourceModelHelper.isRestoreFromSnapshot(model) && model.getClusterScalabilityType() != null) { + throw new RequestValidationException("The ClusterScalabilityType parameter is not allowed when creating a DB cluster from a snapshot restore. This value is automatically inherited from the snapshot."); + } + + } +} diff --git a/aws-rds-dbcluster/src/test/java/software/amazon/rds/dbcluster/AbstractHandlerTest.java b/aws-rds-dbcluster/src/test/java/software/amazon/rds/dbcluster/AbstractHandlerTest.java index 999cd393f..c86730cfb 100644 --- a/aws-rds-dbcluster/src/test/java/software/amazon/rds/dbcluster/AbstractHandlerTest.java +++ b/aws-rds-dbcluster/src/test/java/software/amazon/rds/dbcluster/AbstractHandlerTest.java @@ -18,6 +18,7 @@ import com.google.common.collect.ImmutableSet; import software.amazon.awssdk.services.ec2.Ec2Client; import software.amazon.awssdk.services.rds.RdsClient; +import software.amazon.awssdk.services.rds.model.ClusterScalabilityType; import software.amazon.awssdk.services.rds.model.CreateDbClusterRequest; import software.amazon.awssdk.services.rds.model.CreateDbClusterResponse; import software.amazon.awssdk.services.rds.model.DBCluster; @@ -51,7 +52,7 @@ import software.amazon.rds.test.common.verification.AccessPermissionVerificationMode; public abstract class AbstractHandlerTest extends AbstractTestBase { - + private static final String DB_CLUSTER_ARN = "arn:partition:rds:region:account-id:dbcluster:resource-id"; protected static final String LOGICAL_RESOURCE_IDENTIFIER = "dbcluster"; protected static final Credentials MOCK_CREDENTIALS; @@ -66,6 +67,7 @@ public abstract class AbstractHandlerTest extends AbstractTestBase Duration.ofSeconds(600).toMillis()); rdsProxy = MOCK_PROXY(proxy, rdsClient); + IdempotencyHelper.setBypass(true); } @AfterEach @@ -281,6 +289,10 @@ public void handleRequest_RestoreDbClusterFromSnapshot_UnsetPort() { .thenReturn(ModifyDbClusterResponse.builder().build()); when(rdsProxy.client().describeEvents(any(DescribeEventsRequest.class))) .thenReturn(DescribeEventsResponse.builder().build()); + when(rdsProxy.client().describeDBClusterSnapshots(any(DescribeDbClusterSnapshotsRequest.class))) + .thenReturn(DescribeDbClusterSnapshotsResponse.builder().dbClusterSnapshots(DBClusterSnapshot.builder() + .dbClusterIdentifier(DBCLUSTER_IDENTIFIER) + .build()).build()); test_handleRequest_base( new CallbackContext(), @@ -298,6 +310,7 @@ public void handleRequest_RestoreDbClusterFromSnapshot_UnsetPort() { final ArgumentCaptor modifyCaptor = ArgumentCaptor.forClass(ModifyDbClusterRequest.class); verify(rdsProxy.client(), times(1)).modifyDBCluster(modifyCaptor.capture()); verify(rdsProxy.client(), times(3)).describeDBClusters(any(DescribeDbClustersRequest.class)); + verify(rdsProxy.client(), times(1)).describeDBClusterSnapshots(any(DescribeDbClusterSnapshotsRequest.class)); verify(rdsProxy.client(), times(1)).describeEvents(any(DescribeEventsRequest.class)); // We expect the default engine-specific port to be set @@ -314,6 +327,10 @@ public void handleRequest_RestoreDbClusterFromSnapshot_ModifyAfterCreate() { .thenReturn(ModifyDbClusterResponse.builder().build()); when(rdsProxy.client().describeEvents(any(DescribeEventsRequest.class))) .thenReturn(DescribeEventsResponse.builder().build()); + when(rdsProxy.client().describeDBClusterSnapshots(any(DescribeDbClusterSnapshotsRequest.class))) + .thenReturn(DescribeDbClusterSnapshotsResponse.builder().dbClusterSnapshots(DBClusterSnapshot.builder() + .dbClusterIdentifier(DBCLUSTER_IDENTIFIER) + .build()).build()); test_handleRequest_base( new CallbackContext(), @@ -325,6 +342,7 @@ public void handleRequest_RestoreDbClusterFromSnapshot_ModifyAfterCreate() { verify(rdsProxy.client(), times(1)).restoreDBClusterFromSnapshot(any(RestoreDbClusterFromSnapshotRequest.class)); verify(rdsProxy.client(), times(1)).modifyDBCluster(any(ModifyDbClusterRequest.class)); verify(rdsProxy.client(), times(3)).describeDBClusters(any(DescribeDbClustersRequest.class)); + verify(rdsProxy.client(), times(1)).describeDBClusterSnapshots(any(DescribeDbClusterSnapshotsRequest.class)); verify(rdsProxy.client(), times(1)).describeEvents(any(DescribeEventsRequest.class)); } @@ -337,6 +355,9 @@ public void handleRequest_RestoreDbClusterFromSnapshot_AccessDeniedTagging() { .errorCode(ErrorCode.AccessDeniedException.toString()) .build() ).build()); + when(rdsProxy.client().describeDBClusterSnapshots(any(DescribeDbClusterSnapshotsRequest.class))) + .thenReturn(DescribeDbClusterSnapshotsResponse.builder().dbClusterSnapshots(DBClusterSnapshot.builder() + .build()).build()); test_handleRequest_base( new CallbackContext(), @@ -353,6 +374,7 @@ public void handleRequest_RestoreDbClusterFromSnapshot_AccessDeniedTagging() { ArgumentCaptor createCaptor = ArgumentCaptor.forClass(RestoreDbClusterFromSnapshotRequest.class); verify(rdsProxy.client(), times(1)).restoreDBClusterFromSnapshot(createCaptor.capture()); + verify(rdsProxy.client(), times(1)).describeDBClusterSnapshots(any(DescribeDbClusterSnapshotsRequest.class)); final RestoreDbClusterFromSnapshotRequest requestWithAllTags = createCaptor.getAllValues().get(0); Assertions.assertThat(requestWithAllTags.tags()).containsExactlyInAnyOrder( @@ -364,6 +386,10 @@ public void handleRequest_RestoreDbClusterFromSnapshot_AccessDeniedTagging() { public void handleRequest_RestoreDbClusterFromSnapshot_Success() { when(rdsProxy.client().restoreDBClusterFromSnapshot(any(RestoreDbClusterFromSnapshotRequest.class))) .thenReturn(RestoreDbClusterFromSnapshotResponse.builder().build()); + when(rdsProxy.client().describeDBClusterSnapshots(any(DescribeDbClusterSnapshotsRequest.class))) + .thenReturn(DescribeDbClusterSnapshotsResponse.builder().dbClusterSnapshots(DBClusterSnapshot.builder() + .dbClusterIdentifier(DBCLUSTER_IDENTIFIER) + .build()).build()); final CallbackContext context = new CallbackContext(); context.setModified(true); @@ -377,6 +403,72 @@ public void handleRequest_RestoreDbClusterFromSnapshot_Success() { verify(rdsProxy.client(), times(1)).restoreDBClusterFromSnapshot(any(RestoreDbClusterFromSnapshotRequest.class)); verify(rdsProxy.client(), times(2)).describeDBClusters(any(DescribeDbClustersRequest.class)); + verify(rdsProxy.client(), times(1)).describeDBClusterSnapshots(any(DescribeDbClusterSnapshotsRequest.class)); + } + + @Test + public void handleRequest_RestoreLimitlessDbClusterFromSnapshot_Success() { + when(rdsProxy.client().restoreDBClusterFromSnapshot(any(RestoreDbClusterFromSnapshotRequest.class))) + .thenReturn(RestoreDbClusterFromSnapshotResponse.builder().build()); + when(rdsProxy.client().describeDBClusterSnapshots(any(DescribeDbClusterSnapshotsRequest.class))) + .thenReturn(DescribeDbClusterSnapshotsResponse.builder().dbClusterSnapshots(DBClusterSnapshot.builder() + .dbClusterIdentifier(DBCLUSTER_IDENTIFIER) + .build()).build()); + + final CallbackContext context = new CallbackContext(); + context.setModified(true); + + test_handleRequest_base( + context, + () -> LIMITLESS_DBCLUSTER_ACTIVE, + () -> RESOURCE_MODEL_ON_RESTORE_WITH_PIEM, + expectSuccess() + ); + + verify(rdsProxy.client(), times(1)).restoreDBClusterFromSnapshot(any(RestoreDbClusterFromSnapshotRequest.class)); + verify(rdsProxy.client(), times(2)).describeDBClusters(any(DescribeDbClustersRequest.class)); + verify(rdsProxy.client(), times(1)).describeDBClusterSnapshots(any(DescribeDbClusterSnapshotsRequest.class)); + } + + @Test + public void handleRequest_RestoreLimitlessDbClusterFromSnapshot_NoPIEMParamsModified() { + when(rdsProxy.client().restoreDBClusterFromSnapshot(any(RestoreDbClusterFromSnapshotRequest.class))) + .thenReturn(RestoreDbClusterFromSnapshotResponse.builder().build()); + when(rdsProxy.client().describeEvents(any(DescribeEventsRequest.class))) + .thenReturn(DescribeEventsResponse.builder().build()); + when(rdsProxy.client().describeDBClusterSnapshots(any(DescribeDbClusterSnapshotsRequest.class))) + .thenReturn(DescribeDbClusterSnapshotsResponse.builder().dbClusterSnapshots(DBClusterSnapshot.builder() + .dbClusterIdentifier(DBCLUSTER_IDENTIFIER) + .engineVersion("xx.x-limitless") + .build()).build()); + + final CallbackContext context = new CallbackContext(); + + test_handleRequest_base( + context, + () -> LIMITLESS_DBCLUSTER_ACTIVE, + () -> RESOURCE_MODEL_ON_RESTORE_WITH_PIEM.toBuilder() + .build(), + expectSuccess() + ); + + // for limitless clusters, PIEM params should go to restore API + final ArgumentCaptor restoreCaptor = ArgumentCaptor.forClass(RestoreDbClusterFromSnapshotRequest.class); + verify(rdsProxy.client(), times(1)).restoreDBClusterFromSnapshot(restoreCaptor.capture()); + Assertions.assertThat(restoreCaptor.getValue().enablePerformanceInsights()).isEqualTo(RESOURCE_MODEL_ON_RESTORE_WITH_PIEM.getPerformanceInsightsEnabled()); + Assertions.assertThat(restoreCaptor.getValue().performanceInsightsKMSKeyId()).isEqualTo(RESOURCE_MODEL_ON_RESTORE_WITH_PIEM.getPerformanceInsightsKmsKeyId()); + Assertions.assertThat(restoreCaptor.getValue().performanceInsightsRetentionPeriod()).isEqualTo(RESOURCE_MODEL_ON_RESTORE_WITH_PIEM.getPerformanceInsightsRetentionPeriod()); + Assertions.assertThat(restoreCaptor.getValue().monitoringRoleArn()).isEqualTo(RESOURCE_MODEL_ON_RESTORE_WITH_PIEM.getMonitoringRoleArn()); + Assertions.assertThat(restoreCaptor.getValue().monitoringInterval()).isEqualTo(RESOURCE_MODEL_ON_RESTORE_WITH_PIEM.getMonitoringInterval()); + + verify(rdsProxy.client(), times(3)).describeDBClusters(any(DescribeDbClustersRequest.class)); + verify(rdsProxy.client(), times(1)).describeDBClusterSnapshots(any(DescribeDbClusterSnapshotsRequest.class)); + + // for limitless clusters, PIEM params should not go to modify + final ArgumentCaptor modifyCaptor = ArgumentCaptor.forClass(ModifyDbClusterRequest.class); + verify(rdsProxy.client(), times(1)).modifyDBCluster(modifyCaptor.capture()); + Assertions.assertThat(modifyCaptor.getValue().enablePerformanceInsights()).isNull(); + Assertions.assertThat(modifyCaptor.getValue().performanceInsightsKMSKeyId()).isNull(); } @Test @@ -385,6 +477,9 @@ public void handleRequest_RestoreDbClusterFromSnapshot_ServerlessV2ScalingConfig .thenReturn(RestoreDbClusterFromSnapshotResponse.builder().build()); when(rdsProxy.client().describeEvents(any(DescribeEventsRequest.class))) .thenReturn(DescribeEventsResponse.builder().build()); + when(rdsProxy.client().describeDBClusterSnapshots(any(DescribeDbClusterSnapshotsRequest.class))) + .thenReturn(DescribeDbClusterSnapshotsResponse.builder().dbClusterSnapshots(DBClusterSnapshot.builder() + .dbClusterIdentifier(DBCLUSTER_IDENTIFIER).build()).build()); final CallbackContext context = new CallbackContext(); @@ -400,6 +495,7 @@ public void handleRequest_RestoreDbClusterFromSnapshot_ServerlessV2ScalingConfig final ArgumentCaptor captor = ArgumentCaptor.forClass(RestoreDbClusterFromSnapshotRequest.class); verify(rdsProxy.client(), times(1)).restoreDBClusterFromSnapshot(captor.capture()); verify(rdsProxy.client(), times(3)).describeDBClusters(any(DescribeDbClustersRequest.class)); + verify(rdsProxy.client(), times(1)).describeDBClusterSnapshots(any(DescribeDbClusterSnapshotsRequest.class)); Assertions.assertThat(captor.getValue().serverlessV2ScalingConfiguration()).isNotNull(); Assertions.assertThat(captor.getValue().serverlessV2ScalingConfiguration()).isEqualTo( @@ -418,7 +514,10 @@ public void handleRequest_RestoreDbClusterFromSnapshot_ServerlessV2ScalingConfig public void handleRequest_RestoreDbClusterFromSnapshot_SetKmsKeyId() { when(rdsProxy.client().restoreDBClusterFromSnapshot(any(RestoreDbClusterFromSnapshotRequest.class))) .thenReturn(RestoreDbClusterFromSnapshotResponse.builder().build()); - + when(rdsProxy.client().describeDBClusterSnapshots(any(DescribeDbClusterSnapshotsRequest.class))) + .thenReturn(DescribeDbClusterSnapshotsResponse.builder().dbClusterSnapshots(DBClusterSnapshot.builder() + .dbClusterIdentifier(DBCLUSTER_IDENTIFIER) + .build()).build()); final CallbackContext context = new CallbackContext(); context.setModified(true); @@ -433,6 +532,7 @@ public void handleRequest_RestoreDbClusterFromSnapshot_SetKmsKeyId() { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(RestoreDbClusterFromSnapshotRequest.class); verify(rdsProxy.client(), times(1)).restoreDBClusterFromSnapshot(argumentCaptor.capture()); + verify(rdsProxy.client(), times(1)).describeDBClusterSnapshots(any(DescribeDbClusterSnapshotsRequest.class)); verify(rdsProxy.client(), times(2)).describeDBClusters(any(DescribeDbClustersRequest.class)); Assertions.assertThat(argumentCaptor.getValue().kmsKeyId()).isEqualTo(kmsKeyId); @@ -456,10 +556,70 @@ public void handleRequest_RestoreDbClusterToPointInTime_Success() { verify(rdsProxy.client(), times(1)).restoreDBClusterToPointInTime(any(RestoreDbClusterToPointInTimeRequest.class)); verify(rdsProxy.client(), times(1)).modifyDBCluster(any(ModifyDbClusterRequest.class)); - verify(rdsProxy.client(), times(3)).describeDBClusters(any(DescribeDbClustersRequest.class)); + verify(rdsProxy.client(), times(4)).describeDBClusters(any(DescribeDbClustersRequest.class)); + verify(rdsProxy.client(), times(1)).describeEvents(any(DescribeEventsRequest.class)); + } + + @Test + public void handleRequest_RestoreLimitlessDbClusterToPointInTime_Success() { + when(rdsProxy.client().restoreDBClusterToPointInTime(any(RestoreDbClusterToPointInTimeRequest.class))) + .thenReturn(RestoreDbClusterToPointInTimeResponse.builder().build()); + when(rdsProxy.client().modifyDBCluster(any(ModifyDbClusterRequest.class))) + .thenReturn(ModifyDbClusterResponse.builder().build()); + when(rdsProxy.client().describeEvents(any(DescribeEventsRequest.class))) + .thenReturn(DescribeEventsResponse.builder().build()); + + test_handleRequest_base( + new CallbackContext(), + () -> LIMITLESS_DBCLUSTER_ACTIVE, + () -> RESOURCE_MODEL_ON_RESTORE_IN_TIME_WITH_PIEM, + expectSuccess() + ); + + verify(rdsProxy.client(), times(1)).restoreDBClusterToPointInTime(any(RestoreDbClusterToPointInTimeRequest.class)); + verify(rdsProxy.client(), times(1)).modifyDBCluster(any(ModifyDbClusterRequest.class)); + verify(rdsProxy.client(), times(4)).describeDBClusters(any(DescribeDbClustersRequest.class)); verify(rdsProxy.client(), times(1)).describeEvents(any(DescribeEventsRequest.class)); } + @Test + public void handleRequest_RestoreLimitlessDbClusterToPointInTime_NoPIEMParamsModified() { + when(rdsProxy.client().restoreDBClusterToPointInTime(any(RestoreDbClusterToPointInTimeRequest.class))) + .thenReturn(RestoreDbClusterToPointInTimeResponse.builder().build()); + when(rdsProxy.client().describeEvents(any(DescribeEventsRequest.class))) + .thenReturn(DescribeEventsResponse.builder().build()); + + final CallbackContext context = new CallbackContext(); + + test_handleRequest_base( + context, + () -> LIMITLESS_DBCLUSTER_ACTIVE, + () -> RESOURCE_MODEL_ON_RESTORE_IN_TIME_WITH_PIEM.toBuilder() + .build(), + expectSuccess() + ); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(RestoreDbClusterToPointInTimeRequest.class); + + // for limitless clusters, PIEM params should go to restore API + final ArgumentCaptor restoreCaptor = ArgumentCaptor.forClass(RestoreDbClusterToPointInTimeRequest.class); + verify(rdsProxy.client(), times(1)).restoreDBClusterToPointInTime(restoreCaptor.capture()); + Assertions.assertThat(restoreCaptor.getValue().enablePerformanceInsights()).isEqualTo(RESOURCE_MODEL_ON_RESTORE_WITH_PIEM.getPerformanceInsightsEnabled()); + Assertions.assertThat(restoreCaptor.getValue().performanceInsightsKMSKeyId()).isEqualTo(RESOURCE_MODEL_ON_RESTORE_WITH_PIEM.getPerformanceInsightsKmsKeyId()); + Assertions.assertThat(restoreCaptor.getValue().performanceInsightsRetentionPeriod()).isEqualTo(RESOURCE_MODEL_ON_RESTORE_WITH_PIEM.getPerformanceInsightsRetentionPeriod()); + Assertions.assertThat(restoreCaptor.getValue().monitoringRoleArn()).isEqualTo(RESOURCE_MODEL_ON_RESTORE_WITH_PIEM.getMonitoringRoleArn()); + Assertions.assertThat(restoreCaptor.getValue().monitoringInterval()).isEqualTo(RESOURCE_MODEL_ON_RESTORE_WITH_PIEM.getMonitoringInterval()); + + verify(rdsProxy.client(), times(1)).restoreDBClusterToPointInTime(captor.capture()); + verify(rdsProxy.client(), times(4)).describeDBClusters(any(DescribeDbClustersRequest.class)); + + // for limitless clusters, PIEM params should not go to modify + final ArgumentCaptor modifyCaptor = ArgumentCaptor.forClass(ModifyDbClusterRequest.class); + verify(rdsProxy.client(), times(1)).modifyDBCluster(modifyCaptor.capture()); + Assertions.assertThat(modifyCaptor.getValue().enablePerformanceInsights()).isNull(); + Assertions.assertThat(modifyCaptor.getValue().performanceInsightsKMSKeyId()).isNull(); + } + @Test public void handleRequest_RestoreDbClusterToPointInTime_UpdateVpcSecurityGroups() { when(rdsProxy.client().restoreDBClusterToPointInTime(any(RestoreDbClusterToPointInTimeRequest.class))) @@ -468,6 +628,7 @@ public void handleRequest_RestoreDbClusterToPointInTime_UpdateVpcSecurityGroups( .thenReturn(ModifyDbClusterResponse.builder().build()); when(rdsProxy.client().describeEvents(any(DescribeEventsRequest.class))) .thenReturn(DescribeEventsResponse.builder().build()); + test_handleRequest_base( new CallbackContext(), () -> DBCLUSTER_ACTIVE, @@ -481,7 +642,7 @@ public void handleRequest_RestoreDbClusterToPointInTime_UpdateVpcSecurityGroups( verify(rdsProxy.client(), times(1)).restoreDBClusterToPointInTime(argumentCaptor.capture()); Assertions.assertThat(argumentCaptor.getValue().vpcSecurityGroupIds()).isEqualTo(VPC_SG_IDS); verify(rdsProxy.client(), times(1)).modifyDBCluster(any(ModifyDbClusterRequest.class)); - verify(rdsProxy.client(), times(3)).describeDBClusters(any(DescribeDbClustersRequest.class)); + verify(rdsProxy.client(), times(4)).describeDBClusters(any(DescribeDbClustersRequest.class)); verify(rdsProxy.client(), times(1)).describeEvents(any(DescribeEventsRequest.class)); } @@ -505,7 +666,7 @@ public void handleRequest_RestoreDbClusterToPointInTime_ServerlessV2ScalingConfi final ArgumentCaptor captor = ArgumentCaptor.forClass(RestoreDbClusterToPointInTimeRequest.class); verify(rdsProxy.client(), times(1)).restoreDBClusterToPointInTime(captor.capture()); - verify(rdsProxy.client(), times(3)).describeDBClusters(any(DescribeDbClustersRequest.class)); + verify(rdsProxy.client(), times(4)).describeDBClusters(any(DescribeDbClustersRequest.class)); Assertions.assertThat(captor.getValue().serverlessV2ScalingConfiguration()).isNotNull(); Assertions.assertThat(captor.getValue().serverlessV2ScalingConfiguration()).isEqualTo( @@ -540,7 +701,7 @@ public void handleRequest_RestoreDbClusterToPointInTime_AccessDeniedTagging() { ResourceHandlerRequest.builder() .systemTags(Translator.translateTagsToRequest(Translator.translateTagsFromSdk(TAG_SET.getSystemTags()))) .desiredResourceTags(Translator.translateTagsToRequest(Translator.translateTagsFromSdk(TAG_SET.getStackTags()))), - null, + () -> DBCLUSTER_ACTIVE, null, () -> RESOURCE_MODEL_ON_RESTORE_IN_TIME.toBuilder() .tags(Translator.translateTagsFromSdk(TAG_SET.getResourceTags())) @@ -579,7 +740,7 @@ public void handleRequest_RestoreDbClusterToPointInTime_SetEnableCloudwatchLogsE final ArgumentCaptor restoreCaptor = ArgumentCaptor.forClass(RestoreDbClusterToPointInTimeRequest.class); verify(rdsProxy.client(), times(1)).restoreDBClusterToPointInTime(restoreCaptor.capture()); verify(rdsProxy.client(), times(1)).modifyDBCluster(any(ModifyDbClusterRequest.class)); - verify(rdsProxy.client(), times(3)).describeDBClusters(any(DescribeDbClustersRequest.class)); + verify(rdsProxy.client(), times(4)).describeDBClusters(any(DescribeDbClustersRequest.class)); verify(rdsProxy.client(), times(1)).describeEvents(any(DescribeEventsRequest.class)); Assertions.assertThat(restoreCaptor.getValue().enableCloudwatchLogsExports()).containsExactlyElementsOf(cloudwatchLogsExports); @@ -604,7 +765,7 @@ public void handleRequest_RestoreDbClusterToPointInTime_ModifyAfterCreate() { verify(rdsProxy.client(), times(1)).restoreDBClusterToPointInTime(any(RestoreDbClusterToPointInTimeRequest.class)); verify(rdsProxy.client(), times(1)).modifyDBCluster(any(ModifyDbClusterRequest.class)); - verify(rdsProxy.client(), times(3)).describeDBClusters(any(DescribeDbClustersRequest.class)); + verify(rdsProxy.client(), times(4)).describeDBClusters(any(DescribeDbClustersRequest.class)); verify(rdsProxy.client(), times(1)).describeEvents(any(DescribeEventsRequest.class)); } @@ -632,7 +793,7 @@ public void handleRequest_RestoreDbClusterToPointInTime_RestoreAsClone() { final ArgumentCaptor captor = ArgumentCaptor.forClass(RestoreDbClusterToPointInTimeRequest.class); verify(rdsProxy.client(), times(1)).restoreDBClusterToPointInTime(captor.capture()); verify(rdsProxy.client(), times(1)).modifyDBCluster(any(ModifyDbClusterRequest.class)); - verify(rdsProxy.client(), times(3)).describeDBClusters(any(DescribeDbClustersRequest.class)); + verify(rdsProxy.client(), times(4)).describeDBClusters(any(DescribeDbClustersRequest.class)); Assertions.assertThat(captor.getValue().restoreType()).isEqualTo(RESTORE_TYPE_COPY_ON_WRITE); @@ -684,6 +845,149 @@ public void handleRequest_CreateDbCluster_SetDefaultPortForServerlessPostgresql( Assertions.assertThat(captor.getValue().port()).isEqualTo(5432); } + @Test + public void getClusterScalabilityType_FromClusterSnapshot_Limitless() { + when(rdsProxy.client().describeDBClusterSnapshots(any(DescribeDbClusterSnapshotsRequest.class))) + .thenReturn(DescribeDbClusterSnapshotsResponse.builder().dbClusterSnapshots(DBClusterSnapshot.builder().engineVersion("16.4-limitless").build()).build()); + ClusterScalabilityType clusterScalabilityType = handler.getClusterScalabilityTypeFromSnapshot(rdsProxy, RESOURCE_MODEL_ON_RESTORE_WITH_PIEM); + Assertions.assertThat(clusterScalabilityType).isEqualTo(ClusterScalabilityType.LIMITLESS); + rdsClient.serviceName(); + } + + @Test + public void getClusterScalabilityType_FromClusterSnapshot_Standard() { + when(rdsProxy.client().describeDBClusterSnapshots(any(DescribeDbClusterSnapshotsRequest.class))) + .thenReturn(DescribeDbClusterSnapshotsResponse.builder().dbClusterSnapshots(DBClusterSnapshot.builder().engineVersion("16.4").build()).build()); + ClusterScalabilityType clusterScalabilityType = handler.getClusterScalabilityTypeFromSnapshot(rdsProxy, RESOURCE_MODEL_ON_RESTORE_WITH_PIEM); + Assertions.assertThat(clusterScalabilityType).isEqualTo(ClusterScalabilityType.STANDARD); + rdsClient.serviceName(); + } + + @Test + public void getClusterScalabilityType_FromClusterSnapshot_NoPermission_Standard() { + when(rdsProxy.client().describeDBClusterSnapshots(any(DescribeDbClusterSnapshotsRequest.class))) + .thenThrow( RdsException.builder() + .awsErrorDetails(AwsErrorDetails.builder() + .errorCode(ErrorCode.AccessDeniedException.toString()) + .build() + ).build()); + RequestLogger requestLoggerMock = mock(RequestLogger.class); + handler.requestLogger = requestLoggerMock; + ClusterScalabilityType clusterScalabilityType = handler.getClusterScalabilityTypeFromSnapshot(rdsProxy, RESOURCE_MODEL_ON_RESTORE_WITH_PIEM); + verify(requestLoggerMock, times(1)).log(eq("DBClusterValidationMissingPermissions"), eq(Map.of("MissingPermission", "DescribeDBClusterSnapshots"))); + Assertions.assertThat(clusterScalabilityType).isEqualTo(ClusterScalabilityType.STANDARD); + rdsClient.serviceName(); + } + + @Test + public void getClusterScalabilityType_FromInstanceSnapshot_Standard() { + ClusterScalabilityType clusterScalabilityType = handler.getClusterScalabilityTypeFromSnapshot(rdsProxy, RESOURCE_MODEL_ON_RESTORE_SNAPSHOT_WITH_PIEM); + verify(rdsProxy.client(), never()).describeDBClusterSnapshots(); + Assertions.assertThat(clusterScalabilityType).isEqualTo(ClusterScalabilityType.STANDARD); + rdsClient.serviceName(); + } + + @Test + public void getClusterScalabilityType_PointInTime_Limitless() { + when(rdsProxy.client().describeDBClusters(any(DescribeDbClustersRequest.class))) + .thenReturn(DescribeDbClustersResponse.builder().dbClusters(DBCluster.builder().clusterScalabilityType(ClusterScalabilityType.LIMITLESS).build()).build()); + ClusterScalabilityType clusterScalabilityType = handler.getClusterScalabilityTypeFromSourceDBCluster("111111111111", rdsProxy, RESOURCE_MODEL_ON_RESTORE_IN_TIME_WITH_PIEM); + Assertions.assertThat(clusterScalabilityType).isEqualTo(ClusterScalabilityType.LIMITLESS); + rdsClient.serviceName(); + } + + @Test + public void getClusterScalabilityType_PointInTime_Standard() { + when(rdsProxy.client().describeDBClusters(any(DescribeDbClustersRequest.class))) + .thenReturn(DescribeDbClustersResponse.builder().dbClusters(DBCluster.builder().build()).build()); + ClusterScalabilityType clusterScalabilityType = handler.getClusterScalabilityTypeFromSourceDBCluster("111111111111", rdsProxy, RESOURCE_MODEL_ON_RESTORE_IN_TIME_WITH_PIEM); + Assertions.assertThat(clusterScalabilityType).isEqualTo(ClusterScalabilityType.STANDARD); + rdsClient.serviceName(); + } + + + @Test + public void handleRequest_RestoreStandardDbClusterFromSnapshot_NoPIEMParamsRestored() { + when(rdsProxy.client().restoreDBClusterFromSnapshot(any(RestoreDbClusterFromSnapshotRequest.class))) + .thenReturn(RestoreDbClusterFromSnapshotResponse.builder().build()); + when(rdsProxy.client().describeEvents(any(DescribeEventsRequest.class))) + .thenReturn(DescribeEventsResponse.builder().build()); + when(rdsProxy.client().describeDBClusterSnapshots(any(DescribeDbClusterSnapshotsRequest.class))) + .thenReturn(DescribeDbClusterSnapshotsResponse.builder().dbClusterSnapshots(DBClusterSnapshot.builder() + .dbClusterIdentifier(DBCLUSTER_IDENTIFIER) + .build()).build()); + + final CallbackContext context = new CallbackContext(); + + test_handleRequest_base( + context, + () -> DBCLUSTER_ACTIVE, + () -> RESOURCE_MODEL_ON_RESTORE_WITH_PIEM.toBuilder() + .build(), + expectSuccess() + ); + + verify(rdsProxy.client(), times(3)).describeDBClusters(any(DescribeDbClustersRequest.class)); + verify(rdsProxy.client(), times(1)).describeDBClusterSnapshots(any(DescribeDbClusterSnapshotsRequest.class)); + + // for standard clusters, PIEM params should go to modify + final ArgumentCaptor modifyCaptor = ArgumentCaptor.forClass(ModifyDbClusterRequest.class); + verify(rdsProxy.client(), times(1)).modifyDBCluster(modifyCaptor.capture()); + Assertions.assertThat(modifyCaptor.getValue().enablePerformanceInsights()).isEqualTo(RESOURCE_MODEL_ON_RESTORE_WITH_PIEM.getPerformanceInsightsEnabled()); + Assertions.assertThat(modifyCaptor.getValue().performanceInsightsKMSKeyId()).isEqualTo(RESOURCE_MODEL_ON_RESTORE_WITH_PIEM.getPerformanceInsightsKmsKeyId()); + Assertions.assertThat(modifyCaptor.getValue().performanceInsightsRetentionPeriod()).isEqualTo(RESOURCE_MODEL_ON_RESTORE_WITH_PIEM.getPerformanceInsightsRetentionPeriod()); + Assertions.assertThat(modifyCaptor.getValue().monitoringRoleArn()).isEqualTo(RESOURCE_MODEL_ON_RESTORE_WITH_PIEM.getMonitoringRoleArn()); + Assertions.assertThat(modifyCaptor.getValue().monitoringInterval()).isEqualTo(RESOURCE_MODEL_ON_RESTORE_WITH_PIEM.getMonitoringInterval()); + + // for standard clusters, PIEM params should not got to restore + final ArgumentCaptor restoreCaptor = ArgumentCaptor.forClass(RestoreDbClusterFromSnapshotRequest.class); + verify(rdsProxy.client(), times(1)).restoreDBClusterFromSnapshot(restoreCaptor.capture()); + Assertions.assertThat(restoreCaptor.getValue().enablePerformanceInsights()).isNull(); + Assertions.assertThat(restoreCaptor.getValue().performanceInsightsKMSKeyId()).isNull(); + Assertions.assertThat(restoreCaptor.getValue().performanceInsightsRetentionPeriod()).isNull(); + Assertions.assertThat(restoreCaptor.getValue().monitoringRoleArn()).isNull(); + Assertions.assertThat(restoreCaptor.getValue().monitoringInterval()).isNull(); + } + + @Test + public void handleRequest_RestoreStandardDbClusterToPointInTime_NoPIEMParamsRestored () { + when(rdsProxy.client().restoreDBClusterToPointInTime(any(RestoreDbClusterToPointInTimeRequest.class))) + .thenReturn(RestoreDbClusterToPointInTimeResponse.builder().build()); + when(rdsProxy.client().describeEvents(any(DescribeEventsRequest.class))) + .thenReturn(DescribeEventsResponse.builder().build()); + + final CallbackContext context = new CallbackContext(); + + test_handleRequest_base( + context, + () -> DBCLUSTER_ACTIVE, + () -> RESOURCE_MODEL_ON_RESTORE_IN_TIME_WITH_PIEM.toBuilder() + .build(), + expectSuccess() + ); + + verify(rdsProxy.client(), times(4)).describeDBClusters(any(DescribeDbClustersRequest.class)); + + // for standard clusters, PIEM params should go to modify + final ArgumentCaptor modifyCaptor = ArgumentCaptor.forClass(ModifyDbClusterRequest.class); + verify(rdsProxy.client(), times(1)).modifyDBCluster(modifyCaptor.capture()); + Assertions.assertThat(modifyCaptor.getValue().enablePerformanceInsights()).isEqualTo(RESOURCE_MODEL_ON_RESTORE_IN_TIME_WITH_PIEM.getPerformanceInsightsEnabled()); + Assertions.assertThat(modifyCaptor.getValue().performanceInsightsKMSKeyId()).isEqualTo(RESOURCE_MODEL_ON_RESTORE_IN_TIME_WITH_PIEM.getPerformanceInsightsKmsKeyId()); + Assertions.assertThat(modifyCaptor.getValue().performanceInsightsRetentionPeriod()).isEqualTo(RESOURCE_MODEL_ON_RESTORE_IN_TIME_WITH_PIEM.getPerformanceInsightsRetentionPeriod()); + Assertions.assertThat(modifyCaptor.getValue().monitoringRoleArn()).isEqualTo(RESOURCE_MODEL_ON_RESTORE_IN_TIME_WITH_PIEM.getMonitoringRoleArn()); + Assertions.assertThat(modifyCaptor.getValue().monitoringInterval()).isEqualTo(RESOURCE_MODEL_ON_RESTORE_IN_TIME_WITH_PIEM.getMonitoringInterval()); + + // for standard clusters, PIEM params should not go to restore + final ArgumentCaptor restoreCaptor = ArgumentCaptor.forClass(RestoreDbClusterToPointInTimeRequest.class); + verify(rdsProxy.client(), times(1)).restoreDBClusterToPointInTime(restoreCaptor.capture()); + Assertions.assertThat(restoreCaptor.getValue().enablePerformanceInsights()).isNull(); + Assertions.assertThat(restoreCaptor.getValue().performanceInsightsKMSKeyId()).isNull(); + Assertions.assertThat(restoreCaptor.getValue().performanceInsightsRetentionPeriod()).isNull(); + Assertions.assertThat(restoreCaptor.getValue().monitoringRoleArn()).isNull(); + Assertions.assertThat(restoreCaptor.getValue().monitoringInterval()).isNull(); + + } + static class CreateDBClusterExceptionArgumentsProvider implements ArgumentsProvider { @Override public Stream provideArguments(ExtensionContext extensionContext) throws Exception { @@ -720,16 +1024,14 @@ public void handleRequest_CreateDBCluster_HandleException( public void handleRequest_CreateDBCluster_DBClusterInTerminalState() { final CallbackContext context = new CallbackContext(); - Assertions.assertThatThrownBy(() -> { - test_handleRequest_base( - context, - () -> DBCLUSTER_ACTIVE.toBuilder() - .status(DBClusterStatus.InaccessibleEncryptionCredentials.toString()) - .build(), - () -> RESOURCE_MODEL, - expectFailed(HandlerErrorCode.NotStabilized) - ); - }).isInstanceOf(CfnNotStabilizedException.class); + test_handleRequest_base( + context, + () -> DBCLUSTER_ACTIVE.toBuilder() + .status(DBClusterStatus.InaccessibleEncryptionCredentials.toString()) + .build(), + () -> RESOURCE_MODEL, + expectFailed(HandlerErrorCode.NotStabilized) + ); verify(rdsProxy.client(), times(1)).createDBCluster(any(CreateDbClusterRequest.class)); } diff --git a/aws-rds-dbcluster/src/test/java/software/amazon/rds/dbcluster/SchemaTest.java b/aws-rds-dbcluster/src/test/java/software/amazon/rds/dbcluster/SchemaTest.java index 21309e477..da6e51082 100644 --- a/aws-rds-dbcluster/src/test/java/software/amazon/rds/dbcluster/SchemaTest.java +++ b/aws-rds-dbcluster/src/test/java/software/amazon/rds/dbcluster/SchemaTest.java @@ -155,6 +155,21 @@ void testDrift_EnableHttpEndpoint_Aurora_Postgresql_Drifted() { }).isInstanceOf(AssertionError.class); } + @Test + void testDrift_EnableHttpEndpoint_Aurora_Mysql_Drifted() { + final ResourceModel input = ResourceModel.builder() + .enableHttpEndpoint(true) + .engine("aurora-mysql") + .build(); + final ResourceModel output = ResourceModel.builder() + .enableHttpEndpoint(false) + .engine("aurora-mysql") + .build(); + Assertions.assertThatThrownBy(() -> { + assertResourceNotDrifted(input, output, resourceSchema); + }).isInstanceOf(AssertionError.class); + } + @Test void testDrift_EnableHttpEndpoint_Provisioned() { final ResourceModel input = ResourceModel.builder() diff --git a/aws-rds-dbcluster/src/test/java/software/amazon/rds/dbcluster/TranslatorTest.java b/aws-rds-dbcluster/src/test/java/software/amazon/rds/dbcluster/TranslatorTest.java index 81f9ba867..6b34fa49a 100644 --- a/aws-rds-dbcluster/src/test/java/software/amazon/rds/dbcluster/TranslatorTest.java +++ b/aws-rds-dbcluster/src/test/java/software/amazon/rds/dbcluster/TranslatorTest.java @@ -10,6 +10,7 @@ import com.google.common.collect.ImmutableList; import software.amazon.awssdk.services.ec2.Ec2Client; import software.amazon.awssdk.services.rds.RdsClient; +import software.amazon.awssdk.services.rds.model.ClusterScalabilityType; import software.amazon.awssdk.services.rds.model.CreateDbClusterRequest; import software.amazon.awssdk.services.rds.model.DBCluster; import software.amazon.awssdk.services.rds.model.DomainMembership; @@ -30,6 +31,11 @@ public class TranslatorTest extends AbstractHandlerTest { private final static String STORAGE_TYPE_GP3 = "gp3"; private final static boolean IS_NOT_ROLLBACK = false; private final static boolean IS_ROLLBACK = true; + private final static String MONITORING_ROLE_ARN = "arn:aws:iam::999999999999:role/emaaccess"; + private final static int MONITORING_INTERVAL = 30; + private final static boolean ENABLE_PERFORMANCE_INSIGHTS= true; + private final static int PERFORMANCE_INSIGHTS_RETENTION_PERIOD = 31; + private final static String PERFORMANCE_INSIGHTS_KMS_KEY_ID = "arn:aws:kms:999999999999:key/key"; @Test @@ -89,6 +95,44 @@ public void restoreDbClusterToPointInTimeRequest_setStorageType() { assertThat(request.iops()).isEqualTo(100); } + @Test + public void restoreDbClusterToPointInTimeRequest_validatePiEmParams_StandardPath() { + final ResourceModel model = ResourceModel.builder() + .monitoringRoleArn(MONITORING_ROLE_ARN) + .monitoringInterval(MONITORING_INTERVAL) + .performanceInsightsEnabled(ENABLE_PERFORMANCE_INSIGHTS) + .performanceInsightsRetentionPeriod(PERFORMANCE_INSIGHTS_RETENTION_PERIOD) + .performanceInsightsKmsKeyId(PERFORMANCE_INSIGHTS_KMS_KEY_ID) + .clusterScalabilityType(ClusterScalabilityType.STANDARD.toString()) + .build(); + + final RestoreDbClusterToPointInTimeRequest request = Translator.restoreDbClusterToPointInTimeRequest(model, Tagging.TagSet.emptySet()); + assertThat(request.monitoringRoleArn()).isNull(); + assertThat(request.monitoringInterval()).isNull(); + assertThat(request.enablePerformanceInsights()).isNull(); + assertThat(request.performanceInsightsRetentionPeriod()).isNull(); + assertThat(request.performanceInsightsKMSKeyId()).isNull(); + } + + @Test + public void restoreDbClusterToPointInTimeRequest_validatePiEmParams_LimitlessPath() { + final ResourceModel model = ResourceModel.builder() + .monitoringRoleArn(MONITORING_ROLE_ARN) + .monitoringInterval(MONITORING_INTERVAL) + .performanceInsightsEnabled(ENABLE_PERFORMANCE_INSIGHTS) + .performanceInsightsRetentionPeriod(PERFORMANCE_INSIGHTS_RETENTION_PERIOD) + .performanceInsightsKmsKeyId(PERFORMANCE_INSIGHTS_KMS_KEY_ID) + .clusterScalabilityType(ClusterScalabilityType.LIMITLESS.toString()) + .build(); + + final RestoreDbClusterToPointInTimeRequest request = Translator.restoreLimitlessDbClusterToPointInTimeRequest(model, Tagging.TagSet.emptySet()); + assertThat(request.monitoringRoleArn()).isEqualTo(MONITORING_ROLE_ARN); + assertThat(request.monitoringInterval()).isEqualTo(MONITORING_INTERVAL); + assertThat(request.enablePerformanceInsights()).isEqualTo(ENABLE_PERFORMANCE_INSIGHTS); + assertThat(request.performanceInsightsRetentionPeriod()).isEqualTo(PERFORMANCE_INSIGHTS_RETENTION_PERIOD); + assertThat(request.performanceInsightsKMSKeyId()).isEqualTo(PERFORMANCE_INSIGHTS_KMS_KEY_ID); + } + @Test public void modifyDbClusterRequest_omitPreferredMaintenanceWindowIfUnchanged() { final ResourceModel model = RESOURCE_MODEL.toBuilder().preferredMaintenanceWindow("old").build(); @@ -594,6 +638,44 @@ public void restoreDBClusterFromSnapshotServerlessV2() { assertThat(modifyRequest.serverlessV2ScalingConfiguration()).isNull(); } + @Test + public void restoreDbClusterFromSnapshot_validatePiEmParams_LimitlessPath() { + final ResourceModel model = ResourceModel.builder() + .monitoringRoleArn(MONITORING_ROLE_ARN) + .monitoringInterval(MONITORING_INTERVAL) + .performanceInsightsEnabled(ENABLE_PERFORMANCE_INSIGHTS) + .performanceInsightsRetentionPeriod(PERFORMANCE_INSIGHTS_RETENTION_PERIOD) + .performanceInsightsKmsKeyId(PERFORMANCE_INSIGHTS_KMS_KEY_ID) + .clusterScalabilityType(ClusterScalabilityType.LIMITLESS.toString()) + .build(); + + final RestoreDbClusterFromSnapshotRequest request = Translator.restoreLimitlessDbClusterFromSnapshotRequest(model, Tagging.TagSet.emptySet()); + assertThat(request.monitoringRoleArn()).isEqualTo(MONITORING_ROLE_ARN); + assertThat(request.monitoringInterval()).isEqualTo(MONITORING_INTERVAL); + assertThat(request.enablePerformanceInsights()).isEqualTo(ENABLE_PERFORMANCE_INSIGHTS); + assertThat(request.performanceInsightsRetentionPeriod()).isEqualTo(PERFORMANCE_INSIGHTS_RETENTION_PERIOD); + assertThat(request.performanceInsightsKMSKeyId()).isEqualTo(PERFORMANCE_INSIGHTS_KMS_KEY_ID); + } + + @Test + public void restoreDbClusterFromSnapshot_validatePiEmParams_StandardPath() { + final ResourceModel model = ResourceModel.builder() + .monitoringRoleArn(MONITORING_ROLE_ARN) + .monitoringInterval(MONITORING_INTERVAL) + .performanceInsightsEnabled(ENABLE_PERFORMANCE_INSIGHTS) + .performanceInsightsRetentionPeriod(PERFORMANCE_INSIGHTS_RETENTION_PERIOD) + .performanceInsightsKmsKeyId(PERFORMANCE_INSIGHTS_KMS_KEY_ID) + .clusterScalabilityType(ClusterScalabilityType.STANDARD.toString()) + .build(); + + final RestoreDbClusterFromSnapshotRequest request = Translator.restoreDbClusterFromSnapshotRequest(model, Tagging.TagSet.emptySet()); + assertThat(request.monitoringRoleArn()).isNull(); + assertThat(request.monitoringInterval()).isNull(); + assertThat(request.enablePerformanceInsights()).isNull(); + assertThat(request.performanceInsightsRetentionPeriod()).isNull(); + assertThat(request.performanceInsightsKMSKeyId()).isNull(); + } + @Test public void restoreDBClusterToPointInTimePort() { final ResourceModel model = ResourceModel.builder() diff --git a/aws-rds-dbcluster/src/test/java/software/amazon/rds/dbcluster/UpdateHandlerTest.java b/aws-rds-dbcluster/src/test/java/software/amazon/rds/dbcluster/UpdateHandlerTest.java index ba14cd845..a3118a7ea 100644 --- a/aws-rds-dbcluster/src/test/java/software/amazon/rds/dbcluster/UpdateHandlerTest.java +++ b/aws-rds-dbcluster/src/test/java/software/amazon/rds/dbcluster/UpdateHandlerTest.java @@ -25,6 +25,7 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.ArgumentsProvider; import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.CsvSource; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -439,6 +440,87 @@ void handleRequest_WithUpdateToDefaultVPC() { verify(rdsProxy.client(), times(1)).describeEvents(any(DescribeEventsRequest.class)); } + @Test + void handleRequest_WithUpdateToDefaultVPC2_previousNotNull_desiredNotNull() { + when(rdsProxy.client().describeEvents(any(DescribeEventsRequest.class))) + .thenReturn(DescribeEventsResponse.builder().build()); + + final ResourceModel resourceModel = RESOURCE_MODEL_EMPTY_VPC.toBuilder().build(); + final CallbackContext context = new CallbackContext(); + context.setModified(true); + context.setAddTagsComplete(true); + + // When the previous.vPCSecurityGroups != null && desired.vPCSecurityGroups != null + test_handleRequest_base( + context, + ResourceHandlerRequest.builder() + .previousResourceTags(Translator.translateTagsToRequest(TAG_LIST)) + .desiredResourceTags(Translator.translateTagsToRequest(TAG_LIST)), + () -> DBCLUSTER_ACTIVE, + () -> resourceModel.toBuilder().vpcSecurityGroupIds(ImmutableList.of("group-id")).build(), + () -> resourceModel.toBuilder().vpcSecurityGroupIds(ImmutableList.of("group-id")).build(), + expectSuccess() + ); + + // Expect that the security group has NOT been updated + verify(rdsProxy.client(), times(1)).describeDBClusters(any(DescribeDbClustersRequest.class)); + verify(rdsProxy.client(), times(1)).describeEvents(any(DescribeEventsRequest.class)); + } + + @Test + void handleRequest_WithUpdateToDefaultVPC2_previousNull_desiredNotNull() { + when(rdsProxy.client().describeEvents(any(DescribeEventsRequest.class))) + .thenReturn(DescribeEventsResponse.builder().build()); + + final ResourceModel resourceModel = RESOURCE_MODEL_EMPTY_VPC.toBuilder().build(); + final CallbackContext context = new CallbackContext(); + context.setModified(true); + context.setAddTagsComplete(true); + + // When the previous.vPCSecurityGroups == null && desired.vPCSecurityGroups != null + test_handleRequest_base( + context, + ResourceHandlerRequest.builder() + .previousResourceTags(Translator.translateTagsToRequest(TAG_LIST)) + .desiredResourceTags(Translator.translateTagsToRequest(TAG_LIST)), + () -> DBCLUSTER_ACTIVE, + () -> resourceModel.toBuilder().vpcSecurityGroupIds(ImmutableList.of()).build(), + () -> resourceModel.toBuilder().vpcSecurityGroupIds(ImmutableList.of("group-id")).build(), + expectSuccess() + ); + + // Expect that the security group has NOT been updated + verify(rdsProxy.client(), times(1)).describeDBClusters(any(DescribeDbClustersRequest.class)); + verify(rdsProxy.client(), times(1)).describeEvents(any(DescribeEventsRequest.class)); + } + + @Test + void handleRequest_WithUpdateToDefaultVPC2_previousNull_desiredNull() { + when(rdsProxy.client().describeEvents(any(DescribeEventsRequest.class))) + .thenReturn(DescribeEventsResponse.builder().build()); + + final ResourceModel resourceModel = RESOURCE_MODEL_EMPTY_VPC.toBuilder().build(); + final CallbackContext context = new CallbackContext(); + context.setModified(true); + context.setAddTagsComplete(true); + + // When the previous.vPCSecurityGroups == null && desired.vPCSecurityGroups == null + test_handleRequest_base( + context, + ResourceHandlerRequest.builder() + .previousResourceTags(Translator.translateTagsToRequest(TAG_LIST)) + .desiredResourceTags(Translator.translateTagsToRequest(TAG_LIST)), + () -> DBCLUSTER_ACTIVE, + () -> resourceModel.toBuilder().vpcSecurityGroupIds(ImmutableList.of()).build(), + () -> resourceModel.toBuilder().vpcSecurityGroupIds(ImmutableList.of()).build(), + expectSuccess() + ); + + // Expect that the security group has NOT been updated + verify(rdsProxy.client(), times(1)).describeDBClusters(any(DescribeDbClustersRequest.class)); + verify(rdsProxy.client(), times(1)).describeEvents(any(DescribeEventsRequest.class)); + } + @Test void handleRequest_WithUpdateToDefaultVpcFromDefaultVpc() { final ResourceModel resourceModel = RESOURCE_MODEL_EMPTY_VPC.toBuilder().build(); @@ -824,8 +906,18 @@ void handleRequest_EngineVersionUpdateIfMismatch() { Assertions.assertThat(argument.getValue().allowMajorVersionUpgrade()).isTrue(); } - @Test - void handleRequest_ServerlessV2ScalingConfiguration_Success() { + @ParameterizedTest + @CsvSource({ + "1, , 3, , ", // modify minCapacity + "1, , 0, 600, 600", // enable auto-pause with specific seconds until auto-pause + "1, , 0, , 300", // enable auto-pause with default seconds until auto-pause + "0, 600, 0, , 300", // reset seconds until auto-pause to default + " , , 3, , " // update from null to non-null capacity [CFN-582] + }) + void handleRequest_ServerlessV2ScalingConfiguration_Success( + final Double minCapacityBefore, final Integer secondsUntilAutoPauseBefore, + final Double minCapacityAfter, final Integer secondsUntilAutoPauseAfter, + final Integer expectedSecondsUntilAutoPause) { when(rdsProxy.client().modifyDBCluster(any(ModifyDbClusterRequest.class))) .thenReturn(ModifyDbClusterResponse.builder().build()); when(rdsProxy.client().removeTagsFromResource(any(RemoveTagsFromResourceRequest.class))) @@ -841,17 +933,22 @@ void handleRequest_ServerlessV2ScalingConfiguration_Success() { transitions.add(DBCLUSTER_ACTIVE_NO_ROLE); final ServerlessV2ScalingConfiguration previousServerlessV2ScalingConfiguration = ServerlessV2ScalingConfiguration.builder() - .minCapacity(1.0) + .minCapacity(minCapacityBefore) .maxCapacity(2.0) + .secondsUntilAutoPause(secondsUntilAutoPauseBefore) .build(); final ServerlessV2ScalingConfiguration desiredServerlessV2ScalingConfiguration = ServerlessV2ScalingConfiguration.builder() - .minCapacity(3.0) + .minCapacity(minCapacityAfter) .maxCapacity(4.0) + .secondsUntilAutoPause(secondsUntilAutoPauseAfter) .build(); + final CallbackContext callbackContext = new CallbackContext(); + callbackContext.setWaitTime(301); // force a large wait time so we don't have to wait during tests + test_handleRequest_base( - new CallbackContext(), + callbackContext, ResourceHandlerRequest.builder() .previousResourceTags(Translator.translateTagsToRequest(TAG_LIST)) .desiredResourceTags(Translator.translateTagsToRequest(TAG_LIST_ALTER)), @@ -881,6 +978,7 @@ void handleRequest_ServerlessV2ScalingConfiguration_Success() { .isEqualTo(software.amazon.awssdk.services.rds.model.ServerlessV2ScalingConfiguration.builder() .maxCapacity(desiredServerlessV2ScalingConfiguration.getMaxCapacity()) .minCapacity(desiredServerlessV2ScalingConfiguration.getMinCapacity()) + .secondsUntilAutoPause(expectedSecondsUntilAutoPause) .build()); } @@ -997,7 +1095,7 @@ void handleRequest_ModifyDBCluster_HandleException( } @Test - public void handleRequest_EngineLifecycleSupportShouldFail() { + void handleRequest_EngineLifecycleSupportShouldFail() { expectServiceInvocation = false; test_handleRequest_base( new CallbackContext(), diff --git a/aws-rds-dbcluster/src/test/java/software/amazon/rds/dbcluster/util/ResourceModelHelperTest.java b/aws-rds-dbcluster/src/test/java/software/amazon/rds/dbcluster/util/ResourceModelHelperTest.java new file mode 100644 index 000000000..254058840 --- /dev/null +++ b/aws-rds-dbcluster/src/test/java/software/amazon/rds/dbcluster/util/ResourceModelHelperTest.java @@ -0,0 +1,100 @@ +package software.amazon.rds.dbcluster.util; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import software.amazon.rds.dbcluster.EngineMode; +import software.amazon.rds.dbcluster.ResourceModel; + +public class ResourceModelHelperTest { + + private ResourceModel model; + + @BeforeEach + void setUp() { + model = new ResourceModel(); + } + + @Test + void isRestoreToPointInTime_withSourceDBClusterIdentifier_returnsTrue() { + model.setSourceDBClusterIdentifier("source-cluster-id"); + Assertions.assertTrue(ResourceModelHelper.isRestoreToPointInTime(model)); + } + + @Test + void isRestoreToPointInTime_withNullSourceDBClusterIdentifier_returnsFalse() { + model.setSourceDBClusterIdentifier(null); + Assertions.assertFalse(ResourceModelHelper.isRestoreToPointInTime(model)); + } + + @Test + void isRestoreToPointInTime_withEmptySourceDBClusterIdentifier_returnsFalse() { + model.setSourceDBClusterIdentifier(""); + Assertions.assertFalse(ResourceModelHelper.isRestoreToPointInTime(model)); + } + + @Test + void isRestoreFromSnapshot_withSnapshotIdentifier_returnsTrue() { + model.setSnapshotIdentifier("snapshot-id"); + Assertions.assertTrue(ResourceModelHelper.isRestoreFromSnapshot(model)); + } + + @Test + void isRestoreFromSnapshot_withNullSnapshotIdentifier_returnsFalse() { + model.setSnapshotIdentifier(null); + Assertions.assertFalse(ResourceModelHelper.isRestoreFromSnapshot(model)); + } + + @Test + void isRestoreFromSnapshot_withEmptySnapshotIdentifier_returnsFalse() { + model.setSnapshotIdentifier(""); + Assertions.assertFalse(ResourceModelHelper.isRestoreFromSnapshot(model)); + } + + @Test + void shouldUpdateAfterCreate_withSnapshotRestore_returnsTrue() { + model.setSnapshotIdentifier("snapshot-id"); + Assertions.assertTrue(ResourceModelHelper.shouldUpdateAfterCreate(model)); + } + + @Test + void shouldUpdateAfterCreate_withPointInTimeRestore_returnsTrue() { + model.setSourceDBClusterIdentifier("source-cluster-id"); + Assertions.assertTrue(ResourceModelHelper.shouldUpdateAfterCreate(model)); + } + + @Test + void shouldUpdateAfterCreate_withNoRestore_returnsFalse() { + model.setSnapshotIdentifier(null); + model.setSourceDBClusterIdentifier(null); + Assertions.assertFalse(ResourceModelHelper.shouldUpdateAfterCreate(model)); + } + + @Test + void shouldEnableHttpEndpointV2AfterCreate_withEnabledHttpAndNotServerless_returnsTrue() { + model.setEnableHttpEndpoint(true); + model.setEngineMode(EngineMode.Provisioned.toString()); + Assertions.assertTrue(ResourceModelHelper.shouldEnableHttpEndpointV2AfterCreate(model)); + } + + @Test + void shouldEnableHttpEndpointV2AfterCreate_withDisabledHttp_returnsFalse() { + model.setEnableHttpEndpoint(false); + model.setEngineMode(EngineMode.Provisioned.toString()); + Assertions.assertFalse(ResourceModelHelper.shouldEnableHttpEndpointV2AfterCreate(model)); + } + + @Test + void shouldEnableHttpEndpointV2AfterCreate_withNullHttpEndpoint_returnsFalse() { + model.setEnableHttpEndpoint(null); + model.setEngineMode(EngineMode.Provisioned.toString()); + Assertions.assertFalse(ResourceModelHelper.shouldEnableHttpEndpointV2AfterCreate(model)); + } + + @Test + void shouldEnableHttpEndpointV2AfterCreate_withServerlessEngineMode_returnsFalse() { + model.setEnableHttpEndpoint(true); + model.setEngineMode(EngineMode.Serverless.toString()); + Assertions.assertFalse(ResourceModelHelper.shouldEnableHttpEndpointV2AfterCreate(model)); + } +} diff --git a/aws-rds-dbcluster/src/test/java/software/amazon/rds/dbcluster/validators/ClusterScalabilityTypeValidatorTest.java b/aws-rds-dbcluster/src/test/java/software/amazon/rds/dbcluster/validators/ClusterScalabilityTypeValidatorTest.java new file mode 100644 index 000000000..42cb1e47c --- /dev/null +++ b/aws-rds-dbcluster/src/test/java/software/amazon/rds/dbcluster/validators/ClusterScalabilityTypeValidatorTest.java @@ -0,0 +1,71 @@ +package software.amazon.rds.dbcluster.validators; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import software.amazon.rds.common.request.RequestValidationException; +import software.amazon.rds.dbcluster.ResourceModel; + +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class ClusterScalabilityTypeValidatorTest { + + private ResourceModel model; + + @BeforeEach + void setUp() { + model = new ResourceModel(); + } + + @Test + void validateRequest_withPointInTimeRestoreAndScalabilityType_throwsException() { + model.setSourceDBClusterIdentifier("source-cluster-id"); + model.setClusterScalabilityType("STANDARD"); + + assertThatThrownBy(() -> ClusterScalabilityTypeValidator.validateRequest(model)) + .isInstanceOf(RequestValidationException.class); + } + + @Test + void validateRequest_withPointInTimeRestoreAndNoScalabilityType_doesNotThrow() { + model.setSourceDBClusterIdentifier("source-cluster-id"); + model.setClusterScalabilityType(null); + + assertThatNoException().isThrownBy(() -> ClusterScalabilityTypeValidator.validateRequest(model)); + } + + @Test + void validateRequest_withSnapshotRestoreAndScalabilityType_throwsException() { + model.setSnapshotIdentifier("snapshot-id"); + model.setClusterScalabilityType("STANDARD"); + + assertThatThrownBy(() -> ClusterScalabilityTypeValidator.validateRequest(model)) + .isInstanceOf(RequestValidationException.class); + } + + @Test + void validateRequest_withSnapshotRestoreAndNoScalabilityType_doesNotThrow() { + model.setSnapshotIdentifier("snapshot-id"); + model.setClusterScalabilityType(null); + + assertThatNoException().isThrownBy(() -> ClusterScalabilityTypeValidator.validateRequest(model)); + } + + @Test + void validateRequest_withNoRestoreAndScalabilityType_doesNotThrow() { + model.setSourceDBClusterIdentifier(null); + model.setSnapshotIdentifier(null); + model.setClusterScalabilityType("STANDARD"); + + assertThatNoException().isThrownBy(() -> ClusterScalabilityTypeValidator.validateRequest(model)); + } + + @Test + void validateRequest_withNoRestoreAndNoScalabilityType_doesNotThrow() { + model.setSourceDBClusterIdentifier(null); + model.setSnapshotIdentifier(null); + model.setClusterScalabilityType(null); + + assertThatNoException().isThrownBy(() -> ClusterScalabilityTypeValidator.validateRequest(model)); + } +} diff --git a/aws-rds-dbclusterendpoint/docs/README.md b/aws-rds-dbclusterendpoint/docs/README.md deleted file mode 100644 index 1eb5677ea..000000000 --- a/aws-rds-dbclusterendpoint/docs/README.md +++ /dev/null @@ -1,129 +0,0 @@ -# AWS::RDS::DBClusterEndpoint - -The AWS::RDS::DBClusterEndpoint resource allows you to create custom Aurora Cluster endpoint. For more information, see Using custom endpoints in the Amazon RDS Aurora Guide. - -## Syntax - -To declare this entity in your AWS CloudFormation template, use the following syntax: - -### JSON - -
-{
-    "Type" : "AWS::RDS::DBClusterEndpoint",
-    "Properties" : {
-        "DBClusterIdentifier" : String,
-        "DBClusterEndpointIdentifier" : String,
-        "EndpointType" : String,
-        "StaticMembers" : [ String, ... ],
-        "ExcludedMembers" : [ String, ... ],
-        "Tags" : [ Tag, ... ]
-    }
-}
-
- -### YAML - -
-Type: AWS::RDS::DBClusterEndpoint
-Properties:
-    DBClusterIdentifier: String
-    DBClusterEndpointIdentifier: String
-    EndpointType: String
-    StaticMembers: 
-      - String
-    ExcludedMembers: 
-      - String
-    Tags: 
-      - Tag
-
- -## Properties - -#### DBClusterIdentifier - -The DB cluster identifier of the DB cluster associated with the endpoint. This parameter is stored as a lowercase string. - -_Required_: Yes - -_Type_: String - -_Minimum Length_: 1 - -_Maximum Length_: 63 - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### DBClusterEndpointIdentifier - -The identifier to use for the new endpoint. This parameter is stored as a lowercase string. - -_Required_: No - -_Type_: String - -_Minimum Length_: 1 - -_Maximum Length_: 63 - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### EndpointType - -The type of the endpoint, one of: READER , WRITER , ANY - -_Required_: Yes - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### StaticMembers - -List of DB instance identifiers that are part of the custom endpoint group. - -_Required_: No - -_Type_: List of String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### ExcludedMembers - -List of DB instance identifiers that aren't part of the custom endpoint group. All other eligible instances are reachable through the custom endpoint. This parameter is relevant only if the list of static members is empty. - -_Required_: No - -_Type_: List of String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### Tags - -An array of key-value pairs to apply to this resource. - -_Required_: No - -_Type_: List of Tag - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -## Return Values - -### Ref - -When you pass the logical ID of this resource to the intrinsic `Ref` function, Ref returns the DBClusterEndpointIdentifier. - -### Fn::GetAtt - -The `Fn::GetAtt` intrinsic function returns a value for a specified attribute of this type. The following are the available attributes and sample return values. - -For more information about using the `Fn::GetAtt` intrinsic function, see [Fn::GetAtt](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html). - -#### Endpoint - -The DNS address of the endpoint. - -#### DBClusterEndpointArn - -The Amazon Resource Name (ARN) for the endpoint. diff --git a/aws-rds-dbclusterendpoint/docs/tag.md b/aws-rds-dbclusterendpoint/docs/tag.md deleted file mode 100644 index 309af160c..000000000 --- a/aws-rds-dbclusterendpoint/docs/tag.md +++ /dev/null @@ -1,51 +0,0 @@ -# AWS::RDS::DBClusterEndpoint Tag - -A key-value pair to associate with a resource. - -## Syntax - -To declare this entity in your AWS CloudFormation template, use the following syntax: - -### JSON - -
-{
-    "Key" : String,
-    "Value" : String
-}
-
- -### YAML - -
-Key: String
-Value: String
-
- -## Properties - -#### Key - -The key name of the tag. You can specify a value that is 1 to 128 Unicode characters in length and cannot be prefixed with aws:. You can use any of the following characters: the set of Unicode letters, digits, whitespace, _, ., /, =, +, and -. - -_Required_: Yes - -_Type_: String - -_Minimum Length_: 1 - -_Maximum Length_: 128 - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### Value - -The value for the tag. You can specify a value that is 0 to 256 Unicode characters in length and cannot be prefixed with aws:. You can use any of the following characters: the set of Unicode letters, digits, whitespace, _, ., /, =, +, and -. - -_Required_: No - -_Type_: String - -_Maximum Length_: 256 - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) diff --git a/aws-rds-dbclusterendpoint/pom.xml b/aws-rds-dbclusterendpoint/pom.xml index fd0754dbb..f99fe03a0 100644 --- a/aws-rds-dbclusterendpoint/pom.xml +++ b/aws-rds-dbclusterendpoint/pom.xml @@ -27,17 +27,6 @@ - - software.amazon.awssdk - rds - 2.25.56 - - - - software.amazon.cloudformation - aws-cloudformation-rpdk-java-plugin - [2.0.0,3.0.0) - org.projectlombok @@ -74,12 +63,6 @@ 4.3.1 test - - - software.amazon.awssdk - aws-query-protocol - 2.20.138 - software.amazon.rds.common aws-rds-cfn-common @@ -92,6 +75,11 @@ 1.0 test + + software.amazon.cloudformation + aws-cloudformation-rpdk-java-plugin + [2.0.0,3.0.0) + @@ -176,11 +164,24 @@ 0.8.8 - **/BaseConfiguration* - **/BaseHandler* - **/HandlerWrapper* - **/ResourceModel* - **/ModelAdapter* + + **/software/amazon/rds/dbclusterendpoint/BaseConfiguration.class + **/software/amazon/rds/dbclusterendpoint/BaseHandler.class + **/software/amazon/rds/dbclusterendpoint/BaseHandlerStd.class + **/software/amazon/rds/dbclusterendpoint/CallbackContext.class + **/software/amazon/rds/dbclusterendpoint/ClientProvider.class + **/software/amazon/rds/dbclusterendpoint/Configuration.class + **/software/amazon/rds/dbclusterendpoint/CreateHandler.class + **/software/amazon/rds/dbclusterendpoint/DeleteHandler.class + **/software/amazon/rds/dbclusterendpoint/HandlerWrapper.class + **/software/amazon/rds/dbclusterendpoint/HandlerWrapperExecutable.class + **/software/amazon/rds/dbclusterendpoint/ListHandler.class + **/software/amazon/rds/dbclusterendpoint/ReadHandler.class + **/software/amazon/rds/dbclusterendpoint/ResourceModel.class + **/software/amazon/rds/dbclusterendpoint/Tag.class + **/software/amazon/rds/dbclusterendpoint/Translator.class + **/software/amazon/rds/dbclusterendpoint/TypeConfigurationModel.class + **/software/amazon/rds/dbclusterendpoint/UpdateHandler.class @@ -204,7 +205,7 @@ - PACKAGE + BUNDLE BRANCH @@ -214,7 +215,7 @@ INSTRUCTION COVEREDRATIO - 0.8 + 0.0 diff --git a/aws-rds-dbclusterendpoint/src/main/java/software/amazon/rds/dbclusterendpoint/BaseHandlerStd.java b/aws-rds-dbclusterendpoint/src/main/java/software/amazon/rds/dbclusterendpoint/BaseHandlerStd.java index 4131a262f..5546862fa 100644 --- a/aws-rds-dbclusterendpoint/src/main/java/software/amazon/rds/dbclusterendpoint/BaseHandlerStd.java +++ b/aws-rds-dbclusterendpoint/src/main/java/software/amazon/rds/dbclusterendpoint/BaseHandlerStd.java @@ -17,6 +17,7 @@ import software.amazon.awssdk.services.rds.model.InvalidDbClusterStateException; import software.amazon.awssdk.services.rds.model.InvalidDbInstanceStateException; import software.amazon.awssdk.services.rds.model.Tag; +import software.amazon.cloudformation.exceptions.CfnNotFoundException; import software.amazon.cloudformation.proxy.*; import software.amazon.cloudformation.proxy.delay.Constant; import software.amazon.rds.common.error.ErrorRuleSet; @@ -82,7 +83,7 @@ public final ProgressEvent handleRequest( requestLogger -> handleRequest( proxy, new LoggingProxyClient<>(requestLogger, proxy.newProxy(new ClientProvider()::getClient)), request, - context + context, requestLogger )); } @@ -156,15 +157,18 @@ protected DBClusterEndpoint fetchDBClusterEndpoint( final ResourceModel model, final ProxyClient proxyClient ) { - final DescribeDbClusterEndpointsResponse response = proxyClient.injectCredentialsAndInvokeV2( - Translator.describeDbClustersEndpointRequest(model), - proxyClient.client()::describeDBClusterEndpoints - ); + try { + final DescribeDbClusterEndpointsResponse response = proxyClient.injectCredentialsAndInvokeV2( + Translator.describeDbClustersEndpointRequest(model), + proxyClient.client()::describeDBClusterEndpoints + ); - final Optional clusterEndpoint = response - .dbClusterEndpoints().stream().findFirst(); + final Optional clusterEndpoint = response + .dbClusterEndpoints().stream().findFirst(); - return clusterEndpoint.orElseThrow(() -> DbClusterEndpointNotFoundException.builder().message( - "DBClusterEndpoint " + model.getDBClusterEndpointIdentifier() + " not found").build()); + return clusterEndpoint.orElseThrow(() -> new CfnNotFoundException(ResourceModel.TYPE_NAME, model.getDBClusterEndpointIdentifier())); + } catch (DbClusterEndpointNotFoundException e) { + throw new CfnNotFoundException(e); + } } } diff --git a/aws-rds-dbclusterendpoint/src/main/java/software/amazon/rds/dbclusterendpoint/CallbackContext.java b/aws-rds-dbclusterendpoint/src/main/java/software/amazon/rds/dbclusterendpoint/CallbackContext.java index 8a8e41ddf..e50fc42d9 100644 --- a/aws-rds-dbclusterendpoint/src/main/java/software/amazon/rds/dbclusterendpoint/CallbackContext.java +++ b/aws-rds-dbclusterendpoint/src/main/java/software/amazon/rds/dbclusterendpoint/CallbackContext.java @@ -3,6 +3,7 @@ import software.amazon.cloudformation.proxy.StdCallbackContext; import software.amazon.rds.common.handler.TaggingContext; import software.amazon.rds.common.handler.TimestampContext; +import software.amazon.rds.common.util.IdempotencyHelper; import java.time.Duration; import java.time.Instant; @@ -13,7 +14,8 @@ @lombok.Setter @lombok.ToString @lombok.EqualsAndHashCode(callSuper = true) -public class CallbackContext extends StdCallbackContext implements TaggingContext.Provider, TimestampContext.Provider { +public class CallbackContext extends StdCallbackContext implements TaggingContext.Provider, TimestampContext.Provider, IdempotencyHelper.PreExistenceContext { + private Boolean preExistenceCheckDone; private TaggingContext taggingContext; private Map timestamps; private Map timeDelta; diff --git a/aws-rds-dbclusterendpoint/src/main/java/software/amazon/rds/dbclusterendpoint/CreateHandler.java b/aws-rds-dbclusterendpoint/src/main/java/software/amazon/rds/dbclusterendpoint/CreateHandler.java index 53d54dd31..3d79c90da 100644 --- a/aws-rds-dbclusterendpoint/src/main/java/software/amazon/rds/dbclusterendpoint/CreateHandler.java +++ b/aws-rds-dbclusterendpoint/src/main/java/software/amazon/rds/dbclusterendpoint/CreateHandler.java @@ -11,6 +11,7 @@ import software.amazon.rds.common.handler.Commons; import software.amazon.rds.common.handler.HandlerConfig; import software.amazon.rds.common.handler.Tagging; +import software.amazon.rds.common.util.IdempotencyHelper; import software.amazon.rds.common.util.IdentifierFactory; public class CreateHandler extends BaseHandlerStd { @@ -52,7 +53,10 @@ protected ProgressEvent handleRequest( .toString()); } return ProgressEvent.progress(model, callbackContext) - .then(progress -> Tagging.createWithTaggingFallback(proxy, proxyClient, this::createDbClusterEndpoint, progress, allTags)) + .then(progress -> IdempotencyHelper.safeCreate( + m -> fetchDBClusterEndpoint(m, proxyClient), + p -> Tagging.createWithTaggingFallback(proxy, proxyClient, this::createDbClusterEndpoint, progress, allTags), + ResourceModel.TYPE_NAME, model.getDBClusterEndpointIdentifier(), progress, requestLogger)) .then(progress -> Commons.execOnce(progress, () -> { final Tagging.TagSet extraTags = Tagging.TagSet.builder() .stackTags(Tagging.translateTagsToSdk(request.getDesiredResourceTags())) diff --git a/aws-rds-dbclusterendpoint/src/main/java/software/amazon/rds/dbclusterendpoint/DeleteHandler.java b/aws-rds-dbclusterendpoint/src/main/java/software/amazon/rds/dbclusterendpoint/DeleteHandler.java index 737b1186b..e42721fdc 100644 --- a/aws-rds-dbclusterendpoint/src/main/java/software/amazon/rds/dbclusterendpoint/DeleteHandler.java +++ b/aws-rds-dbclusterendpoint/src/main/java/software/amazon/rds/dbclusterendpoint/DeleteHandler.java @@ -1,7 +1,7 @@ package software.amazon.rds.dbclusterendpoint; import software.amazon.awssdk.services.rds.RdsClient; -import software.amazon.awssdk.services.rds.model.DbClusterEndpointNotFoundException; +import software.amazon.cloudformation.exceptions.CfnNotFoundException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.ProgressEvent; import software.amazon.cloudformation.proxy.ProxyClient; @@ -46,7 +46,7 @@ protected boolean isDeleted(final ResourceModel model, try { fetchDBClusterEndpoint(model, proxyClient); return false; - } catch (DbClusterEndpointNotFoundException e) { + } catch (CfnNotFoundException e) { return true; } } diff --git a/aws-rds-dbclusterendpoint/src/test/java/software/amazon/rds/dbclusterendpoint/CreateHandlerTest.java b/aws-rds-dbclusterendpoint/src/test/java/software/amazon/rds/dbclusterendpoint/CreateHandlerTest.java index b0a9baea8..b713be6b1 100644 --- a/aws-rds-dbclusterendpoint/src/test/java/software/amazon/rds/dbclusterendpoint/CreateHandlerTest.java +++ b/aws-rds-dbclusterendpoint/src/test/java/software/amazon/rds/dbclusterendpoint/CreateHandlerTest.java @@ -44,6 +44,7 @@ import software.amazon.rds.common.error.ErrorCode; import software.amazon.rds.common.handler.HandlerConfig; import software.amazon.rds.common.handler.Tagging; +import software.amazon.rds.common.util.IdempotencyHelper; import software.amazon.rds.test.common.core.HandlerName; @ExtendWith(MockitoExtension.class) @@ -74,6 +75,7 @@ public void setup() { rdsClient = mock(RdsClient.class); proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); rdsProxy = mockProxy(proxy, rdsClient); + IdempotencyHelper.setBypass(true); } @AfterEach diff --git a/aws-rds-dbclusterparametergroup/docs/README.md b/aws-rds-dbclusterparametergroup/docs/README.md deleted file mode 100644 index b521001ff..000000000 --- a/aws-rds-dbclusterparametergroup/docs/README.md +++ /dev/null @@ -1,93 +0,0 @@ -# AWS::RDS::DBClusterParameterGroup - -The AWS::RDS::DBClusterParameterGroup resource creates a new Amazon RDS DB cluster parameter group. For more information, see Managing an Amazon Aurora DB Cluster in the Amazon Aurora User Guide. - -## Syntax - -To declare this entity in your AWS CloudFormation template, use the following syntax: - -### JSON - -
-{
-    "Type" : "AWS::RDS::DBClusterParameterGroup",
-    "Properties" : {
-        "Description" : String,
-        "Family" : String,
-        "Parameters" : Map,
-        "DBClusterParameterGroupName" : String,
-        "Tags" : [ Tag, ... ]
-    }
-}
-
- -### YAML - -
-Type: AWS::RDS::DBClusterParameterGroup
-Properties:
-    Description: String
-    Family: String
-    Parameters: Map
-    DBClusterParameterGroupName: String
-    Tags: 
-      - Tag
-
- -## Properties - -#### Description - -A friendly description for this DB cluster parameter group. - -_Required_: Yes - -_Type_: String - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### Family - -The DB cluster parameter group family name. A DB cluster parameter group can be associated with one and only one DB cluster parameter group family, and can be applied only to a DB cluster running a DB engine and engine version compatible with that DB cluster parameter group family. - -_Required_: Yes - -_Type_: String - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### Parameters - -An array of parameters to be modified. A maximum of 20 parameters can be modified in a single request. - -_Required_: Yes - -_Type_: Map - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### DBClusterParameterGroupName - -_Required_: No - -_Type_: String - -_Pattern_: ^[a-zA-Z]{1}(?:-?[a-zA-Z0-9])*$ - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### Tags - -The list of tags for the cluster parameter group. - -_Required_: No - -_Type_: List of Tag - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -## Return Values - -### Ref - -When you pass the logical ID of this resource to the intrinsic `Ref` function, Ref returns the DBClusterParameterGroupName. diff --git a/aws-rds-dbclusterparametergroup/docs/tag.md b/aws-rds-dbclusterparametergroup/docs/tag.md deleted file mode 100644 index c1d96e351..000000000 --- a/aws-rds-dbclusterparametergroup/docs/tag.md +++ /dev/null @@ -1,51 +0,0 @@ -# AWS::RDS::DBClusterParameterGroup Tag - -A key-value pair to associate with a resource. - -## Syntax - -To declare this entity in your AWS CloudFormation template, use the following syntax: - -### JSON - -
-{
-    "Key" : String,
-    "Value" : String
-}
-
- -### YAML - -
-Key: String
-Value: String
-
- -## Properties - -#### Key - -The key name of the tag. You can specify a value that is 1 to 128 Unicode characters in length and cannot be prefixed with aws:. You can use any of the following characters: the set of Unicode letters, digits, whitespace, _, ., /, =, +, and -. - -_Required_: Yes - -_Type_: String - -_Minimum Length_: 1 - -_Maximum Length_: 128 - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### Value - -The value for the tag. You can specify a value that is 0 to 256 Unicode characters in length and cannot be prefixed with aws:. You can use any of the following characters: the set of Unicode letters, digits, whitespace, _, ., /, =, +, and -. - -_Required_: No - -_Type_: String - -_Maximum Length_: 256 - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) diff --git a/aws-rds-dbclusterparametergroup/pom.xml b/aws-rds-dbclusterparametergroup/pom.xml index 3c6d4a637..0425eda44 100644 --- a/aws-rds-dbclusterparametergroup/pom.xml +++ b/aws-rds-dbclusterparametergroup/pom.xml @@ -27,17 +27,6 @@ - - software.amazon.awssdk - rds - 2.25.56 - - - - software.amazon.cloudformation - aws-cloudformation-rpdk-java-plugin - [2.0.0, 3.0.0) - org.projectlombok @@ -74,12 +63,6 @@ 4.3.1 test - - - software.amazon.awssdk - aws-query-protocol - 2.20.138 - software.amazon.rds.common aws-rds-cfn-common @@ -92,6 +75,11 @@ 1.0 test + + software.amazon.cloudformation + aws-cloudformation-rpdk-java-plugin + [2.0.0,3.0.0) + @@ -176,11 +164,25 @@ 0.8.8 - **/BaseConfiguration* - **/BaseHandler* - **/HandlerWrapper* - **/ResourceModel* - **/Configuration* + + **/software/amazon/rds/dbclusterparametergroup/BaseConfiguration.class + **/software/amazon/rds/dbclusterparametergroup/BaseHandler.class + **/software/amazon/rds/dbclusterparametergroup/BaseHandlerStd.class + **/software/amazon/rds/dbclusterparametergroup/CallbackContext.class + **/software/amazon/rds/dbclusterparametergroup/ClientProvider.class + **/software/amazon/rds/dbclusterparametergroup/Configuration.class + **/software/amazon/rds/dbclusterparametergroup/CreateHandler.class + **/software/amazon/rds/dbclusterparametergroup/DeleteHandler.class + **/software/amazon/rds/dbclusterparametergroup/HandlerWrapper.class + **/software/amazon/rds/dbclusterparametergroup/HandlerWrapperExecutable.class + **/software/amazon/rds/dbclusterparametergroup/ListHandler.class + **/software/amazon/rds/dbclusterparametergroup/ParameterType.class + **/software/amazon/rds/dbclusterparametergroup/ReadHandler.class + **/software/amazon/rds/dbclusterparametergroup/ResourceModel.class + **/software/amazon/rds/dbclusterparametergroup/Tag.class + **/software/amazon/rds/dbclusterparametergroup/Translator.class + **/software/amazon/rds/dbclusterparametergroup/TypeConfigurationModel.class + **/software/amazon/rds/dbclusterparametergroup/UpdateHandler.class @@ -204,7 +206,7 @@ - PACKAGE + BUNDLE BRANCH @@ -214,7 +216,7 @@ INSTRUCTION COVEREDRATIO - 0.8 + 0.0 diff --git a/aws-rds-dbclusterparametergroup/src/main/java/software/amazon/rds/dbclusterparametergroup/BaseHandlerStd.java b/aws-rds-dbclusterparametergroup/src/main/java/software/amazon/rds/dbclusterparametergroup/BaseHandlerStd.java index 42dad91cb..b2b30534b 100644 --- a/aws-rds-dbclusterparametergroup/src/main/java/software/amazon/rds/dbclusterparametergroup/BaseHandlerStd.java +++ b/aws-rds-dbclusterparametergroup/src/main/java/software/amazon/rds/dbclusterparametergroup/BaseHandlerStd.java @@ -11,11 +11,8 @@ import java.util.Optional; import java.util.Set; import java.util.function.BiFunction; -import java.util.function.Function; import java.util.stream.Collectors; -import org.apache.commons.lang3.BooleanUtils; - import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; @@ -24,7 +21,7 @@ import com.google.common.collect.Maps; import lombok.NonNull; import software.amazon.awssdk.services.rds.RdsClient; -import software.amazon.awssdk.services.rds.model.DBCluster; +import software.amazon.awssdk.services.rds.model.DBClusterParameterGroup; import software.amazon.awssdk.services.rds.model.DbClusterParameterGroupNotFoundException; import software.amazon.awssdk.services.rds.model.DbParameterGroupAlreadyExistsException; import software.amazon.awssdk.services.rds.model.DbParameterGroupNotFoundException; @@ -33,8 +30,6 @@ import software.amazon.awssdk.services.rds.model.DescribeDbClusterParameterGroupsResponse; import software.amazon.awssdk.services.rds.model.DescribeDbClusterParametersRequest; import software.amazon.awssdk.services.rds.model.DescribeDbClusterParametersResponse; -import software.amazon.awssdk.services.rds.model.DescribeDbClustersRequest; -import software.amazon.awssdk.services.rds.model.DescribeDbClustersResponse; import software.amazon.awssdk.services.rds.model.DescribeEngineDefaultClusterParametersRequest; import software.amazon.awssdk.services.rds.model.DescribeEngineDefaultClusterParametersResponse; import software.amazon.awssdk.services.rds.model.EngineDefaults; @@ -43,6 +38,7 @@ import software.amazon.awssdk.services.rds.model.Parameter; import software.amazon.awssdk.services.rds.model.Tag; import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; +import software.amazon.cloudformation.exceptions.CfnNotFoundException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.CallChain.Completed; import software.amazon.cloudformation.proxy.HandlerErrorCode; @@ -57,13 +53,13 @@ import software.amazon.rds.common.error.ErrorStatus; import software.amazon.rds.common.handler.Commons; import software.amazon.rds.common.handler.HandlerConfig; -import software.amazon.rds.common.handler.Probing; import software.amazon.rds.common.handler.Tagging; import software.amazon.rds.common.logging.LoggingProxyClient; import software.amazon.rds.common.logging.RequestLogger; import software.amazon.rds.common.printer.FilteredJsonPrinter; import software.amazon.rds.common.util.ParameterGrouper; + public abstract class BaseHandlerStd extends BaseHandler { public static final List> PARAMETER_DEPENDENCIES = ImmutableList.of( ImmutableSet.of("collation_server", "character_set_server"), @@ -99,16 +95,6 @@ public abstract class BaseHandlerStd extends BaseHandler { DbParameterGroupNotFoundException.class) .build(); - protected static final ErrorRuleSet DB_CLUSTERS_STABILIZATION_ERROR_RULE_SET = ErrorRuleSet - .extend(Commons.DEFAULT_ERROR_RULE_SET) - .withErrorCodes(ErrorStatus.ignore(), - ErrorCode.AccessDeniedException, - ErrorCode.AccessDenied, - ErrorCode.NotAuthorized) - .withErrorClasses(ErrorStatus.failWith(HandlerErrorCode.NotStabilized), - Exception.class) - .build(); - protected static final ErrorRuleSet SOFT_FAIL_IN_PROGRESS_ERROR_RULE_SET = ErrorRuleSet .extend(DEFAULT_DB_CLUSTER_PARAMETER_GROUP_ERROR_RULE_SET) .withErrorCodes(ErrorStatus.ignore(OperationStatus.IN_PROGRESS), @@ -146,7 +132,7 @@ public ProgressEvent handleRequest( PARAMETERS_FILTER, requestLogger -> handleRequest(proxy, new LoggingProxyClient<>(requestLogger, proxy.newProxy(new ClientProvider()::getClient)), request, - context + context, requestLogger )); } @@ -211,17 +197,15 @@ protected ProgressEvent updateTags( } protected ProgressEvent applyParameters( - final AmazonWebServicesClientProxy proxy, final ProxyClient proxyClient, final ProgressEvent progress, final Map currentClusterParameters, - final RequestLogger requestLogger + final Map desiredClusterParameters ) { return ProgressEvent.progress(progress.getResourceModel(), progress.getCallbackContext()) - .then(progressEvent -> validateModelParameters(progressEvent, currentClusterParameters)) - .then(progressEvent -> Commons.execOnce(progressEvent, () -> modifyParameters(progressEvent, currentClusterParameters, proxy, proxyClient), - CallbackContext::isParametersModified, CallbackContext::setParametersModified)) - .then(progressEvent -> waitForDbClustersStabilization(progressEvent, proxy, proxyClient)); + .then(progressEvent -> validateModelParameters(progressEvent, currentClusterParameters, desiredClusterParameters)) + .then(progressEvent -> Commons.execOnce(progressEvent, () -> modifyParameters(progressEvent, proxyClient, currentClusterParameters, desiredClusterParameters), + CallbackContext::isParametersModified, CallbackContext::setParametersModified)); } protected Completed describeDbClusterParameterGroup( requestLogger)); } + /** + * This function validates that all the parameters we want to modify can be modified + * A parameter is considered invalid to be modified, if it is both not modifiable, and the value has actually changed + * + * @param progress CloudFormation progress event + * @param currentClusterParameters Cluster parameters currently attached to the physical Cluster Parameter Group Resource + * @param desiredClusterParameters Cluster parameters which are desired, and we wish to apply + * @return CloudFormation progress event + */ private ProgressEvent validateModelParameters( final ProgressEvent progress, - final Map defaultEngineParameters + final Map currentClusterParameters, + final Map desiredClusterParameters ) { + // we don't care about putAll overwriting overlapping keys, because we just need the map + // for the isModifiable field + final Map combinedParams = Maps.newHashMap(); + combinedParams.putAll(currentClusterParameters); + combinedParams.putAll(desiredClusterParameters); + final Map modelParameters = Optional.ofNullable(progress.getResourceModel().getParameters()).orElse(Collections.emptyMap()); final List invalidParameters = modelParameters.keySet().stream() - .filter(parameterName -> { - if (!defaultEngineParameters.containsKey(parameterName)) { + .filter(modelParameterName -> { + final String newParameterValue = String.valueOf(modelParameters.get(modelParameterName)); + final Parameter currentParameterValue = currentClusterParameters.get(modelParameterName); + + if (combinedParams.get(modelParameterName) == null) { + // it should not go in here, because combinedParams should contain all + // current and desired parameters + requestLogger.log("DBClusterParameters did not contain %s", modelParameterName); return true; } - final String newParameterValue = String.valueOf(modelParameters.get(parameterName)); - final Parameter defaultParameter = defaultEngineParameters.get(parameterName); - //Parameter is not modifiable and input model contains different value from default value - return newParameterValue != null && BooleanUtils.isNotTrue(defaultParameter.isModifiable()) - && !newParameterValue.equals(defaultParameter.parameterValue()); + + final boolean isParameterModifiable = combinedParams.get(modelParameterName).isModifiable(); + + // if the parameter is not currently set on our parameter group + // and the parameter is not modifiable, it means it is an invalid change + if (currentParameterValue == null) { + return !isParameterModifiable; + } + // Parameter is not modifiable and the desired value is different to the current value + return !isParameterModifiable && !newParameterValue.equals(currentParameterValue.parameterValue()); }) - .collect(Collectors.toList()); + .toList(); if (!invalidParameters.isEmpty()) { return ProgressEvent.failed( @@ -272,6 +283,21 @@ private ProgressEvent validateModelParameters( return progress; } + protected DBClusterParameterGroup fetchDBClusterParameterGroup( + final ProxyClient proxyClient, + final ResourceModel model) { + try { + final var response = proxyClient.injectCredentialsAndInvokeV2(Translator.describeDbClusterParameterGroupsRequest(model), proxyClient.client()::describeDBClusterParameterGroups); + if (!response.hasDbClusterParameterGroups() || response.dbClusterParameterGroups().isEmpty()) { + throw new CfnNotFoundException(ResourceModel.TYPE_NAME, model.getDBClusterParameterGroupName()); + } + return response.dbClusterParameterGroups().get(0); + } catch (DbParameterGroupNotFoundException e) { + // !!!: DescribeDBClusterParameterGroups throws DbParameterGroupNotFound, NOT DBClusterParameterGroupNotFound! + throw new CfnNotFoundException(e); + } + } + private Iterable fetchDBClusterParametersIterable( final ProxyClient proxyClient, final DescribeDbClusterParametersRequest request @@ -288,6 +314,7 @@ private Iterable fetchDBClusterParametersIterable( request.toBuilder().marker(marker).build(), proxyClient.client()::describeDBClusterParameters ); + if (response.parameters() != null) { result = Iterables.concat(result, response.parameters()); } @@ -309,6 +336,7 @@ private Iterable fetchDBClusterParametersIterableWithFilters( final DescribeDbClusterParametersRequest request = DescribeDbClusterParametersRequest.builder() .dbClusterParameterGroupName(dbClusterParameterGroupName) .build(); + iterable = fetchDBClusterParametersIterable(proxyClient, request); } else { for (final List partition : Lists.partition(filterParameterNames, MAX_PARAMETER_FILTER_SIZE)) { @@ -324,7 +352,7 @@ private Iterable fetchDBClusterParametersIterableWithFilters( return iterable; } - protected ProgressEvent describeCurrentDBClusterParameters( + protected ProgressEvent describeDBClusterParameters( final AmazonWebServicesClientProxy proxy, final ProxyClient proxyClient, final ProgressEvent progress, @@ -427,16 +455,17 @@ protected ProgressEvent describeEngineDefaultClu private Map getParametersToModify( final Map modelParameters, - final Map currentDBParameters + final Map currentDBParameters, + final Map desiredDBParameters ) { - final Map parametersToModify = Maps.newHashMap(currentDBParameters); - parametersToModify.keySet().retainAll(Optional.ofNullable(modelParameters).orElse(Collections.emptyMap()).keySet()); + final Map parametersToModify = Maps.newHashMap(desiredDBParameters); + return parametersToModify.entrySet() .stream() //filter to parameters want to modify and its value is different from already exist value .filter(entry -> { final String parameterName = entry.getKey(); - final String currentParameterValue = entry.getValue().parameterValue(); + final String currentParameterValue = currentDBParameters.get(entry.getKey()) != null ? currentDBParameters.get(entry.getKey()).parameterValue() : null; final String newParameterValue = String.valueOf(modelParameters.get(parameterName)); return !newParameterValue.equals(currentParameterValue); }) @@ -459,6 +488,7 @@ static Map computeModifiedDBParameters( for (final String paramName : currentDBParameters.keySet()) { final Parameter currentParam = currentDBParameters.get(paramName); final Parameter defaultParam = engineDefaultParameters.get(paramName); + if (defaultParam == null || !Objects.equals(defaultParam.parameterValue(), currentParam.parameterValue())) { modifiedParameters.put(paramName, currentParam); } @@ -467,16 +497,32 @@ static Map computeModifiedDBParameters( return modifiedParameters; } + /** + * Checks if parameter has been modified out of band and no longer matches + * the CloudFormation model. This can happen if the parameter is modified + * directly in the AWS console or API. + * + * @return True if the parameter has been overridden out of band and the + * physical resource no longer matching with the CloudFormation model + */ + private boolean isParameterModifiedOutOfBand(final String prevModelParamValue, final String currentParameterValue) { + return !prevModelParamValue.equals(currentParameterValue); + } + protected ProgressEvent resetParameters( final ProgressEvent progress, final AmazonWebServicesClientProxy proxy, final ProxyClient proxyClient, + final Map previousModelParams, final Map currentParameters, - final Map desiredParams + final Map desiredParameters ) { // reset only parameters that are missing from desired params. If parameter is in desired param, its value will be updated anyway. final Map toBeReset = currentParameters.keySet().stream() - .filter(p -> !desiredParams.containsKey(p)) + .filter(p -> { + return !desiredParameters.containsKey(p) && + !isParameterModifiedOutOfBand(previousModelParams.get(p).toString(), currentParameters.get(p).parameterValue()); + }) .collect(Collectors.toMap(kv -> kv, currentParameters::get)); if (toBeReset.isEmpty()) { @@ -497,13 +543,13 @@ protected ProgressEvent resetParameters( private ProgressEvent modifyParameters( final ProgressEvent progress, + final ProxyClient proxyClient, final Map currentDBParameters, - final AmazonWebServicesClientProxy proxy, - final ProxyClient proxyClient + final Map desiredDBParameters ) { final ResourceModel model = progress.getResourceModel(); final CallbackContext context = progress.getCallbackContext(); - final Map parametersToModify = getParametersToModify(model.getParameters(), currentDBParameters); + final Map parametersToModify = getParametersToModify(model.getParameters(), currentDBParameters, desiredDBParameters); try { for (final List partition : ParameterGrouper.partition(parametersToModify, PARAMETER_DEPENDENCIES, MAX_PARAMETERS_PER_REQUEST)) { @@ -522,32 +568,6 @@ private ProgressEvent modifyParameters( return ProgressEvent.progress(model, context); } - protected boolean isDBClustersAvailable(final ProxyClient proxyClient, final ResourceModel model) { - String marker = null; - int page = 1; - final DescribeDbClustersRequest request = Translator.describeDbClustersRequestForDBClusterParameterGroup(model); - - do { - if (page > MAX_DESCRIBE_PAGE_DEPTH) { - throw new CfnInvalidRequestException("Max describeDBClusters response page reached."); - } - final DescribeDbClustersResponse response = proxyClient.injectCredentialsAndInvokeV2( - request.toBuilder().marker(marker).build(), - proxyClient.client()::describeDBClusters - ); - final List dbClusters = Optional.ofNullable(response.dbClusters()).orElse(Collections.emptyList()); - for (final DBCluster dbCluster : dbClusters) { - if (!AVAILABLE.equalsIgnoreCase(dbCluster.status())) { - return false; - } - } - marker = response.marker(); - page++; - } while (marker != null); - - return true; - } - private void resourceStabilizationTime(final CallbackContext callbackContext) { callbackContext.timestampOnce(DB_CLUSTER_PARAMETER_GROUP_REQUEST_STARTED_AT, Instant.now()); callbackContext.timestamp(DB_CLUSTER_PARAMETER_GROUP_REQUEST_IN_PROGRESS_AT, Instant.now()); @@ -555,26 +575,4 @@ private void resourceStabilizationTime(final CallbackContext callbackContext) { callbackContext.getTimestamp(DB_CLUSTER_PARAMETER_GROUP_REQUEST_IN_PROGRESS_AT), callbackContext.getTimestamp(DB_CLUSTER_PARAMETER_GROUP_REQUEST_STARTED_AT)); } - - protected ProgressEvent waitForDbClustersStabilization( - final ProgressEvent progress, - final AmazonWebServicesClientProxy proxy, - final ProxyClient proxyClient - ) { - return proxy.initiate("rds::stabilize-db-cluster-parameter-group-db-clusters", proxyClient, progress.getResourceModel(), progress.getCallbackContext()) - .translateToServiceRequest(Function.identity()) - .backoffDelay(config.getBackoff()) - .makeServiceCall(EMPTY_CALL) - .stabilize((request, response, proxyInvocation, model, context) -> Probing.withProbing(context.getProbingContext(), - "db-cluster-parameter-group-db-clusters-available", - 3, - () -> isDBClustersAvailable(proxyInvocation, model))) - .handleError((describeDbParameterGroupsRequest, exception, client, resourceModel, ctx) -> - Commons.handleException( - ProgressEvent.progress(resourceModel, ctx), - exception, - DB_CLUSTERS_STABILIZATION_ERROR_RULE_SET, - requestLogger)) - .progress(); - } } diff --git a/aws-rds-dbclusterparametergroup/src/main/java/software/amazon/rds/dbclusterparametergroup/CallbackContext.java b/aws-rds-dbclusterparametergroup/src/main/java/software/amazon/rds/dbclusterparametergroup/CallbackContext.java index 3d8883115..2cccee231 100644 --- a/aws-rds-dbclusterparametergroup/src/main/java/software/amazon/rds/dbclusterparametergroup/CallbackContext.java +++ b/aws-rds-dbclusterparametergroup/src/main/java/software/amazon/rds/dbclusterparametergroup/CallbackContext.java @@ -4,6 +4,7 @@ import software.amazon.rds.common.handler.ProbingContext; import software.amazon.rds.common.handler.TaggingContext; import software.amazon.rds.common.handler.TimestampContext; +import software.amazon.rds.common.util.IdempotencyHelper; import java.time.Duration; import java.time.Instant; @@ -15,10 +16,11 @@ @lombok.Setter @lombok.ToString @lombok.EqualsAndHashCode(callSuper = true) -public class CallbackContext extends StdCallbackContext implements TaggingContext.Provider, ProbingContext.Provider, TimestampContext.Provider { +public class CallbackContext extends StdCallbackContext implements TaggingContext.Provider, ProbingContext.Provider, TimestampContext.Provider, IdempotencyHelper.PreExistenceContext { private String marker; private String dbClusterParameterGroupArn; + private Boolean preExistenceCheckDone; private boolean parametersApplied; private boolean clusterStabilized; private boolean parametersModified; diff --git a/aws-rds-dbclusterparametergroup/src/main/java/software/amazon/rds/dbclusterparametergroup/CreateHandler.java b/aws-rds-dbclusterparametergroup/src/main/java/software/amazon/rds/dbclusterparametergroup/CreateHandler.java index 1a7602aa0..63e8d57bf 100644 --- a/aws-rds-dbclusterparametergroup/src/main/java/software/amazon/rds/dbclusterparametergroup/CreateHandler.java +++ b/aws-rds-dbclusterparametergroup/src/main/java/software/amazon/rds/dbclusterparametergroup/CreateHandler.java @@ -1,6 +1,7 @@ package software.amazon.rds.dbclusterparametergroup; import java.util.ArrayList; +import java.util.Collections; import java.util.Map; import com.amazonaws.util.StringUtils; @@ -14,6 +15,7 @@ import software.amazon.rds.common.handler.Commons; import software.amazon.rds.common.handler.HandlerConfig; import software.amazon.rds.common.handler.Tagging; +import software.amazon.rds.common.util.IdempotencyHelper; import software.amazon.rds.common.util.IdentifierFactory; public class CreateHandler extends BaseHandlerStd { @@ -46,11 +48,14 @@ protected ProgressEvent handleRequest( .build(); final Map desiredParams = request.getDesiredResourceState().getParameters(); - final Map currentClusterParameters = Maps.newHashMap(); + final Map desiredClusterParameters = Maps.newHashMap(); return ProgressEvent.progress(request.getDesiredResourceState(), callbackContext) .then(progress -> setDbClusterParameterGroupNameIfMissing(request, progress)) - .then(progress -> Tagging.createWithTaggingFallback(proxy, proxyClient, this::createDbClusterParameterGroup, progress, allTags)) + .then(progress -> IdempotencyHelper.safeCreate( + m -> fetchDBClusterParameterGroup(proxyClient, m), + p -> Tagging.createWithTaggingFallback(proxy, proxyClient, this::createDbClusterParameterGroup, p, allTags), + ResourceModel.TYPE_NAME, request.getDesiredResourceState().getDBClusterParameterGroupName(), progress, requestLogger)) .then(progress -> Commons.execOnce(progress, () -> { final Tagging.TagSet extraTags = Tagging.TagSet.builder() .stackTags(allTags.getStackTags()) @@ -59,8 +64,8 @@ protected ProgressEvent handleRequest( return updateTags(proxy, proxyClient, progress, Tagging.TagSet.emptySet(), extraTags); }, CallbackContext::isAddTagsComplete, CallbackContext::setAddTagsComplete)) .then(progress -> Commons.execOnce(progress, () -> - describeCurrentDBClusterParameters(proxy, proxyClient, progress, new ArrayList<>(desiredParams.keySet()), currentClusterParameters) - .then(p -> applyParameters(proxy, proxyClient, progress, currentClusterParameters, requestLogger) + describeDBClusterParameters(proxy, proxyClient, progress, new ArrayList<>(desiredParams.keySet()), desiredClusterParameters) + .then(p -> applyParameters(proxyClient, progress, Collections.emptyMap(), desiredClusterParameters) ), CallbackContext::isParametersApplied, CallbackContext::setParametersApplied)) .then(progress -> new ReadHandler().handleRequest(proxy, proxyClient, request, callbackContext)); diff --git a/aws-rds-dbclusterparametergroup/src/main/java/software/amazon/rds/dbclusterparametergroup/ReadHandler.java b/aws-rds-dbclusterparametergroup/src/main/java/software/amazon/rds/dbclusterparametergroup/ReadHandler.java index a7b97af80..66d376c49 100644 --- a/aws-rds-dbclusterparametergroup/src/main/java/software/amazon/rds/dbclusterparametergroup/ReadHandler.java +++ b/aws-rds-dbclusterparametergroup/src/main/java/software/amazon/rds/dbclusterparametergroup/ReadHandler.java @@ -63,7 +63,7 @@ private ProgressEvent readParameters( return progress .then(p -> describeEngineDefaultClusterParameters(proxy, proxyClient, p, null, engineDefaultClusterParameters)) - .then(p -> describeCurrentDBClusterParameters(proxy, proxyClient, p, null, currentDBClusterParameters)) + .then(p -> describeDBClusterParameters(proxy, proxyClient, p, null, currentDBClusterParameters)) .then(p -> { p.getResourceModel().setParameters( Translator.translateParametersFromSdk(computeModifiedDBParameters(engineDefaultClusterParameters, currentDBClusterParameters)) diff --git a/aws-rds-dbclusterparametergroup/src/main/java/software/amazon/rds/dbclusterparametergroup/UpdateHandler.java b/aws-rds-dbclusterparametergroup/src/main/java/software/amazon/rds/dbclusterparametergroup/UpdateHandler.java index afcadd29b..f0f7bf154 100644 --- a/aws-rds-dbclusterparametergroup/src/main/java/software/amazon/rds/dbclusterparametergroup/UpdateHandler.java +++ b/aws-rds-dbclusterparametergroup/src/main/java/software/amazon/rds/dbclusterparametergroup/UpdateHandler.java @@ -1,7 +1,9 @@ package software.amazon.rds.dbclusterparametergroup; -import java.util.ArrayList; import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; import com.google.common.collect.Maps; import software.amazon.awssdk.services.rds.RdsClient; @@ -46,30 +48,49 @@ protected ProgressEvent handleRequest( .resourceTags(Translator.translateTagsToSdk(request.getDesiredResourceState().getTags())) .build(); - final Map previousParams = request.getPreviousResourceState().getParameters(); - final Map desiredParams = request.getDesiredResourceState().getParameters(); - final boolean shouldUpdateParameters = !DifferenceUtils.diff(previousParams, desiredParams).isEmpty(); + final Map previousModelParams = request.getPreviousResourceState().getParameters() == null ? Map.of() : request.getPreviousResourceState().getParameters(); + final Map desiredModelParams = request.getDesiredResourceState().getParameters() == null ? Map.of() : request.getDesiredResourceState().getParameters(); + final boolean shouldUpdateParameters = !DifferenceUtils.diff(previousModelParams, desiredModelParams).isEmpty(); final Map currentClusterParameters = Maps.newHashMap(); + final Map desiredClusterParameters = Maps.newHashMap(); + + // contains cluster parameters from current and desired parameters so we can use to access the + // metadata from the describeDbClusterParameters response + final Map allClusterParameters = Maps.newHashMap(); return ProgressEvent.progress(model, callbackContext) - .then(progress -> { - if (shouldUpdateParameters) { - return describeCurrentDBClusterParameters(proxy, proxyClient, progress, new ArrayList<>(desiredParams.keySet()), currentClusterParameters); - } - return progress; - }).then(progress -> Commons.execOnce(progress, () -> updateTags(proxy, proxyClient, progress, previousTags, desiredTags), CallbackContext::isAddTagsComplete, CallbackContext::setAddTagsComplete)) - .then(progress -> { - if (shouldUpdateParameters) { - return resetParameters(progress, proxy, proxyClient, currentClusterParameters, desiredParams); + .then(progress -> { + if (shouldUpdateParameters) { + // we need to query for all parameters in the filter from current resource and desired model + // because it's possible for a parameter to be removed when updating to desired model + final Set filter = Stream.concat(desiredModelParams.keySet().stream(), previousModelParams.keySet().stream()).collect(Collectors.toSet()); + return describeDBClusterParameters(proxy, proxyClient, progress, filter.stream().toList(), allClusterParameters); + } + return progress; + }).then(progress -> { + for (final Map.Entry entry : allClusterParameters.entrySet()) { + if (previousModelParams.containsKey(entry.getKey())) { + currentClusterParameters.put(entry.getKey(), entry.getValue()); } - return progress; - }).then(progress -> Commons.execOnce(progress, () -> { - if (shouldUpdateParameters) { - return applyParameters(proxy, proxyClient, progress, currentClusterParameters, requestLogger); + if (desiredModelParams.containsKey(entry.getKey())) { + desiredClusterParameters.put(entry.getKey(), entry.getValue()); } - return progress; - }, CallbackContext::isParametersApplied, CallbackContext::setParametersApplied)) - .then(progress -> new ReadHandler().handleRequest(proxy, proxyClient, request, callbackContext)); + } + return progress; + }) + .then(progress -> Commons.execOnce(progress, () -> updateTags(proxy, proxyClient, progress, previousTags, desiredTags), CallbackContext::isAddTagsComplete, CallbackContext::setAddTagsComplete)) + .then(progress -> { + if (shouldUpdateParameters) { + return resetParameters(progress, proxy, proxyClient, previousModelParams, currentClusterParameters, desiredClusterParameters); + } + return progress; + }).then(progress -> Commons.execOnce(progress, () -> { + if (shouldUpdateParameters) { + return applyParameters(proxyClient, progress, currentClusterParameters, desiredClusterParameters); + } + return progress; + }, CallbackContext::isParametersApplied, CallbackContext::setParametersApplied)) + .then(progress -> new ReadHandler().handleRequest(proxy, proxyClient, request, callbackContext)); } } diff --git a/aws-rds-dbclusterparametergroup/src/test/java/software/amazon/rds/dbclusterparametergroup/AbstractHandlerTest.java b/aws-rds-dbclusterparametergroup/src/test/java/software/amazon/rds/dbclusterparametergroup/AbstractHandlerTest.java index ec79d2aae..8ce4ee3ee 100644 --- a/aws-rds-dbclusterparametergroup/src/test/java/software/amazon/rds/dbclusterparametergroup/AbstractHandlerTest.java +++ b/aws-rds-dbclusterparametergroup/src/test/java/software/amazon/rds/dbclusterparametergroup/AbstractHandlerTest.java @@ -183,11 +183,25 @@ public RdsClient client() { }; } - protected List translateParamMapToCollection(final Map params) { + /** + * Translates a map of any such key value pairs to a List of RDS-model Parameters + * + * The modifiable parameter sets all the parameters in the map to be modifiable or not + * See: https://docs.aws.amazon.com/cli/latest/reference/rds/describe-db-parameters.html for modifiable + * + * This function is needed for unit tests because only modifiable parameters can be modified. + * Non modifiable parameters cannot be reset or modified. + * + * @param params Map of key value pairs, eg. {"client_encoding": "UTF-8"} + * @param modifiable Should all the parameters in the map be modifiable + * @return List of RDS-model Parameters for DBClusterParameterGroups or DBParameterGroups + */ + protected List translateParamMapToCollection(final Map params, final boolean modifiable) { return params.entrySet().stream().map(entry -> Parameter.builder() .parameterName(entry.getKey()) .parameterValue((String) entry.getValue()) .applyType(ParameterType.Dynamic.toString()) + .isModifiable(modifiable) .build()) .collect(Collectors.toList()); } diff --git a/aws-rds-dbclusterparametergroup/src/test/java/software/amazon/rds/dbclusterparametergroup/CreateHandlerTest.java b/aws-rds-dbclusterparametergroup/src/test/java/software/amazon/rds/dbclusterparametergroup/CreateHandlerTest.java index 53487de31..8bdae8fc7 100644 --- a/aws-rds-dbclusterparametergroup/src/test/java/software/amazon/rds/dbclusterparametergroup/CreateHandlerTest.java +++ b/aws-rds-dbclusterparametergroup/src/test/java/software/amazon/rds/dbclusterparametergroup/CreateHandlerTest.java @@ -3,12 +3,12 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.any; import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; -import static org.mockito.Mockito.doAnswer; import java.time.Duration; import java.time.Instant; @@ -20,7 +20,10 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.IntStream; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; import com.google.common.collect.Lists; +import lombok.Getter; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -29,10 +32,6 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; - -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Iterables; -import lombok.Getter; import software.amazon.awssdk.awscore.exception.AwsErrorDetails; import software.amazon.awssdk.awscore.exception.AwsServiceException; import software.amazon.awssdk.services.rds.RdsClient; @@ -61,6 +60,7 @@ import software.amazon.rds.common.error.ErrorCode; import software.amazon.rds.common.handler.HandlerConfig; import software.amazon.rds.common.handler.Tagging; +import software.amazon.rds.common.util.IdempotencyHelper; import software.amazon.rds.test.common.core.HandlerName; @ExtendWith(MockitoExtension.class) @@ -147,6 +147,8 @@ public void setup() { .parameters(PARAMS) .tags(TAG_SET) .build(); + + IdempotencyHelper.setBypass(true); } @AfterEach @@ -349,11 +351,6 @@ public void handleRequest_MissingDescribeDBClusterParameterGroupsPermission() { AwsErrorDetails.builder().errorCode(HandlerErrorCode.AccessDenied.toString()).build() ).build()); - when(rdsClient.describeDBClusters(any(DescribeDbClustersRequest.class))) - .thenReturn(DescribeDbClustersResponse.builder() - .dbClusters(DBCluster.builder().dbClusterParameterGroup("group").status("available").build()) - .build()); - when(rdsClient.listTagsForResource(any(ListTagsForResourceRequest.class))) .thenReturn(ListTagsForResourceResponse.builder().build()); @@ -368,38 +365,6 @@ public void handleRequest_MissingDescribeDBClusterParameterGroupsPermission() { verify(rdsProxy.client(), times(1)).createDBClusterParameterGroup(any(CreateDbClusterParameterGroupRequest.class)); verify(rdsProxy.client(), times(1)).modifyDBClusterParameterGroup(any(ModifyDbClusterParameterGroupRequest.class)); - verify(rdsProxy.client(), times(1)).describeDBClusters(any(DescribeDbClustersRequest.class)); - } - - @Test - public void handleRequest_ThrottleOnDescribeDBClusters() { - when(rdsClient.createDBClusterParameterGroup(any(CreateDbClusterParameterGroupRequest.class))) - .thenReturn(CreateDbClusterParameterGroupResponse.builder() - .dbClusterParameterGroup(DB_CLUSTER_PARAMETER_GROUP) - .build()); - - when(rdsClient.modifyDBClusterParameterGroup(any(ModifyDbClusterParameterGroupRequest.class))) - .thenReturn(ModifyDbClusterParameterGroupResponse.builder().build()); - - when(rdsClient.describeDBClusters(any(DescribeDbClustersRequest.class))) - .thenThrow(AwsServiceException.builder() - .awsErrorDetails(AwsErrorDetails.builder() - .errorCode(HandlerErrorCode.Throttling.toString()) - .build()) - .build()); - - mockDescribeDbClusterParametersResponse("static", "dynamic", true); - - test_handleRequest_base( - new CallbackContext(), - null, - () -> RESOURCE_MODEL, - expectFailed(HandlerErrorCode.Throttling) - ); - - verify(rdsProxy.client(), times(1)).createDBClusterParameterGroup(any(CreateDbClusterParameterGroupRequest.class)); - verify(rdsProxy.client(), times(1)).modifyDBClusterParameterGroup(any(ModifyDbClusterParameterGroupRequest.class)); - verify(rdsProxy.client(), times(1)).describeDBClusters(any(DescribeDbClustersRequest.class)); } @Test @@ -413,11 +378,6 @@ public void handleRequest_SuccessWithParameters() { when(rdsClient.modifyDBClusterParameterGroup(any(ModifyDbClusterParameterGroupRequest.class))) .thenReturn(ModifyDbClusterParameterGroupResponse.builder().build()); - when(rdsClient.describeDBClusters(any(DescribeDbClustersRequest.class))) - .thenReturn(DescribeDbClustersResponse.builder() - .dbClusters(DBCluster.builder().dbClusterParameterGroup("group").status("available").build()) - .build()); - final DBClusterParameterGroup dbClusterParameterGroup = DBClusterParameterGroup.builder() .dbClusterParameterGroupArn(ARN) .dbClusterParameterGroupName(RESOURCE_MODEL.getDBClusterParameterGroupName()) @@ -440,7 +400,6 @@ public void handleRequest_SuccessWithParameters() { verify(rdsProxy.client(), times(1)).createDBClusterParameterGroup(any(CreateDbClusterParameterGroupRequest.class)); verify(rdsProxy.client(), times(1)).modifyDBClusterParameterGroup(any(ModifyDbClusterParameterGroupRequest.class)); - verify(rdsProxy.client(), times(1)).describeDBClusters(any(DescribeDbClustersRequest.class)); } /** @@ -468,11 +427,6 @@ public void handleRequest_SuccessSplitParameters() { when(rdsClient.modifyDBClusterParameterGroup(any(ModifyDbClusterParameterGroupRequest.class))) .thenReturn(ModifyDbClusterParameterGroupResponse.builder().build()); - when(rdsClient.describeDBClusters(any(DescribeDbClustersRequest.class))) - .thenReturn(DescribeDbClustersResponse.builder() - .dbClusters(DBCluster.builder().dbClusterParameterGroup("group").status("available").build()) - .build()); - final DBClusterParameterGroup dbClusterParameterGroup = DBClusterParameterGroup.builder() .dbClusterParameterGroupArn(ARN) .dbClusterParameterGroupName(RESOURCE_MODEL.getDBClusterParameterGroupName()) @@ -509,7 +463,6 @@ public void handleRequest_SuccessSplitParameters() { verify(rdsProxy.client(), times(1)).createDBClusterParameterGroup(any(CreateDbClusterParameterGroupRequest.class)); verify(rdsProxy.client(), times(3)).modifyDBClusterParameterGroup(captor.capture()); - verify(rdsProxy.client(), times(1)).describeDBClusters(any(DescribeDbClustersRequest.class)); ModifyDbClusterParameterGroupRequest firstRequest = captor.getAllValues().get(0); assertThat(verifyParameterExistsInRequest("aurora_enhanced_binlog", firstRequest)).isEqualTo(true); @@ -524,11 +477,6 @@ public void handleRequest_SuccessWithEmptyParameters() { .dbClusterParameterGroup(DB_CLUSTER_PARAMETER_GROUP) .build()); - when(rdsClient.describeDBClusters(any(DescribeDbClustersRequest.class))) - .thenReturn(DescribeDbClustersResponse.builder() - .dbClusters(DBCluster.builder().dbClusterParameterGroup("group").status("available").build()) - .build()); - final DBClusterParameterGroup dbClusterParameterGroup = DBClusterParameterGroup.builder() .dbClusterParameterGroupArn(ARN) .dbClusterParameterGroupName(RESOURCE_MODEL.getDBClusterParameterGroupName()) @@ -550,7 +498,6 @@ public void handleRequest_SuccessWithEmptyParameters() { ); verify(rdsProxy.client()).createDBClusterParameterGroup(any(CreateDbClusterParameterGroupRequest.class)); - verify(rdsProxy.client()).describeDBClusters(any(DescribeDbClustersRequest.class)); } @Test @@ -582,16 +529,16 @@ public void handleRequest_InProgressFailedUnsupportedParams() { .dbClusterParameterGroup(DB_CLUSTER_PARAMETER_GROUP) .build()); - mockDescribeDbClusterParametersResponse("static", "dynamic", true); + mockDescribeDbClusterParametersResponse("static", "dynamic", false); final ProgressEvent response = test_handleRequest_base( new CallbackContext(), null, - () -> RESOURCE_MODEL.toBuilder().parameters(Collections.singletonMap("Wrong Key", "Wrong value")).build(), + () -> RESOURCE_MODEL.toBuilder().parameters(Collections.singletonMap("param", "updated value")).build(), expectFailed(HandlerErrorCode.InvalidRequest) ); - assertThat(response.getMessage()).isEqualTo("Invalid / Unmodifiable / Unsupported DB Parameter: Wrong Key"); + assertThat(response.getMessage()).isEqualTo("Invalid / Unmodifiable / Unsupported DB Parameter: param"); verify(rdsProxy.client()).createDBClusterParameterGroup(any(CreateDbClusterParameterGroupRequest.class)); verify(rdsProxy.client(), times(2)).describeDBClusterParameters(any(DescribeDbClusterParametersRequest.class)); diff --git a/aws-rds-dbclusterparametergroup/src/test/java/software/amazon/rds/dbclusterparametergroup/UpdateHandlerTest.java b/aws-rds-dbclusterparametergroup/src/test/java/software/amazon/rds/dbclusterparametergroup/UpdateHandlerTest.java index 07b1ffff6..a224f3536 100644 --- a/aws-rds-dbclusterparametergroup/src/test/java/software/amazon/rds/dbclusterparametergroup/UpdateHandlerTest.java +++ b/aws-rds-dbclusterparametergroup/src/test/java/software/amazon/rds/dbclusterparametergroup/UpdateHandlerTest.java @@ -13,6 +13,7 @@ import java.util.List; import java.util.Map; +import lombok.Getter; import org.assertj.core.api.Assertions; import org.assertj.core.util.Lists; import org.junit.jupiter.api.AfterEach; @@ -22,8 +23,6 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; - -import lombok.Getter; import software.amazon.awssdk.services.rds.RdsClient; import software.amazon.awssdk.services.rds.model.AddTagsToResourceRequest; import software.amazon.awssdk.services.rds.model.AddTagsToResourceResponse; @@ -165,7 +164,10 @@ public void handleRequest_ResetRemovedParameters() { when(rdsClient.describeDBClusterParameters(any(DescribeDbClusterParametersRequest.class))) .thenReturn(DescribeDbClusterParametersResponse.builder() - .parameters(Parameter.builder().parameterName("parameter1").build()).build()); + .parameters( + Parameter.builder().parameterName("param").parameterValue("value").build(), + Parameter.builder().parameterName("param2").parameterValue("value").build() + ).build()); CallbackContext callbackContext = new CallbackContext(); callbackContext.setParametersApplied(true); @@ -173,7 +175,7 @@ public void handleRequest_ResetRemovedParameters() { test_handleRequest_base( callbackContext, () -> DBClusterParameterGroup.builder().dbClusterParameterGroupArn(ARN).build(), - () -> RESOURCE_MODEL_PREV, + () -> RESOURCE_MODEL, () -> RESOURCE_MODEL_UPD, expectSuccess() ); @@ -181,11 +183,11 @@ public void handleRequest_ResetRemovedParameters() { ArgumentCaptor captor = ArgumentCaptor.forClass(ResetDbClusterParameterGroupRequest.class); verify(rdsProxy.client(), times(1)).resetDBClusterParameterGroup(captor.capture()); Assertions.assertThat(captor.getValue().parameters()).hasSize(1); - Assertions.assertThat(captor.getValue().parameters().get(0).parameterName()).isEqualTo("parameter1"); - + Assertions.assertThat(captor.getValue().parameters().get(0).parameterName()).isEqualTo("param2"); verify(rdsProxy.client(), times(1)).describeDBClusterParameterGroups(any(DescribeDbClusterParameterGroupsRequest.class)); verify(rdsProxy.client(), times(1)).addTagsToResource(any(AddTagsToResourceRequest.class)); + verify(rdsProxy.client(), times(1)).removeTagsFromResource(any(RemoveTagsFromResourceRequest.class)); } @Test @@ -220,7 +222,7 @@ public void handleRequest_StabilizeDBClusters() { when(rdsClient.describeDBClusterParameters(any(DescribeDbClusterParametersRequest.class))) .thenReturn(DescribeDbClusterParametersResponse.builder() - .parameters(translateParamMapToCollection(PARAMS)) + .parameters(translateParamMapToCollection(PARAMS, true)) .build()); final DBClusterParameterGroup dbClusterParameterGroup = DBClusterParameterGroup.builder() @@ -242,14 +244,6 @@ public void handleRequest_StabilizeDBClusters() { .dbClusterParameterGroup("group-name") .build(); - when(rdsClient.describeDBClusters(any(DescribeDbClustersRequest.class))) - .thenReturn(DescribeDbClustersResponse.builder() - .dbClusters(dbCluster.toBuilder().status("modifying").build()) - .build()) - .thenReturn(DescribeDbClustersResponse.builder() - .dbClusters(DBCluster.builder().status("available").build()) - .build()); - test_handleRequest_base( new CallbackContext(), () -> dbClusterParameterGroup, @@ -260,7 +254,6 @@ public void handleRequest_StabilizeDBClusters() { verify(rdsProxy.client(), times(1)).resetDBClusterParameterGroup(any(ResetDbClusterParameterGroupRequest.class)); verify(rdsProxy.client(), times(1)).describeDBClusterParameters(any(DescribeDbClusterParametersRequest.class)); - verify(rdsProxy.client(), times(2)).describeDBClusters(any(DescribeDbClustersRequest.class)); verify(rdsProxy.client(), times(1)).describeDBClusterParameterGroups(any(DescribeDbClusterParameterGroupsRequest.class)); verify(rdsProxy.client(), times(1)).listTagsForResource(any(ListTagsForResourceRequest.class)); verify(rdsProxy.client(), times(1)).addTagsToResource(any(AddTagsToResourceRequest.class)); diff --git a/aws-rds-dbinstance/aws-rds-dbinstance.json b/aws-rds-dbinstance/aws-rds-dbinstance.json index d99265e88..2a23ce6e0 100644 --- a/aws-rds-dbinstance/aws-rds-dbinstance.json +++ b/aws-rds-dbinstance/aws-rds-dbinstance.json @@ -138,6 +138,11 @@ "type": "string", "description": "The Amazon Web Services KMS key identifier for encryption of the replicated automated backups. The KMS key ID is the Amazon Resource Name (ARN) for the KMS encryption key in the destination Amazon Web Services Region, for example, `arn:aws:kms:us-east-1:123456789012:key/AKIAIOSFODNN7EXAMPLE` ." }, + "AutomaticBackupReplicationRetentionPeriod": { + "type": "integer", + "minimum": 1, + "description": "The number of days for which automated cross-region replicated backups are retained. If this value is unset, default to BackupRetentionPeriod." + }, "AvailabilityZone": { "type": "string", "description": "The Availability Zone (AZ) where the database will be created. For information on AWS Regions and Availability Zones." @@ -171,6 +176,10 @@ "type": "string", "description": "The instance profile associated with the underlying Amazon EC2 instance of an RDS Custom DB instance. The instance profile must meet the following requirements:\n * The profile must exist in your account.\n * The profile must have an IAM role that Amazon EC2 has permissions to assume.\n * The instance profile name and the associated IAM role name must start with the prefix AWSRDSCustom .\nFor the list of permissions required for the IAM role, see Configure IAM and your VPC in the Amazon RDS User Guide .\n\nThis setting is required for RDS Custom." }, + "DatabaseInsightsMode": { + "description": "A value that indicates the mode of Database Insights to enable for the DB instance", + "type": "string" + }, "DBClusterIdentifier": { "type": "string", "description": "The identifier of the DB cluster that the instance will belong to." @@ -469,6 +478,10 @@ "type": "string" }, "description": "A list of the VPC security group IDs to assign to the DB instance. The list can include both the physical IDs of existing VPC security groups and references to AWS::EC2::SecurityGroup resources created in the template." + }, + "ApplyImmediately": { + "type": "boolean", + "description": "Specifies whether the modifications in this request and any pending modifications are asynchronously applied as soon as possible, regardless of the PreferredMaintenanceWindow setting for the DB instance. By default, this parameter is enabled." } }, "additionalProperties": false, @@ -480,13 +493,14 @@ "/properties/DBParameterGroupName": "$lowercase(DBParameterGroupName)", "/properties/DBSnapshotIdentifier": "$lowercase(DBSnapshotIdentifier)", "/properties/DBSubnetGroupName": "$lowercase(DBSubnetGroupName)", + "/properties/DBSystemId": "$uppercase(DBSystemId)", "/properties/Engine": "$lowercase(Engine)", "/properties/EngineVersion": "$join([$string(EngineVersion), \".*\"])", - "/properties/KmsKeyId": "$join([\"arn:(aws)[-]{0,1}[a-z]{0,2}[-]{0,1}[a-z]{0,3}:kms:[a-z]{2}[-]{1}[a-z]{3,10}[-]{0,1}[a-z]{0,10}[-]{1}[1-3]{1}:[0-9]{12}[:]{1}key\\/\", KmsKeyId])", - "/properties/MasterUserSecret/KmsKeyId": "$join([\"arn:(aws)[-]{0,1}[a-z]{0,2}[-]{0,1}[a-z]{0,3}:kms:[a-z]{2}[-]{1}[a-z]{3,10}[-]{0,1}[a-z]{0,10}[-]{1}[1-3]{1}:[0-9]{12}[:]{1}key\\/\", MasterUserSecret.KmsKeyId])", + "/properties/KmsKeyId": "$join([\"arn:.+?:kms:.+?:.+?:key\\/\", KmsKeyId])", + "/properties/MasterUserSecret/KmsKeyId": "$join([\"arn:.+?:kms:.+?:.+?:key\\/\", MasterUserSecret.KmsKeyId])", "/properties/NetworkType": "$lowercase(NetworkType)", "/properties/OptionGroupName": "$lowercase(OptionGroupName)", - "/properties/PerformanceInsightsKMSKeyId": "$join([\"arn:(aws)[-]{0,1}[a-z]{0,2}[-]{0,1}[a-z]{0,3}:kms:[a-z]{2}[-]{1}[a-z]{3,10}[-]{0,1}[a-z]{0,10}[-]{1}[1-3]{1}:[0-9]{12}[:]{1}key\\/\", PerformanceInsightsKMSKeyId])", + "/properties/PerformanceInsightsKMSKeyId": "$join([\"arn:.+?:kms:.+?:.+?:key\\/\", PerformanceInsightsKMSKeyId])", "/properties/PreferredMaintenanceWindow": "$lowercase(PreferredMaintenanceWindow)", "/properties/SourceDBInstanceAutomatedBackupsArn": "$lowercase(SourceDBInstanceAutomatedBackupsArn)", "/properties/SourceDBInstanceIdentifier": "$lowercase(SourceDBInstanceIdentifier)", @@ -499,10 +513,10 @@ "/properties/DBInstanceIdentifier", "/properties/DBName", "/properties/DBSubnetGroupName", + "/properties/DBSystemId", "/properties/KmsKeyId", "/properties/MasterUsername", "/properties/NcharCharacterSetName", - "/properties/Port", "/properties/SourceRegion", "/properties/StorageEncrypted", "/properties/Timezone" @@ -537,7 +551,6 @@ "/properties/DBSnapshotIdentifier", "/properties/DeleteAutomatedBackups", "/properties/MasterUserPassword", - "/properties/Port", "/properties/RestoreTime", "/properties/SourceDBInstanceAutomatedBackupsArn", "/properties/SourceDBInstanceIdentifier", @@ -545,16 +558,18 @@ "/properties/SourceRegion", "/properties/TdeCredentialPassword", "/properties/UseDefaultProcessorFeatures", - "/properties/UseLatestRestorableTime" + "/properties/UseLatestRestorableTime", + "/properties/ApplyImmediately" ], "readOnlyProperties": [ + "/properties/Endpoint", "/properties/Endpoint/Address", "/properties/Endpoint/Port", "/properties/Endpoint/HostedZoneId", "/properties/DbiResourceId", "/properties/DBInstanceArn", - "/properties/DBSystemId", "/properties/MasterUserSecret/SecretArn", + "/properties/CertificateDetails", "/properties/CertificateDetails/CAIdentifier", "/properties/CertificateDetails/ValidTill" ], @@ -630,6 +645,7 @@ "rds:DescribeDBEngineVersions", "rds:DescribeDBInstances", "rds:DescribeDBParameterGroups", + "rds:DescribeDBInstanceAutomatedBackups", "rds:DescribeEvents", "rds:ModifyDBInstance", "rds:PromoteReadReplica", diff --git a/aws-rds-dbinstance/docs/README.md b/aws-rds-dbinstance/docs/README.md deleted file mode 100644 index 82cfe3ab3..000000000 --- a/aws-rds-dbinstance/docs/README.md +++ /dev/null @@ -1,1044 +0,0 @@ -# AWS::RDS::DBInstance - -The AWS::RDS::DBInstance resource creates an Amazon RDS DB instance. - -## Syntax - -To declare this entity in your AWS CloudFormation template, use the following syntax: - -### JSON - -
-{
-    "Type" : "AWS::RDS::DBInstance",
-    "Properties" : {
-        "AllocatedStorage" : String,
-        "AllowMajorVersionUpgrade" : Boolean,
-        "AssociatedRoles" : [ DBInstanceRole, ... ],
-        "AutoMinorVersionUpgrade" : Boolean,
-        "AutomaticBackupReplicationRegion" : String,
-        "AutomaticBackupReplicationKmsKeyId" : String,
-        "AvailabilityZone" : String,
-        "BackupRetentionPeriod" : Integer,
-        "CACertificateIdentifier" : String,
-        "CertificateDetails" : CertificateDetails,
-        "CertificateRotationRestart" : Boolean,
-        "CharacterSetName" : String,
-        "CopyTagsToSnapshot" : Boolean,
-        "CustomIAMInstanceProfile" : String,
-        "DBClusterIdentifier" : String,
-        "DBClusterSnapshotIdentifier" : String,
-        "DBInstanceClass" : String,
-        "DBInstanceIdentifier" : String,
-        "DBName" : String,
-        "DBParameterGroupName" : String,
-        "DBSecurityGroups" : [ String, ... ],
-        "DBSnapshotIdentifier" : String,
-        "DBSubnetGroupName" : String,
-        "DedicatedLogVolume" : Boolean,
-        "DeleteAutomatedBackups" : Boolean,
-        "DeletionProtection" : Boolean,
-        "Domain" : String,
-        "DomainAuthSecretArn" : String,
-        "DomainDnsIps" : [ String, ... ],
-        "DomainFqdn" : String,
-        "DomainIAMRoleName" : String,
-        "DomainOu" : String,
-        "EnableCloudwatchLogsExports" : [ String, ... ],
-        "EnableIAMDatabaseAuthentication" : Boolean,
-        "EnablePerformanceInsights" : Boolean,
-        "Endpoint" : Endpoint,
-        "Engine" : String,
-        "EngineLifecycleSupport" : String,
-        "EngineVersion" : String,
-        "ManageMasterUserPassword" : Boolean,
-        "Iops" : Integer,
-        "KmsKeyId" : String,
-        "LicenseModel" : String,
-        "MasterUsername" : String,
-        "MasterUserPassword" : String,
-        "MasterUserSecret" : MasterUserSecret,
-        "MaxAllocatedStorage" : Integer,
-        "MonitoringInterval" : Integer,
-        "MonitoringRoleArn" : String,
-        "MultiAZ" : Boolean,
-        "NcharCharacterSetName" : String,
-        "NetworkType" : String,
-        "OptionGroupName" : String,
-        "PerformanceInsightsKMSKeyId" : String,
-        "PerformanceInsightsRetentionPeriod" : Integer,
-        "Port" : String,
-        "PreferredBackupWindow" : String,
-        "PreferredMaintenanceWindow" : String,
-        "ProcessorFeatures" : [ ProcessorFeature, ... ],
-        "PromotionTier" : Integer,
-        "PubliclyAccessible" : Boolean,
-        "ReplicaMode" : String,
-        "RestoreTime" : String,
-        "SourceDBClusterIdentifier" : String,
-        "SourceDbiResourceId" : String,
-        "SourceDBInstanceAutomatedBackupsArn" : String,
-        "SourceDBInstanceIdentifier" : String,
-        "SourceRegion" : String,
-        "StorageEncrypted" : Boolean,
-        "StorageType" : String,
-        "StorageThroughput" : Integer,
-        "Tags" : [ Tag, ... ],
-        "TdeCredentialArn" : String,
-        "TdeCredentialPassword" : String,
-        "Timezone" : String,
-        "UseDefaultProcessorFeatures" : Boolean,
-        "UseLatestRestorableTime" : Boolean,
-        "VPCSecurityGroups" : [ String, ... ]
-    }
-}
-
- -### YAML - -
-Type: AWS::RDS::DBInstance
-Properties:
-    AllocatedStorage: String
-    AllowMajorVersionUpgrade: Boolean
-    AssociatedRoles: 
-      - DBInstanceRole
-    AutoMinorVersionUpgrade: Boolean
-    AutomaticBackupReplicationRegion: String
-    AutomaticBackupReplicationKmsKeyId: String
-    AvailabilityZone: String
-    BackupRetentionPeriod: Integer
-    CACertificateIdentifier: String
-    CertificateDetails: CertificateDetails
-    CertificateRotationRestart: Boolean
-    CharacterSetName: String
-    CopyTagsToSnapshot: Boolean
-    CustomIAMInstanceProfile: String
-    DBClusterIdentifier: String
-    DBClusterSnapshotIdentifier: String
-    DBInstanceClass: String
-    DBInstanceIdentifier: String
-    DBName: String
-    DBParameterGroupName: String
-    DBSecurityGroups: 
-      - String
-    DBSnapshotIdentifier: String
-    DBSubnetGroupName: String
-    DedicatedLogVolume: Boolean
-    DeleteAutomatedBackups: Boolean
-    DeletionProtection: Boolean
-    Domain: String
-    DomainAuthSecretArn: String
-    DomainDnsIps: 
-      - String
-    DomainFqdn: String
-    DomainIAMRoleName: String
-    DomainOu: String
-    EnableCloudwatchLogsExports: 
-      - String
-    EnableIAMDatabaseAuthentication: Boolean
-    EnablePerformanceInsights: Boolean
-    Endpoint: Endpoint
-    Engine: String
-    EngineLifecycleSupport: String
-    EngineVersion: String
-    ManageMasterUserPassword: Boolean
-    Iops: Integer
-    KmsKeyId: String
-    LicenseModel: String
-    MasterUsername: String
-    MasterUserPassword: String
-    MasterUserSecret: MasterUserSecret
-    MaxAllocatedStorage: Integer
-    MonitoringInterval: Integer
-    MonitoringRoleArn: String
-    MultiAZ: Boolean
-    NcharCharacterSetName: String
-    NetworkType: String
-    OptionGroupName: String
-    PerformanceInsightsKMSKeyId: String
-    PerformanceInsightsRetentionPeriod: Integer
-    Port: String
-    PreferredBackupWindow: String
-    PreferredMaintenanceWindow: String
-    ProcessorFeatures: 
-      - ProcessorFeature
-    PromotionTier: Integer
-    PubliclyAccessible: Boolean
-    ReplicaMode: String
-    RestoreTime: String
-    SourceDBClusterIdentifier: String
-    SourceDbiResourceId: String
-    SourceDBInstanceAutomatedBackupsArn: String
-    SourceDBInstanceIdentifier: String
-    SourceRegion: String
-    StorageEncrypted: Boolean
-    StorageType: String
-    StorageThroughput: Integer
-    Tags: 
-      - Tag
-    TdeCredentialArn: String
-    TdeCredentialPassword: String
-    Timezone: String
-    UseDefaultProcessorFeatures: Boolean
-    UseLatestRestorableTime: Boolean
-    VPCSecurityGroups: 
-      - String
-
- -## Properties - -#### AllocatedStorage - -The amount of storage (in gigabytes) to be initially allocated for the database instance. - -_Required_: No - -_Type_: String - -_Pattern_: ^[0-9]*$ - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### AllowMajorVersionUpgrade - -A value that indicates whether major version upgrades are allowed. Changing this parameter doesn't result in an outage and the change is asynchronously applied as soon as possible. - -_Required_: No - -_Type_: Boolean - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### AssociatedRoles - -The AWS Identity and Access Management (IAM) roles associated with the DB instance. - -_Required_: No - -_Type_: List of DBInstanceRole - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### AutoMinorVersionUpgrade - -A value that indicates whether minor engine upgrades are applied automatically to the DB instance during the maintenance window. By default, minor engine upgrades are applied automatically. - -_Required_: No - -_Type_: Boolean - -_Update requires_: [Some interruptions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-some-interrupt) - -#### AutomaticBackupReplicationRegion - -Enables replication of automated backups to a different Amazon Web Services Region. - -_Required_: No - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### AutomaticBackupReplicationKmsKeyId - -The Amazon Web Services KMS key identifier for encryption of the replicated automated backups. The KMS key ID is the Amazon Resource Name (ARN) for the KMS encryption key in the destination Amazon Web Services Region, for example, `arn:aws:kms:us-east-1:123456789012:key/AKIAIOSFODNN7EXAMPLE` . - -_Required_: No - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### AvailabilityZone - -The Availability Zone (AZ) where the database will be created. For information on AWS Regions and Availability Zones. - -_Required_: No - -_Type_: String - -_Update requires_: [Some interruptions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-some-interrupt) - -#### BackupRetentionPeriod - -The number of days for which automated backups are retained. Setting this parameter to a positive number enables backups. Setting this parameter to 0 disables automated backups. - -_Required_: No - -_Type_: Integer - -_Update requires_: [Some interruptions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-some-interrupt) - -#### CACertificateIdentifier - -The identifier of the CA certificate for this DB instance. - -_Required_: No - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### CertificateDetails - -_Required_: No - -_Type_: CertificateDetails - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### CertificateRotationRestart - -A value that indicates whether the DB instance is restarted when you rotate your SSL/TLS certificate. -By default, the DB instance is restarted when you rotate your SSL/TLS certificate. The certificate is not updated until the DB instance is restarted. -If you are using SSL/TLS to connect to the DB instance, follow the appropriate instructions for your DB engine to rotate your SSL/TLS certificate -This setting doesn't apply to RDS Custom. - -_Required_: No - -_Type_: Boolean - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### CharacterSetName - -For supported engines, indicates that the DB instance should be associated with the specified character set. - -_Required_: No - -_Type_: String - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### CopyTagsToSnapshot - -A value that indicates whether to copy tags from the DB instance to snapshots of the DB instance. By default, tags are not copied. - -_Required_: No - -_Type_: Boolean - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### CustomIAMInstanceProfile - -The instance profile associated with the underlying Amazon EC2 instance of an RDS Custom DB instance. The instance profile must meet the following requirements: - * The profile must exist in your account. - * The profile must have an IAM role that Amazon EC2 has permissions to assume. - * The instance profile name and the associated IAM role name must start with the prefix AWSRDSCustom . -For the list of permissions required for the IAM role, see Configure IAM and your VPC in the Amazon RDS User Guide . - -This setting is required for RDS Custom. - -_Required_: No - -_Type_: String - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### DBClusterIdentifier - -The identifier of the DB cluster that the instance will belong to. - -_Required_: No - -_Type_: String - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### DBClusterSnapshotIdentifier - -The identifier for the RDS for MySQL Multi-AZ DB cluster snapshot to restore from. For more information on Multi-AZ DB clusters, see Multi-AZ deployments with two readable standby DB instances in the Amazon RDS User Guide . - -Constraints: - * Must match the identifier of an existing Multi-AZ DB cluster snapshot. - * Can't be specified when DBSnapshotIdentifier is specified. - * Must be specified when DBSnapshotIdentifier isn't specified. - * If you are restoring from a shared manual Multi-AZ DB cluster snapshot, the DBClusterSnapshotIdentifier must be the ARN of the shared snapshot. - * Can't be the identifier of an Aurora DB cluster snapshot. - * Can't be the identifier of an RDS for PostgreSQL Multi-AZ DB cluster snapshot. - -_Required_: No - -_Type_: String - -_Update requires_: [Some interruptions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-some-interrupt) - -#### DBInstanceClass - -The compute and memory capacity of the DB instance, for example, db.m4.large. Not all DB instance classes are available in all AWS Regions, or for all database engines. - -_Required_: No - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### DBInstanceIdentifier - -A name for the DB instance. If you specify a name, AWS CloudFormation converts it to lowercase. If you don't specify a name, AWS CloudFormation generates a unique physical ID and uses that ID for the DB instance. - -_Required_: No - -_Type_: String - -_Minimum Length_: 1 - -_Maximum Length_: 63 - -_Pattern_: ^$|^[a-zA-Z]{1}(?:-?[a-zA-Z0-9]){0,62}$ - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### DBName - -The meaning of this parameter differs according to the database engine you use. - -_Required_: No - -_Type_: String - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### DBParameterGroupName - -The name of an existing DB parameter group or a reference to an AWS::RDS::DBParameterGroup resource created in the template. - -_Required_: No - -_Type_: String - -_Update requires_: [Some interruptions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-some-interrupt) - -#### DBSecurityGroups - -A list of the DB security groups to assign to the DB instance. The list can include both the name of existing DB security groups or references to AWS::RDS::DBSecurityGroup resources created in the template. - -_Required_: No - -_Type_: List of String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### DBSnapshotIdentifier - -The name or Amazon Resource Name (ARN) of the DB snapshot that's used to restore the DB instance. If you're restoring from a shared manual DB snapshot, you must specify the ARN of the snapshot. - -_Required_: No - -_Type_: String - -_Update requires_: [Some interruptions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-some-interrupt) - -#### DBSubnetGroupName - -A DB subnet group to associate with the DB instance. If you update this value, the new subnet group must be a subnet group in a new VPC. - -_Required_: No - -_Type_: String - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### DedicatedLogVolume - -Indicates whether the DB instance has a dedicated log volume (DLV) enabled. - -_Required_: No - -_Type_: Boolean - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### DeleteAutomatedBackups - -A value that indicates whether to remove automated backups immediately after the DB instance is deleted. This parameter isn't case-sensitive. The default is to remove automated backups immediately after the DB instance is deleted. - -_Required_: No - -_Type_: Boolean - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### DeletionProtection - -A value that indicates whether the DB instance has deletion protection enabled. The database can't be deleted when deletion protection is enabled. By default, deletion protection is disabled. - -_Required_: No - -_Type_: Boolean - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### Domain - -The Active Directory directory ID to create the DB instance in. Currently, only MySQL, Microsoft SQL Server, Oracle, and PostgreSQL DB instances can be created in an Active Directory Domain. - -_Required_: No - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### DomainAuthSecretArn - -The ARN for the Secrets Manager secret with the credentials for the user joining the domain. - -_Required_: No - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### DomainDnsIps - -The IPv4 DNS IP addresses of your primary and secondary Active Directory domain controllers. - -_Required_: No - -_Type_: List of String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### DomainFqdn - -The fully qualified domain name (FQDN) of an Active Directory domain. - -_Required_: No - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### DomainIAMRoleName - -Specify the name of the IAM role to be used when making API calls to the Directory Service. - -_Required_: No - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### DomainOu - -The Active Directory organizational unit for your DB instance to join. - -_Required_: No - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### EnableCloudwatchLogsExports - -The list of log types that need to be enabled for exporting to CloudWatch Logs. The values in the list depend on the DB engine being used. - -_Required_: No - -_Type_: List of String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### EnableIAMDatabaseAuthentication - -A value that indicates whether to enable mapping of AWS Identity and Access Management (IAM) accounts to database accounts. By default, mapping is disabled. - -_Required_: No - -_Type_: Boolean - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### EnablePerformanceInsights - -A value that indicates whether to enable Performance Insights for the DB instance. - -_Required_: No - -_Type_: Boolean - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### Endpoint - -_Required_: No - -_Type_: Endpoint - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### Engine - -The name of the database engine that you want to use for this DB instance. - -_Required_: No - -_Type_: String - -_Update requires_: [Some interruptions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-some-interrupt) - -#### EngineLifecycleSupport - -The life cycle type of the DB instance. You can use this setting to enroll your DB instance into Amazon RDS Extended Support. - -_Required_: No - -_Type_: String - -_Update requires_: [Some interruptions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-some-interrupt) - -#### EngineVersion - -The version number of the database engine to use. - -_Required_: No - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### ManageMasterUserPassword - -A value that indicates whether to manage the master user password with AWS Secrets Manager. - -_Required_: No - -_Type_: Boolean - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### Iops - -The number of I/O operations per second (IOPS) that the database provisions. - -_Required_: No - -_Type_: Integer - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### KmsKeyId - -The ARN of the AWS Key Management Service (AWS KMS) master key that's used to encrypt the DB instance. - -_Required_: No - -_Type_: String - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### LicenseModel - -License model information for this DB instance. - -_Required_: No - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### MasterUsername - -The master user name for the DB instance. - -_Required_: No - -_Type_: String - -_Minimum Length_: 1 - -_Maximum Length_: 128 - -_Pattern_: ^[a-zA-Z][a-zA-Z0-9_]{0,127}$ - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### MasterUserPassword - -The password for the master user. - -_Required_: No - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### MasterUserSecret - -_Required_: No - -_Type_: MasterUserSecret - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### MaxAllocatedStorage - -The upper limit to which Amazon RDS can automatically scale the storage of the DB instance. - -_Required_: No - -_Type_: Integer - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### MonitoringInterval - -The interval, in seconds, between points when Enhanced Monitoring metrics are collected for the DB instance. To disable collecting Enhanced Monitoring metrics, specify 0. The default is 0. - -_Required_: No - -_Type_: Integer - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### MonitoringRoleArn - -The ARN for the IAM role that permits RDS to send enhanced monitoring metrics to Amazon CloudWatch Logs. - -_Required_: No - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### MultiAZ - -Specifies whether the database instance is a multiple Availability Zone deployment. - -_Required_: No - -_Type_: Boolean - -_Update requires_: [Some interruptions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-some-interrupt) - -#### NcharCharacterSetName - -The name of the NCHAR character set for the Oracle DB instance. This parameter doesn't apply to RDS Custom. - -_Required_: No - -_Type_: String - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### NetworkType - -The network type of the DB cluster. - -_Required_: No - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### OptionGroupName - -Indicates that the DB instance should be associated with the specified option group. - -_Required_: No - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### PerformanceInsightsKMSKeyId - -The AWS KMS key identifier for encryption of Performance Insights data. The KMS key ID is the Amazon Resource Name (ARN), KMS key identifier, or the KMS key alias for the KMS encryption key. - -_Required_: No - -_Type_: String - -_Update requires_: [Some interruptions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-some-interrupt) - -#### PerformanceInsightsRetentionPeriod - -The amount of time, in days, to retain Performance Insights data. Valid values are 7 or 731 (2 years). - -_Required_: No - -_Type_: Integer - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### Port - -The port number on which the database accepts connections. - -_Required_: No - -_Type_: String - -_Pattern_: ^\d*$ - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### PreferredBackupWindow - -The daily time range during which automated backups are created if automated backups are enabled, using the BackupRetentionPeriod parameter. - -_Required_: No - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### PreferredMaintenanceWindow - -he weekly time range during which system maintenance can occur, in Universal Coordinated Time (UTC). - -_Required_: No - -_Type_: String - -_Update requires_: [Some interruptions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-some-interrupt) - -#### ProcessorFeatures - -The number of CPU cores and the number of threads per core for the DB instance class of the DB instance. - -_Required_: No - -_Type_: List of ProcessorFeature - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### PromotionTier - -A value that specifies the order in which an Aurora Replica is promoted to the primary instance after a failure of the existing primary instance. - -_Required_: No - -_Type_: Integer - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### PubliclyAccessible - -Indicates whether the DB instance is an internet-facing instance. If you specify true, AWS CloudFormation creates an instance with a publicly resolvable DNS name, which resolves to a public IP address. If you specify false, AWS CloudFormation creates an internal instance with a DNS name that resolves to a private IP address. - -_Required_: No - -_Type_: Boolean - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### ReplicaMode - -The open mode of an Oracle read replica. The default is open-read-only. - -_Required_: No - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### RestoreTime - -The date and time to restore from. - -_Required_: No - -_Type_: String - -_Update requires_: [Some interruptions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-some-interrupt) - -#### SourceDBClusterIdentifier - -The identifier of the Multi-AZ DB cluster that will act as the source for the read replica. Each DB cluster can have up to 15 read replicas. - -_Required_: No - -_Type_: String - -_Update requires_: [Some interruptions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-some-interrupt) - -#### SourceDbiResourceId - -The resource ID of the source DB instance from which to restore. - -_Required_: No - -_Type_: String - -_Update requires_: [Some interruptions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-some-interrupt) - -#### SourceDBInstanceAutomatedBackupsArn - -The Amazon Resource Name (ARN) of the replicated automated backups from which to restore. - -_Required_: No - -_Type_: String - -_Update requires_: [Some interruptions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-some-interrupt) - -#### SourceDBInstanceIdentifier - -If you want to create a Read Replica DB instance, specify the ID of the source DB instance. Each DB instance can have a limited number of Read Replicas. - -_Required_: No - -_Type_: String - -_Update requires_: [Some interruptions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-some-interrupt) - -#### SourceRegion - -The ID of the region that contains the source DB instance for the Read Replica. - -_Required_: No - -_Type_: String - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### StorageEncrypted - -A value that indicates whether the DB instance is encrypted. By default, it isn't encrypted. - -_Required_: No - -_Type_: Boolean - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### StorageType - -Specifies the storage type to be associated with the DB instance. - -_Required_: No - -_Type_: String - -_Update requires_: [Some interruptions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-some-interrupt) - -#### StorageThroughput - -Specifies the storage throughput for the DB instance. - -_Required_: No - -_Type_: Integer - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### Tags - -Tags to assign to the DB instance. - -_Required_: No - -_Type_: List of Tag - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### TdeCredentialArn - -The ARN from the key store with which to associate the instance for TDE encryption. - -_Required_: No - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### TdeCredentialPassword - -The password for the given ARN from the key store in order to access the device. - -_Required_: No - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### Timezone - -The time zone of the DB instance. The time zone parameter is currently supported only by Microsoft SQL Server. - -_Required_: No - -_Type_: String - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### UseDefaultProcessorFeatures - -A value that indicates whether the DB instance class of the DB instance uses its default processor features. - -_Required_: No - -_Type_: Boolean - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### UseLatestRestorableTime - -A value that indicates whether the DB instance is restored from the latest backup time. By default, the DB instance isn't restored from the latest backup time. - -_Required_: No - -_Type_: Boolean - -_Update requires_: [Some interruptions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-some-interrupt) - -#### VPCSecurityGroups - -A list of the VPC security group IDs to assign to the DB instance. The list can include both the physical IDs of existing VPC security groups and references to AWS::EC2::SecurityGroup resources created in the template. - -_Required_: No - -_Type_: List of String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -## Return Values - -### Ref - -When you pass the logical ID of this resource to the intrinsic `Ref` function, Ref returns the DBInstanceIdentifier. - -### Fn::GetAtt - -The `Fn::GetAtt` intrinsic function returns a value for a specified attribute of this type. The following are the available attributes and sample return values. - -For more information about using the `Fn::GetAtt` intrinsic function, see [Fn::GetAtt](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html). - -#### Address - -Returns the Address value. - -#### Port - -Returns the Port value. - -#### HostedZoneId - -Returns the HostedZoneId value. - -#### DbiResourceId - -The AWS Region-unique, immutable identifier for the DB instance. This identifier is found in AWS CloudTrail log entries whenever the AWS KMS key for the DB instance is accessed. - -#### DBInstanceArn - -The Amazon Resource Name (ARN) for the DB instance. - -#### DBSystemId - -The Oracle system ID (Oracle SID) for a container database (CDB). The Oracle SID is also the name of the CDB. This setting is valid for RDS Custom only. - -#### SecretArn - -Returns the SecretArn value. - -#### CAIdentifier - -Returns the CAIdentifier value. - -#### ValidTill - -Returns the ValidTill value. diff --git a/aws-rds-dbinstance/docs/certificatedetails.md b/aws-rds-dbinstance/docs/certificatedetails.md deleted file mode 100644 index 013fb4385..000000000 --- a/aws-rds-dbinstance/docs/certificatedetails.md +++ /dev/null @@ -1,19 +0,0 @@ -# AWS::RDS::DBInstance CertificateDetails - -## Syntax - -To declare this entity in your AWS CloudFormation template, use the following syntax: - -### JSON - -
-{
-}
-
- -### YAML - -
-
- -## Properties diff --git a/aws-rds-dbinstance/docs/dbinstancerole.md b/aws-rds-dbinstance/docs/dbinstancerole.md deleted file mode 100644 index 53546597c..000000000 --- a/aws-rds-dbinstance/docs/dbinstancerole.md +++ /dev/null @@ -1,43 +0,0 @@ -# AWS::RDS::DBInstance DBInstanceRole - -## Syntax - -To declare this entity in your AWS CloudFormation template, use the following syntax: - -### JSON - -
-{
-    "FeatureName" : String,
-    "RoleArn" : String
-}
-
- -### YAML - -
-FeatureName: String
-RoleArn: String
-
- -## Properties - -#### FeatureName - -The name of the feature associated with the AWS Identity and Access Management (IAM) role. IAM roles that are associated with a DB instance grant permission for the DB instance to access other AWS services on your behalf. - -_Required_: Yes - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### RoleArn - -The Amazon Resource Name (ARN) of the IAM role that is associated with the DB instance. - -_Required_: Yes - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) diff --git a/aws-rds-dbinstance/docs/endpoint.md b/aws-rds-dbinstance/docs/endpoint.md deleted file mode 100644 index 14eb58f9f..000000000 --- a/aws-rds-dbinstance/docs/endpoint.md +++ /dev/null @@ -1,19 +0,0 @@ -# AWS::RDS::DBInstance Endpoint - -## Syntax - -To declare this entity in your AWS CloudFormation template, use the following syntax: - -### JSON - -
-{
-}
-
- -### YAML - -
-
- -## Properties diff --git a/aws-rds-dbinstance/docs/masterusersecret.md b/aws-rds-dbinstance/docs/masterusersecret.md deleted file mode 100644 index 26a92a2bf..000000000 --- a/aws-rds-dbinstance/docs/masterusersecret.md +++ /dev/null @@ -1,31 +0,0 @@ -# AWS::RDS::DBInstance MasterUserSecret - -## Syntax - -To declare this entity in your AWS CloudFormation template, use the following syntax: - -### JSON - -
-{
-    "KmsKeyId" : String
-}
-
- -### YAML - -
-KmsKeyId: String
-
- -## Properties - -#### KmsKeyId - -The AWS KMS key identifier that is used to encrypt the secret. - -_Required_: No - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) diff --git a/aws-rds-dbinstance/docs/processorfeature.md b/aws-rds-dbinstance/docs/processorfeature.md deleted file mode 100644 index 8b3fc36f9..000000000 --- a/aws-rds-dbinstance/docs/processorfeature.md +++ /dev/null @@ -1,45 +0,0 @@ -# AWS::RDS::DBInstance ProcessorFeature - -## Syntax - -To declare this entity in your AWS CloudFormation template, use the following syntax: - -### JSON - -
-{
-    "Name" : String,
-    "Value" : String
-}
-
- -### YAML - -
-Name: String
-Value: String
-
- -## Properties - -#### Name - -The name of the processor feature. Valid names are coreCount and threadsPerCore. - -_Required_: No - -_Type_: String - -_Allowed Values_: coreCount | threadsPerCore - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### Value - -The value of a processor feature name. - -_Required_: No - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) diff --git a/aws-rds-dbinstance/docs/tag.md b/aws-rds-dbinstance/docs/tag.md deleted file mode 100644 index d020bf14e..000000000 --- a/aws-rds-dbinstance/docs/tag.md +++ /dev/null @@ -1,51 +0,0 @@ -# AWS::RDS::DBInstance Tag - -A key-value pair to associate with a resource. - -## Syntax - -To declare this entity in your AWS CloudFormation template, use the following syntax: - -### JSON - -
-{
-    "Key" : String,
-    "Value" : String
-}
-
- -### YAML - -
-Key: String
-Value: String
-
- -## Properties - -#### Key - -The key name of the tag. You can specify a value that is 1 to 128 Unicode characters in length and cannot be prefixed with aws:. You can use any of the following characters: the set of Unicode letters, digits, whitespace, _, ., /, =, +, and -. - -_Required_: Yes - -_Type_: String - -_Minimum Length_: 1 - -_Maximum Length_: 128 - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### Value - -The value for the tag. You can specify a value that is 0 to 256 Unicode characters in length and cannot be prefixed with aws:. You can use any of the following characters: the set of Unicode letters, digits, whitespace, _, ., /, =, +, and -. - -_Required_: No - -_Type_: String - -_Maximum Length_: 256 - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) diff --git a/aws-rds-dbinstance/pom.xml b/aws-rds-dbinstance/pom.xml index bf7694fb3..92f1ce776 100644 --- a/aws-rds-dbinstance/pom.xml +++ b/aws-rds-dbinstance/pom.xml @@ -32,20 +32,14 @@ 1.0
- software.amazon.awssdk - rds - 2.25.56 + software.amazon.cloudformation + aws-cloudformation-rpdk-java-plugin + [2.0.0,3.0.0) software.amazon.awssdk ec2 - 2.21.17 - - - - software.amazon.cloudformation - aws-cloudformation-rpdk-java-plugin - [2.0.0,3.0.0) + 2.30.38 @@ -54,12 +48,6 @@ ${org.projectlombok.version} provided - - - software.amazon.awssdk - aws-query-protocol - 2.20.138 - org.assertj @@ -188,10 +176,57 @@ 0.8.8 - **/BaseConfiguration* - **/BaseHandler* - **/HandlerWrapper* - **/ResourceModel* + + **/software/amazon/rds/dbinstance/BaseConfiguration.class + **/software/amazon/rds/dbinstance/BaseHandler.class + **/software/amazon/rds/dbinstance/BaseHandlerStd.class + **/software/amazon/rds/dbinstance/CallbackContext.class + **/software/amazon/rds/dbinstance/CertificateDetails.class + **/software/amazon/rds/dbinstance/Configuration.class + **/software/amazon/rds/dbinstance/CreateHandler.class + **/software/amazon/rds/dbinstance/DBInstancePredicates.class + **/software/amazon/rds/dbinstance/DBInstanceRole.class + **/software/amazon/rds/dbinstance/DeleteHandler.class + **/software/amazon/rds/dbinstance/Endpoint.class + **/software/amazon/rds/dbinstance/HandlerWrapper.class + **/software/amazon/rds/dbinstance/HandlerWrapperExecutable.class + **/software/amazon/rds/dbinstance/ListHandler.class + **/software/amazon/rds/dbinstance/MasterUserSecret.class + **/software/amazon/rds/dbinstance/ProcessorFeature.class + **/software/amazon/rds/dbinstance/ReadHandler.class + **/software/amazon/rds/dbinstance/ResourceModel.class + **/software/amazon/rds/dbinstance/StorageType.class + **/software/amazon/rds/dbinstance/Tag.class + **/software/amazon/rds/dbinstance/Translator.class + **/software/amazon/rds/dbinstance/TypeConfigurationModel.class + **/software/amazon/rds/dbinstance/UpdateHandler.class + + **/software/amazon/rds/dbinstance/client/ApiVersion.class + **/software/amazon/rds/dbinstance/client/ApiVersionDispatcher.class + **/software/amazon/rds/dbinstance/client/Ec2ClientProvider.class + **/software/amazon/rds/dbinstance/client/RdsClientProvider.class + **/software/amazon/rds/dbinstance/client/UnknownVersionException.class + **/software/amazon/rds/dbinstance/client/VersionedProxyClient.class + + **/software/amazon/rds/dbinstance/common/Errors.class + **/software/amazon/rds/dbinstance/common/Fetch.class + + **/software/amazon/rds/dbinstance/common/create/DBInstanceFactory.class + **/software/amazon/rds/dbinstance/common/create/DBInstanceFactoryFactory.class + **/software/amazon/rds/dbinstance/common/create/FreshInstance.class + **/software/amazon/rds/dbinstance/common/create/FromPointInTime.class + **/software/amazon/rds/dbinstance/common/create/FromSnapshot.class + **/software/amazon/rds/dbinstance/common/create/ReadReplica.class + + **/software/amazon/rds/dbinstance/status/DBInstanceStatus.class + **/software/amazon/rds/dbinstance/status/DBParameterGroupStatus.class + **/software/amazon/rds/dbinstance/status/DomainMembershipStatus.class + **/software/amazon/rds/dbinstance/status/OptionGroupStatus.class + **/software/amazon/rds/dbinstance/status/ReadReplicaStatus.class + **/software/amazon/rds/dbinstance/status/VPCSecurityGroupStatus.class + + **/software/amazon/rds/dbinstance/util/ImmutabilityHelper.class + **/software/amazon/rds/dbinstance/util/ResourceModelHelper.class @@ -215,7 +250,7 @@ - PACKAGE + BUNDLE BRANCH @@ -225,7 +260,7 @@ INSTRUCTION COVEREDRATIO - 0.8 + 0.0 diff --git a/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/BaseHandlerStd.java b/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/BaseHandlerStd.java index 6ea67dc87..9b15fb041 100644 --- a/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/BaseHandlerStd.java +++ b/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/BaseHandlerStd.java @@ -1,133 +1,47 @@ package software.amazon.rds.dbinstance; -import java.time.Duration; -import java.time.Instant; -import java.util.Collection; -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.function.BiFunction; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import org.apache.commons.lang3.BooleanUtils; - -import com.amazonaws.arn.Arn; -import com.amazonaws.util.CollectionUtils; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; +import org.apache.commons.lang3.BooleanUtils; import software.amazon.awssdk.services.ec2.Ec2Client; import software.amazon.awssdk.services.ec2.model.DescribeSecurityGroupsResponse; import software.amazon.awssdk.services.ec2.model.SecurityGroup; import software.amazon.awssdk.services.rds.RdsClient; -import software.amazon.awssdk.services.rds.model.AuthorizationNotFoundException; -import software.amazon.awssdk.services.rds.model.CertificateNotFoundException; -import software.amazon.awssdk.services.rds.model.DBCluster; -import software.amazon.awssdk.services.rds.model.DBClusterSnapshot; -import software.amazon.awssdk.services.rds.model.DBInstance; -import software.amazon.awssdk.services.rds.model.DBInstanceAutomatedBackup; -import software.amazon.awssdk.services.rds.model.DBSnapshot; -import software.amazon.awssdk.services.rds.model.DbClusterNotFoundException; -import software.amazon.awssdk.services.rds.model.DbClusterSnapshotNotFoundException; -import software.amazon.awssdk.services.rds.model.DbInstanceAlreadyExistsException; -import software.amazon.awssdk.services.rds.model.DbInstanceAutomatedBackupQuotaExceededException; -import software.amazon.awssdk.services.rds.model.DbInstanceNotFoundException; -import software.amazon.awssdk.services.rds.model.DbInstanceRoleAlreadyExistsException; -import software.amazon.awssdk.services.rds.model.DbInstanceRoleNotFoundException; -import software.amazon.awssdk.services.rds.model.DbParameterGroupNotFoundException; -import software.amazon.awssdk.services.rds.model.DbSecurityGroupNotFoundException; -import software.amazon.awssdk.services.rds.model.DbSnapshotAlreadyExistsException; -import software.amazon.awssdk.services.rds.model.DbSnapshotNotFoundException; -import software.amazon.awssdk.services.rds.model.DbSubnetGroupDoesNotCoverEnoughAZsException; -import software.amazon.awssdk.services.rds.model.DbSubnetGroupNotFoundException; -import software.amazon.awssdk.services.rds.model.DbUpgradeDependencyFailureException; -import software.amazon.awssdk.services.rds.model.DescribeDbClusterSnapshotsResponse; -import software.amazon.awssdk.services.rds.model.DescribeDbClustersResponse; -import software.amazon.awssdk.services.rds.model.DescribeDbInstanceAutomatedBackupsResponse; -import software.amazon.awssdk.services.rds.model.DescribeDbInstancesResponse; -import software.amazon.awssdk.services.rds.model.DescribeDbSnapshotsResponse; -import software.amazon.awssdk.services.rds.model.DomainMembership; -import software.amazon.awssdk.services.rds.model.DomainNotFoundException; -import software.amazon.awssdk.services.rds.model.Event; -import software.amazon.awssdk.services.rds.model.InstanceQuotaExceededException; -import software.amazon.awssdk.services.rds.model.InsufficientDbInstanceCapacityException; -import software.amazon.awssdk.services.rds.model.InvalidDbClusterStateException; -import software.amazon.awssdk.services.rds.model.InvalidDbInstanceAutomatedBackupStateException; -import software.amazon.awssdk.services.rds.model.InvalidDbInstanceStateException; -import software.amazon.awssdk.services.rds.model.InvalidDbSecurityGroupStateException; -import software.amazon.awssdk.services.rds.model.InvalidDbSnapshotStateException; -import software.amazon.awssdk.services.rds.model.InvalidRestoreException; -import software.amazon.awssdk.services.rds.model.InvalidSubnetException; -import software.amazon.awssdk.services.rds.model.InvalidVpcNetworkStateException; -import software.amazon.awssdk.services.rds.model.KmsKeyNotAccessibleException; -import software.amazon.awssdk.services.rds.model.NetworkTypeNotSupportedException; -import software.amazon.awssdk.services.rds.model.OptionGroupMembership; -import software.amazon.awssdk.services.rds.model.OptionGroupNotFoundException; -import software.amazon.awssdk.services.rds.model.PendingModifiedValues; -import software.amazon.awssdk.services.rds.model.ProvisionedIopsNotAvailableInAzException; -import software.amazon.awssdk.services.rds.model.SnapshotQuotaExceededException; -import software.amazon.awssdk.services.rds.model.StorageQuotaExceededException; -import software.amazon.awssdk.services.rds.model.StorageTypeNotSupportedException; import software.amazon.awssdk.services.rds.model.Tag; +import software.amazon.awssdk.services.rds.model.*; import software.amazon.awssdk.utils.StringUtils; import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; -import software.amazon.cloudformation.exceptions.CfnNotStabilizedException; -import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; -import software.amazon.cloudformation.proxy.HandlerErrorCode; -import software.amazon.cloudformation.proxy.Logger; -import software.amazon.cloudformation.proxy.OperationStatus; -import software.amazon.cloudformation.proxy.ProgressEvent; -import software.amazon.cloudformation.proxy.ProxyClient; -import software.amazon.cloudformation.proxy.ResourceHandlerRequest; +import software.amazon.cloudformation.exceptions.CfnNotFoundException; +import software.amazon.cloudformation.proxy.*; import software.amazon.cloudformation.proxy.delay.Constant; import software.amazon.cloudformation.resource.ResourceTypeSchema; import software.amazon.rds.common.error.ErrorCode; import software.amazon.rds.common.error.ErrorRuleSet; import software.amazon.rds.common.error.ErrorStatus; -import software.amazon.rds.common.handler.Commons; -import software.amazon.rds.common.handler.Events; -import software.amazon.rds.common.handler.HandlerConfig; -import software.amazon.rds.common.handler.HandlerMethod; -import software.amazon.rds.common.handler.Tagging; +import software.amazon.rds.common.handler.*; import software.amazon.rds.common.logging.LoggingProxyClient; import software.amazon.rds.common.logging.RequestLogger; import software.amazon.rds.common.printer.FilteredJsonPrinter; import software.amazon.rds.common.request.RequestValidationException; import software.amazon.rds.common.request.ValidatedRequest; import software.amazon.rds.common.request.Validations; -import software.amazon.rds.dbinstance.client.ApiVersion; -import software.amazon.rds.dbinstance.client.ApiVersionDispatcher; -import software.amazon.rds.dbinstance.client.Ec2ClientProvider; -import software.amazon.rds.dbinstance.client.RdsClientProvider; -import software.amazon.rds.dbinstance.client.VersionedProxyClient; -import software.amazon.rds.dbinstance.status.DBInstanceStatus; -import software.amazon.rds.dbinstance.status.DBParameterGroupStatus; -import software.amazon.rds.dbinstance.status.DomainMembershipStatus; -import software.amazon.rds.dbinstance.status.OptionGroupStatus; -import software.amazon.rds.dbinstance.status.ReadReplicaStatus; -import software.amazon.rds.dbinstance.status.VPCSecurityGroupStatus; +import software.amazon.rds.dbinstance.client.*; +import software.amazon.rds.dbinstance.util.ResourceModelHelper; + +import java.time.Duration; +import java.time.Instant; +import java.util.*; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Stream; public abstract class BaseHandlerStd extends BaseHandler { - public static final String SECRET_STATUS_ACTIVE = "active"; public static final String RESOURCE_IDENTIFIER = "dbinstance"; public static final String STACK_NAME = "rds"; public static final String API_VERSION_V12 = "2012-09-17"; - static final String READ_REPLICA_STATUS_TYPE = "read replication"; - - protected static final List RDS_CUSTOM_ORACLE_ENGINES = ImmutableList.of( - "custom-oracle-ee", - "custom-oracle-ee-cdb" - ); - protected static final int RESOURCE_ID_MAX_LENGTH = 63; protected final static HandlerConfig DEFAULT_DB_INSTANCE_HANDLER_CONFIG = HandlerConfig.builder() @@ -161,6 +75,8 @@ public abstract class BaseHandlerStd extends BaseHandler { protected static final String DB_INSTANCE_STABILIZATION_TIME = "dbinstance-stabilization-time"; + protected static final int CALLBACK_DELAY = 6; + protected final HandlerConfig config; protected RequestLogger requestLogger; @@ -201,6 +117,9 @@ public abstract class BaseHandlerStd extends BaseHandler { ErrorCode.InsufficientDBInstanceCapacity, ErrorCode.SnapshotQuotaExceeded, ErrorCode.StorageQuotaExceeded) + .withErrorCodes(ErrorStatus.retry(CALLBACK_DELAY, HandlerErrorCode.Throttling), + ErrorCode.ThrottlingException, + ErrorCode.Throttling) .withErrorCodes(ErrorStatus.failWith(HandlerErrorCode.InvalidRequest), ErrorCode.DBSubnetGroupNotAllowedFault, ErrorCode.InvalidParameterCombination, @@ -217,6 +136,7 @@ public abstract class BaseHandlerStd extends BaseHandler { ErrorCode.DBSnapshotNotFound, ErrorCode.DBSubnetGroupNotFoundFault) .withErrorClasses(ErrorStatus.failWith(HandlerErrorCode.NotFound), + CfnNotFoundException.class, // FIXME: handle BaseHandlerException in ErrorRuleSet CertificateNotFoundException.class, DbClusterNotFoundException.class, DbInstanceNotFoundException.class, @@ -252,6 +172,23 @@ public abstract class BaseHandlerStd extends BaseHandler { InvalidSubnetException.class) .build(); + + protected static final ErrorRuleSet READ_HANDLER_ERROR_RULE_SET = ErrorRuleSet + .extend(DEFAULT_DB_INSTANCE_ERROR_RULE_SET) + .withErrorCodes(ErrorStatus.failWith(HandlerErrorCode.Throttling), + ErrorCode.ThrottlingException, + ErrorCode.Throttling) + .build(); + + protected static final ErrorRuleSet DESCRIBE_AUTOMATED_BACKUPS_SOFTFAIL_ERROR_RULE_SET = ErrorRuleSet + .extend(READ_HANDLER_ERROR_RULE_SET) + .withErrorClasses(ErrorStatus.ignore(OperationStatus.IN_PROGRESS), + DbInstanceAutomatedBackupNotFoundException.class) + .withErrorCodes(ErrorStatus.ignore(OperationStatus.IN_PROGRESS), + ErrorCode.AccessDenied, + ErrorCode.AccessDeniedException) + .build(); + protected static final ErrorRuleSet DB_INSTANCE_FETCH_ENGINE_RULE_SET = ErrorRuleSet .extend(DEFAULT_DB_INSTANCE_ERROR_RULE_SET) .withErrorClasses(ErrorStatus.failWith(HandlerErrorCode.InvalidRequest), @@ -308,7 +245,8 @@ public abstract class BaseHandlerStd extends BaseHandler { protected static final ErrorRuleSet MODIFY_DB_INSTANCE_AUTOMATIC_BACKUP_REPLICATION_ERROR_RULE_SET = ErrorRuleSet .extend(DEFAULT_DB_INSTANCE_ERROR_RULE_SET) .withErrorClasses(ErrorStatus.ignore(OperationStatus.IN_PROGRESS), - InvalidDbInstanceAutomatedBackupStateException.class) + InvalidDbInstanceAutomatedBackupStateException.class, + InvalidDbInstanceStateException.class) .withErrorClasses(ErrorStatus.failWith(HandlerErrorCode.ServiceLimitExceeded), DbInstanceAutomatedBackupQuotaExceededException.class) .build(); @@ -473,12 +411,14 @@ protected ProgressEvent updateDbInstance( final AmazonWebServicesClientProxy proxy, final ResourceHandlerRequest request, final ProxyClient rdsProxyClient, - final ProgressEvent progress + final ProgressEvent progress, + final DBInstance dbInstance ) { return proxy.initiate("rds::modify-db-instance", rdsProxyClient, progress.getResourceModel(), progress.getCallbackContext()) .translateToServiceRequest(resourceModel -> Translator.modifyDbInstanceRequest( request.getPreviousResourceState(), request.getDesiredResourceState(), + dbInstance, BooleanUtils.isTrue(request.getRollback())) ) .backoffDelay(config.getBackoff()) @@ -496,14 +436,6 @@ protected ProgressEvent updateDbInstance( .progress(); } - protected boolean isDBClusterMember(final ResourceModel model) { - return StringUtils.isNotBlank(model.getDBClusterIdentifier()); - } - - protected boolean isRdsCustomOracleInstance(final ResourceModel model) { - return RDS_CUSTOM_ORACLE_ENGINES.contains(model.getEngine()); - } - protected boolean isFailureEvent(final Event event) { return EVENT_FAIL_CHECKERS.stream().anyMatch(p -> p.test(event)); } @@ -512,22 +444,25 @@ protected DBInstance fetchDBInstance( final ProxyClient rdsProxyClient, final ResourceModel model ) { - final DescribeDbInstancesResponse response = rdsProxyClient.injectCredentialsAndInvokeV2( - Translator.describeDbInstancesRequest(model), - rdsProxyClient.client()::describeDBInstances - ); - return response.dbInstances().get(0); + return fetchDBInstance(rdsProxyClient, model.getDBInstanceIdentifier()); } protected DBInstance fetchDBInstance( final ProxyClient rdsProxyClient, final String dbInstanceIdentifier ) { - final DescribeDbInstancesResponse response = rdsProxyClient.injectCredentialsAndInvokeV2( - Translator.describeDbInstanceByDBInstanceIdentifierRequest(dbInstanceIdentifier), - rdsProxyClient.client()::describeDBInstances - ); - return response.dbInstances().get(0); + try { + final DescribeDbInstancesResponse response = rdsProxyClient.injectCredentialsAndInvokeV2( + Translator.describeDbInstanceByDBInstanceIdentifierRequest(dbInstanceIdentifier), + rdsProxyClient.client()::describeDBInstances + ); + if (!response.hasDbInstances() || response.dbInstances().isEmpty()) { + throw new CfnNotFoundException(ResourceModel.TYPE_NAME, dbInstanceIdentifier); + } + return response.dbInstances().get(0); + } catch (DbInstanceNotFoundException e) { + throw new CfnNotFoundException(e); + } } protected DBInstance fetchDBInstanceByResourceId( @@ -546,7 +481,7 @@ protected DBInstanceAutomatedBackup fetchAutomaticBackup( final String automaticBackupArn ) { final DescribeDbInstanceAutomatedBackupsResponse response = rdsProxyClient.injectCredentialsAndInvokeV2( - Translator.describeDBInstanceAutomaticBackup(automaticBackupArn), + Translator.describeDBInstanceAutomaticBackupRequest(automaticBackupArn), rdsProxyClient.client()::describeDBInstanceAutomatedBackups ); return response.dbInstanceAutomatedBackups().get(0); @@ -579,47 +514,47 @@ protected DBSnapshot fetchDBSnapshot( final ResourceModel model ) { final DescribeDbSnapshotsResponse response = rdsProxyClient.injectCredentialsAndInvokeV2( - Translator.describeDbSnapshotsRequest(model), - rdsProxyClient.client()::describeDBSnapshots + Translator.describeDbSnapshotsRequest(model), + rdsProxyClient.client()::describeDBSnapshots ); return response.dbSnapshots().get(0); } protected DBClusterSnapshot fetchDBClusterSnapshot( - final ProxyClient rdsProxyClient, - final ResourceModel model + final ProxyClient rdsProxyClient, + final ResourceModel model ) { final DescribeDbClusterSnapshotsResponse response = rdsProxyClient.injectCredentialsAndInvokeV2( - Translator.describeDbClusterSnapshotsRequest(model), - rdsProxyClient.client()::describeDBClusterSnapshots + Translator.describeDbClusterSnapshotsRequest(model), + rdsProxyClient.client()::describeDBClusterSnapshots ); return response.dbClusterSnapshots().get(0); } protected SecurityGroup fetchSecurityGroup( - final ProxyClient ec2ProxyClient, - final String vpcId, - final String groupName + final ProxyClient ec2ProxyClient, + final String vpcId, + final String groupName ) { final DescribeSecurityGroupsResponse response = ec2ProxyClient.injectCredentialsAndInvokeV2( - Translator.describeSecurityGroupsRequest(vpcId, groupName), - ec2ProxyClient.client()::describeSecurityGroups + Translator.describeSecurityGroupsRequest(vpcId, groupName), + ec2ProxyClient.client()::describeSecurityGroups ); return Optional.ofNullable(response.securityGroups()) - .orElse(Collections.emptyList()) - .stream() - .findFirst() - .orElse(null); + .orElse(Collections.emptyList()) + .stream() + .findFirst() + .orElse(null); } protected boolean isDbInstanceDeleted( - final ProxyClient rdsProxyClient, - final ResourceModel model + final ProxyClient rdsProxyClient, + final ResourceModel model ) { DBInstance dbInstance; try { fetchDBInstance(rdsProxyClient, model); - } catch (DbInstanceNotFoundException e) { + } catch (CfnNotFoundException e) { // the instance is gone, exactly what we need return true; } @@ -627,291 +562,128 @@ protected boolean isDbInstanceDeleted( return false; } - private void assertNoDBInstanceTerminalStatus(final DBInstance dbInstance) throws CfnNotStabilizedException { - final DBInstanceStatus status = DBInstanceStatus.fromString(dbInstance.dbInstanceStatus()); - if (status != null && status.isTerminal()) { - throw new CfnNotStabilizedException(new Exception("DB Instance is in state: " + status.toString())); - } - } - - private void assertNoOptionGroupTerminalStatus(final DBInstance dbInstance) throws CfnNotStabilizedException { - final List termOptionGroups = Optional.ofNullable(dbInstance.optionGroupMemberships()).orElse(Collections.emptyList()) - .stream() - .filter(optionGroup -> { - final OptionGroupStatus status = OptionGroupStatus.fromString(optionGroup.status()); - return status != null && status.isTerminal(); - }) - .collect(Collectors.toList()); - - if (!termOptionGroups.isEmpty()) { - throw new CfnNotStabilizedException(new Exception( - String.format("OptionGroup %s is in a terminal state", - termOptionGroups.get(0).optionGroupName()))); - } - } - - private void assertNoDomainMembershipTerminalStatus(final DBInstance dbInstance) throws CfnNotStabilizedException { - final List terminalDomainMemberships = Optional.ofNullable(dbInstance.domainMemberships()).orElse(Collections.emptyList()) - .stream() - .filter(domainMembership -> { - final DomainMembershipStatus status = DomainMembershipStatus.fromString(domainMembership.status()); - return status != null && status.isTerminal(); - }) - .collect(Collectors.toList()); - - if (!terminalDomainMemberships.isEmpty()) { - throw new CfnNotStabilizedException(new Exception(String.format("Domain %s is in a terminal state", - terminalDomainMemberships.get(0).domain()))); - } - } - - private void assertNoTerminalStatus(final DBInstance dbInstance) throws CfnNotStabilizedException { - assertNoDBInstanceTerminalStatus(dbInstance); - assertNoOptionGroupTerminalStatus(dbInstance); - assertNoDomainMembershipTerminalStatus(dbInstance); - } - protected boolean isDBInstanceStabilizedAfterMutate( - final ProxyClient rdsProxyClient, - final ResourceModel model, - final CallbackContext context + final ProxyClient rdsProxyClient, + final ResourceModel model, + final CallbackContext context ) { final DBInstance dbInstance = fetchDBInstance(rdsProxyClient, model); - assertNoTerminalStatus(dbInstance); - - - final boolean isDBInstanceStabilizedAfterMutateResult = isDBInstanceAvailable(dbInstance) && - isReplicationComplete(dbInstance) && - isDBParameterGroupNotApplying(dbInstance) && - isNoPendingChanges(dbInstance) && - isCaCertificateChangesApplied(dbInstance, model) && - isVpcSecurityGroupsActive(dbInstance) && - isDomainMembershipsJoined(dbInstance) && - isMasterUserSecretStabilized(dbInstance); - - requestLogger.log(String.format("isDBInstanceStabilizedAfterMutate: %b", isDBInstanceStabilizedAfterMutateResult), - ImmutableMap.of("isDBInstanceAvailable", isDBInstanceAvailable(dbInstance), - "isReplicationComplete", isReplicationComplete(dbInstance), - "isDBParameterGroupNotApplying", isDBParameterGroupNotApplying(dbInstance), - "isNoPendingChanges", isNoPendingChanges(dbInstance), - "isCaCertificateChangesApplied", isCaCertificateChangesApplied(dbInstance, model), - "isVpcSecurityGroupsActive", isVpcSecurityGroupsActive(dbInstance), - "isDomainMembershipsJoined", isDomainMembershipsJoined(dbInstance), - "isMasterUserSecretStabilized", isMasterUserSecretStabilized(dbInstance)), - ImmutableMap.of("Description", "isDBInstanceStabilizedAfterMutate method will be repeatedly" + - " called with a backoff mechanism after the modify call until it returns true. This" + - " process will continue until all included flags are true.")); - - return isDBInstanceStabilizedAfterMutateResult; + return DBInstancePredicates.isDBInstanceStabilizedAfterMutate(dbInstance, model, context, requestLogger); } private void resourceStabilizationTime(final CallbackContext context) { context.timestampOnce(DB_INSTANCE_REQUEST_STARTED_AT, Instant.now()); context.timestamp(DB_INSTANCE_REQUEST_IN_PROGRESS_AT, Instant.now()); context.calculateTimeDeltaInMinutes(DB_INSTANCE_STABILIZATION_TIME, - context.getTimestamp(DB_INSTANCE_REQUEST_IN_PROGRESS_AT), - context.getTimestamp(DB_INSTANCE_REQUEST_STARTED_AT)); + context.getTimestamp(DB_INSTANCE_REQUEST_IN_PROGRESS_AT), + context.getTimestamp(DB_INSTANCE_REQUEST_STARTED_AT)); } - protected boolean isInstanceStabilizedAfterReplicationStop(final ProxyClient rdsProxyClient, - final ResourceModel model) { + protected boolean isInstanceStabilizedAfterReplicationStop( + final ProxyClient rdsProxyClient, + final ResourceModel model + ) { final DBInstance dbInstance = fetchDBInstance(rdsProxyClient, model); - assertNoTerminalStatus(dbInstance); - return isDBInstanceAvailable(dbInstance) - && !dbInstance.hasDbInstanceAutomatedBackupsReplications(); + return DBInstancePredicates.isInstanceStabilizedAfterReplicationStop(dbInstance, model); } protected boolean isInstanceStabilizedAfterReplicationStart(final ProxyClient rdsProxyClient, final ResourceModel model) { final DBInstance dbInstance = fetchDBInstance(rdsProxyClient, model); - assertNoTerminalStatus(dbInstance); - return isDBInstanceAvailable(dbInstance) - && dbInstance.hasDbInstanceAutomatedBackupsReplications() && - !dbInstance.dbInstanceAutomatedBackupsReplications().isEmpty() && - model.getAutomaticBackupReplicationRegion() - .equalsIgnoreCase( - Arn.fromString(dbInstance.dbInstanceAutomatedBackupsReplications().get(0).dbInstanceAutomatedBackupsArn()).getRegion()); + return DBInstancePredicates.isInstanceStabilizedAfterReplicationStart(dbInstance, model); } protected boolean isDBInstanceStabilizedAfterReboot( - final ProxyClient rdsProxyClient, - final ResourceModel model + final ProxyClient rdsProxyClient, + final ResourceModel model ) { final DBInstance dbInstance = fetchDBInstance(rdsProxyClient, model); - - assertNoTerminalStatus(dbInstance); - - final boolean isDBClusterParameterGroupStabilized = !isDBClusterMember(model) || isDBClusterParameterGroupStabilized(rdsProxyClient, model); - final boolean isDBInstanceStabilizedAfterReboot = isDBInstanceAvailable(dbInstance) && - isDBParameterGroupInSync(dbInstance) && - isOptionGroupInSync(dbInstance) && - isDBClusterParameterGroupStabilized; - - requestLogger.log(String.format("isDBInstanceStabilizedAfterReboot: %b", isDBInstanceStabilizedAfterReboot), - ImmutableMap.of("isDBInstanceAvailable", isDBInstanceAvailable(dbInstance), - "isDBParameterGroupInSync", isDBParameterGroupInSync(dbInstance), - "isOptionGroupInSync", isOptionGroupInSync(dbInstance), - "isDBClusterParameterGroupStabilized", isDBClusterParameterGroupStabilized), - ImmutableMap.of("Description", "isDBInstanceStabilizedAfterReboot method will be repeatedly" + - " called with a backoff mechanism after the reboot call until it returns true. This" + - " process will continue until all included flags are true.")); - - return isDBInstanceStabilizedAfterReboot; - } - - boolean isDBInstanceAvailable(final DBInstance dbInstance) { - return DBInstanceStatus.Available.equalsString(dbInstance.dbInstanceStatus()); - } - - boolean isDomainMembershipsJoined(final DBInstance dbInstance) { - return Optional.ofNullable(dbInstance.domainMemberships()).orElse(Collections.emptyList()) - .stream() - .allMatch(membership -> DomainMembershipStatus.Joined.equalsString(membership.status()) || - DomainMembershipStatus.KerberosEnabled.equalsString(membership.status())); - } - - boolean isVpcSecurityGroupsActive(final DBInstance dbInstance) { - return Optional.ofNullable(dbInstance.vpcSecurityGroups()).orElse(Collections.emptyList()) - .stream() - .allMatch(group -> VPCSecurityGroupStatus.Active.equalsString(group.status())); - } - - boolean isNoPendingChanges(final DBInstance dbInstance) { - final PendingModifiedValues pending = dbInstance.pendingModifiedValues(); - return (pending == null) || (pending.dbInstanceClass() == null && - pending.allocatedStorage() == null && - pending.automationMode() == null && - pending.backupRetentionPeriod() == null && - pending.dbInstanceIdentifier() == null && - pending.dbSubnetGroupName() == null && - pending.engine() == null && - pending.engineVersion() == null && - pending.iamDatabaseAuthenticationEnabled() == null && - pending.iops() == null && - pending.licenseModel() == null && - pending.masterUserPassword() == null && - pending.multiAZ() == null && - pending.pendingCloudwatchLogsExports() == null && - pending.port() == null && - CollectionUtils.isNullOrEmpty(pending.processorFeatures()) && - pending.resumeFullAutomationModeTime() == null && - pending.storageThroughput() == null && - pending.storageType() == null - ); - } - - boolean isCaCertificateChangesApplied(final DBInstance dbInstance, final ResourceModel model) { - final PendingModifiedValues pending = dbInstance.pendingModifiedValues(); - return pending == null || - pending.caCertificateIdentifier() == null || - BooleanUtils.isNotTrue(model.getCertificateRotationRestart()); - } - - boolean isDBParameterGroupNotApplying(final DBInstance dbInstance) { - return Optional.ofNullable(dbInstance.dbParameterGroups()).orElse(Collections.emptyList()) - .stream() - .noneMatch(group -> DBParameterGroupStatus.Applying.equalsString(group.parameterApplyStatus())); - } - - boolean isReplicationComplete(final DBInstance dbInstance) { - return Optional.ofNullable(dbInstance.statusInfos()).orElse(Collections.emptyList()) - .stream() - .filter(statusInfo -> READ_REPLICA_STATUS_TYPE.equals(statusInfo.statusType())) - .allMatch(statusInfo -> ReadReplicaStatus.Replicating.equalsString(statusInfo.status())); + if (DBInstancePredicates.isDBClusterMember(model)) { + final DBCluster dbCluster = fetchDBCluster(rdsProxyClient, model); + return DBInstancePredicates.isDBInstanceStabilizedAfterReboot(dbInstance, dbCluster, model, requestLogger); + } else { + return DBInstancePredicates.isDBInstanceStabilizedAfterReboot(dbInstance, requestLogger); + } } protected boolean isOptionGroupStabilized( - final ProxyClient rdsProxyClient, - final ResourceModel model + final ProxyClient rdsProxyClient, + final ResourceModel model ) { - return isOptionGroupInSync(fetchDBInstance(rdsProxyClient, model)); - } + final DBInstance dbInstance = fetchDBInstance(rdsProxyClient, model); - protected boolean isOptionGroupInSync(final DBInstance dbInstance) { - return Optional.ofNullable(dbInstance.optionGroupMemberships()).orElse(Collections.emptyList()) - .stream() - .allMatch(optionGroup -> OptionGroupStatus.InSync.equalsString(optionGroup.status())); + return DBInstancePredicates.isOptionGroupInSync(dbInstance); } protected boolean isDBParameterGroupStabilized( - final ProxyClient rdsProxyClient, - final ResourceModel model + final ProxyClient rdsProxyClient, + final ResourceModel model ) { - return isDBParameterGroupInSync(fetchDBInstance(rdsProxyClient, model)); - } + final DBInstance dbInstance = fetchDBInstance(rdsProxyClient, model); - protected boolean isDBParameterGroupInSync(final DBInstance dbInstance) { - return Optional.ofNullable(dbInstance.dbParameterGroups()).orElse(Collections.emptyList()) - .stream() - .allMatch(parameterGroup -> DBParameterGroupStatus.InSync.equalsString(parameterGroup.parameterApplyStatus())); + if(ResourceModelHelper.shouldApplyImmediately(model)) { + return DBInstancePredicates.isDBParameterGroupInSync(dbInstance); + } + return DBInstancePredicates.isDBParameterGroupNotApplying(dbInstance); } protected boolean isDBClusterParameterGroupStabilized( - final ProxyClient rdsProxyClient, - final ResourceModel model + final ProxyClient rdsProxyClient, + final ResourceModel model ) { - return isDBClusterParameterGroupInSync(model, fetchDBCluster(rdsProxyClient, model)); - } + final DBCluster dbCluster = fetchDBCluster(rdsProxyClient, model); - protected boolean isDBClusterParameterGroupInSync(final ResourceModel model, final DBCluster dbCluster) { - return Optional.ofNullable(dbCluster.dbClusterMembers()).orElse(Collections.emptyList()) - .stream() - .filter(member -> model.getDBInstanceIdentifier().equalsIgnoreCase(member.dbInstanceIdentifier())) - .anyMatch(member -> DBParameterGroupStatus.InSync.equalsString(member.dbClusterParameterGroupStatus())); + if(ResourceModelHelper.shouldApplyImmediately(model)) { + return DBInstancePredicates.isDBClusterParameterGroupInSync(model, dbCluster); + } + return DBInstancePredicates.isDBClusterParameterGroupNotApplying(model, dbCluster); } protected boolean isDBInstanceRoleStabilized( - final ProxyClient rdsProxyClient, - final ResourceModel model, - final Function, Boolean> predicate + final ProxyClient rdsProxyClient, + final ResourceModel model, + final Function, Boolean> predicate ) { final DBInstance dbInstance = fetchDBInstance(rdsProxyClient, model); return predicate.apply(Optional.ofNullable( - dbInstance.associatedRoles() + dbInstance.associatedRoles() ).orElse(Collections.emptyList()).stream()); } protected boolean isDBInstanceRoleAdditionStabilized( - final ProxyClient rdsProxyClient, - final ResourceModel model, - final DBInstanceRole lookupRole + final ProxyClient rdsProxyClient, + final ResourceModel model, + final DBInstanceRole lookupRole ) { return isDBInstanceRoleStabilized( - rdsProxyClient, - model, - (roles) -> roles.anyMatch(role -> role.roleArn().equals(lookupRole.getRoleArn()) && - Objects.equals(StringUtils.trimToNull(role.featureName()), StringUtils.trimToNull(lookupRole.getFeatureName()))) + rdsProxyClient, + model, + (roles) -> roles.anyMatch(role -> role.roleArn().equals(lookupRole.getRoleArn()) && + Objects.equals(StringUtils.trimToNull(role.featureName()), StringUtils.trimToNull(lookupRole.getFeatureName()))) ); } protected boolean isDBInstanceRoleRemovalStabilized( - final ProxyClient rdsProxyClient, - final ResourceModel model, - final DBInstanceRole lookupRole + final ProxyClient rdsProxyClient, + final ResourceModel model, + final DBInstanceRole lookupRole ) { return isDBInstanceRoleStabilized( - rdsProxyClient, - model, - (roles) -> roles.noneMatch(role -> role.roleArn().equals(lookupRole.getRoleArn())) + rdsProxyClient, + model, + (roles) -> roles.noneMatch(role -> role.roleArn().equals(lookupRole.getRoleArn())) ); } - protected boolean isMasterUserSecretStabilized(final DBInstance instance) { - if (instance.masterUserSecret() == null) { - return true; - } - return SECRET_STATUS_ACTIVE.equalsIgnoreCase(instance.masterUserSecret().secretStatus()); - } - protected ProgressEvent updateAssociatedRoles( - final AmazonWebServicesClientProxy proxy, - final ProxyClient rdsProxyClient, - ProgressEvent progress, - Collection previousRoles, - Collection desiredRoles + final AmazonWebServicesClientProxy proxy, + final ProxyClient rdsProxyClient, + ProgressEvent progress, + Collection previousRoles, + Collection desiredRoles ) { final Set rolesToRemove = new LinkedHashSet<>(Optional.ofNullable(previousRoles).orElse(Collections.emptyList())); final Set rolesToAdd = new LinkedHashSet<>(Optional.ofNullable(desiredRoles).orElse(Collections.emptyList())); @@ -920,33 +692,33 @@ protected ProgressEvent updateAssociatedRoles( rolesToRemove.removeAll(Optional.ofNullable(desiredRoles).orElse(Collections.emptyList())); return progress - .then(p -> removeOldRoles(proxy, rdsProxyClient, p, rolesToRemove)) - .then(p -> addNewRoles(proxy, rdsProxyClient, p, rolesToAdd)); + .then(p -> removeOldRoles(proxy, rdsProxyClient, p, rolesToRemove)) + .then(p -> addNewRoles(proxy, rdsProxyClient, p, rolesToAdd)); } protected ProgressEvent addNewRoles( - final AmazonWebServicesClientProxy proxy, - final ProxyClient rdsProxyClient, - final ProgressEvent progress, - final Collection rolesToAdd + final AmazonWebServicesClientProxy proxy, + final ProxyClient rdsProxyClient, + final ProgressEvent progress, + final Collection rolesToAdd ) { for (final DBInstanceRole role : rolesToAdd) { final ProgressEvent progressEvent = proxy.initiate("rds::add-roles-to-db-instance", rdsProxyClient, progress.getResourceModel(), progress.getCallbackContext()) - .translateToServiceRequest(addRequest -> Translator.addRoleToDbInstanceRequest(progress.getResourceModel(), role)) - .backoffDelay(config.getBackoff()) - .makeServiceCall((request, proxyInvocation) -> { - return proxyInvocation.injectCredentialsAndInvokeV2(request, proxyInvocation.client()::addRoleToDBInstance); - }) - .stabilize((request, response, proxyInvocation, modelRequest, callbackContext) -> isDBInstanceRoleAdditionStabilized( - proxyInvocation, modelRequest, role - )) - .handleError((request, exception, proxyInvocation, resourceModel, context) -> Commons.handleException( - ProgressEvent.progress(resourceModel, context), - exception, - UPDATE_ASSOCIATED_ROLES_ERROR_RULE_SET, - requestLogger - )) - .success(); + .translateToServiceRequest(addRequest -> Translator.addRoleToDbInstanceRequest(progress.getResourceModel(), role)) + .backoffDelay(config.getBackoff()) + .makeServiceCall((request, proxyInvocation) -> { + return proxyInvocation.injectCredentialsAndInvokeV2(request, proxyInvocation.client()::addRoleToDBInstance); + }) + .stabilize((request, response, proxyInvocation, modelRequest, callbackContext) -> isDBInstanceRoleAdditionStabilized( + proxyInvocation, modelRequest, role + )) + .handleError((request, exception, proxyInvocation, resourceModel, context) -> Commons.handleException( + ProgressEvent.progress(resourceModel, context), + exception, + UPDATE_ASSOCIATED_ROLES_ERROR_RULE_SET, + requestLogger + )) + .success(); if (!progressEvent.isSuccess()) { return progressEvent; } @@ -955,30 +727,30 @@ protected ProgressEvent addNewRoles( } protected ProgressEvent removeOldRoles( - final AmazonWebServicesClientProxy proxy, - final ProxyClient rdsProxyClient, - final ProgressEvent progress, - final Collection rolesToRemove + final AmazonWebServicesClientProxy proxy, + final ProxyClient rdsProxyClient, + final ProgressEvent progress, + final Collection rolesToRemove ) { for (final DBInstanceRole role : rolesToRemove) { final ProgressEvent progressEvent = proxy.initiate("rds::remove-roles-from-db-instance", rdsProxyClient, progress.getResourceModel(), progress.getCallbackContext()) - .translateToServiceRequest(removeRequest -> Translator.removeRoleFromDbInstanceRequest( - progress.getResourceModel(), role - )) - .backoffDelay(config.getBackoff()) - .makeServiceCall((request, proxyInvocation) -> proxyInvocation.injectCredentialsAndInvokeV2( - request, proxyInvocation.client()::removeRoleFromDBInstance - )) - .stabilize((request, response, proxyInvocation, modelRequest, callbackContext) -> isDBInstanceRoleRemovalStabilized( - proxyInvocation, modelRequest, role - )) - .handleError((request, exception, proxyInvocation, resourceModel, context) -> Commons.handleException( - ProgressEvent.progress(resourceModel, context), - exception, - UPDATE_ASSOCIATED_ROLES_ERROR_RULE_SET, - requestLogger - )) - .success(); + .translateToServiceRequest(removeRequest -> Translator.removeRoleFromDbInstanceRequest( + progress.getResourceModel(), role + )) + .backoffDelay(config.getBackoff()) + .makeServiceCall((request, proxyInvocation) -> proxyInvocation.injectCredentialsAndInvokeV2( + request, proxyInvocation.client()::removeRoleFromDBInstance + )) + .stabilize((request, response, proxyInvocation, modelRequest, callbackContext) -> isDBInstanceRoleRemovalStabilized( + proxyInvocation, modelRequest, role + )) + .handleError((request, exception, proxyInvocation, resourceModel, context) -> Commons.handleException( + ProgressEvent.progress(resourceModel, context), + exception, + UPDATE_ASSOCIATED_ROLES_ERROR_RULE_SET, + requestLogger + )) + .success(); if (!progressEvent.isSuccess()) { return progressEvent; } @@ -987,65 +759,65 @@ protected ProgressEvent removeOldRoles( } protected ProgressEvent reboot( - final AmazonWebServicesClientProxy proxy, - final ProxyClient rdsProxyClient, - final ProgressEvent progress + final AmazonWebServicesClientProxy proxy, + final ProxyClient rdsProxyClient, + final ProgressEvent progress ) { return proxy.initiate( - "rds::reboot-db-instance", - rdsProxyClient, - progress.getResourceModel(), - progress.getCallbackContext() - ).translateToServiceRequest(Translator::rebootDbInstanceRequest) - .backoffDelay(config.getBackoff()) - .makeServiceCall((rebootRequest, proxyInvocation) -> proxyInvocation.injectCredentialsAndInvokeV2( - rebootRequest, - proxyInvocation.client()::rebootDBInstance - )) - .handleError((request, exception, client, model, context) -> Commons.handleException( - ProgressEvent.progress(model, context), - exception, - REBOOT_DB_INSTANCE_ERROR_RULE_SET, - requestLogger - )) - .progress(); + "rds::reboot-db-instance", + rdsProxyClient, + progress.getResourceModel(), + progress.getCallbackContext() + ).translateToServiceRequest(Translator::rebootDbInstanceRequest) + .backoffDelay(config.getBackoff()) + .makeServiceCall((rebootRequest, proxyInvocation) -> proxyInvocation.injectCredentialsAndInvokeV2( + rebootRequest, + proxyInvocation.client()::rebootDBInstance + )) + .handleError((request, exception, client, model, context) -> Commons.handleException( + ProgressEvent.progress(model, context), + exception, + REBOOT_DB_INSTANCE_ERROR_RULE_SET, + requestLogger + )) + .progress(); } protected ProgressEvent rebootAwait( - final AmazonWebServicesClientProxy proxy, - final ProxyClient rdsProxyClient, - final ProgressEvent progress + final AmazonWebServicesClientProxy proxy, + final ProxyClient rdsProxyClient, + final ProgressEvent progress ) { return reboot(proxy, rdsProxyClient, progress).then(p -> stabilizeDBInstanceAfterReboot(proxy, rdsProxyClient, p)); } protected ProgressEvent stabilizeDBInstanceAfterReboot( - final AmazonWebServicesClientProxy proxy, - final ProxyClient rdsProxyClient, - final ProgressEvent progress + final AmazonWebServicesClientProxy proxy, + final ProxyClient rdsProxyClient, + final ProgressEvent progress ) { return proxy.initiate( - "rds::stabilize-db-instance-after-reboot-" + getClass().getSimpleName(), - rdsProxyClient, - progress.getResourceModel(), - progress.getCallbackContext() - ) - .translateToServiceRequest(Function.identity()) - .backoffDelay(config.getBackoff()) - .makeServiceCall(NOOP_CALL) - .stabilize((request, response, proxyInvocation, model, context) -> isDBInstanceStabilizedAfterReboot(proxyInvocation, model)) - .handleError((request, exception, proxyInvocation, resourceModel, context) -> Commons.handleException( - ProgressEvent.progress(resourceModel, context), - exception, - UPDATE_ASSOCIATED_ROLES_ERROR_RULE_SET, - requestLogger - )) - .progress(); + "rds::stabilize-db-instance-after-reboot-" + getClass().getSimpleName(), + rdsProxyClient, + progress.getResourceModel(), + progress.getCallbackContext() + ) + .translateToServiceRequest(Function.identity()) + .backoffDelay(config.getBackoff()) + .makeServiceCall(NOOP_CALL) + .stabilize((request, response, proxyInvocation, model, context) -> isDBInstanceStabilizedAfterReboot(proxyInvocation, model)) + .handleError((request, exception, proxyInvocation, resourceModel, context) -> Commons.handleException( + ProgressEvent.progress(resourceModel, context), + exception, + UPDATE_ASSOCIATED_ROLES_ERROR_RULE_SET, + requestLogger + )) + .progress(); } protected ProgressEvent ensureEngineSet( - final ProxyClient rdsProxyClient, - final ProgressEvent progress + final ProxyClient rdsProxyClient, + final ProgressEvent progress ) { final ResourceModel model = progress.getResourceModel(); if (StringUtils.isEmpty(model.getEngine())) { @@ -1060,11 +832,11 @@ protected ProgressEvent ensureEngineSet( } protected ProgressEvent updateTags( - final AmazonWebServicesClientProxy proxy, - final ProxyClient rdsProxyClient, - final ProgressEvent progress, - final Tagging.TagSet previousTags, - final Tagging.TagSet desiredTags + final AmazonWebServicesClientProxy proxy, + final ProxyClient rdsProxyClient, + final ProgressEvent progress, + final Tagging.TagSet previousTags, + final Tagging.TagSet desiredTags ) { final Collection effectivePreviousTags = Tagging.translateTagsToSdk(previousTags); @@ -1094,10 +866,10 @@ protected ProgressEvent updateTags( Tagging.addTags(rdsProxyClient, arn, Tagging.translateTagsToSdk(tagsToAdd)); } catch (Exception exception) { return Commons.handleException( - progress, - exception, - DEFAULT_DB_INSTANCE_ERROR_RULE_SET.extendWith(Tagging.getUpdateTagsAccessDeniedRuleSet(rulesetTagsToAdd, rulesetTagsToRemove)), - requestLogger + progress, + exception, + DEFAULT_DB_INSTANCE_ERROR_RULE_SET.extendWith(Tagging.getUpdateTagsAccessDeniedRuleSet(rulesetTagsToAdd, rulesetTagsToRemove)), + requestLogger ); } @@ -1105,11 +877,11 @@ protected ProgressEvent updateTags( } protected ProgressEvent versioned( - final AmazonWebServicesClientProxy proxy, - final VersionedProxyClient rdsProxyClient, - final ProgressEvent progress, - final Tagging.TagSet allTags, - final Map> methodVersions + final AmazonWebServicesClientProxy proxy, + final VersionedProxyClient rdsProxyClient, + final ProgressEvent progress, + final Tagging.TagSet allTags, + final Map> methodVersions ) { final ResourceModel model = progress.getResourceModel(); final CallbackContext callbackContext = progress.getCallbackContext(); @@ -1121,66 +893,67 @@ protected ProgressEvent versioned( } protected ProgressEvent stopAutomaticBackupReplicationInRegion( - final String dbInstanceArn, - final AmazonWebServicesClientProxy proxy, - final ProgressEvent progress, - final ProxyClient sourceRegionClient, - final String region + final String dbInstanceArn, + final AmazonWebServicesClientProxy proxy, + final ProgressEvent progress, + final ProxyClient sourceRegionClient, + final String region ) { final ProxyClient rdsClient = new LoggingProxyClient<>(requestLogger, proxy.newProxy(() -> new RdsClientProvider().getClientForRegion(region))); return proxy.initiate("rds::stop-db-instance-automatic-backup-replication", rdsClient, progress.getResourceModel(), progress.getCallbackContext()) - .translateToServiceRequest(resourceModel -> Translator.stopDbInstanceAutomatedBackupsReplicationRequest(dbInstanceArn)) - .backoffDelay(config.getBackoff()) - .makeServiceCall((request, client) -> rdsClient.injectCredentialsAndInvokeV2( - request, - rdsClient.client()::stopDBInstanceAutomatedBackupsReplication - )) - .stabilize((request, response, client, model, context) -> - isInstanceStabilizedAfterReplicationStop(sourceRegionClient, model)) - .handleError((request, exception, client, model, context) -> Commons.handleException( - ProgressEvent.progress(model, context), - exception, - MODIFY_DB_INSTANCE_AUTOMATIC_BACKUP_REPLICATION_ERROR_RULE_SET, - requestLogger - )) - .progress(); + .translateToServiceRequest(resourceModel -> Translator.stopDbInstanceAutomatedBackupsReplicationRequest(dbInstanceArn)) + .backoffDelay(config.getBackoff()) + .makeServiceCall((request, client) -> rdsClient.injectCredentialsAndInvokeV2( + request, + rdsClient.client()::stopDBInstanceAutomatedBackupsReplication + )) + .stabilize((request, response, client, model, context) -> + isInstanceStabilizedAfterReplicationStop(sourceRegionClient, model)) + .handleError((request, exception, client, model, context) -> Commons.handleException( + ProgressEvent.progress(model, context), + exception, + MODIFY_DB_INSTANCE_AUTOMATIC_BACKUP_REPLICATION_ERROR_RULE_SET, + requestLogger + )) + .progress(); } protected ProgressEvent startAutomaticBackupReplicationInRegion( - final String dbInstanceArn, - final String kmsKeyId, - final AmazonWebServicesClientProxy proxy, - final ProgressEvent progress, - final ProxyClient sourceRegionClient, - final String region + final String dbInstanceArn, + final Integer backupRetentionPeriod, + final String kmsKeyId, + final AmazonWebServicesClientProxy proxy, + final ProgressEvent progress, + final ProxyClient sourceRegionClient, + final String region ) { final ProxyClient rdsClient = new LoggingProxyClient<>(requestLogger, proxy.newProxy(() -> new RdsClientProvider().getClientForRegion(region))); final String AUTOMATIC_REPLICATION_KMS_KEY_ERROR = "Encrypted instances require a valid KMS key ID"; final String AUTOMATIC_REPLICATION_KMS_KEY_EVENT_MESSAGE = "Provide a valid value for the AutomaticBackupReplicationKmsKeyId property."; return proxy.initiate("rds::start-db-instance-automatic-backup-replication", rdsClient, progress.getResourceModel(), progress.getCallbackContext()) - .translateToServiceRequest(resourceModel -> Translator.startDbInstanceAutomatedBackupsReplicationRequest(dbInstanceArn, kmsKeyId)) - .backoffDelay(config.getBackoff()) - .makeServiceCall((request, client) -> rdsClient.injectCredentialsAndInvokeV2( - request, - rdsClient.client()::startDBInstanceAutomatedBackupsReplication - )) - .stabilize((request, response, proxyInvocation, model, context) -> - isInstanceStabilizedAfterReplicationStart(sourceRegionClient, model)) - .handleError((request, exception, client, model, context) -> { - ProgressEvent progressEvent = Commons.handleException( - ProgressEvent.progress(model, context), - exception, - MODIFY_DB_INSTANCE_AUTOMATIC_BACKUP_REPLICATION_ERROR_RULE_SET, - requestLogger - ); - if (exception.getMessage().contains(AUTOMATIC_REPLICATION_KMS_KEY_ERROR)) { - progressEvent.setMessage(StringUtils.trimToEmpty(progressEvent.getMessage()) - .concat(" " + AUTOMATIC_REPLICATION_KMS_KEY_EVENT_MESSAGE)); - } - return progressEvent; - }) - .progress(); + .translateToServiceRequest(resourceModel -> Translator.startDbInstanceAutomatedBackupsReplicationRequest(dbInstanceArn, backupRetentionPeriod, kmsKeyId)) + .backoffDelay(config.getBackoff()) + .makeServiceCall((request, client) -> rdsClient.injectCredentialsAndInvokeV2( + request, + rdsClient.client()::startDBInstanceAutomatedBackupsReplication + )) + .stabilize((request, response, proxyInvocation, model, context) -> + isInstanceStabilizedAfterReplicationStart(sourceRegionClient, model)) + .handleError((request, exception, client, model, context) -> { + ProgressEvent progressEvent = Commons.handleException( + ProgressEvent.progress(model, context), + exception, + MODIFY_DB_INSTANCE_AUTOMATIC_BACKUP_REPLICATION_ERROR_RULE_SET, + requestLogger + ); + if (exception.getMessage().contains(AUTOMATIC_REPLICATION_KMS_KEY_ERROR)) { + progressEvent.setMessage(StringUtils.trimToEmpty(progressEvent.getMessage()) + .concat(" " + AUTOMATIC_REPLICATION_KMS_KEY_EVENT_MESSAGE)); + } + return progressEvent; + }) + .progress(); } } diff --git a/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/CallbackContext.java b/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/CallbackContext.java index 6a6cf2754..22c536696 100644 --- a/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/CallbackContext.java +++ b/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/CallbackContext.java @@ -8,12 +8,14 @@ import software.amazon.cloudformation.proxy.StdCallbackContext; import software.amazon.rds.common.handler.TaggingContext; import software.amazon.rds.common.handler.TimestampContext; +import software.amazon.rds.common.util.IdempotencyHelper; @lombok.Getter @lombok.Setter @lombok.ToString @lombok.EqualsAndHashCode(callSuper = true) -public class CallbackContext extends StdCallbackContext implements TaggingContext.Provider, TimestampContext.Provider { +public class CallbackContext extends StdCallbackContext implements TaggingContext.Provider, TimestampContext.Provider, IdempotencyHelper.PreExistenceContext { + private Boolean preExistenceCheckDone; private boolean described; private boolean created; private boolean deleted; @@ -26,6 +28,7 @@ public class CallbackContext extends StdCallbackContext implements TaggingContex private boolean automaticBackupReplicationStopped; private boolean automaticBackupReplicationStarted; private String dbInstanceArn; + private String automaticBackupReplicationArn; private String currentRegion; private String kmsKeyId; private String snapshotIdentifier; diff --git a/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/CreateHandler.java b/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/CreateHandler.java index 499be340c..6761c3220 100644 --- a/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/CreateHandler.java +++ b/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/CreateHandler.java @@ -27,11 +27,14 @@ import software.amazon.rds.common.request.RequestValidationException; import software.amazon.rds.common.request.ValidatedRequest; import software.amazon.rds.common.request.Validations; +import software.amazon.rds.common.util.IdempotencyHelper; import software.amazon.rds.common.util.IdentifierFactory; import software.amazon.rds.dbinstance.client.ApiVersion; import software.amazon.rds.dbinstance.client.RdsClientProvider; import software.amazon.rds.dbinstance.client.VersionedProxyClient; import software.amazon.rds.dbinstance.util.ResourceModelHelper; +import software.amazon.rds.dbinstance.validators.AutomaticBackupReplicationValidator; +import software.amazon.rds.dbinstance.validators.OracleCustomSystemId; public class CreateHandler extends BaseHandlerStd { @@ -56,10 +59,14 @@ protected void validateRequest(final ResourceHandlerRequest reque super.validateRequest(request); validateDeletionPolicyForClusterInstance(request); Validations.validateTimestamp(request.getDesiredResourceState().getRestoreTime()); + + OracleCustomSystemId.validateRequest(request.getDesiredResourceState()); + + AutomaticBackupReplicationValidator.validateRequest(request.getDesiredResourceState()); } private void validateDeletionPolicyForClusterInstance(final ResourceHandlerRequest request) throws RequestValidationException { - if (isDBClusterMember(request.getDesiredResourceState()) && BooleanUtils.isTrue(request.getSnapshotRequested())) { + if (DBInstancePredicates.isDBClusterMember(request.getDesiredResourceState()) && BooleanUtils.isTrue(request.getSnapshotRequested())) { throw new RequestValidationException(ILLEGAL_DELETION_POLICY_ERROR); } } @@ -101,41 +108,43 @@ protected ProgressEvent handleRequest( } return progress; }) - .then(progress -> Commons.execOnce(progress, () -> { - if (ResourceModelHelper.isRestoreToPointInTime(progress.getResourceModel())) { - // restoreDBInstanceToPointInTime is not a versioned call. - return safeAddTags(this::restoreDbInstanceToPointInTimeRequest) - .invoke(proxy, rdsProxyClient.defaultClient(), progress, allTags); - } else if (ResourceModelHelper.isReadReplica(progress.getResourceModel())) { - // createDBInstanceReadReplica is not a versioned call. - return safeAddTags(this::createDbInstanceReadReplica) - .invoke(proxy, rdsProxyClient.defaultClient(), progress, allTags); - } else if (ResourceModelHelper.isRestoreFromSnapshot(progress.getResourceModel()) || - ResourceModelHelper.isRestoreFromClusterSnapshot(progress.getResourceModel())) { - if (ResourceModelHelper.isRestoreFromSnapshot(progress.getResourceModel()) && !isMultiAZ) { - try { - final DBSnapshot snapshot = fetchDBSnapshot(rdsProxyClient.defaultClient(), model); - final String engine = snapshot.engine(); - if (StringUtils.isNullOrEmpty(progress.getResourceModel().getEngine())) { - progress.getResourceModel().setEngine(engine); - } - if (progress.getResourceModel().getMultiAZ() == null) { - progress.getResourceModel().setMultiAZ(ResourceModelHelper.getDefaultMultiAzForEngine(engine)); + .then(progress -> IdempotencyHelper.safeCreate( + m -> fetchDBInstance(rdsProxyClient.defaultClient(), m), + p -> Commons.execOnce(progress, () -> { + if (ResourceModelHelper.isRestoreToPointInTime(progress.getResourceModel())) { + // restoreDBInstanceToPointInTime is not a versioned call. + return safeAddTags(this::restoreDbInstanceToPointInTimeRequest) + .invoke(proxy, rdsProxyClient.defaultClient(), progress, allTags); + } else if (ResourceModelHelper.isReadReplica(progress.getResourceModel())) { + // createDBInstanceReadReplica is not a versioned call. + return safeAddTags(this::createDbInstanceReadReplica) + .invoke(proxy, rdsProxyClient.defaultClient(), progress, allTags); + } else if (ResourceModelHelper.isRestoreFromSnapshot(progress.getResourceModel()) || + ResourceModelHelper.isRestoreFromClusterSnapshot(progress.getResourceModel())) { + if (ResourceModelHelper.isRestoreFromSnapshot(progress.getResourceModel()) && !isMultiAZ) { + try { + final DBSnapshot snapshot = fetchDBSnapshot(rdsProxyClient.defaultClient(), model); + final String engine = snapshot.engine(); + if (StringUtils.isNullOrEmpty(progress.getResourceModel().getEngine())) { + progress.getResourceModel().setEngine(engine); + } + if (progress.getResourceModel().getMultiAZ() == null) { + progress.getResourceModel().setMultiAZ(ResourceModelHelper.getDefaultMultiAzForEngine(engine)); + } + } catch (Exception e) { + return Commons.handleException(progress, e, RESTORE_DB_INSTANCE_ERROR_RULE_SET, requestLogger); } - } catch (Exception e) { - return Commons.handleException(progress, e, RESTORE_DB_INSTANCE_ERROR_RULE_SET, requestLogger); } + return versioned(proxy, rdsProxyClient, progress, allTags, ImmutableMap.of( + ApiVersion.V12, this::restoreDbInstanceFromSnapshotV12, + ApiVersion.DEFAULT, safeAddTags(this::restoreDbInstanceFromSnapshot) + )); } return versioned(proxy, rdsProxyClient, progress, allTags, ImmutableMap.of( - ApiVersion.V12, this::restoreDbInstanceFromSnapshotV12, - ApiVersion.DEFAULT, safeAddTags(this::restoreDbInstanceFromSnapshot) + ApiVersion.V12, this::createDbInstanceV12, + ApiVersion.DEFAULT, safeAddTags(this::createDbInstance) )); - } - return versioned(proxy, rdsProxyClient, progress, allTags, ImmutableMap.of( - ApiVersion.V12, this::createDbInstanceV12, - ApiVersion.DEFAULT, safeAddTags(this::createDbInstance) - )); - }, CallbackContext::isCreated, CallbackContext::setCreated)) + }, CallbackContext::isCreated, CallbackContext::setCreated), ResourceModel.TYPE_NAME, model.getDBInstanceIdentifier(), progress, requestLogger)) .then(progress -> Commons.execOnce(progress, () -> { final Tagging.TagSet extraTags = Tagging.TagSet.builder() .stackTags(allTags.getStackTags()) @@ -189,18 +198,21 @@ ApiVersion.DEFAULT, safeAddTags(this::createDbInstance) return progress; }, (m) -> !StringUtils.isNullOrEmpty(callbackContext.getDbInstanceArn()), (v, c) -> {})) .then(progress -> Commons.execOnce(progress, () -> { - if (ResourceModelHelper.shouldStartAutomaticBackupReplication(request.getPreviousResourceState(), request.getDesiredResourceState())) { - return startAutomaticBackupReplicationInRegion( - callbackContext.getDbInstanceArn(), - progress.getResourceModel().getAutomaticBackupReplicationKmsKeyId(), - proxy, - progress, - rdsProxyClient.defaultClient(), - ResourceModelHelper.getAutomaticBackupReplicationRegion(request.getDesiredResourceState()) - ); - } - return progress; - }, + final ResourceModel resourceModel = progress.getResourceModel(); + + if (ResourceModelHelper.shouldStartAutomaticBackupReplication(request.getPreviousResourceState(), request.getDesiredResourceState())) { + return startAutomaticBackupReplicationInRegion( + callbackContext.getDbInstanceArn(), + ResourceModelHelper.getAutomaticBackupReplicationRetentionPeriod(resourceModel), + resourceModel.getAutomaticBackupReplicationKmsKeyId(), + proxy, + progress, + rdsProxyClient.defaultClient(), + ResourceModelHelper.getAutomaticBackupReplicationRegion(request.getDesiredResourceState()) + ); + } + return progress; + }, CallbackContext::isAutomaticBackupReplicationStarted, CallbackContext::setAutomaticBackupReplicationStarted)) .then(progress -> { model.setTags(Translator.translateTagsFromSdk(Tagging.translateTagsToSdk(allTags))); diff --git a/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/DBInstancePredicates.java b/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/DBInstancePredicates.java new file mode 100644 index 000000000..5fc935906 --- /dev/null +++ b/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/DBInstancePredicates.java @@ -0,0 +1,332 @@ +package software.amazon.rds.dbinstance; + +import com.amazonaws.arn.Arn; +import com.amazonaws.util.CollectionUtils; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.BooleanUtils; +import software.amazon.awssdk.services.rds.model.DBCluster; +import software.amazon.awssdk.services.rds.model.DBInstance; +import software.amazon.awssdk.services.rds.model.DomainMembership; +import software.amazon.awssdk.services.rds.model.OptionGroupMembership; +import software.amazon.awssdk.services.rds.model.PendingModifiedValues; +import software.amazon.awssdk.utils.StringUtils; +import software.amazon.cloudformation.exceptions.CfnNotStabilizedException; +import software.amazon.rds.common.logging.RequestLogger; +import software.amazon.rds.dbinstance.status.DBInstanceStatus; +import software.amazon.rds.dbinstance.status.DBParameterGroupStatus; +import software.amazon.rds.dbinstance.status.DomainMembershipStatus; +import software.amazon.rds.dbinstance.status.OptionGroupStatus; +import software.amazon.rds.dbinstance.status.ReadReplicaStatus; +import software.amazon.rds.dbinstance.status.VPCSecurityGroupStatus; +import software.amazon.rds.dbinstance.util.ResourceModelHelper; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class DBInstancePredicates { + + private static final String SECRET_STATUS_ACTIVE = "active"; + private static final List RDS_CUSTOM_ORACLE_ENGINES = ImmutableList.of( + "custom-oracle-ee", + "custom-oracle-ee-cdb" + ); + private static final String READ_REPLICA_STATUS_TYPE = "read replication"; + + public static void assertNoDBInstanceTerminalStatus(final DBInstance dbInstance) throws CfnNotStabilizedException { + final DBInstanceStatus status = DBInstanceStatus.fromString(dbInstance.dbInstanceStatus()); + if (status != null && status.isTerminal()) { + throw new CfnNotStabilizedException(new Exception("DB Instance is in state: " + status.toString())); + } + } + + public static void assertNoOptionGroupTerminalStatus(final DBInstance dbInstance) throws CfnNotStabilizedException { + final List termOptionGroups = Optional.ofNullable(dbInstance.optionGroupMemberships()).orElse(Collections.emptyList()) + .stream() + .filter(optionGroup -> { + final OptionGroupStatus status = OptionGroupStatus.fromString(optionGroup.status()); + return status != null && status.isTerminal(); + }) + .collect(Collectors.toList()); + + if (!termOptionGroups.isEmpty()) { + throw new CfnNotStabilizedException(new Exception( + String.format("OptionGroup %s is in a terminal state", + termOptionGroups.get(0).optionGroupName()))); + } + } + + public static void assertNoDomainMembershipTerminalStatus(final DBInstance dbInstance) throws CfnNotStabilizedException { + final List terminalDomainMemberships = Optional.ofNullable(dbInstance.domainMemberships()).orElse(Collections.emptyList()) + .stream() + .filter(domainMembership -> { + final DomainMembershipStatus status = DomainMembershipStatus.fromString(domainMembership.status()); + return status != null && status.isTerminal(); + }) + .collect(Collectors.toList()); + + if (!terminalDomainMemberships.isEmpty()) { + throw new CfnNotStabilizedException(new Exception(String.format("Domain %s is in a terminal state", + terminalDomainMemberships.get(0).domain()))); + } + } + + public static void assertNoTerminalStatus(final DBInstance dbInstance) throws CfnNotStabilizedException { + assertNoDBInstanceTerminalStatus(dbInstance); + assertNoOptionGroupTerminalStatus(dbInstance); + assertNoDomainMembershipTerminalStatus(dbInstance); + } + + public static boolean isInstanceStabilizedAfterReplicationStop( + final DBInstance dbInstance, + final ResourceModel model + ) { + assertNoTerminalStatus(dbInstance); + return isDBInstanceAvailable(dbInstance) + && !dbInstance.hasDbInstanceAutomatedBackupsReplications(); + } + + public static boolean isDBInstanceAvailable(final DBInstance dbInstance) { + return DBInstanceStatus.Available.equalsString(dbInstance.dbInstanceStatus()); + } + + public static boolean isDomainMembershipsJoined(final DBInstance dbInstance) { + return Optional.ofNullable(dbInstance.domainMemberships()).orElse(Collections.emptyList()) + .stream() + .allMatch(membership -> DomainMembershipStatus.Joined.equalsString(membership.status()) || + DomainMembershipStatus.KerberosEnabled.equalsString(membership.status())); + } + + public static boolean isVpcSecurityGroupsActive(final DBInstance dbInstance) { + return Optional.ofNullable(dbInstance.vpcSecurityGroups()).orElse(Collections.emptyList()) + .stream() + .allMatch(group -> VPCSecurityGroupStatus.Active.equalsString(group.status())); + } + + public static boolean isNoPendingChanges(final DBInstance dbInstance) { + final PendingModifiedValues pending = dbInstance.pendingModifiedValues(); + return (pending == null) || (pending.dbInstanceClass() == null && + pending.allocatedStorage() == null && + pending.automationMode() == null && + pending.backupRetentionPeriod() == null && + pending.dbInstanceIdentifier() == null && + pending.dbSubnetGroupName() == null && + pending.engine() == null && + pending.engineVersion() == null && + pending.iamDatabaseAuthenticationEnabled() == null && + pending.iops() == null && + pending.licenseModel() == null && + pending.masterUserPassword() == null && + pending.multiAZ() == null && + pending.pendingCloudwatchLogsExports() == null && + pending.port() == null && + CollectionUtils.isNullOrEmpty(pending.processorFeatures()) && + pending.resumeFullAutomationModeTime() == null && + pending.storageThroughput() == null && + pending.storageType() == null + ); + } + + public static boolean isCaCertificateChangesApplied(final DBInstance dbInstance, final ResourceModel model) { + final PendingModifiedValues pending = dbInstance.pendingModifiedValues(); + return pending == null || + pending.caCertificateIdentifier() == null || + BooleanUtils.isNotTrue(model.getCertificateRotationRestart()); + } + + public static boolean isDBParameterGroupNotApplying(final DBInstance dbInstance) { + return Optional.ofNullable(dbInstance.dbParameterGroups()).orElse(Collections.emptyList()) + .stream() + .noneMatch(group -> DBParameterGroupStatus.Applying.equalsString(group.parameterApplyStatus())); + } + + public static boolean isReplicationComplete(final DBInstance dbInstance) { + return Optional.ofNullable(dbInstance.statusInfos()).orElse(Collections.emptyList()) + .stream() + .filter(statusInfo -> READ_REPLICA_STATUS_TYPE.equals(statusInfo.statusType())) + .allMatch(statusInfo -> ReadReplicaStatus.Replicating.equalsString(statusInfo.status())); + } + + public static boolean isDBClusterParameterGroupInSync(final ResourceModel model, final DBCluster dbCluster) { + return Optional.ofNullable(dbCluster.dbClusterMembers()).orElse(Collections.emptyList()) + .stream() + .filter(member -> model.getDBInstanceIdentifier().equalsIgnoreCase(member.dbInstanceIdentifier())) + .anyMatch(member -> DBParameterGroupStatus.InSync.equalsString(member.dbClusterParameterGroupStatus())); + } + + public static boolean isDBClusterParameterGroupNotApplying(final ResourceModel model, final DBCluster dbCluster) { + return Optional.ofNullable(dbCluster.dbClusterMembers()).orElse(Collections.emptyList()) + .stream() + .filter(member -> model.getDBInstanceIdentifier().equalsIgnoreCase(member.dbInstanceIdentifier())) + .noneMatch(member -> DBParameterGroupStatus.Applying.equalsString(member.dbClusterParameterGroupStatus())); + } + + public static boolean isDBClusterMember(final ResourceModel model) { + return StringUtils.isNotBlank(model.getDBClusterIdentifier()); + } + + public static boolean isRdsCustomOracleInstance(final ResourceModel model) { + return RDS_CUSTOM_ORACLE_ENGINES.contains(model.getEngine()); + } + + public static boolean isOptionGroupInSync(final DBInstance dbInstance) { + return Optional.ofNullable(dbInstance.optionGroupMemberships()).orElse(Collections.emptyList()) + .stream() + .allMatch(optionGroup -> OptionGroupStatus.InSync.equalsString(optionGroup.status())); + } + + public static boolean isDBParameterGroupInSync(final DBInstance dbInstance) { + return Optional.ofNullable(dbInstance.dbParameterGroups()).orElse(Collections.emptyList()) + .stream() + .allMatch(parameterGroup -> DBParameterGroupStatus.InSync.equalsString(parameterGroup.parameterApplyStatus())); + } + + public static boolean isMasterUserSecretStabilized(final DBInstance instance) { + if (instance.masterUserSecret() == null) { + return true; + } + return SECRET_STATUS_ACTIVE.equalsIgnoreCase(instance.masterUserSecret().secretStatus()); + } + + public static boolean isDBInstanceStabilizedAfterMutate( + final DBInstance dbInstance, + final ResourceModel model, + final CallbackContext context, + final RequestLogger requestLogger + ) { + assertNoTerminalStatus(dbInstance); + + if(ResourceModelHelper.shouldApplyImmediately(model)){ + return isStabilizedWithChangesAppliedImmediately(dbInstance, model, requestLogger); + } + + return isStabilizedWithoutChangesAppliedImmediately(dbInstance, requestLogger); + } + + /*** + * Stabilization logic that ensures all the changes are applied. + */ + private static boolean isStabilizedWithChangesAppliedImmediately( + final DBInstance dbInstance, + final ResourceModel model, + final RequestLogger requestLogger + ) { + assertNoTerminalStatus(dbInstance); + + final boolean isDBInstanceStabilizedAfterMutateResult = isDBInstanceAvailable(dbInstance) && + isReplicationComplete(dbInstance) && + isDBParameterGroupNotApplying(dbInstance) && + isNoPendingChanges(dbInstance) && + isCaCertificateChangesApplied(dbInstance, model) && + isVpcSecurityGroupsActive(dbInstance) && + isDomainMembershipsJoined(dbInstance) && + isMasterUserSecretStabilized(dbInstance); + + requestLogger.log(String.format("isStabilizedWithChangesAppliedImmediately: %b", isDBInstanceStabilizedAfterMutateResult), + ImmutableMap.of("isDBInstanceAvailable", isDBInstanceAvailable(dbInstance), + "isReplicationComplete", isReplicationComplete(dbInstance), + "isDBParameterGroupNotApplying", isDBParameterGroupNotApplying(dbInstance), + "isNoPendingChanges", isNoPendingChanges(dbInstance), + "isCaCertificateChangesApplied", isCaCertificateChangesApplied(dbInstance, model), + "isVpcSecurityGroupsActive", isVpcSecurityGroupsActive(dbInstance), + "isDomainMembershipsJoined", isDomainMembershipsJoined(dbInstance), + "isMasterUserSecretStabilized", isMasterUserSecretStabilized(dbInstance)), + ImmutableMap.of("Description", "isStabilizedWithChangesAppliedImmediately method will be repeatedly" + + " called with a backoff mechanism after the modify call until it returns true. This" + + " process will continue until all included flags are true.")); + + return isDBInstanceStabilizedAfterMutateResult; + } + + /*** + * Stabilization logic that excludes the settings that are not applied immediately. This happens when + * ApplyImmediately is set to false. The excluded settings is based on the following doc + * https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_ModifyInstance.Settings.html + */ + private static boolean isStabilizedWithoutChangesAppliedImmediately( + final DBInstance dbInstance, + final RequestLogger requestLogger + ) { + assertNoTerminalStatus(dbInstance); + + final boolean isDBInstanceStabilizedAfterMutateResult = isDBInstanceAvailable(dbInstance) && + isReplicationComplete(dbInstance) && + isDBParameterGroupNotApplying(dbInstance) && + isVpcSecurityGroupsActive(dbInstance) && + isMasterUserSecretStabilized(dbInstance); + + requestLogger.log(String.format("isStabilizedWithoutChangesAppliedImmediately: %b", isDBInstanceStabilizedAfterMutateResult), + ImmutableMap.of("isDBInstanceAvailable", isDBInstanceAvailable(dbInstance), + "isReplicationComplete", isReplicationComplete(dbInstance), + "isDBParameterGroupNotApplying", isDBParameterGroupNotApplying(dbInstance), + "isVpcSecurityGroupsActive", isVpcSecurityGroupsActive(dbInstance), + "isMasterUserSecretStabilized", isMasterUserSecretStabilized(dbInstance)), + ImmutableMap.of("Description", "isStabilizedWithoutChangesAppliedImmediately method will be repeatedly" + + " called with a backoff mechanism after the modify call until it returns true. This" + + " process will continue until all included flags are true.")); + + return isDBInstanceStabilizedAfterMutateResult; + } + + public static boolean isDBInstanceStabilizedAfterReboot( + final DBInstance dbInstance, + final RequestLogger requestLogger + ) { + assertNoTerminalStatus(dbInstance); + + final boolean isDBClusterParameterGroupStabilized = true; + return isDBInstanceStabilizedAfterReboot(dbInstance, isDBClusterParameterGroupStabilized, requestLogger); + } + + public static boolean isDBInstanceStabilizedAfterReboot( + final DBInstance dbInstance, + final DBCluster dbCluster, + final ResourceModel model, + final RequestLogger requestLogger + ) { + assertNoTerminalStatus(dbInstance); + + final boolean isDBClusterParameterGroupStabilized = isDBClusterParameterGroupInSync(model, dbCluster); + return isDBInstanceStabilizedAfterReboot(dbInstance, isDBClusterParameterGroupStabilized, requestLogger); + } + + private static boolean isDBInstanceStabilizedAfterReboot( + final DBInstance dbInstance, + final boolean isDBClusterParameterGroupStabilized, + final RequestLogger requestLogger + ) { + final boolean isDBInstanceStabilizedAfterReboot = isDBInstanceAvailable(dbInstance) && + isDBParameterGroupInSync(dbInstance) && + isOptionGroupInSync(dbInstance) && + isDBClusterParameterGroupStabilized; + + requestLogger.log(String.format("isDBInstanceStabilizedAfterReboot: %b", isDBInstanceStabilizedAfterReboot), + ImmutableMap.of("isDBInstanceAvailable", isDBInstanceAvailable(dbInstance), + "isDBParameterGroupInSync", isDBParameterGroupInSync(dbInstance), + "isOptionGroupInSync", isOptionGroupInSync(dbInstance), + "isDBClusterParameterGroupStabilized", isDBClusterParameterGroupStabilized), + ImmutableMap.of("Description", "isDBInstanceStabilizedAfterReboot method will be repeatedly" + + " called with a backoff mechanism after the reboot call until it returns true. This" + + " process will continue until all included flags are true.")); + + return isDBInstanceStabilizedAfterReboot; + } + + public static boolean isInstanceStabilizedAfterReplicationStart( + final DBInstance dbInstance, + final ResourceModel model + ) { + assertNoTerminalStatus(dbInstance); + return isDBInstanceAvailable(dbInstance) + && dbInstance.hasDbInstanceAutomatedBackupsReplications() && + !dbInstance.dbInstanceAutomatedBackupsReplications().isEmpty() && + model.getAutomaticBackupReplicationRegion() + .equalsIgnoreCase( + Arn.fromString(dbInstance.dbInstanceAutomatedBackupsReplications().get(0).dbInstanceAutomatedBackupsArn()).getRegion()); + } +} diff --git a/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/DeleteHandler.java b/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/DeleteHandler.java index 6b79b1322..83f5995dc 100644 --- a/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/DeleteHandler.java +++ b/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/DeleteHandler.java @@ -1,13 +1,8 @@ package software.amazon.rds.dbinstance; -import java.util.Collections; -import java.util.function.Function; -import java.util.Optional; - import com.google.common.annotations.VisibleForTesting; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; - import software.amazon.awssdk.services.ec2.Ec2Client; import software.amazon.awssdk.services.rds.RdsClient; import software.amazon.awssdk.services.rds.model.DBInstance; @@ -20,6 +15,8 @@ import software.amazon.rds.common.util.IdentifierFactory; import software.amazon.rds.dbinstance.client.VersionedProxyClient; +import java.util.function.Function; + public class DeleteHandler extends BaseHandlerStd { private static final String SNAPSHOT_PREFIX = "Snapshot-"; diff --git a/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/ReadHandler.java b/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/ReadHandler.java index 5a58cef73..6bd0a8cc7 100644 --- a/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/ReadHandler.java +++ b/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/ReadHandler.java @@ -1,15 +1,27 @@ package software.amazon.rds.dbinstance; +import com.amazonaws.util.StringUtils; import software.amazon.awssdk.services.ec2.Ec2Client; import software.amazon.awssdk.services.rds.RdsClient; import software.amazon.awssdk.services.rds.model.DBInstance; +import software.amazon.awssdk.services.rds.model.DBInstanceAutomatedBackup; +import software.amazon.awssdk.services.rds.model.DBInstanceAutomatedBackupsReplication; +import software.amazon.awssdk.services.rds.model.RdsException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.HandlerErrorCode; import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.ProxyClient; +import software.amazon.rds.common.error.ErrorStatus; +import software.amazon.rds.common.error.HandlerErrorStatus; import software.amazon.rds.common.handler.Commons; import software.amazon.rds.common.handler.HandlerConfig; import software.amazon.rds.common.request.ValidatedRequest; +import software.amazon.rds.dbinstance.util.ResourceModelHelper; +import software.amazon.rds.dbinstance.client.RdsClientProvider; import software.amazon.rds.dbinstance.client.VersionedProxyClient; +import java.util.List; + public class ReadHandler extends BaseHandlerStd { public ReadHandler() { @@ -20,6 +32,8 @@ public ReadHandler(final HandlerConfig config) { super(config); } + + protected ProgressEvent handleRequest( final AmazonWebServicesClientProxy proxy, final ValidatedRequest request, @@ -27,21 +41,89 @@ protected ProgressEvent handleRequest( final VersionedProxyClient rdsProxyClient, final VersionedProxyClient ec2ProxyClient ) { - return proxy.initiate("rds::describe-db-instance", rdsProxyClient.defaultClient(), request.getDesiredResourceState(), callbackContext) - .translateToServiceRequest(Translator::describeDbInstancesRequest) - .makeServiceCall((describeRequest, proxyInvocation) -> proxyInvocation.injectCredentialsAndInvokeV2( - describeRequest, - proxyInvocation.client()::describeDBInstances - )) - .handleError((describeRequest, exception, client, model, context) -> Commons.handleException( - ProgressEvent.progress(model, context), - exception, - DEFAULT_DB_INSTANCE_ERROR_RULE_SET, - requestLogger - )) - .done((describeRequest, describeResponse, proxyInvocation, resourceModel, context) -> { - final DBInstance dbInstance = describeResponse.dbInstances().get(0); - return ProgressEvent.success(Translator.translateDbInstanceFromSdk(dbInstance), context); - }); + return ProgressEvent.progress(request.getDesiredResourceState(), callbackContext) + .then(progress -> describeDbInstance(progress, proxy, rdsProxyClient)) + .then(progress -> { + // If replication found for DB instance, we describe it + if (!StringUtils.isNullOrEmpty(progress.getCallbackContext().getAutomaticBackupReplicationArn())) { + return describeAutomatedBackupsReplication(progress, proxy); + } + return progress; + }) + .then(progress -> ProgressEvent.success(progress.getResourceModel(), progress.getCallbackContext())); + } + + protected ProgressEvent describeDbInstance( + final ProgressEvent progress, + final AmazonWebServicesClientProxy proxy, + final VersionedProxyClient rdsProxyClient + ) { + final CallbackContext callbackContext = progress.getCallbackContext(); + final ResourceModel resourceModel = progress.getResourceModel(); + + return proxy.initiate("rds::describe-db-instance", rdsProxyClient.defaultClient(), resourceModel, callbackContext) + .translateToServiceRequest(Translator::describeDbInstancesRequest) + .makeServiceCall((describeRequest, proxyInvocation) -> { + return proxyInvocation.injectCredentialsAndInvokeV2( + describeRequest, + proxyInvocation.client()::describeDBInstances + ); + }) + .handleError((describeRequest, exception, client, model, context) -> Commons.handleException( + ProgressEvent.progress(model, context), + exception, + READ_HANDLER_ERROR_RULE_SET, + requestLogger + )) + .done((describeRequest, describeResponse, automatedBackupProxyInvocation, model, context) -> { + final DBInstance dbInstance = describeResponse.dbInstances().get(0); + final ResourceModel currentModel = Translator.translateDbInstanceFromSdk(dbInstance); + final List replications = dbInstance.dbInstanceAutomatedBackupsReplications(); + if (replications.isEmpty()) { + context.setAutomaticBackupReplicationStopped(true); + return ProgressEvent.progress(currentModel, context); + } + context.setAutomaticBackupReplicationArn(replications.get(0).dbInstanceAutomatedBackupsArn()); + + return ProgressEvent.progress(currentModel, context); + }); + } + + protected ProgressEvent describeAutomatedBackupsReplication( + final ProgressEvent progress, + final AmazonWebServicesClientProxy proxy + ) { + final CallbackContext callbackContext = progress.getCallbackContext(); + final ResourceModel resourceModel = progress.getResourceModel(); + + if (StringUtils.isNullOrEmpty(callbackContext.getAutomaticBackupReplicationArn())) { + return ProgressEvent.progress(resourceModel, callbackContext); + } + + final String replicationRegion = ResourceModelHelper.getRegionFromArn(callbackContext.getAutomaticBackupReplicationArn()); + final ProxyClient replicationRegionProxyClient = + proxy.newProxy(() -> new RdsClientProvider().getClientForRegion(replicationRegion)); + + return proxy.initiate("rds::describe-db-instance-automated-backups", replicationRegionProxyClient, resourceModel, callbackContext) + .translateToServiceRequest(model -> Translator.describeDBInstanceAutomaticBackupRequest(callbackContext.getAutomaticBackupReplicationArn())) + .backoffDelay(config.getBackoff()) + .makeServiceCall((describeRequest, proxyInvocation) -> proxyInvocation.injectCredentialsAndInvokeV2( + describeRequest, + proxyInvocation.client()::describeDBInstanceAutomatedBackups + )) + .handleError((describeRequest, exception, client, model, context) ->Commons.handleException( + ProgressEvent.progress(model, context), + exception, + DESCRIBE_AUTOMATED_BACKUPS_SOFTFAIL_ERROR_RULE_SET, + requestLogger + )) + .done((describeRequest, describeResponse, proxyInvocation, model, context) -> { + DBInstanceAutomatedBackup dbInstanceAutomatedBackup = describeResponse.dbInstanceAutomatedBackups().get(0); + model.setAutomaticBackupReplicationRetentionPeriod(dbInstanceAutomatedBackup.backupRetentionPeriod()); + model.setAutomaticBackupReplicationRegion(replicationRegion); + model.setAutomaticBackupReplicationKmsKeyId(dbInstanceAutomatedBackup.kmsKeyId()); + context.setAutomaticBackupReplicationStarted(true); + return ProgressEvent.progress(model, context); + }); } } diff --git a/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/Translator.java b/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/Translator.java index 76f7c5853..e6c4a34d5 100644 --- a/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/Translator.java +++ b/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/Translator.java @@ -28,6 +28,7 @@ import software.amazon.awssdk.services.rds.model.CloudwatchLogsExportConfiguration; import software.amazon.awssdk.services.rds.model.CreateDbInstanceReadReplicaRequest; import software.amazon.awssdk.services.rds.model.CreateDbInstanceRequest; +import software.amazon.awssdk.services.rds.model.DBInstance; import software.amazon.awssdk.services.rds.model.DeleteDbInstanceRequest; import software.amazon.awssdk.services.rds.model.DescribeDbClusterSnapshotsRequest; import software.amazon.awssdk.services.rds.model.DescribeDbClustersRequest; @@ -46,6 +47,7 @@ import software.amazon.awssdk.services.rds.model.StartDbInstanceAutomatedBackupsReplicationRequest; import software.amazon.awssdk.services.rds.model.StopDbInstanceAutomatedBackupsReplicationRequest; import software.amazon.awssdk.utils.StringUtils; +import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; import software.amazon.rds.common.handler.Commons; import software.amazon.rds.common.handler.Tagging; import software.amazon.rds.dbinstance.util.ResourceModelHelper; @@ -88,7 +90,7 @@ public static DescribeDbInstancesRequest describeDbInstanceByResourceIdRequest(f .build(); } - public static DescribeDbInstanceAutomatedBackupsRequest describeDBInstanceAutomaticBackup(final String automaticBackupArn) { + public static DescribeDbInstanceAutomatedBackupsRequest describeDBInstanceAutomaticBackupRequest(final String automaticBackupArn) { return DescribeDbInstanceAutomatedBackupsRequest.builder() .dbInstanceAutomatedBackupsArn(automaticBackupArn) .build(); @@ -116,6 +118,7 @@ public static CreateDbInstanceReadReplicaRequest createDbInstanceReadReplicaRequ .availabilityZone(model.getAvailabilityZone()) .copyTagsToSnapshot(model.getCopyTagsToSnapshot()) .customIamInstanceProfile(model.getCustomIAMInstanceProfile()) + .databaseInsightsMode(model.getDatabaseInsightsMode()) .dbInstanceClass(model.getDBInstanceClass()) .dbInstanceIdentifier(model.getDBInstanceIdentifier()) .dbSubnetGroupName(model.getDBSubnetGroupName()) @@ -284,12 +287,14 @@ public static CreateDbInstanceRequest createDbInstanceRequest( .characterSetName(model.getCharacterSetName()) .copyTagsToSnapshot(model.getCopyTagsToSnapshot()) .customIamInstanceProfile(model.getCustomIAMInstanceProfile()) + .databaseInsightsMode(model.getDatabaseInsightsMode()) .dbClusterIdentifier(model.getDBClusterIdentifier()) .dbInstanceClass(model.getDBInstanceClass()) .dbInstanceIdentifier(model.getDBInstanceIdentifier()) .dbName(model.getDBName()) .dbParameterGroupName(model.getDBParameterGroupName()) .dbSubnetGroupName(model.getDBSubnetGroupName()) + .dbSystemId(model.getDBSystemId()) .dedicatedLogVolume(model.getDedicatedLogVolume()) .deletionProtection(model.getDeletionProtection()) .domain(model.getDomain()) @@ -411,7 +416,7 @@ public static ModifyDbInstanceRequest modifyDbInstanceRequestV12( ) { final ModifyDbInstanceRequest.Builder builder = ModifyDbInstanceRequest.builder() .allowMajorVersionUpgrade(desiredModel.getAllowMajorVersionUpgrade()) - .applyImmediately(Boolean.TRUE) + .applyImmediately(ResourceModelHelper.shouldApplyImmediately(desiredModel)) .autoMinorVersionUpgrade(diff(previousModel.getAutoMinorVersionUpgrade(), desiredModel.getAutoMinorVersionUpgrade())) .backupRetentionPeriod(diff(previousModel.getBackupRetentionPeriod(), desiredModel.getBackupRetentionPeriod())) .dbInstanceClass(diff(previousModel.getDBInstanceClass(), desiredModel.getDBInstanceClass())) @@ -443,14 +448,16 @@ public static ModifyDbInstanceRequest modifyDbInstanceRequestV12( public static ModifyDbInstanceRequest modifyDbInstanceRequest( final ResourceModel previousModel, final ResourceModel desiredModel, + final DBInstance physicalDBInstance, final Boolean isRollback ) { final ModifyDbInstanceRequest.Builder builder = ModifyDbInstanceRequest.builder() .allowMajorVersionUpgrade(desiredModel.getAllowMajorVersionUpgrade()) - .applyImmediately(Boolean.TRUE) + .applyImmediately(ResourceModelHelper.shouldApplyImmediately(desiredModel)) .autoMinorVersionUpgrade(diff(previousModel.getAutoMinorVersionUpgrade(), desiredModel.getAutoMinorVersionUpgrade())) .backupRetentionPeriod(diff(previousModel.getBackupRetentionPeriod(), desiredModel.getBackupRetentionPeriod())) .copyTagsToSnapshot(diff(previousModel.getCopyTagsToSnapshot(), desiredModel.getCopyTagsToSnapshot())) + .databaseInsightsMode(diff(previousModel.getDatabaseInsightsMode(), desiredModel.getDatabaseInsightsMode())) .dbInstanceClass(diff(previousModel.getDBInstanceClass(), desiredModel.getDBInstanceClass())) .dbInstanceIdentifier(desiredModel.getDBInstanceIdentifier()) .dbParameterGroupName(diff(previousModel.getDBParameterGroupName(), desiredModel.getDBParameterGroupName())) @@ -494,16 +501,29 @@ public static ModifyDbInstanceRequest modifyDbInstanceRequest( if (BooleanUtils.isTrue(isRollback)) { if (isProvisionedIoStorage(desiredModel)) { - builder.allocatedStorage(max(getAllocatedStorage(previousModel), getAllocatedStorage(desiredModel))); + // 3-way max between previous model, desired model and current instance storage + // because you cannot shrink storage in RDS + final Integer allocatedStorage = max(getAllocatedStorage(previousModel), getAllocatedStorage(desiredModel)); + builder.allocatedStorage(max(physicalDBInstance.allocatedStorage(), allocatedStorage)); builder.iops(desiredModel.getIops()); } } else { builder.engineVersion(diff(previousModel.getEngineVersion(), desiredModel.getEngineVersion())); + final Integer allocatedStorageDiff = diff(getAllocatedStorage(previousModel), getAllocatedStorage(desiredModel)); + + // When you have an IOPS configurable storage type + // both parameters, allocatedStorage and iops MUST be specified in the modifyDBInstance call if (isProvisionedIoStorage(desiredModel)) { - builder.allocatedStorage(getAllocatedStorage(desiredModel)); + if (allocatedStorageDiff != null) { + // if user specifies allocated storage, then use it + builder.allocatedStorage(allocatedStorageDiff); + } else { + // if not, we should take max of physical and the template's in case their instance has scaled out + builder.allocatedStorage(max(physicalDBInstance.allocatedStorage(), getAllocatedStorage(desiredModel))); + } builder.iops(desiredModel.getIops()); } else { - builder.allocatedStorage(diff(getAllocatedStorage(previousModel), getAllocatedStorage(desiredModel))); + builder.allocatedStorage(allocatedStorageDiff); builder.iops(diff(previousModel.getIops(), desiredModel.getIops())); } } @@ -623,6 +643,7 @@ public static ModifyDbInstanceRequest modifyDbInstanceAfterCreateRequest(final R .applyImmediately(Boolean.TRUE) .backupRetentionPeriod(model.getBackupRetentionPeriod()) .caCertificateIdentifier(model.getCACertificateIdentifier()) + .databaseInsightsMode(model.getDatabaseInsightsMode()) .dbInstanceIdentifier(model.getDBInstanceIdentifier()) .dbParameterGroupName(model.getDBParameterGroupName()) .deletionProtection(model.getDeletionProtection()) @@ -657,7 +678,7 @@ public static ModifyDbInstanceRequest updateAllocatedStorageRequest(final Resour return ModifyDbInstanceRequest.builder() .dbInstanceIdentifier(desiredModel.getDBInstanceIdentifier()) .allocatedStorage(getAllocatedStorage(desiredModel)) - .applyImmediately(Boolean.TRUE) + .applyImmediately(ResourceModelHelper.shouldApplyImmediately(desiredModel)) .build(); } @@ -760,10 +781,12 @@ public static DescribeDbEngineVersionsRequest describeDbEngineVersionsRequest( public static StartDbInstanceAutomatedBackupsReplicationRequest startDbInstanceAutomatedBackupsReplicationRequest( final String dbInstanceArn, + final Integer backupRetentionPeriod, final String kmsKeyId ) { return StartDbInstanceAutomatedBackupsReplicationRequest.builder() .sourceDBInstanceArn(dbInstanceArn) + .backupRetentionPeriod(backupRetentionPeriod) .kmsKeyId(kmsKeyId) .build(); } @@ -853,6 +876,7 @@ public static ResourceModel.ResourceModelBuilder translateDbInstanceFromSdkBuild .characterSetName(dbInstance.characterSetName()) .copyTagsToSnapshot(dbInstance.copyTagsToSnapshot()) .customIAMInstanceProfile(dbInstance.customIamInstanceProfile()) + .databaseInsightsMode(dbInstance.databaseInsightsModeAsString()) .dBClusterIdentifier(dbInstance.dbClusterIdentifier()) .dBInstanceArn(dbInstance.dbInstanceArn()) .dBInstanceClass(dbInstance.dbInstanceClass()) diff --git a/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/UpdateHandler.java b/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/UpdateHandler.java index a20895e4a..5152abb00 100644 --- a/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/UpdateHandler.java +++ b/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/UpdateHandler.java @@ -1,39 +1,22 @@ package software.amazon.rds.dbinstance; -import java.time.Instant; -import java.util.Collection; -import java.util.Collections; -import java.util.Objects; -import java.util.Optional; -import java.util.function.Function; - -import org.apache.commons.lang3.BooleanUtils; -import org.apache.commons.lang3.ObjectUtils; - import com.amazonaws.util.CollectionUtils; import com.amazonaws.util.StringUtils; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.ObjectUtils; import software.amazon.awssdk.services.ec2.Ec2Client; import software.amazon.awssdk.services.ec2.model.SecurityGroup; import software.amazon.awssdk.services.rds.RdsClient; -import software.amazon.awssdk.services.rds.model.DBCluster; -import software.amazon.awssdk.services.rds.model.DBClusterMember; -import software.amazon.awssdk.services.rds.model.DBInstance; -import software.amazon.awssdk.services.rds.model.DBParameterGroup; -import software.amazon.awssdk.services.rds.model.DbInstanceNotFoundException; -import software.amazon.awssdk.services.rds.model.DescribeDbEngineVersionsResponse; -import software.amazon.awssdk.services.rds.model.DescribeDbParameterGroupsResponse; -import software.amazon.awssdk.services.rds.model.SourceType; +import software.amazon.awssdk.services.rds.model.*; import software.amazon.awssdk.utils.ImmutableMap; import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; -import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; -import software.amazon.cloudformation.proxy.HandlerErrorCode; -import software.amazon.cloudformation.proxy.ProgressEvent; -import software.amazon.cloudformation.proxy.ProxyClient; -import software.amazon.cloudformation.proxy.ResourceHandlerRequest; +import software.amazon.cloudformation.proxy.*; import software.amazon.rds.common.handler.Commons; import software.amazon.rds.common.handler.Events; import software.amazon.rds.common.handler.HandlerConfig; import software.amazon.rds.common.handler.Tagging; +import software.amazon.rds.common.handler.Vpc; +import software.amazon.rds.common.request.RequestValidationException; import software.amazon.rds.common.request.ValidatedRequest; import software.amazon.rds.dbinstance.client.ApiVersion; import software.amazon.rds.dbinstance.client.VersionedProxyClient; @@ -41,6 +24,15 @@ import software.amazon.rds.dbinstance.status.DBParameterGroupStatus; import software.amazon.rds.dbinstance.util.ImmutabilityHelper; import software.amazon.rds.dbinstance.util.ResourceModelHelper; +import software.amazon.rds.dbinstance.validators.AutomaticBackupReplicationValidator; + +import java.time.Instant; +import java.util.Collection; +import java.util.Collections; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; + public class UpdateHandler extends BaseHandlerStd { public UpdateHandler() { @@ -53,6 +45,13 @@ public UpdateHandler(final HandlerConfig config) { final String handlerOperation = "UPDATE"; + @Override + protected void validateRequest(final ResourceHandlerRequest request) throws RequestValidationException { + super.validateRequest(request); + + AutomaticBackupReplicationValidator.validateRequest(request.getDesiredResourceState()); + } + protected ProgressEvent handleRequest( final AmazonWebServicesClientProxy proxy, final ValidatedRequest request, @@ -106,7 +105,7 @@ protected ProgressEvent handleRequest( return ProgressEvent.progress(request.getDesiredResourceState(), callbackContext) .then(progress -> { try { - if(!Objects.equals(request.getDesiredResourceState().getEngineLifecycleSupport(), + if (!Objects.equals(request.getDesiredResourceState().getEngineLifecycleSupport(), request.getPreviousResourceState().getEngineLifecycleSupport()) && !request.getRollback()) { throw new CfnInvalidRequestException("EngineLifecycleSupport cannot be modified."); @@ -156,7 +155,10 @@ protected ProgressEvent handleRequest( progress.getCallbackContext().timestampOnce(RESOURCE_UPDATED_AT, Instant.now()); return versioned(proxy, rdsProxyClient, progress, null, ImmutableMap.of( ApiVersion.V12, (pxy, pcl, prg, tgs) -> updateDbInstanceV12(pxy, request, pcl, prg), - ApiVersion.DEFAULT, (pxy, pcl, prg, tgs) -> updateDbInstance(pxy, request, pcl, prg) + ApiVersion.DEFAULT, (pxy, pcl, prg, tgs) -> { + final DBInstance dbInstance = fetchDBInstance(rdsProxyClient.defaultClient(), progress.getResourceModel()); + return updateDbInstance(pxy, request, pcl, prg, dbInstance); + } )).then(p -> Events.checkFailedEvents( rdsProxyClient.defaultClient(), p.getResourceModel().getDBInstanceIdentifier(), @@ -190,28 +192,34 @@ protected ProgressEvent handleRequest( } } return progress; - }, (m) -> !StringUtils.isNullOrEmpty(callbackContext.getDbInstanceArn()), (v, c) -> {})) - .then(progress -> Commons.execOnce(progress, () -> { - if (ResourceModelHelper.shouldStopAutomaticBackupReplication(request.getPreviousResourceState(), request.getDesiredResourceState())) { - return stopAutomaticBackupReplicationInRegion(callbackContext.getDbInstanceArn(), proxy, progress, rdsProxyClient.defaultClient(), - ResourceModelHelper.getAutomaticBackupReplicationRegion(request.getPreviousResourceState())); - } - return progress;}, - CallbackContext::isAutomaticBackupReplicationStopped, CallbackContext::setAutomaticBackupReplicationStopped)) - .then(progress -> Commons.execOnce(progress, () -> { - if (ResourceModelHelper.shouldStartAutomaticBackupReplication(request.getPreviousResourceState(), request.getDesiredResourceState())) { - return startAutomaticBackupReplicationInRegion( - callbackContext.getDbInstanceArn(), - progress.getResourceModel().getAutomaticBackupReplicationKmsKeyId(), - proxy, - progress, - rdsProxyClient.defaultClient(), - ResourceModelHelper.getAutomaticBackupReplicationRegion(request.getDesiredResourceState()) - ); - } - return progress; - }, - CallbackContext::isAutomaticBackupReplicationStarted, CallbackContext::setAutomaticBackupReplicationStarted)) + }, (m) -> !StringUtils.isNullOrEmpty(callbackContext.getDbInstanceArn()), (v, c) -> { + })) + .then(progress -> { + if (ResourceModelHelper.shouldStopAutomaticBackupReplication(request.getPreviousResourceState(), request.getDesiredResourceState())) { + return Commons.execOnce(progress, () -> stopAutomaticBackupReplicationInRegion( + callbackContext.getDbInstanceArn(), + proxy, + progress, + rdsProxyClient.defaultClient(), + ResourceModelHelper.getAutomaticBackupReplicationRegion(request.getPreviousResourceState())), + CallbackContext::isAutomaticBackupReplicationStopped, CallbackContext::setAutomaticBackupReplicationStopped); + } + return progress; + }) + .then(progress -> { + if (ResourceModelHelper.shouldStartAutomaticBackupReplication(request.getPreviousResourceState(), request.getDesiredResourceState())) { + return Commons.execOnce(progress, () -> startAutomaticBackupReplicationInRegion( + callbackContext.getDbInstanceArn(), + ResourceModelHelper.getAutomaticBackupReplicationRetentionPeriod(request.getDesiredResourceState()), + ResourceModelHelper.getAutomaticBackupReplicationKmsKeyId(request.getDesiredResourceState()), + proxy, + progress, + rdsProxyClient.defaultClient(), + ResourceModelHelper.getAutomaticBackupReplicationRegion(request.getDesiredResourceState())), + CallbackContext::isAutomaticBackupReplicationStarted, CallbackContext::setAutomaticBackupReplicationStarted); + } + return progress; + }) .then(progress -> updateTags(proxy, rdsClient, progress, previousTags, desiredTags)) .then(progress -> { final ResourceModel model = request.getDesiredResourceState(); @@ -236,7 +244,7 @@ private ProgressEvent handleResourceDrift( return ProgressEvent.progress(request.getDesiredResourceState(), callbackContext) .then(progress -> { if (shouldReboot(rdsProxyClient.defaultClient(), progress) || - (isDBClusterMember(progress.getResourceModel()) && shouldRebootCluster(rdsProxyClient.defaultClient(), progress))) { + (DBInstancePredicates.isDBClusterMember(progress.getResourceModel()) && shouldRebootCluster(rdsProxyClient.defaultClient(), progress))) { return rebootAwait(proxy, rdsProxyClient.defaultClient(), progress); } return progress; @@ -244,7 +252,7 @@ private ProgressEvent handleResourceDrift( .then(progress -> awaitDBParameterGroupInSyncStatus(proxy, rdsProxyClient.defaultClient(), progress)) .then(progress -> awaitOptionGroupInSyncStatus(proxy, rdsProxyClient.defaultClient(), progress)) .then(progress -> { - if (isDBClusterMember(progress.getResourceModel())) { + if (DBInstancePredicates.isDBClusterMember(progress.getResourceModel())) { return awaitDBClusterParameterGroup(proxy, rdsProxyClient.defaultClient(), progress); } return progress; @@ -258,8 +266,9 @@ private boolean shouldReboot( ) { try { final DBInstance dbInstance = fetchDBInstance(proxyClient, progress.getResourceModel()); + final boolean applyImmediately = ResourceModelHelper.shouldApplyImmediately(progress.getResourceModel()); if (!CollectionUtils.isNullOrEmpty(dbInstance.dbParameterGroups())) { - return DBParameterGroupStatus.PendingReboot.equalsString(dbInstance.dbParameterGroups().get(0).parameterApplyStatus()); + return applyImmediately && DBParameterGroupStatus.PendingReboot.equalsString(dbInstance.dbParameterGroups().get(0).parameterApplyStatus()); } } catch (DbInstanceNotFoundException e) { return false; @@ -273,10 +282,11 @@ private boolean shouldRebootCluster( ) { final String dbInstanceIdentifier = progress.getResourceModel().getDBInstanceIdentifier(); final DBCluster dbCluster = fetchDBCluster(proxyClient, progress.getResourceModel()); + final boolean applyImmediately = ResourceModelHelper.shouldApplyImmediately(progress.getResourceModel()); if (!CollectionUtils.isNullOrEmpty(dbCluster.dbClusterMembers())) { for (final DBClusterMember member : dbCluster.dbClusterMembers()) { if (dbInstanceIdentifier.equalsIgnoreCase(member.dbInstanceIdentifier())) { - return DBParameterGroupStatus.PendingReboot.equalsString(member.dbClusterParameterGroupStatus()); + return applyImmediately && DBParameterGroupStatus.PendingReboot.equalsString(member.dbClusterParameterGroupStatus()); } } } @@ -372,9 +382,16 @@ private ProgressEvent setParameterGroupName( private boolean shouldSetDefaultVpcId(final ResourceHandlerRequest request) { // DBCluster member instances inherit default vpc security groups from the corresponding umbrella cluster - return !isDBClusterMember(request.getDesiredResourceState()) && - !isRdsCustomOracleInstance(request.getDesiredResourceState()) && - CollectionUtils.isNullOrEmpty(request.getDesiredResourceState().getVPCSecurityGroups()); + return canInstanceBeSetDefaultVpc(request) && + Vpc.shouldSetDefaultVpcId(request.getPreviousResourceState().getVPCSecurityGroups(), request.getDesiredResourceState().getVPCSecurityGroups()); + } + + /** + * There are some types of databases that can never be set with a default VPC. + */ + private boolean canInstanceBeSetDefaultVpc(final ResourceHandlerRequest request) { + return !DBInstancePredicates.isDBClusterMember(request.getDesiredResourceState()) && + !DBInstancePredicates.isRdsCustomOracleInstance(request.getDesiredResourceState()); } private boolean shouldUnsetMaxAllocatedStorage(final ResourceHandlerRequest request) { diff --git a/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/client/RdsClientProvider.java b/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/client/RdsClientProvider.java index 83031b3f8..1c1743f78 100644 --- a/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/client/RdsClientProvider.java +++ b/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/client/RdsClientProvider.java @@ -17,6 +17,7 @@ import software.amazon.rds.common.client.BaseSdkClientProvider; import software.amazon.rds.common.client.RdsUserAgentProvider; + public class RdsClientProvider extends BaseSdkClientProvider { public static final String VERSION_QUERY_PARAM = "Version"; diff --git a/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/util/ResourceModelHelper.java b/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/util/ResourceModelHelper.java index e64b9164d..40fa271e2 100644 --- a/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/util/ResourceModelHelper.java +++ b/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/util/ResourceModelHelper.java @@ -1,5 +1,6 @@ package software.amazon.rds.dbinstance.util; +import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -127,6 +128,12 @@ public static boolean shouldReboot(final ResourceModel model) { return StringUtils.hasValue(model.getDBParameterGroupName()); } + public static boolean shouldApplyImmediately(final ResourceModel model) { + Boolean applyImmediately = model.getApplyImmediately(); + // default to true + return applyImmediately == null || applyImmediately; + } + public static Boolean getDefaultMultiAzForEngine(final String engine) { if (SQLSERVER_ENGINES_WITH_MIRRORING.contains(engine)) { return null; @@ -152,30 +159,83 @@ public static boolean isReadReplicaPromotion(final ResourceModel previous, final public static boolean shouldStartAutomaticBackupReplication(final ResourceModel previous, final ResourceModel desired) { final String previousRegion = getAutomaticBackupReplicationRegion(previous); final String desiredRegion = getAutomaticBackupReplicationRegion(desired); - return !StringUtils.isNullOrEmpty(desiredRegion) && !desiredRegion.equalsIgnoreCase(previousRegion); + + if (StringUtils.isNullOrEmpty(desiredRegion)) { + return false; + } + + if (StringUtils.isNullOrEmpty(previousRegion)) { + return true; + } + + return hasAutomaticBackupReplicationChanged(previous, desired); } public static boolean shouldStopAutomaticBackupReplication(final ResourceModel previous, final ResourceModel desired) { final String previousRegion = getAutomaticBackupReplicationRegion(previous); final String desiredRegion = getAutomaticBackupReplicationRegion(desired); - return !StringUtils.isNullOrEmpty(previousRegion) && !previousRegion.equalsIgnoreCase(desiredRegion); + + // if region not provided and previous region was, then we stop replication + if (StringUtils.isNullOrEmpty(previousRegion)) { + return false; + } + + if (StringUtils.isNullOrEmpty(desiredRegion)) { + return true; + } + + return hasAutomaticBackupReplicationChanged(previous, desired); + } + + private static boolean hasBackupRetentionPeriodChangedWithNoOverride(final ResourceModel previous, final ResourceModel desired) { + return (!Objects.equals(getBackupRetentionPeriod(previous), getBackupRetentionPeriod(desired))) && (getBackupRetentionPeriod(desired) != null && getAutomaticBackupReplicationRetentionPeriod(desired) == null); } - public static int getBackupRetentionPeriod(final ResourceModel model) { + private static boolean hasAutomaticBackupReplicationChanged(final ResourceModel previous, final ResourceModel desired) { + // we only want to use change status of BackupRetentionPeriod if AutomaticBackupReplicationRetentionPeriod is unset / null + if (hasBackupRetentionPeriodChangedWithNoOverride(previous, desired)) { + return true; + } + + // check replication parameters for changes + boolean crossRegionRetentionChanged = !Objects.equals(getAutomaticBackupReplicationRetentionPeriod(previous), getAutomaticBackupReplicationRetentionPeriod(desired)); + boolean regionChanged = !Objects.equals(getAutomaticBackupReplicationRegion(previous), getAutomaticBackupReplicationRegion(desired)); + boolean kmsKeyIdChanged = !Objects.equals(getAutomaticBackupReplicationKmsKeyId(previous), getAutomaticBackupReplicationKmsKeyId(desired)); + + // if any replication parameters have changed + return crossRegionRetentionChanged || regionChanged || kmsKeyIdChanged; + } + + + public static Integer getBackupRetentionPeriod(final ResourceModel model) { if (model == null) { - return 0; + return null; } - return Optional.ofNullable(model.getBackupRetentionPeriod()).orElse(0); + + return model.getBackupRetentionPeriod(); } public static String getAutomaticBackupReplicationRegion(final ResourceModel model) { if (model == null) { return null; } - if (getBackupRetentionPeriod(model) == 0) { + + return model.getAutomaticBackupReplicationRegion(); + } + + public static Integer getAutomaticBackupReplicationRetentionPeriod(final ResourceModel model) { + if (model == null) { return null; } - return model.getAutomaticBackupReplicationRegion(); + + return model.getAutomaticBackupReplicationRetentionPeriod(); + } + + public static String getAutomaticBackupReplicationKmsKeyId(final ResourceModel model) { + if (model == null) { + return null; + } + return model.getAutomaticBackupReplicationKmsKeyId(); } public static boolean isOracleCDBEngine(final String engine) { diff --git a/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/validators/AutomaticBackupReplicationValidator.java b/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/validators/AutomaticBackupReplicationValidator.java new file mode 100644 index 000000000..bd5370bd6 --- /dev/null +++ b/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/validators/AutomaticBackupReplicationValidator.java @@ -0,0 +1,23 @@ +package software.amazon.rds.dbinstance.validators; + +import com.amazonaws.util.StringUtils; +import software.amazon.rds.common.request.RequestValidationException; +import software.amazon.rds.dbinstance.ResourceModel; +import software.amazon.rds.dbinstance.util.ResourceModelHelper; + +public class AutomaticBackupReplicationValidator { + public static void validateRequest(final ResourceModel model) throws RequestValidationException { + if (StringUtils.isNullOrEmpty(ResourceModelHelper.getAutomaticBackupReplicationRegion(model))) { + if (!StringUtils.isNullOrEmpty(ResourceModelHelper.getAutomaticBackupReplicationKmsKeyId(model) )) { + throw new RequestValidationException("You must specify the AutomaticBackupReplicationRegion parameter when setting the AutomaticBackupReplicationKmsKeyId parameter."); + } + if (ResourceModelHelper.getAutomaticBackupReplicationRetentionPeriod(model) != null) { + throw new RequestValidationException("You must specify the AutomaticBackupReplicationRegion parameter when setting the AutomaticBackupReplicationRetentionPeriod parameter."); + } + } else { + if (ResourceModelHelper.getBackupRetentionPeriod(model) != null && ResourceModelHelper.getBackupRetentionPeriod(model) == 0) { + throw new RequestValidationException("AutomaticBackupReplicationRegion cannot be specified when BackupRetentionPeriod is 0."); + } + } + } +} diff --git a/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/validators/OracleCustomSystemId.java b/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/validators/OracleCustomSystemId.java new file mode 100644 index 000000000..eded785cd --- /dev/null +++ b/aws-rds-dbinstance/src/main/java/software/amazon/rds/dbinstance/validators/OracleCustomSystemId.java @@ -0,0 +1,21 @@ +package software.amazon.rds.dbinstance.validators; + +import software.amazon.rds.common.request.RequestValidationException; +import software.amazon.rds.dbinstance.ResourceModel; +import software.amazon.rds.dbinstance.util.ResourceModelHelper; + +public class OracleCustomSystemId { + public static void validateRequest(final ResourceModel model) throws RequestValidationException { + if (ResourceModelHelper.isRestoreToPointInTime(model) && model.getDBSystemId() != null) { + throw new RequestValidationException("The DBSystemId parameter cannot be specified when you create a DB instance from a point-in-time restore."); + } + + if (ResourceModelHelper.isDBInstanceReadReplica(model) && model.getDBSystemId() != null) { + throw new RequestValidationException("The DBSystemId parameter cannot be specified when you create a read replica."); + } + + if (ResourceModelHelper.isRestoreFromSnapshot(model) && model.getDBSystemId() != null) { + throw new RequestValidationException("The DBSystemId parameter cannot be specified when you create a DB instance from a snapshot restore."); + } + } +} diff --git a/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/AbstractHandlerTest.java b/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/AbstractHandlerTest.java index cbb49e560..2a8723067 100644 --- a/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/AbstractHandlerTest.java +++ b/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/AbstractHandlerTest.java @@ -9,8 +9,12 @@ import java.util.LinkedList; import java.util.List; import java.util.function.Supplier; +import java.util.stream.Stream; import org.json.JSONObject; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.mockito.stubbing.OngoingStubbing; @@ -42,6 +46,7 @@ import software.amazon.cloudformation.proxy.ProxyClient; import software.amazon.cloudformation.proxy.ResourceHandlerRequest; import software.amazon.cloudformation.proxy.delay.Constant; +import software.amazon.rds.common.error.ErrorCode; import software.amazon.rds.common.handler.Tagging; import software.amazon.rds.common.logging.RequestLogger; import software.amazon.rds.common.printer.FilteredJsonPrinter; @@ -95,6 +100,7 @@ public abstract class AbstractHandlerTest extends AbstractTestBase verify() { protected static String getAutomaticBackupArn(final String region) { return String.format("arn:aws:rds:%s:1234567890:auto-backup:ab-test", region); } + + static class ThrottleExceptionArgumentsProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext extensionContext) throws Exception { + return Stream.of( + Arguments.of(ErrorCode.ThrottlingException), + Arguments.of(ErrorCode.Throttling) + ); + } + } } diff --git a/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/BaseHandlerStdTest.java b/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/BaseHandlerStdTest.java index 05afa0f78..eee44aead 100644 --- a/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/BaseHandlerStdTest.java +++ b/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/BaseHandlerStdTest.java @@ -1,17 +1,24 @@ package software.amazon.rds.dbinstance; -import java.time.Instant; -import java.util.Collections; - import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; import software.amazon.awssdk.services.ec2.Ec2Client; import software.amazon.awssdk.services.rds.RdsClient; +import software.amazon.awssdk.services.rds.model.DBCluster; +import software.amazon.awssdk.services.rds.model.DBClusterMember; import software.amazon.awssdk.services.rds.model.DBInstance; import software.amazon.awssdk.services.rds.model.DBInstanceStatusInfo; import software.amazon.awssdk.services.rds.model.DBParameterGroupStatus; +import software.amazon.awssdk.services.rds.model.DescribeDbClustersRequest; +import software.amazon.awssdk.services.rds.model.DescribeDbClustersResponse; +import software.amazon.awssdk.services.rds.model.DescribeDbInstancesRequest; +import software.amazon.awssdk.services.rds.model.DescribeDbInstancesResponse; import software.amazon.awssdk.services.rds.model.DomainMembership; import software.amazon.awssdk.services.rds.model.MasterUserSecret; import software.amazon.awssdk.services.rds.model.PendingCloudwatchLogsExports; @@ -20,18 +27,37 @@ import software.amazon.awssdk.services.rds.model.VpcSecurityGroupMembership; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.ProxyClient; import software.amazon.cloudformation.proxy.ResourceHandlerRequest; import software.amazon.rds.common.handler.HandlerConfig; +import software.amazon.rds.common.logging.RequestLogger; import software.amazon.rds.common.request.RequestValidationException; import software.amazon.rds.common.request.ValidatedRequest; import software.amazon.rds.dbinstance.client.VersionedProxyClient; +import java.time.Duration; +import java.time.Instant; +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static software.amazon.rds.dbinstance.AbstractHandlerTest.MOCK_CREDENTIALS; +import static software.amazon.rds.dbinstance.AbstractHandlerTest.logger; +import static software.amazon.rds.dbinstance.AbstractHandlerTest.mockProxy; +import static software.amazon.rds.dbinstance.status.DBParameterGroupStatus.Applying; +import static software.amazon.rds.dbinstance.status.DBParameterGroupStatus.InSync; +import static software.amazon.rds.dbinstance.status.DBParameterGroupStatus.PendingReboot; + class BaseHandlerStdTest { static class TestBaseHandlerStd extends BaseHandlerStd { public TestBaseHandlerStd(HandlerConfig config) { super(config); + requestLogger= new RequestLogger(logger, ResourceHandlerRequest.builder().build(), null); } @Override @@ -48,21 +74,27 @@ protected ProgressEvent handleRequest( private TestBaseHandlerStd handler; + @Mock + private ProxyClient rdsProxyV12; + @BeforeEach public void setUp() { handler = new TestBaseHandlerStd(null); + AmazonWebServicesClientProxy proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); + RdsClient rdsClientV12 = mock(RdsClient.class); + rdsProxyV12 = mockProxy(proxy, rdsClientV12); } @Test void isDomainMembershipsJoined_NullDomainMembershipReturnsTrue() { - Assertions.assertThat(handler.isDomainMembershipsJoined( + Assertions.assertThat(DBInstancePredicates.isDomainMembershipsJoined( DBInstance.builder().build() )).isTrue(); } @Test void isDomainMembershipsJoined_EmptyDomainMembershipReturnsTrue() { - Assertions.assertThat(handler.isDomainMembershipsJoined( + Assertions.assertThat(DBInstancePredicates.isDomainMembershipsJoined( DBInstance.builder() .domainMemberships(Collections.emptyList()) .build() @@ -71,7 +103,7 @@ void isDomainMembershipsJoined_EmptyDomainMembershipReturnsTrue() { @Test void isDomainMembershipsJoined_NonEmptyListJoinedAndKerberosReturnsTrue() { - Assertions.assertThat(handler.isDomainMembershipsJoined( + Assertions.assertThat(DBInstancePredicates.isDomainMembershipsJoined( DBInstance.builder() .domainMemberships( DomainMembership.builder().status("joined").build(), @@ -83,7 +115,7 @@ void isDomainMembershipsJoined_NonEmptyListJoinedAndKerberosReturnsTrue() { @Test void isDomainMembershipsJoined_NonEmptyListJoinedAndKerberosAndAnythingElseReturnsFalse() { - Assertions.assertThat(handler.isDomainMembershipsJoined( + Assertions.assertThat(DBInstancePredicates.isDomainMembershipsJoined( DBInstance.builder() .domainMemberships( DomainMembership.builder().status("joined").build(), @@ -96,14 +128,14 @@ void isDomainMembershipsJoined_NonEmptyListJoinedAndKerberosAndAnythingElseRetur @Test void isVpcSecurityGroupsActive_NullVpcSecurityGroupsReturnsTrue() { - Assertions.assertThat(handler.isVpcSecurityGroupsActive( + Assertions.assertThat(DBInstancePredicates.isVpcSecurityGroupsActive( DBInstance.builder().build() )).isTrue(); } @Test void isVpcSecurityGroupsActive_EmptyVpcSecurityGroupsReturnsTrue() { - Assertions.assertThat(handler.isVpcSecurityGroupsActive( + Assertions.assertThat(DBInstancePredicates.isVpcSecurityGroupsActive( DBInstance.builder() .vpcSecurityGroups(Collections.emptyList()) .build() @@ -112,7 +144,7 @@ void isVpcSecurityGroupsActive_EmptyVpcSecurityGroupsReturnsTrue() { @Test void isVpcSecurityGroupsActive_NonEmptyVpcSecurityGroupsActiveReturnsTrue() { - Assertions.assertThat(handler.isVpcSecurityGroupsActive( + Assertions.assertThat(DBInstancePredicates.isVpcSecurityGroupsActive( DBInstance.builder() .vpcSecurityGroups( VpcSecurityGroupMembership.builder().status("active").build(), @@ -124,7 +156,7 @@ void isVpcSecurityGroupsActive_NonEmptyVpcSecurityGroupsActiveReturnsTrue() { @Test void isVpcSecurityGroupsActive_NonEmptyVpcSecurityGroupsNotActiveReturnsFalse() { - Assertions.assertThat(handler.isVpcSecurityGroupsActive( + Assertions.assertThat(DBInstancePredicates.isVpcSecurityGroupsActive( DBInstance.builder() .vpcSecurityGroups( VpcSecurityGroupMembership.builder().status("active").build(), @@ -136,14 +168,14 @@ void isVpcSecurityGroupsActive_NonEmptyVpcSecurityGroupsNotActiveReturnsFalse() @Test void isNoPendingChanges_NullPendingChangesReturnsTrue() { - Assertions.assertThat(handler.isNoPendingChanges( + Assertions.assertThat(DBInstancePredicates.isNoPendingChanges( DBInstance.builder().build() )).isTrue(); } @Test void isNoPendingChanges_EmptyPendingChangesReturnsTrue() { - Assertions.assertThat(handler.isNoPendingChanges( + Assertions.assertThat(DBInstancePredicates.isNoPendingChanges( DBInstance.builder() .pendingModifiedValues(PendingModifiedValues.builder().build()) .build() @@ -152,7 +184,7 @@ void isNoPendingChanges_EmptyPendingChangesReturnsTrue() { @Test void isNoPendingChanges_NonEmptyAllocatedStorageReturnsFalse() { - Assertions.assertThat(handler.isNoPendingChanges( + Assertions.assertThat(DBInstancePredicates.isNoPendingChanges( DBInstance.builder() .pendingModifiedValues( PendingModifiedValues.builder() @@ -164,7 +196,7 @@ void isNoPendingChanges_NonEmptyAllocatedStorageReturnsFalse() { @Test void isCaCertificateChangesApplied_NonEmptyCACertificateIdentifierReturnsFalse_WhenCertificateRotationRestartIsTrue() { - Assertions.assertThat(handler.isCaCertificateChangesApplied( + Assertions.assertThat(DBInstancePredicates.isCaCertificateChangesApplied( DBInstance.builder() .pendingModifiedValues( PendingModifiedValues.builder() @@ -177,7 +209,7 @@ void isCaCertificateChangesApplied_NonEmptyCACertificateIdentifierReturnsFalse_W @Test void isCaCertificateChangesApplied_NonEmptyCACertificateIdentifierReturnsTrue_WhenCertificateRotationRestartIsFalse() { - Assertions.assertThat(handler.isCaCertificateChangesApplied( + Assertions.assertThat(DBInstancePredicates.isCaCertificateChangesApplied( DBInstance.builder() .pendingModifiedValues( PendingModifiedValues.builder() @@ -190,7 +222,7 @@ void isCaCertificateChangesApplied_NonEmptyCACertificateIdentifierReturnsTrue_Wh @Test void isNoPendingChanges_NonEmptyMasterUserPasswordReturnsFalse() { - Assertions.assertThat(handler.isNoPendingChanges( + Assertions.assertThat(DBInstancePredicates.isNoPendingChanges( DBInstance.builder() .pendingModifiedValues( PendingModifiedValues.builder() @@ -202,7 +234,7 @@ void isNoPendingChanges_NonEmptyMasterUserPasswordReturnsFalse() { @Test void isNoPendingChanges_NonEmptyBackupRetentionPeriodReturnsFalse() { - Assertions.assertThat(handler.isNoPendingChanges( + Assertions.assertThat(DBInstancePredicates.isNoPendingChanges( DBInstance.builder() .pendingModifiedValues( PendingModifiedValues.builder() @@ -214,7 +246,7 @@ void isNoPendingChanges_NonEmptyBackupRetentionPeriodReturnsFalse() { @Test void isNoPendingChanges_NonEmptyMultiAZReturnsFalse() { - Assertions.assertThat(handler.isNoPendingChanges( + Assertions.assertThat(DBInstancePredicates.isNoPendingChanges( DBInstance.builder() .pendingModifiedValues( PendingModifiedValues.builder() @@ -226,7 +258,7 @@ void isNoPendingChanges_NonEmptyMultiAZReturnsFalse() { @Test void isNoPendingChanges_NonEmptyEngineVersionReturnsFalse() { - Assertions.assertThat(handler.isNoPendingChanges( + Assertions.assertThat(DBInstancePredicates.isNoPendingChanges( DBInstance.builder() .pendingModifiedValues( PendingModifiedValues.builder() @@ -238,7 +270,7 @@ void isNoPendingChanges_NonEmptyEngineVersionReturnsFalse() { @Test void isNoPendingChanges_NonEmptyIopsReturnsFalse() { - Assertions.assertThat(handler.isNoPendingChanges( + Assertions.assertThat(DBInstancePredicates.isNoPendingChanges( DBInstance.builder() .pendingModifiedValues( PendingModifiedValues.builder() @@ -250,7 +282,7 @@ void isNoPendingChanges_NonEmptyIopsReturnsFalse() { @Test void isNoPendingChanges_NonEmptyDBInstanceIdentifierReturnsFalse() { - Assertions.assertThat(handler.isNoPendingChanges( + Assertions.assertThat(DBInstancePredicates.isNoPendingChanges( DBInstance.builder() .pendingModifiedValues( PendingModifiedValues.builder() @@ -262,7 +294,7 @@ void isNoPendingChanges_NonEmptyDBInstanceIdentifierReturnsFalse() { @Test void isNoPendingChanges_NonEmptyLicenseModelReturnsFalse() { - Assertions.assertThat(handler.isNoPendingChanges( + Assertions.assertThat(DBInstancePredicates.isNoPendingChanges( DBInstance.builder() .pendingModifiedValues( PendingModifiedValues.builder() @@ -274,7 +306,7 @@ void isNoPendingChanges_NonEmptyLicenseModelReturnsFalse() { @Test void isNoPendingChanges_NonEmptyStorageTypeReturnsFalse() { - Assertions.assertThat(handler.isNoPendingChanges( + Assertions.assertThat(DBInstancePredicates.isNoPendingChanges( DBInstance.builder() .pendingModifiedValues( PendingModifiedValues.builder() @@ -286,7 +318,7 @@ void isNoPendingChanges_NonEmptyStorageTypeReturnsFalse() { @Test void isNoPendingChanges_NonEmptyDBSubnetGroupNameReturnsFalse() { - Assertions.assertThat(handler.isNoPendingChanges( + Assertions.assertThat(DBInstancePredicates.isNoPendingChanges( DBInstance.builder() .pendingModifiedValues( PendingModifiedValues.builder() @@ -298,7 +330,7 @@ void isNoPendingChanges_NonEmptyDBSubnetGroupNameReturnsFalse() { @Test void isNoPendingChanges_NonEmptyPendingCloudWatchLogsExportsReturnsFalse() { - Assertions.assertThat(handler.isNoPendingChanges( + Assertions.assertThat(DBInstancePredicates.isNoPendingChanges( DBInstance.builder() .pendingModifiedValues( PendingModifiedValues.builder() @@ -310,7 +342,7 @@ void isNoPendingChanges_NonEmptyPendingCloudWatchLogsExportsReturnsFalse() { @Test void isNoPendingChanges_EmptyProcessorFeaturesReturnsTrue() { - Assertions.assertThat(handler.isNoPendingChanges( + Assertions.assertThat(DBInstancePredicates.isNoPendingChanges( DBInstance.builder() .pendingModifiedValues( PendingModifiedValues.builder() @@ -322,7 +354,7 @@ void isNoPendingChanges_EmptyProcessorFeaturesReturnsTrue() { @Test void isNoPendingChanges_NonEmptyIamDatabaseAutenticationEnabledReturnsFalse() { - Assertions.assertThat(handler.isNoPendingChanges( + Assertions.assertThat(DBInstancePredicates.isNoPendingChanges( DBInstance.builder() .pendingModifiedValues( PendingModifiedValues.builder() @@ -334,7 +366,7 @@ void isNoPendingChanges_NonEmptyIamDatabaseAutenticationEnabledReturnsFalse() { @Test void isNoPendingChanges_NonEmptyAutomationModeReturnsFalse() { - Assertions.assertThat(handler.isNoPendingChanges( + Assertions.assertThat(DBInstancePredicates.isNoPendingChanges( DBInstance.builder() .pendingModifiedValues( PendingModifiedValues.builder() @@ -346,7 +378,7 @@ void isNoPendingChanges_NonEmptyAutomationModeReturnsFalse() { @Test void isNoPendingChanges_NonEmptyResumeFullAutomationModeTimeReturnsFalse() { - Assertions.assertThat(handler.isNoPendingChanges( + Assertions.assertThat(DBInstancePredicates.isNoPendingChanges( DBInstance.builder() .pendingModifiedValues( PendingModifiedValues.builder() @@ -358,7 +390,7 @@ void isNoPendingChanges_NonEmptyResumeFullAutomationModeTimeReturnsFalse() { @Test void isNoPendingChanges_NonEmptyReturnsFalse() { - Assertions.assertThat(handler.isNoPendingChanges( + Assertions.assertThat(DBInstancePredicates.isNoPendingChanges( DBInstance.builder() .pendingModifiedValues( PendingModifiedValues.builder() @@ -370,21 +402,21 @@ void isNoPendingChanges_NonEmptyReturnsFalse() { @Test void isDBParameterGroupSyncComplete_NullParameterGroupsReturnsTrue() { - Assertions.assertThat(handler.isDBParameterGroupNotApplying( + Assertions.assertThat(DBInstancePredicates.isDBParameterGroupNotApplying( DBInstance.builder().build() )).isTrue(); } @Test void isDBParameterGroupSyncComplete_EmptyParameterGroupsReturnsTrue() { - Assertions.assertThat(handler.isDBParameterGroupNotApplying( + Assertions.assertThat(DBInstancePredicates.isDBParameterGroupNotApplying( DBInstance.builder().dbParameterGroups(Collections.emptyList()).build() )).isTrue(); } @Test void isDBParameterGroupSyncComplete_NonEmptyParameterGroupsInSyncReturnsTrue() { - Assertions.assertThat(handler.isDBParameterGroupNotApplying( + Assertions.assertThat(DBInstancePredicates.isDBParameterGroupNotApplying( DBInstance.builder() .dbParameterGroups( DBParameterGroupStatus.builder().parameterApplyStatus("in-sync").build(), @@ -396,7 +428,7 @@ void isDBParameterGroupSyncComplete_NonEmptyParameterGroupsInSyncReturnsTrue() { @Test void isDBParameterGroupSyncComplete_NonEmptyParameterGroupsApplyingReturnsFalse() { - Assertions.assertThat(handler.isDBParameterGroupNotApplying( + Assertions.assertThat(DBInstancePredicates.isDBParameterGroupNotApplying( DBInstance.builder() .dbParameterGroups( DBParameterGroupStatus.builder().parameterApplyStatus("in-sync").build(), @@ -408,21 +440,21 @@ void isDBParameterGroupSyncComplete_NonEmptyParameterGroupsApplyingReturnsFalse( @Test void isReplicationComplete_NullStatusInfoReturnsTrue() { - Assertions.assertThat(handler.isReplicationComplete( + Assertions.assertThat(DBInstancePredicates.isReplicationComplete( DBInstance.builder().build() )).isTrue(); } @Test void isReplicationComplete_EmptyStatusInfoReturnsTrue() { - Assertions.assertThat(handler.isReplicationComplete( + Assertions.assertThat(DBInstancePredicates.isReplicationComplete( DBInstance.builder().statusInfos(Collections.emptyList()).build() )).isTrue(); } @Test void isReplicationComplete_NonEmptyListNoReadReplicaInfoReturnsTrue() { - Assertions.assertThat(handler.isReplicationComplete( + Assertions.assertThat(DBInstancePredicates.isReplicationComplete( DBInstance.builder() .statusInfos(DBInstanceStatusInfo.builder() .statusType("something else") @@ -434,7 +466,7 @@ void isReplicationComplete_NonEmptyListNoReadReplicaInfoReturnsTrue() { @Test void isReplicationComplete_NonEmptyListContainingReplicaInfoReplicatingReturnsTrue() { - Assertions.assertThat(handler.isReplicationComplete( + Assertions.assertThat(DBInstancePredicates.isReplicationComplete( DBInstance.builder() .statusInfos( DBInstanceStatusInfo.builder() @@ -452,7 +484,7 @@ void isReplicationComplete_NonEmptyListContainingReplicaInfoReplicatingReturnsTr @Test void isReplicationComplete_NonEmptyListContainingReplicaInfoNotReplicatingReturnsFalse() { - Assertions.assertThat(handler.isReplicationComplete( + Assertions.assertThat(DBInstancePredicates.isReplicationComplete( DBInstance.builder() .statusInfos( DBInstanceStatusInfo.builder() @@ -470,7 +502,7 @@ void isReplicationComplete_NonEmptyListContainingReplicaInfoNotReplicatingReturn @Test void isMasterUserSecretStabilized_masterUserSecretIsNull() { - Assertions.assertThat(handler.isMasterUserSecretStabilized( + Assertions.assertThat(DBInstancePredicates.isMasterUserSecretStabilized( DBInstance.builder() .build() )).isTrue(); @@ -478,7 +510,7 @@ void isMasterUserSecretStabilized_masterUserSecretIsNull() { @Test void isMasterUserSecretStabilized_masterUserSecretStatusActive() { - Assertions.assertThat(handler.isMasterUserSecretStabilized( + Assertions.assertThat(DBInstancePredicates.isMasterUserSecretStabilized( DBInstance.builder() .masterUserSecret(MasterUserSecret.builder() .secretStatus("Active") @@ -489,7 +521,7 @@ void isMasterUserSecretStabilized_masterUserSecretStatusActive() { @Test void isMasterUserSecretStabilized_masterUserSecretStatusCreating() { - Assertions.assertThat(handler.isMasterUserSecretStabilized( + Assertions.assertThat(DBInstancePredicates.isMasterUserSecretStabilized( DBInstance.builder() .masterUserSecret(MasterUserSecret.builder() .secretStatus("Creating") @@ -519,4 +551,86 @@ void validateRequest_UnknownRegionIsRejected() { handler.validateRequest(request); }); } + + private static Stream DBClusterParameterGroupTestCases() { + return Stream.of( + // Test cases in the format: paramStatus, applyImmediately, expectedStabilizationState + // given the DBParameterGroup status and applyImmediately flag, it determines whether the resource should stabilize + Arguments.of(Applying.toString(), false, false), + Arguments.of(Applying.toString(), true, false), + Arguments.of(InSync.toString(), false, true), + Arguments.of(InSync.toString(), true, true), + Arguments.of(PendingReboot.toString(), false, true), + Arguments.of(PendingReboot.toString(), true, false) + ); + } + + @ParameterizedTest() + @MethodSource("DBClusterParameterGroupTestCases") + void isDBClusterParameterGroupStabilizedTests(String paramStatus, Boolean applyImmediately, boolean expectedStabilizationState) { + String dbIdentifier = "testDb"; + + var dbClusterWithMember = DBCluster.builder() + .dbClusterMembers( + (DBClusterMember.builder() + .dbInstanceIdentifier(dbIdentifier) + .dbClusterParameterGroupStatus(paramStatus) + .build()) + ).build(); + + final ResourceModel model = ResourceModel.builder() + .dBInstanceIdentifier(dbIdentifier) + .applyImmediately(applyImmediately) + .build(); + + when(rdsProxyV12.client().describeDBClusters(any(DescribeDbClustersRequest.class))) + .thenReturn(DescribeDbClustersResponse.builder() + .dbClusters(dbClusterWithMember) + .build()); + + + boolean actual = handler.isDBClusterParameterGroupStabilized(rdsProxyV12, model); + + Assertions.assertThat(actual).isEqualTo(expectedStabilizationState); + } + + private static Stream DBParameterGroupTestCases() { + return Stream.of( + // Test cases in the format: paramStatus, applyImmediately, expectedStabilizationState + // given the DBParameterGroup status and applyImmediately flag, it determines whether the resource should stabilize + Arguments.of(Applying.toString(), false, false), + Arguments.of(Applying.toString(), true, false), + Arguments.of(InSync.toString(), false, true), + Arguments.of(InSync.toString(), true, true), + Arguments.of(PendingReboot.toString(), false, true), + Arguments.of(PendingReboot.toString(), true, false) + ); + } + + @ParameterizedTest() + @MethodSource("DBParameterGroupTestCases") + void isDBParameterGroupStabilizedTests(String paramStatus, Boolean applyImmediately, boolean expectedStabilizationState) { + String dbIdentifier = "testDb"; + + var dbInstance = DBInstance.builder() + .dbParameterGroups(DBParameterGroupStatus.builder() + .dbParameterGroupName("test") + .parameterApplyStatus(paramStatus) + .build()) + .build(); + + final ResourceModel model = ResourceModel.builder() + .dBInstanceIdentifier(dbIdentifier) + .applyImmediately(applyImmediately) + .build(); + + when(rdsProxyV12.client().describeDBInstances(any(DescribeDbInstancesRequest.class))) + .thenReturn(DescribeDbInstancesResponse.builder() + .dbInstances(List.of(dbInstance)) + .build()); + + boolean actual = handler.isDBParameterGroupStabilized(rdsProxyV12, model); + + Assertions.assertThat(actual).isEqualTo(expectedStabilizationState); + } } diff --git a/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/CreateHandlerTest.java b/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/CreateHandlerTest.java index d0b2c1a6b..66d50f7f1 100644 --- a/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/CreateHandlerTest.java +++ b/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/CreateHandlerTest.java @@ -97,6 +97,7 @@ import software.amazon.rds.common.error.ErrorCode; import software.amazon.rds.common.handler.HandlerConfig; import software.amazon.rds.common.handler.Tagging; +import software.amazon.rds.common.util.IdempotencyHelper; import software.amazon.rds.dbinstance.status.DomainMembershipStatus; import software.amazon.rds.dbinstance.status.OptionGroupStatus; import software.amazon.rds.test.common.core.HandlerName; @@ -163,6 +164,7 @@ public void setup() { rdsProxyV12 = mockProxy(proxy, rdsClientV12); ec2Proxy = mockProxy(proxy, ec2Client); expectServiceInvocation = true; + IdempotencyHelper.setBypass(true); } @AfterEach @@ -1344,18 +1346,16 @@ public void handleRequest_CreateDBInstance_OptionGroupInTerminalState() { final CallbackContext context = new CallbackContext(); context.setCreated(false); - Assertions.assertThatThrownBy(() -> { - test_handleRequest_base( - context, - () -> DB_INSTANCE_ACTIVE.toBuilder() - .optionGroupMemberships(OptionGroupMembership.builder() - .status(OptionGroupStatus.Failed.toString()) - .optionGroupName(OPTION_GROUP_NAME_MYSQL_DEFAULT) - .build()).build(), - () -> RESOURCE_MODEL_BLDR().build(), - expectFailed(HandlerErrorCode.NotStabilized) - ); - }).isInstanceOf(CfnNotStabilizedException.class); + test_handleRequest_base( + context, + () -> DB_INSTANCE_ACTIVE.toBuilder() + .optionGroupMemberships(OptionGroupMembership.builder() + .status(OptionGroupStatus.Failed.toString()) + .optionGroupName(OPTION_GROUP_NAME_MYSQL_DEFAULT) + .build()).build(), + () -> RESOURCE_MODEL_BLDR().build(), + expectFailed(HandlerErrorCode.NotStabilized) + ); verify(rdsProxy.client(), times(1)).createDBInstance(any(CreateDbInstanceRequest.class)); } @@ -1365,18 +1365,16 @@ public void handleRequest_CreateDBInstance_DomainMembershipInTerminalState() { final CallbackContext context = new CallbackContext(); context.setCreated(false); - Assertions.assertThatThrownBy(() -> { - test_handleRequest_base( - context, - () -> DB_INSTANCE_ACTIVE.toBuilder() - .domainMemberships(DomainMembership.builder() - .status(DomainMembershipStatus.Failed.toString()) - .domain("domain") - .build()).build(), - () -> RESOURCE_MODEL_BLDR().build(), - expectFailed(HandlerErrorCode.NotStabilized) - ); - }).isInstanceOf(CfnNotStabilizedException.class); + test_handleRequest_base( + context, + () -> DB_INSTANCE_ACTIVE.toBuilder() + .domainMemberships(DomainMembership.builder() + .status(DomainMembershipStatus.Failed.toString()) + .domain("domain") + .build()).build(), + () -> RESOURCE_MODEL_BLDR().build(), + expectFailed(HandlerErrorCode.NotStabilized) + ); verify(rdsProxy.client(), times(1)).createDBInstance(any(CreateDbInstanceRequest.class)); } @@ -1629,7 +1627,6 @@ public Stream provideArguments(ExtensionContext extensionCo Arguments.of(ErrorCode.DBSubnetGroupNotAllowedFault, HandlerErrorCode.InvalidRequest), Arguments.of(ErrorCode.InvalidParameterCombination, HandlerErrorCode.InvalidRequest), Arguments.of(ErrorCode.StorageTypeNotSupportedFault, HandlerErrorCode.InvalidRequest), - Arguments.of(ErrorCode.ThrottlingException, HandlerErrorCode.Throttling), // Put exception classes below Arguments.of(AuthorizationNotFoundException.builder().message(MSG_GENERIC_ERR).build(), HandlerErrorCode.InvalidRequest), Arguments.of(CertificateNotFoundException.builder().message(MSG_GENERIC_ERR).build(), HandlerErrorCode.NotFound), @@ -2003,21 +2000,29 @@ public void handleRequest_startAutomaticBackupReplication() { context.setAutomaticBackupReplicationStarted(false); proxy = Mockito.spy(proxy); - final RdsClient crossRegionRdsClient = mock(RdsClient.class); final ProxyClient crossRegionRdsProxy = mockProxy(proxy, crossRegionRdsClient); doReturn(crossRegionRdsProxy).when(proxy).newProxy(ArgumentMatchers.>any()); + when(crossRegionRdsProxy.client().describeDBInstanceAutomatedBackups(any(DescribeDbInstanceAutomatedBackupsRequest.class))) + .thenReturn(DescribeDbInstanceAutomatedBackupsResponse.builder() + .dbInstanceAutomatedBackups(Collections.singletonList(DBInstanceAutomatedBackup.builder() + .dbInstanceAutomatedBackupsArn( + getAutomaticBackupArn(AUTOMATIC_BACKUP_REPLICATION_REGION)) + .backupRetentionPeriod(AUTOMATIC_BACKUP_REPLICATION_RETENTION_PERIOD).build())) + .build()); + test_handleRequest_base( - context, - () -> DB_INSTANCE_ACTIVE.toBuilder().dbInstanceAutomatedBackupsReplications( - Collections.singletonList(DBInstanceAutomatedBackupsReplication.builder() - .dbInstanceAutomatedBackupsArn( - getAutomaticBackupArn(AUTOMATIC_BACKUP_REPLICATION_REGION)).build())).build(), - () -> RESOURCE_MODEL_BLDR() - .automaticBackupReplicationRegion(AUTOMATIC_BACKUP_REPLICATION_REGION) - .build(), - expectSuccess() + context, + () -> DB_INSTANCE_ACTIVE.toBuilder().dbInstanceAutomatedBackupsReplications( + Collections.singletonList(DBInstanceAutomatedBackupsReplication.builder() + .dbInstanceAutomatedBackupsArn( + getAutomaticBackupArn(AUTOMATIC_BACKUP_REPLICATION_REGION)).build())).build(), + () -> RESOURCE_MODEL_BLDR() + .automaticBackupReplicationRegion(AUTOMATIC_BACKUP_REPLICATION_REGION) + .automaticBackupReplicationRetentionPeriod(1) + .build(), + expectSuccess() ); verify(crossRegionRdsProxy.client(), times(1)).startDBInstanceAutomatedBackupsReplication(any(StartDbInstanceAutomatedBackupsReplicationRequest.class)); @@ -2035,17 +2040,30 @@ public void handleRequest_noAutomaticBackupReplication() { context.setUpdatedRoles(true); context.setAddTagsComplete(true); + proxy = Mockito.spy(proxy); + final RdsClient crossRegionRdsClient = mock(RdsClient.class); + final ProxyClient crossRegionRdsProxy = mockProxy(proxy, crossRegionRdsClient); + doReturn(crossRegionRdsProxy).when(proxy).newProxy(ArgumentMatchers.>any()); + + when(crossRegionRdsProxy.client().describeDBInstanceAutomatedBackups(any(DescribeDbInstanceAutomatedBackupsRequest.class))) + .thenReturn(DescribeDbInstanceAutomatedBackupsResponse.builder() + .dbInstanceAutomatedBackups(Collections.singletonList(DBInstanceAutomatedBackup.builder() + .dbInstanceAutomatedBackupsArn( + getAutomaticBackupArn(AUTOMATIC_BACKUP_REPLICATION_REGION)) + .backupRetentionPeriod(AUTOMATIC_BACKUP_REPLICATION_RETENTION_PERIOD).build())) + .build()); + test_handleRequest_base( - context, - () -> DB_INSTANCE_ACTIVE.toBuilder().dbInstanceAutomatedBackupsReplications( - Collections.singletonList(DBInstanceAutomatedBackupsReplication.builder() - .dbInstanceAutomatedBackupsArn( - getAutomaticBackupArn(AUTOMATIC_BACKUP_REPLICATION_REGION)).build())).build(), - () -> RESOURCE_MODEL_BLDR() - .automaticBackupReplicationRegion(AUTOMATIC_BACKUP_REPLICATION_REGION) - .backupRetentionPeriod(0) - .build(), - expectSuccess() + context, + () -> DB_INSTANCE_ACTIVE.toBuilder().dbInstanceAutomatedBackupsReplications( + Collections.singletonList(DBInstanceAutomatedBackupsReplication.builder() + .dbInstanceAutomatedBackupsArn( + getAutomaticBackupArn(AUTOMATIC_BACKUP_REPLICATION_REGION)).build())).build(), + () -> RESOURCE_MODEL_BLDR() + .automaticBackupReplicationRegion(null) + .backupRetentionPeriod(1) + .build(), + expectSuccess() ); verify(rdsProxy.client(), times(2)).describeDBInstances(any(DescribeDbInstancesRequest.class)); @@ -2241,6 +2259,84 @@ public void fetchEngineForPointInTimeRestoreFromAutomatedBackup() { Assertions.assertThat(captor.getValue().dbInstanceAutomatedBackupsArn()).isEqualTo("backup-arn"); } + @Test + public void handleRequest_CreateDBInstanceWithSid_IsSuccessful() { + when(rdsProxy.client().addTagsToResource(any(AddTagsToResourceRequest.class))) + .thenReturn(AddTagsToResourceResponse.builder().build()); + + final CallbackContext context = new CallbackContext(); + context.setCreated(true); + context.setUpdated(true); + context.setRebooted(true); + context.setUpdatedRoles(true); + + test_handleRequest_base( + context, + () -> DB_INSTANCE_ACTIVE_SYSTEM_ID, + () -> RESOURCE_MODEL_BLDR().dBSystemId("UNITTEST").build(), + expectSuccess() + ); + + verify(rdsProxy.client(), times(3)).describeDBInstances(any(DescribeDbInstancesRequest.class)); + verify(rdsProxy.client(), times(1)).addTagsToResource(any(AddTagsToResourceRequest.class)); + } + + @Test + public void handleRequest_CreateReadReplicaWithSid_ThrowsRequestValidationException() { + expectServiceInvocation = false; + final CallbackContext context = new CallbackContext(); + context.setCreated(false); + context.setUpdated(true); + context.setRebooted(true); + context.setUpdatedRoles(true); + context.setAddTagsComplete(true); + + test_handleRequest_base( + context, + null, + () -> RESOURCE_MODEL_READ_REPLICA.toBuilder().dBSystemId("UNITTEST").build(), + expectFailed(HandlerErrorCode.InvalidRequest) + ); + } + + @Test + public void handleRequest_CreateDBInstanceFromSnapshotWithSid_ThrowsRequestValidationException() { + expectServiceInvocation = false; + final CallbackContext context = new CallbackContext(); + context.setCreated(false); + context.setUpdated(true); + context.setRebooted(true); + context.setUpdatedRoles(true); + context.setAddTagsComplete(true); + + test_handleRequest_base( + context, + null, + () -> RESOURCE_MODEL_RESTORING_FROM_SNAPSHOT.toBuilder().dBSystemId("UNITTEST").build(), + expectFailed(HandlerErrorCode.InvalidRequest) + ); + } + + @Test + public void handleRequest_CreateDBInstanceFromPitrWithSid_ThrowsRequestValidationException() { + expectServiceInvocation = false; + final CallbackContext context = new CallbackContext(); + context.setCreated(false); + context.setUpdated(true); + context.setRebooted(true); + context.setUpdatedRoles(true); + context.setAddTagsComplete(true); + + test_handleRequest_base( + context, + null, + () -> RESOURCE_MODEL_RESTORING_TO_POINT_IN_TIME.toBuilder() + .dBSystemId("UNITTEST") + .useLatestRestorableTime(true).build(), + expectFailed(HandlerErrorCode.InvalidRequest) + ); + } + @Test public void fetchEngineForUnknownScenario() { expectServiceInvocation = false; @@ -2258,4 +2354,53 @@ public void fetchEngineForUnknownScenario() { expectFailed(HandlerErrorCode.InvalidRequest) ); } + + @ParameterizedTest + @ArgumentsSource(ThrottleExceptionArgumentsProvider.class) + public void handleRequest_CreateDBInstance_HandleThrottleException( + final Object requestException + ) { + test_handleRequest_throttle( + expectCreateDBInstanceCall(), + new CallbackContext(), + () -> RESOURCE_MODEL_BLDR().build(), + requestException, + CALLBACK_DELAY + ); + } + + @ParameterizedTest + @ArgumentsSource(ThrottleExceptionArgumentsProvider.class) + public void handleRequest_CreateDBInstanceReadReplica_HandleThrottleException( + final Object requestException + ) { + test_handleRequest_throttle( + expectCreateDBInstanceReadReplicaCall(), + new CallbackContext(), + () -> RESOURCE_MODEL_READ_REPLICA, + requestException, + CALLBACK_DELAY + ); + } + + @ParameterizedTest + @ArgumentsSource(ThrottleExceptionArgumentsProvider.class) + public void handleRequest_RestoreDBInstanceFromSnapshot_HandleThrottleException( + final Object requestException + ) { + when(rdsProxy.client().describeDBSnapshots(any(DescribeDbSnapshotsRequest.class))) + .thenReturn(DescribeDbSnapshotsResponse.builder() + .dbSnapshots(DBSnapshot.builder() + .engine(ENGINE_MYSQL) + .build()) + .build()); + + test_handleRequest_throttle( + expectRestoreDBInstanceFromDBSnapshotCall(), + new CallbackContext(), + () -> RESOURCE_MODEL_RESTORING_FROM_SNAPSHOT, + requestException, + CALLBACK_DELAY + ); + } } diff --git a/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/DBInstancePredicatesTest.java b/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/DBInstancePredicatesTest.java new file mode 100644 index 000000000..f92a7266d --- /dev/null +++ b/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/DBInstancePredicatesTest.java @@ -0,0 +1,48 @@ +package software.amazon.rds.dbinstance; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.Mockito; +import software.amazon.awssdk.services.rds.model.DBInstance; +import software.amazon.awssdk.services.rds.model.PendingModifiedValues; +import software.amazon.rds.common.logging.RequestLogger; +import software.amazon.rds.dbinstance.status.DBInstanceStatus; + +import java.util.stream.Stream; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +public class DBInstancePredicatesTest { + @Mock + RequestLogger requestLogger = Mockito.mock(RequestLogger.class); + + @ParameterizedTest + @MethodSource("stabilizeTestCases") + public void test_isDBInstanceStabilizedAfterMutate(Boolean applyImmediate, Boolean isStabilized){ + DBInstance dbInstance = DBInstance.builder() + .dbInstanceStatus(DBInstanceStatus.Available.toString()) + .pendingModifiedValues(PendingModifiedValues.builder() + .backupRetentionPeriod(5) + .build()) + .build(); + + ResourceModel model = ResourceModel.builder() + .applyImmediately(applyImmediate) + .build(); + + boolean actual = DBInstancePredicates.isDBInstanceStabilizedAfterMutate(dbInstance, model, null, requestLogger); + + assertThat(actual).isEqualTo(isStabilized); + } + + private static Stream stabilizeTestCases() { + return Stream.of( + Arguments.of(null, Boolean.FALSE), + Arguments.of(Boolean.TRUE, Boolean.FALSE), + Arguments.of(Boolean.FALSE, Boolean.TRUE) + ); + } +} diff --git a/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/DeleteHandlerTest.java b/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/DeleteHandlerTest.java index d6909ea11..ff397ce5d 100644 --- a/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/DeleteHandlerTest.java +++ b/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/DeleteHandlerTest.java @@ -445,4 +445,36 @@ public void handleRequest_DeleteDBInstance_HandleExceptionInDescribe( expectResponseCode ); } + + @ParameterizedTest + @ArgumentsSource(ThrottleExceptionArgumentsProvider.class) + public void handleRequest_DeleteDBInstance_HandleThrottleExceptionInDelete( + final Object requestException + ) { + when(rdsProxy.client().describeDBInstances(any(DescribeDbInstancesRequest.class))) + .thenReturn(DescribeDbInstancesResponse.builder().dbInstances(DB_INSTANCE_ACTIVE).build()); + + test_handleRequest_throttle( + expectDeleteDBInstanceCall(), + new CallbackContext(), + () -> RESOURCE_MODEL_BLDR().build(), + requestException, + CALLBACK_DELAY + ); + } + + @ParameterizedTest + @ArgumentsSource(ThrottleExceptionArgumentsProvider.class) + public void handleRequest_DeleteDBInstance_HandleThrottleExceptionInDescribe( + final Object requestException + ) { + expectServiceInvocation = false; + test_handleRequest_throttle( + expectDescribeDBInstancesCall(), + new CallbackContext(), + () -> RESOURCE_MODEL_BLDR().build(), + requestException, + CALLBACK_DELAY + ); + } } diff --git a/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/ReadHandlerTest.java b/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/ReadHandlerTest.java index 484321095..92fc2e143 100644 --- a/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/ReadHandlerTest.java +++ b/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/ReadHandlerTest.java @@ -2,25 +2,43 @@ import static org.mockito.Mockito.any; import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; import java.time.Duration; +import java.util.Collections; +import java.util.function.Supplier; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.mockito.ArgumentMatchers; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import lombok.Getter; +import software.amazon.awssdk.awscore.exception.AwsErrorDetails; +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.services.cloudwatchlogs.model.AccessDeniedException; import software.amazon.awssdk.services.ec2.Ec2Client; import software.amazon.awssdk.services.rds.RdsClient; +import software.amazon.awssdk.services.rds.model.DBInstanceAutomatedBackup; +import software.amazon.awssdk.services.rds.model.DBInstanceAutomatedBackupsReplication; +import software.amazon.awssdk.services.rds.model.DbInstanceAutomatedBackupNotFoundException; +import software.amazon.awssdk.services.rds.model.DescribeDbInstanceAutomatedBackupsRequest; +import software.amazon.awssdk.services.rds.model.DescribeDbInstanceAutomatedBackupsResponse; import software.amazon.awssdk.services.rds.model.DescribeDbInstancesRequest; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.HandlerErrorCode; import software.amazon.cloudformation.proxy.ProxyClient; import software.amazon.rds.test.common.core.HandlerName; @@ -83,4 +101,113 @@ public void handleRequest_ReadSuccess() { verify(rdsProxy.client(), times(1)).describeDBInstances(any(DescribeDbInstancesRequest.class)); } + + @Test + public void handleRequest_ValidAutomaticBackupReplicationArn() { + proxy = Mockito.spy(proxy); + final RdsClient crossRegionRdsClient = mock(RdsClient.class); + final ProxyClient crossRegionRdsProxy = mockProxy(proxy, crossRegionRdsClient); + doReturn(crossRegionRdsProxy).when(proxy).newProxy(ArgumentMatchers.>any()); + + final CallbackContext context = new CallbackContext(); + final String automaticBackupReplicationArn = getAutomaticBackupArn(AUTOMATIC_BACKUP_REPLICATION_REGION); + + when(crossRegionRdsProxy.client().describeDBInstanceAutomatedBackups(any(DescribeDbInstanceAutomatedBackupsRequest.class))) + .thenReturn(DescribeDbInstanceAutomatedBackupsResponse.builder() + .dbInstanceAutomatedBackups(Collections.singletonList(DBInstanceAutomatedBackup.builder() + .dbInstanceAutomatedBackupsArn( + getAutomaticBackupArn(AUTOMATIC_BACKUP_REPLICATION_REGION)) + .backupRetentionPeriod(AUTOMATIC_BACKUP_REPLICATION_RETENTION_PERIOD).build())) + .build()); + + test_handleRequest_base( + context, + () -> DB_INSTANCE_ACTIVE.toBuilder() + .dbInstanceAutomatedBackupsReplications(Collections.singletonList(DBInstanceAutomatedBackupsReplication.builder() + .dbInstanceAutomatedBackupsArn(automaticBackupReplicationArn).build())) + .build(), + () -> RESOURCE_MODEL_BLDR().build(), + expectSuccess() + ); + + verify(rdsProxy.client(), times(1)).describeDBInstances(any(DescribeDbInstancesRequest.class)); + verify(crossRegionRdsProxy.client(), times(1)).describeDBInstanceAutomatedBackups(any(DescribeDbInstanceAutomatedBackupsRequest.class)); + Assertions.assertThat(context.isAutomaticBackupReplicationStarted()).isTrue(); + } + + @Test + public void handleRequest_AutomaticBackupReplicationNotFound() { + // This tests the edge case where the describe db instance automated backups request returns not found. + // In this scenario, the request will fail and simply progress to 'success' without replication parameters. + proxy = Mockito.spy(proxy); + final RdsClient crossRegionRdsClient = mock(RdsClient.class); + final ProxyClient crossRegionRdsProxy = mockProxy(proxy, crossRegionRdsClient); + doReturn(crossRegionRdsProxy).when(proxy).newProxy(ArgumentMatchers.>any()); + + final CallbackContext context = new CallbackContext(); + final String automaticBackupReplicationArn = getAutomaticBackupArn(AUTOMATIC_BACKUP_REPLICATION_REGION); + + when(crossRegionRdsProxy.client().describeDBInstanceAutomatedBackups(any(DescribeDbInstanceAutomatedBackupsRequest.class))) + .thenThrow(DbInstanceAutomatedBackupNotFoundException.class); + + test_handleRequest_base( + context, + () -> DB_INSTANCE_ACTIVE.toBuilder() + .dbInstanceAutomatedBackupsReplications(Collections.singletonList(DBInstanceAutomatedBackupsReplication.builder() + .dbInstanceAutomatedBackupsArn(automaticBackupReplicationArn).build())) + .build(), + () -> RESOURCE_MODEL_BLDR().build(), + expectSuccess() + ); + + verify(rdsProxy.client(), times(1)).describeDBInstances(any(DescribeDbInstancesRequest.class)); + verify(crossRegionRdsProxy.client(), times(1)).describeDBInstanceAutomatedBackups(any(DescribeDbInstanceAutomatedBackupsRequest.class)); + Assertions.assertThat(context.isAutomaticBackupReplicationStarted()).isFalse(); + } + + @Test + public void handleRequest_AutomaticBackupReplicationAccessDenied() { + // This tests the edge case where the DB instance has replications, but customer does not have permission to run DescribeDBInstanceAutomatedBackups + // In this scenario, the request will fail and simply progress to 'success' without replication parameters. + proxy = Mockito.spy(proxy); + final RdsClient crossRegionRdsClient = mock(RdsClient.class); + final ProxyClient crossRegionRdsProxy = mockProxy(proxy, crossRegionRdsClient); + doReturn(crossRegionRdsProxy).when(proxy).newProxy(ArgumentMatchers.>any()); + + final CallbackContext context = new CallbackContext(); + final String automaticBackupReplicationArn = getAutomaticBackupArn(AUTOMATIC_BACKUP_REPLICATION_REGION); + + when(crossRegionRdsProxy.client().describeDBInstanceAutomatedBackups(any(DescribeDbInstanceAutomatedBackupsRequest.class))) + .thenThrow(AwsServiceException.builder() + .awsErrorDetails(AwsErrorDetails.builder().errorCode("AccessDenied").build()) + .build()); + + test_handleRequest_base( + context, + () -> DB_INSTANCE_ACTIVE.toBuilder() + .dbInstanceAutomatedBackupsReplications(Collections.singletonList(DBInstanceAutomatedBackupsReplication.builder() + .dbInstanceAutomatedBackupsArn(automaticBackupReplicationArn).build())) + .build(), + () -> RESOURCE_MODEL_BLDR().build(), + expectSuccess() + ); + + verify(rdsProxy.client(), times(1)).describeDBInstances(any(DescribeDbInstancesRequest.class)); + verify(crossRegionRdsProxy.client(), times(1)).describeDBInstanceAutomatedBackups(any(DescribeDbInstanceAutomatedBackupsRequest.class)); + Assertions.assertThat(context.isAutomaticBackupReplicationStarted()).isFalse(); + } + + @ParameterizedTest + @ArgumentsSource(ThrottleExceptionArgumentsProvider.class) + public void handleRequest_DescribeDBInstance_HandleThrottleException( + final Object requestException + ) { + test_handleRequest_error( + expectDescribeDBInstancesCall(), + new CallbackContext(), + () -> RESOURCE_MODEL_BLDR().build(), + requestException, + HandlerErrorCode.Throttling + ); + } } diff --git a/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/TranslatorTest.java b/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/TranslatorTest.java index 83a413525..6fa69571e 100644 --- a/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/TranslatorTest.java +++ b/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/TranslatorTest.java @@ -1,14 +1,11 @@ package software.amazon.rds.dbinstance; -import static org.assertj.core.api.Assertions.assertThat; - -import java.time.Instant; -import java.util.Collection; - +import com.google.common.collect.ImmutableList; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; - -import com.google.common.collect.ImmutableList; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import software.amazon.awssdk.services.ec2.Ec2Client; import software.amazon.awssdk.services.rds.RdsClient; import software.amazon.awssdk.services.rds.model.CreateDbInstanceReadReplicaRequest; @@ -26,6 +23,12 @@ import software.amazon.rds.test.common.core.HandlerName; import software.amazon.rds.test.common.core.TestUtils; +import java.time.Instant; +import java.util.Collection; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + class TranslatorTest extends AbstractHandlerTest { @Test @@ -36,8 +39,9 @@ public void test_modifyDbInstanceRequest_IncreaseAllocatedStorage() { final ResourceModel desiredModel = RESOURCE_MODEL_BLDR() .allocatedStorage(ALLOCATED_STORAGE_INCR.toString()) .build(); + final DBInstance dbInstance = DBInstance.builder().allocatedStorage(ALLOCATED_STORAGE).build(); final Boolean isRollback = false; - final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, isRollback); + final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, dbInstance, isRollback); assertThat(request.allocatedStorage()).isEqualTo(ALLOCATED_STORAGE_INCR); } @@ -62,8 +66,9 @@ public void test_modifyDbInstanceRequest_DecreaseAllocatedStorage() { final ResourceModel desiredModel = RESOURCE_MODEL_BLDR() .allocatedStorage(ALLOCATED_STORAGE_DECR.toString()) .build(); + final DBInstance dbInstance = DBInstance.builder().build(); final Boolean isRollback = false; - final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, isRollback); + final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, dbInstance, isRollback); assertThat(request.allocatedStorage()).isEqualTo(ALLOCATED_STORAGE_DECR); } @@ -88,8 +93,9 @@ public void test_modifyDbInstanceRequest_isRollback_IncreaseAllocatedStorage() { final ResourceModel desiredModel = RESOURCE_MODEL_BLDR() .allocatedStorage(ALLOCATED_STORAGE_INCR.toString()) .build(); + final DBInstance dbInstance = DBInstance.builder().allocatedStorage(ALLOCATED_STORAGE).build(); final Boolean isRollback = true; - final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, isRollback); + final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, dbInstance, isRollback); assertThat(request.allocatedStorage()).isNull(); } @@ -102,8 +108,9 @@ public void test_modifyDbInstanceRequest_isRollback_NoAllocatedStorage() { .allocatedStorage(null) .storageType(STORAGE_TYPE_IO1) .build(); + final DBInstance dbInstance = DBInstance.builder().build(); final Boolean isRollback = true; - final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, isRollback); + final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, dbInstance, isRollback); assertThat(request.allocatedStorage()).isNull(); } @@ -128,8 +135,9 @@ public void test_modifyDbInstanceRequest_isRollback_DecreaseAllocatedStorage() { final ResourceModel desiredModel = RESOURCE_MODEL_BLDR() .allocatedStorage(ALLOCATED_STORAGE_DECR.toString()) .build(); + final DBInstance dbInstance = DBInstance.builder().build(); final Boolean isRollback = true; - final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, isRollback); + final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, dbInstance, isRollback); assertThat(request.allocatedStorage()).isNull(); } @@ -154,8 +162,9 @@ public void test_modifyDbInstanceRequest_IncreaseIops() { final ResourceModel desiredModel = RESOURCE_MODEL_BLDR() .iops(IOPS_INCR) .build(); + final DBInstance dbInstance = DBInstance.builder().build(); final Boolean isRollback = false; - final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, isRollback); + final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, dbInstance, isRollback); assertThat(request.iops()).isEqualTo(IOPS_INCR); } @@ -180,8 +189,9 @@ public void test_modifyDbInstanceRequest_DecreaseIops() { final ResourceModel desiredModel = RESOURCE_MODEL_BLDR() .iops(IOPS_DECR) .build(); + final DBInstance dbInstance = DBInstance.builder().build(); final Boolean isRollback = false; - final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, isRollback); + final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, dbInstance, isRollback); assertThat(request.iops()).isEqualTo(IOPS_DECR); } @@ -206,8 +216,9 @@ public void test_modifyDbInstanceRequest_isRollback_IncreaseIops() { final ResourceModel desiredModel = RESOURCE_MODEL_BLDR() .iops(IOPS_INCR) .build(); + final DBInstance dbInstance = DBInstance.builder().build(); final Boolean isRollback = true; - final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, isRollback); + final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, dbInstance, isRollback); assertThat(request.iops()).isNull(); } @@ -232,8 +243,9 @@ public void test_modifyDbInstanceRequest_isRollback_DecreaseIops() { final ResourceModel desiredModel = RESOURCE_MODEL_BLDR() .iops(IOPS_DECR) .build(); + final DBInstance dbInstance = DBInstance.builder().build(); final Boolean isRollback = true; - final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, isRollback); + final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, dbInstance, isRollback); assertThat(request.iops()).isNull(); } @@ -258,8 +270,9 @@ public void test_modifyDbInstanceRequest_setUseDefaultProcessorFeatures() { final ResourceModel desiredModel = RESOURCE_MODEL_BLDR() .useDefaultProcessorFeatures(true) .build(); + final DBInstance dbInstance = DBInstance.builder().build(); final Boolean isRollback = false; - final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, isRollback); + final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, dbInstance, isRollback); assertThat(request.useDefaultProcessorFeatures()).isTrue(); } @@ -271,8 +284,9 @@ public void test_modifyDbInstanceRequest_setProcessorFeatures() { final ResourceModel desiredModel = RESOURCE_MODEL_BLDR() .processorFeatures(PROCESSOR_FEATURES_ALTER) .build(); + final DBInstance dbInstance = DBInstance.builder().build(); final Boolean isRollback = false; - final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, isRollback); + final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, dbInstance, isRollback); assertThat(request.processorFeatures()).hasSameElementsAs(Translator.translateProcessorFeaturesToSdk(PROCESSOR_FEATURES_ALTER)); } @@ -341,8 +355,8 @@ public void test_createReadReplicaRequest_nonBlankSourceRegionIsSet() { @Test public void test_modifyReadReplicaRequest_parameterGroupNotSet() { final ResourceModel model = RESOURCE_MODEL_BLDR().build(); - - final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(ResourceModel.builder().build(), model, false); + final DBInstance dbInstance = DBInstance.builder().build(); + final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(ResourceModel.builder().build(), model, dbInstance,false); Assertions.assertEquals("default", request.dbParameterGroupName()); } @@ -502,8 +516,8 @@ public void test_modifyDBInstanceRequest_cloudwatchLogsExportConfiguration_uncha final ResourceModel desiredModel = RESOURCE_MODEL_BLDR() .enableCloudwatchLogsExports(ImmutableList.of("config-1", "config-2")) .build(); - - final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, false); + final DBInstance dbInstance = DBInstance.builder().build(); + final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, dbInstance, false); assertThat(request.cloudwatchLogsExportConfiguration()).isNull(); } @@ -515,8 +529,8 @@ public void test_modifyDBInstanceRequest_cloudwatchLogsExportConfiguration_chang final ResourceModel desiredModel = RESOURCE_MODEL_BLDR() .enableCloudwatchLogsExports(ImmutableList.of("config-1", "config-3")) .build(); - - final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, false); + final DBInstance dbInstance = DBInstance.builder().build(); + final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, dbInstance, false); assertThat(request.cloudwatchLogsExportConfiguration().disableLogTypes()).isEqualTo(ImmutableList.of("config-2")); assertThat(request.cloudwatchLogsExportConfiguration().enableLogTypes()).isEqualTo(ImmutableList.of("config-3")); } @@ -529,8 +543,8 @@ public void test_modifyDBInstanceRequest_cloudwatchLogsExportConfiguration_previ final ResourceModel desiredModel = RESOURCE_MODEL_BLDR() .enableCloudwatchLogsExports(ImmutableList.of("config-1", "config-2")) .build(); - - final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, false); + final DBInstance dbInstance = DBInstance.builder().build(); + final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, dbInstance, false); assertThat(request.cloudwatchLogsExportConfiguration().enableLogTypes()).isEqualTo(ImmutableList.of("config-1", "config-2")); } @@ -635,8 +649,8 @@ public void test_translateManageMasterUserPassword_fromUnsetToUnset() { final ResourceModel desired = RESOURCE_MODEL_BLDR() .masterUserPassword("password") .build(); - - final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(prev, desired, false); + final DBInstance dbInstance = DBInstance.builder().build(); + final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(prev, desired, dbInstance, false); assertThat(request.manageMasterUserPassword()).isNull(); assertThat(request.masterUserSecretKmsKeyId()).isNull(); @@ -651,8 +665,8 @@ public void test_translateManageMasterUserPassword_fromSetToUnset() { .build(); final ResourceModel desired = RESOURCE_MODEL_BLDR() .build(); - - final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(prev, desired, false); + final DBInstance dbInstance = DBInstance.builder().build(); + final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(prev, desired, dbInstance, false); assertThat(request.manageMasterUserPassword()).isFalse(); assertThat(request.masterUserSecretKmsKeyId()).isNull(); @@ -668,8 +682,8 @@ public void test_translateManageMasterUserPassword_explicitUnset() { .manageMasterUserPassword(false) .masterUserSecret(MasterUserSecret.builder().kmsKeyId("key1").build()) .build(); - - final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(prev, desired, false); + final DBInstance dbInstance = DBInstance.builder().build(); + final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(prev, desired, dbInstance, false); assertThat(request.manageMasterUserPassword()).isFalse(); assertThat(request.masterUserSecretKmsKeyId()).isNull(); @@ -684,8 +698,8 @@ public void test_translateManageMasterUserPassword_fromUnsetToSet_withDefaultKey .manageMasterUserPassword(true) .masterUserSecret(MasterUserSecret.builder().kmsKeyId(null).build()) .build(); - - final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(prev, desired, false); + final DBInstance dbInstance = DBInstance.builder().build(); + final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(prev, desired, dbInstance,false); assertThat(request.manageMasterUserPassword()).isTrue(); assertThat(request.masterUserSecretKmsKeyId()).isNull(); @@ -700,8 +714,8 @@ public void test_translateManageMasterUserPassword_fromUnsetToSet_emptyMasterUse .manageMasterUserPassword(true) .masterUserSecret(null) .build(); - - final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(prev, desired, false); + final DBInstance dbInstance = DBInstance.builder().build(); + final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(prev, desired, dbInstance, false); assertThat(request.manageMasterUserPassword()).isTrue(); assertThat(request.masterUserSecretKmsKeyId()).isNull(); @@ -718,10 +732,9 @@ public void test_translateManageMasterUserPassword_fromExplicitFalseToExplicitFa .manageMasterUserPassword(false) .masterUserPassword("password") .masterUserSecret(MasterUserSecret.builder().kmsKeyId("key").build()) - .build(); - - final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(prev, desired, false); + final DBInstance dbInstance = DBInstance.builder().build(); + final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(prev, desired, dbInstance, false); assertThat(request.manageMasterUserPassword()).isNull(); assertThat(request.masterUserSecretKmsKeyId()).isNull(); @@ -736,8 +749,8 @@ public void test_translateManageMasterUserPassword_fromUnsetToSet_withSpecificKe .manageMasterUserPassword(true) .masterUserSecret(MasterUserSecret.builder().kmsKeyId("myKey").build()) .build(); - - final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(prev, desired, false); + final DBInstance dbInstance = DBInstance.builder().build(); + final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(prev, desired, dbInstance, false); assertThat(request.manageMasterUserPassword()).isTrue(); assertThat(request.masterUserSecretKmsKeyId()).isEqualTo("myKey"); @@ -793,7 +806,8 @@ public void test_modifyDbInstanceRequest_PerformanceInsightsEnabled() { .enablePerformanceInsights(true) .performanceInsightsKMSKeyId(kmsKeyId) .build(); - final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, false); + final DBInstance dbInstance = DBInstance.builder().build(); + final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, dbInstance, false); assertThat(request.enablePerformanceInsights()).isNull(); assertThat(request.performanceInsightsKMSKeyId()).isNull(); } @@ -809,7 +823,8 @@ public void test_modifyDbInstanceRequest_PerformanceInsightsToggleDisabledToEnab .enablePerformanceInsights(true) .performanceInsightsKMSKeyId(kmsKeyId) .build(); - final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, false); + final DBInstance dbInstance = DBInstance.builder().build(); + final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, dbInstance, false); assertThat(request.enablePerformanceInsights()).isTrue(); assertThat(request.performanceInsightsKMSKeyId()).isEqualTo(kmsKeyId); } @@ -825,7 +840,8 @@ public void test_modifyDbInstanceRequest_PerformanceInsightsToggleEnabledToDisab .enablePerformanceInsights(false) .performanceInsightsKMSKeyId(kmsKeyId) .build(); - final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, false); + final DBInstance dbInstance = DBInstance.builder().build(); + final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, dbInstance, false); assertThat(request.enablePerformanceInsights()).isFalse(); assertThat(request.performanceInsightsKMSKeyId()).isNull(); } @@ -841,7 +857,8 @@ public void test_modifyDbInstanceRequest_PerformanceInsightsChangeRetentionPerio .enablePerformanceInsights(true) .performanceInsightsRetentionPeriod(NEW_RETENTION_PERIOD) .build(); - final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, false); + final DBInstance dbInstance = DBInstance.builder().build(); + final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, dbInstance, false); assertThat(request.enablePerformanceInsights()).isTrue(); assertThat(request.performanceInsightsRetentionPeriod()).isEqualTo(NEW_RETENTION_PERIOD); } @@ -857,7 +874,8 @@ public void test_modifyDbInstanceRequest_NoPerformanceInsightsChangeRetentionPer .enablePerformanceInsights(false) .performanceInsightsRetentionPeriod(NEW_RETENTION_PERIOD) .build(); - final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, false); + final DBInstance dbInstance = DBInstance.builder().build(); + final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, dbInstance, false); assertThat(request.enablePerformanceInsights()).isFalse(); assertThat(request.performanceInsightsRetentionPeriod()).isEqualTo(NEW_RETENTION_PERIOD); } @@ -874,7 +892,8 @@ public void test_modifyDbInstanceRequest_PerformanceInsightsEnabledChangeKMSKeyI .enablePerformanceInsights(true) .performanceInsightsKMSKeyId(kmsKeyId2) .build(); - final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, false); + final DBInstance dbInstance = DBInstance.builder().build(); + final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, dbInstance, false); assertThat(request.enablePerformanceInsights()).isTrue(); assertThat(request.performanceInsightsKMSKeyId()).isEqualTo(kmsKeyId2); } @@ -891,7 +910,8 @@ public void test_modifyDbInstanceRequest_PerformanceInsightsDisabledChangeKMSKey .enablePerformanceInsights(false) .performanceInsightsKMSKeyId(kmsKeyId2) .build(); - final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, false); + final DBInstance dbInstance = DBInstance.builder().build(); + final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, dbInstance, false); assertThat(request.enablePerformanceInsights()).isFalse(); assertThat(request.performanceInsightsKMSKeyId()).isEqualTo(kmsKeyId2); } @@ -908,7 +928,8 @@ public void test_modifyDbInstanceRequest_PerformanceInsightsToggleEnabledToDisab .enablePerformanceInsights(false) .performanceInsightsKMSKeyId(kmsKeyId2) .build(); - final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, false); + final DBInstance dbInstance = DBInstance.builder().build(); + final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, dbInstance, false); assertThat(request.enablePerformanceInsights()).isFalse(); assertThat(request.performanceInsightsKMSKeyId()).isEqualTo(kmsKeyId2); } @@ -925,7 +946,8 @@ public void test_modifyDbInstanceRequest_PerformanceInsightsToggleDisabledToEnab .enablePerformanceInsights(true) .performanceInsightsKMSKeyId(kmsKeyId2) .build(); - final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, false); + final DBInstance dbInstance = DBInstance.builder().build(); + final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previousModel, desiredModel, dbInstance, false); assertThat(request.enablePerformanceInsights()).isTrue(); assertThat(request.performanceInsightsKMSKeyId()).isEqualTo(kmsKeyId2); } @@ -966,8 +988,8 @@ public void translateDbInstanceFromSdk_port_getFromEndpoint() { public void modifyDbInstanceRequest_shouldIncludeAllocatedStorage_ifStorageTypeIsIo1_andIopsOnlyChanged() { final ResourceModel previous = RESOURCE_MODEL_BLDR().storageType("io1").iops(1000).build(); final ResourceModel desired = RESOURCE_MODEL_BLDR().storageType("io1").iops(1200).build(); - - final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previous, desired, false); + final DBInstance dbInstance = DBInstance.builder().allocatedStorage(ALLOCATED_STORAGE).build(); + final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previous, desired, dbInstance, false); assertThat(request.iops()).isEqualTo(1200); assertThat(request.allocatedStorage()).isEqualTo(ALLOCATED_STORAGE); @@ -977,8 +999,8 @@ public void modifyDbInstanceRequest_shouldIncludeAllocatedStorage_ifStorageTypeI public void modifyDbInstanceRequest_shouldIncludeAllocatedStorageANdIops_ifStorageTypeIsIo1_andStorageTypeChangedToIo2() { final ResourceModel previous = RESOURCE_MODEL_BLDR().storageType("io1").iops(1000).allocatedStorage("100").build(); final ResourceModel desired = RESOURCE_MODEL_BLDR().storageType("io2").iops(1000).allocatedStorage("100").build(); - - final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previous, desired, false); + final DBInstance dbInstance = DBInstance.builder().allocatedStorage(ALLOCATED_STORAGE).build(); + final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previous, desired, dbInstance, false); assertThat(request.iops()).isEqualTo(1000); assertThat(request.allocatedStorage()).isEqualTo(100); @@ -988,8 +1010,8 @@ public void modifyDbInstanceRequest_shouldIncludeAllocatedStorageANdIops_ifStora public void modifyDbInstanceRequest_shouldIncludeAllocatedStorage_ifStorageTypeIsImplicitIo1_andIopsOnlyChanged() { final ResourceModel previous = RESOURCE_MODEL_BLDR().storageType(null).iops(1000).build(); final ResourceModel desired = RESOURCE_MODEL_BLDR().storageType(null).iops(1200).build(); - - final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previous, desired, false); + final DBInstance dbInstance = DBInstance.builder().allocatedStorage(ALLOCATED_STORAGE).build(); + final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previous, desired, dbInstance,false); assertThat(request.iops()).isEqualTo(1200); assertThat(request.allocatedStorage()).isEqualTo(ALLOCATED_STORAGE); @@ -999,8 +1021,8 @@ public void modifyDbInstanceRequest_shouldIncludeAllocatedStorage_ifStorageTypeI public void modifyDbInstanceRequest_shouldIncludeAllocatedStorage_ifStorageTypeIsGp3_andIopsOnlyChanged() { final ResourceModel previous = RESOURCE_MODEL_BLDR().storageType("gp3").iops(1000).build(); final ResourceModel desired = RESOURCE_MODEL_BLDR().storageType("gp3").iops(1200).build(); - - final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previous, desired, false); + final DBInstance dbInstance = DBInstance.builder().allocatedStorage(ALLOCATED_STORAGE).build(); + final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previous, desired, dbInstance, false); assertThat(request.iops()).isEqualTo(1200); assertThat(request.allocatedStorage()).isEqualTo(ALLOCATED_STORAGE); @@ -1010,8 +1032,8 @@ public void modifyDbInstanceRequest_shouldIncludeAllocatedStorage_ifStorageTypeI public void modifyDbInstanceRequest_shouldIncludeIops_ifStorageTypeIsGp3_andAllocatedStorageOnlyChanged() { final ResourceModel previous = RESOURCE_MODEL_BLDR().storageType("gp3").allocatedStorage(ALLOCATED_STORAGE.toString()).build(); final ResourceModel desired = RESOURCE_MODEL_BLDR().storageType("gp3").allocatedStorage(ALLOCATED_STORAGE_INCR.toString()).build(); - - final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previous, desired, false); + final DBInstance dbInstance = DBInstance.builder().allocatedStorage(ALLOCATED_STORAGE).build(); + final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previous, desired, dbInstance,false); assertThat(request.iops()).isEqualTo(IOPS_DEFAULT); assertThat(request.allocatedStorage()).isEqualTo(ALLOCATED_STORAGE_INCR); @@ -1021,8 +1043,8 @@ public void modifyDbInstanceRequest_shouldIncludeIops_ifStorageTypeIsGp3_andAllo public void modifyDbInstanceRequest_shouldNotIncludeAllocatedStorage_ifStorageTypeIsGP2_andIopsOnlyChanged() { final ResourceModel previous = RESOURCE_MODEL_BLDR().iops(1000).build(); final ResourceModel desired = RESOURCE_MODEL_BLDR().iops(1200).build(); - - final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previous, desired, false); + final DBInstance dbInstance = DBInstance.builder().build(); + final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previous, desired, dbInstance,false); assertThat(request.iops()).isEqualTo(1200); assertThat(request.allocatedStorage()).isNull(); @@ -1073,9 +1095,9 @@ public void modifyDbInstanceRequest_shouldIncludeAllocatedStorageAndIopsOnRollba .iops(IOPS_DECR) .allocatedStorage(ALLOCATED_STORAGE_DECR.toString()) .build(); - + final DBInstance dbInstance = DBInstance.builder().allocatedStorage(ALLOCATED_STORAGE).build(); final boolean isRollback = true; - final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previous, desired, isRollback); + final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previous, desired, dbInstance, isRollback); assertThat(request.allocatedStorage()).isEqualTo(ALLOCATED_STORAGE_INCR); assertThat(request.iops()).isEqualTo(IOPS_DECR); @@ -1093,9 +1115,9 @@ public void modifyDbInstanceRequest_sholdIncludeAllocatedStorageAndIopsOnRollbac .iops(IOPS_DECR) .allocatedStorage(ALLOCATED_STORAGE_INCR.toString()) .build(); - + final DBInstance dbInstance = DBInstance.builder().allocatedStorage(ALLOCATED_STORAGE_INCR).build(); final boolean isRollback = true; - final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previous, desired, isRollback); + final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previous, desired, dbInstance, isRollback); assertThat(request.allocatedStorage()).isEqualTo(ALLOCATED_STORAGE_INCR); assertThat(request.iops()).isEqualTo(IOPS_DECR); @@ -1113,9 +1135,9 @@ public void modifyDbInstanceRequest_shouldIncludeAllocatedStorageAndIopsOnRollba .iops(IOPS_DECR) .allocatedStorage(null) .build(); - + final DBInstance dbInstance = DBInstance.builder().allocatedStorage(ALLOCATED_STORAGE).build(); final boolean isRollback = true; - final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previous, desired, isRollback); + final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previous, desired, dbInstance, isRollback); assertThat(request.allocatedStorage()).isEqualTo(ALLOCATED_STORAGE_INCR); assertThat(request.iops()).isEqualTo(IOPS_DECR); @@ -1212,11 +1234,45 @@ public void test_modifyDBInstance_dedicatedLogVolumeSplit() { final ResourceModel desired = ResourceModel.builder() .dedicatedLogVolume(true) .build(); - - final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previous, desired, false); + final DBInstance dbInstance = DBInstance.builder().build(); + final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previous, desired, dbInstance, false); assertThat(request.dedicatedLogVolume()).isTrue(); } + private static Stream getApplyImmediatelyTestCases() { + return Stream.of( + Arguments.of(null, Boolean.TRUE), + Arguments.of(Boolean.TRUE, Boolean.TRUE), + Arguments.of(Boolean.FALSE, Boolean.FALSE) + ); + } + + @ParameterizedTest() + @MethodSource("getApplyImmediatelyTestCases") + public void test_modifyDBInstanceV12_ApplyImmediately(Boolean inputValue, Boolean expectedValue) { + final ResourceModel previous = ResourceModel.builder() + .build(); + final ResourceModel desired = ResourceModel.builder() + .applyImmediately(inputValue) + .build(); + + final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequestV12(previous, desired, false); + assertThat(request.applyImmediately()).isEqualTo(expectedValue); + } + + @ParameterizedTest() + @MethodSource("getApplyImmediatelyTestCases") + public void test_modifyDBInstance_ApplyImmediately(Boolean inputValue, Boolean expectedValue) { + final ResourceModel previous = ResourceModel.builder() + .build(); + final ResourceModel desired = ResourceModel.builder() + .applyImmediately(inputValue) + .build(); + final DBInstance dbInstance = DBInstance.builder().build(); + final ModifyDbInstanceRequest request = Translator.modifyDbInstanceRequest(previous, desired, dbInstance, false); + assertThat(request.applyImmediately()).isEqualTo(expectedValue); + } + // Stub methods to satisfy the interface. This is a 1-time thing. @Override diff --git a/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/UpdateHandlerTest.java b/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/UpdateHandlerTest.java index 2901f395c..974eb6ac5 100644 --- a/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/UpdateHandlerTest.java +++ b/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/UpdateHandlerTest.java @@ -4,6 +4,7 @@ import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -14,9 +15,7 @@ import java.time.Duration; import java.time.Instant; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; -import java.util.HashSet; import java.util.List; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; @@ -58,6 +57,7 @@ import software.amazon.awssdk.services.rds.model.DBClusterMember; import software.amazon.awssdk.services.rds.model.DBEngineVersion; import software.amazon.awssdk.services.rds.model.DBInstance; +import software.amazon.awssdk.services.rds.model.DBInstanceAutomatedBackup; import software.amazon.awssdk.services.rds.model.DBInstanceAutomatedBackupsReplication; import software.amazon.awssdk.services.rds.model.DBParameterGroup; import software.amazon.awssdk.services.rds.model.DBParameterGroupStatus; @@ -69,6 +69,8 @@ import software.amazon.awssdk.services.rds.model.DescribeDbClustersResponse; import software.amazon.awssdk.services.rds.model.DescribeDbEngineVersionsRequest; import software.amazon.awssdk.services.rds.model.DescribeDbEngineVersionsResponse; +import software.amazon.awssdk.services.rds.model.DescribeDbInstanceAutomatedBackupsRequest; +import software.amazon.awssdk.services.rds.model.DescribeDbInstanceAutomatedBackupsResponse; import software.amazon.awssdk.services.rds.model.DescribeDbInstancesRequest; import software.amazon.awssdk.services.rds.model.DescribeDbInstancesResponse; import software.amazon.awssdk.services.rds.model.DescribeDbParameterGroupsRequest; @@ -200,7 +202,7 @@ public void handleRequest_modifyDbInstance_Success() { expectSuccess() ); - verify(rdsProxy.client(), times(3)).describeDBInstances(any(DescribeDbInstancesRequest.class)); + verify(rdsProxy.client(), times(4)).describeDBInstances(any(DescribeDbInstancesRequest.class)); verify(rdsProxy.client()).modifyDBInstance(any(ModifyDbInstanceRequest.class)); verify(rdsProxy.client()).addTagsToResource(any(AddTagsToResourceRequest.class)); verify(rdsProxy.client()).removeTagsFromResource(any(RemoveTagsFromResourceRequest.class)); @@ -284,7 +286,7 @@ public void handleRequest_UnsetMaxAllocatedStorage() { verify(rdsProxy.client(), times(1)).modifyDBInstance(argument.capture()); Assertions.assertThat(argument.getValue().maxAllocatedStorage()).isEqualTo(ALLOCATED_STORAGE); - verify(rdsProxy.client(), times(3)).describeDBInstances(any(DescribeDbInstancesRequest.class)); + verify(rdsProxy.client(), times(4)).describeDBInstances(any(DescribeDbInstancesRequest.class)); verify(rdsProxy.client(), times(1)).describeEvents(any(DescribeEventsRequest.class)); } @@ -909,33 +911,116 @@ public void handleRequest_SetParameterGroupName_EmptyDbEngineVersions() { verify(rdsProxy.client(), times(2)).describeDBInstances(any(DescribeDbInstancesRequest.class)); } + @Test - public void handleRequest_SetDefaultVpcId() { + public void handleRequest_SetDefaultVpcId_previousNotNull_desiredNotNull() { + final CallbackContext context = new CallbackContext(); + context.setUpdated(true); // this is an emulation of a re-entrance + context.setStorageAllocated(true); + + // When the previous.vPCSecurityGroups != null && desired.vPCSecurityGroups != null + test_handleRequest_base( + context, + () -> DB_INSTANCE_ACTIVE.toBuilder().dbSubnetGroup( + DBSubnetGroup.builder().vpcId(DB_SECURITY_GROUP_VPC_ID).build() + ).build(), + () -> RESOURCE_MODEL_BLDR() + .vPCSecurityGroups(ImmutableList.of("securityGroupId")) + .build(), + () -> RESOURCE_MODEL_BLDR() + .vPCSecurityGroups(ImmutableList.of("securityGroupId")) + .build(), + expectSuccess() + ); + + // Expect that the security group has NOT been updated + verify(ec2Proxy.client(), never()).describeSecurityGroups(any(DescribeSecurityGroupsRequest.class)); + verify(rdsProxy.client(), times(2)).describeDBInstances(any(DescribeDbInstancesRequest.class)); + } + + @Test + public void handleRequest_SetDefaultVpcId_previousNull_desiredNotNull() { + final CallbackContext context = new CallbackContext(); + context.setUpdated(true); // this is an emulation of a re-entrance + context.setStorageAllocated(true); + + // When the previous.vPCSecurityGroups == null && desired.vPCSecurityGroups != null + test_handleRequest_base( + context, + () -> DB_INSTANCE_ACTIVE.toBuilder().dbSubnetGroup( + DBSubnetGroup.builder().vpcId(DB_SECURITY_GROUP_VPC_ID).build() + ).build(), + () -> RESOURCE_MODEL_BLDR() + .vPCSecurityGroups(Collections.emptyList()) + .build(), + () -> RESOURCE_MODEL_BLDR() + .vPCSecurityGroups(ImmutableList.of("securityGroupId")) + .build(), + expectSuccess() + ); + + // Expect that the security group has NOT been updated + verify(ec2Proxy.client(), never()).describeSecurityGroups(any(DescribeSecurityGroupsRequest.class)); + verify(rdsProxy.client(), times(2)).describeDBInstances(any(DescribeDbInstancesRequest.class)); + } + + @Test + public void handleRequest_SetDefaultVpcId_previousNotNull_desiredNull() { final DescribeSecurityGroupsResponse describeSecurityGroupsResponse = DescribeSecurityGroupsResponse.builder() - .securityGroups(SecurityGroup.builder().groupName(DB_SECURITY_GROUP_DEFAULT).groupId(DB_SECURITY_GROUP_ID).build()) - .build(); + .securityGroups(SecurityGroup.builder().groupName(DB_SECURITY_GROUP_DEFAULT).groupId(DB_SECURITY_GROUP_ID).build()) + .build(); when(ec2Proxy.client().describeSecurityGroups(any(DescribeSecurityGroupsRequest.class))).thenReturn(describeSecurityGroupsResponse); final CallbackContext context = new CallbackContext(); context.setUpdated(true); // this is an emulation of a re-entrance context.setStorageAllocated(true); + // When the previous.vPCSecurityGroups != null && desired.vPCSecurityGroups == null test_handleRequest_base( - context, - () -> DB_INSTANCE_ACTIVE.toBuilder().dbSubnetGroup( - DBSubnetGroup.builder().vpcId(DB_SECURITY_GROUP_VPC_ID).build() - ).build(), - () -> RESOURCE_MODEL_BLDR().build(), - () -> RESOURCE_MODEL_BLDR() - .vPCSecurityGroups(Collections.emptyList()) - .build(), - expectSuccess() + context, + () -> DB_INSTANCE_ACTIVE.toBuilder() + .dbSubnetGroup(DBSubnetGroup.builder().vpcId(DB_SECURITY_GROUP_VPC_ID).build() + ).build(), + () -> RESOURCE_MODEL_BLDR() + .vPCSecurityGroups(ImmutableList.of("securityGroupId")) + .build(), + () -> RESOURCE_MODEL_BLDR() + .vPCSecurityGroups(Collections.emptyList()) + .build(), + expectSuccess() ); + // Expect that the security group has been updated verify(ec2Proxy.client()).describeSecurityGroups(any(DescribeSecurityGroupsRequest.class)); verify(rdsProxy.client(), times(3)).describeDBInstances(any(DescribeDbInstancesRequest.class)); } + @Test + public void handleRequest_SetDefaultVpcId_previousNull_desiredNull() { + final CallbackContext context = new CallbackContext(); + context.setUpdated(true); // this is an emulation of a re-entrance + context.setStorageAllocated(true); + + // When the previous.vPCSecurityGroups == null && desired.vPCSecurityGroups == null + test_handleRequest_base( + context, + () -> DB_INSTANCE_ACTIVE.toBuilder().dbSubnetGroup( + DBSubnetGroup.builder().vpcId(DB_SECURITY_GROUP_VPC_ID).build() + ).build(), + () -> RESOURCE_MODEL_BLDR() + .vPCSecurityGroups(Collections.emptyList()) + .build(), + () -> RESOURCE_MODEL_BLDR() + .vPCSecurityGroups(Collections.emptyList()) + .build(), + expectSuccess() + ); + + // Expect that the security group has NOT been updated + verify(ec2Proxy.client(), never()).describeSecurityGroups(any(DescribeSecurityGroupsRequest.class)); + verify(rdsProxy.client(), times(2)).describeDBInstances(any(DescribeDbInstancesRequest.class)); + } + @Test public void handleRequest_NoDefaultVpcIdForClusterInstance() { final CallbackContext context = new CallbackContext(); @@ -1218,7 +1303,7 @@ public void handleRequest_EmptyVpcSecurityGroupIdList() { expectSuccess() ); - verify(rdsProxy.client(), times(3)).describeDBInstances(any(DescribeDbInstancesRequest.class)); + verify(rdsProxy.client(), times(4)).describeDBInstances(any(DescribeDbInstancesRequest.class)); ArgumentCaptor captor = ArgumentCaptor.forClass(ModifyDbInstanceRequest.class); verify(rdsProxy.client(), times(1)).modifyDBInstance(captor.capture()); @@ -1248,6 +1333,10 @@ public void handleRequest_ModifyDBInstance_HandleException( final Object requestException, final HandlerErrorCode expectResponseCode ) { + final DBInstance fakeDBInstance = DBInstance.builder().build(); + when(rdsProxy.client().describeDBInstances(any(DescribeDbInstancesRequest.class))) + .thenReturn(DescribeDbInstancesResponse.builder().dbInstances(fakeDBInstance).build()); + final CallbackContext context = new CallbackContext(); context.setStorageAllocated(true); @@ -1359,7 +1448,7 @@ public void handleRequest_SetEngineVersionIfChanged() { expectSuccess() ); - verify(rdsProxy.client(), times(3)).describeDBInstances(any(DescribeDbInstancesRequest.class)); + verify(rdsProxy.client(), times(4)).describeDBInstances(any(DescribeDbInstancesRequest.class)); final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(ModifyDbInstanceRequest.class); verify(rdsProxy.client(), times(1)).modifyDBInstance(argumentCaptor.capture()); @@ -1390,7 +1479,7 @@ public void handleRequest_UnsetEngineVersionIfNoChange() { expectSuccess() ); - verify(rdsProxy.client(), times(3)).describeDBInstances(any(DescribeDbInstancesRequest.class)); + verify(rdsProxy.client(), times(4)).describeDBInstances(any(DescribeDbInstancesRequest.class)); final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(ModifyDbInstanceRequest.class); verify(rdsProxy.client(), times(1)).modifyDBInstance(argumentCaptor.capture()); @@ -1605,7 +1694,7 @@ public void handleRequest_FetchEventsFromUpdateMoment() { verify(rdsProxy.client(), times(1)).modifyDBInstance(any(ModifyDbInstanceRequest.class)); ArgumentCaptor describeEventsCaptor = ArgumentCaptor.forClass(DescribeEventsRequest.class); verify(rdsProxy.client(), times(1)).describeEvents(describeEventsCaptor.capture()); - verify(rdsProxy.client(), times(3)).describeDBInstances(any(DescribeDbInstancesRequest.class)); + verify(rdsProxy.client(), times(4)).describeDBInstances(any(DescribeDbInstancesRequest.class)); Assertions.assertThat(describeEventsCaptor.getValue().startTime()).isEqualTo(updatedAt); } @@ -1738,7 +1827,7 @@ public void handleRequest_ObserveFailureEvent() { verify(rdsProxy.client(), times(1)).modifyDBInstance(any(ModifyDbInstanceRequest.class)); verify(rdsProxy.client(), times(1)).describeEvents(any(DescribeEventsRequest.class)); - verify(rdsProxy.client(), times(2)).describeDBInstances(any(DescribeDbInstancesRequest.class)); + verify(rdsProxy.client(), times(3)).describeDBInstances(any(DescribeDbInstancesRequest.class)); } @Test @@ -1753,26 +1842,34 @@ public void handleRequest_startAutomaticBackupReplication() { context.setAutomaticBackupReplicationStopped(true); proxy = Mockito.spy(proxy); - final RdsClient crossRegionRdsClient = mock(RdsClient.class); final ProxyClient crossRegionRdsProxy = mockProxy(proxy, crossRegionRdsClient); doReturn(crossRegionRdsProxy).when(proxy).newProxy(ArgumentMatchers.>any()); + when(crossRegionRdsProxy.client().describeDBInstanceAutomatedBackups(any(DescribeDbInstanceAutomatedBackupsRequest.class))) + .thenReturn(DescribeDbInstanceAutomatedBackupsResponse.builder() + .dbInstanceAutomatedBackups(Collections.singletonList(DBInstanceAutomatedBackup.builder() + .dbInstanceAutomatedBackupsArn( + getAutomaticBackupArn(AUTOMATIC_BACKUP_REPLICATION_REGION)) + .backupRetentionPeriod(AUTOMATIC_BACKUP_REPLICATION_RETENTION_PERIOD).build())) + .build()); + test_handleRequest_base( context, () -> DB_INSTANCE_ACTIVE.toBuilder() .dbInstanceAutomatedBackupsReplications(Collections.singletonList(DBInstanceAutomatedBackupsReplication.builder() - .dbInstanceAutomatedBackupsArn(getAutomaticBackupArn(AUTOMATIC_BACKUP_REPLICATION_REGION_ALTER)).build())) + .dbInstanceAutomatedBackupsArn(getAutomaticBackupArn(AUTOMATIC_BACKUP_REPLICATION_REGION)).build())) .build(), () -> RESOURCE_MODEL_BLDR() - .automaticBackupReplicationRegion(AUTOMATIC_BACKUP_REPLICATION_REGION) + .automaticBackupReplicationRegion("") .build(), () -> RESOURCE_MODEL_BLDR() - .automaticBackupReplicationRegion(AUTOMATIC_BACKUP_REPLICATION_REGION_ALTER) + .automaticBackupReplicationRegion(AUTOMATIC_BACKUP_REPLICATION_REGION) .build(), expectSuccess() ); + verify(crossRegionRdsProxy.client(), times(0)).stopDBInstanceAutomatedBackupsReplication(any(StopDbInstanceAutomatedBackupsReplicationRequest.class)); verify(crossRegionRdsProxy.client(), times(1)).startDBInstanceAutomatedBackupsReplication(any(StartDbInstanceAutomatedBackupsReplicationRequest.class)); verify(crossRegionRdsProxy.client(), atLeastOnce()).serviceName(); verifyNoMoreInteractions(crossRegionRdsProxy.client()); @@ -1805,18 +1902,17 @@ public void handleRequest_stopAutomaticBackupReplication() { .automaticBackupReplicationRegion(AUTOMATIC_BACKUP_REPLICATION_REGION) .build(), () -> RESOURCE_MODEL_BLDR() - .automaticBackupReplicationRegion(AUTOMATIC_BACKUP_REPLICATION_REGION_ALTER) .build(), expectSuccess() ); verify(crossRegionRdsProxy.client(), times(1)).stopDBInstanceAutomatedBackupsReplication(any(StopDbInstanceAutomatedBackupsReplicationRequest.class)); + verify(crossRegionRdsProxy.client(), times(0)).startDBInstanceAutomatedBackupsReplication(any(StartDbInstanceAutomatedBackupsReplicationRequest.class)); verify(crossRegionRdsProxy.client(), atLeastOnce()).serviceName(); verifyAccessPermissions(crossRegionRdsProxy.client()); verifyNoMoreInteractions(crossRegionRdsProxy.client()); verify(rdsProxy.client(), times(4)).describeDBInstances(any(DescribeDbInstancesRequest.class)); } - @Test public void handleRequest_updateStorageTypeFromIo1ToIo2() { final CallbackContext context = new CallbackContext(); @@ -1850,7 +1946,7 @@ public void handleRequest_updateStorageTypeFromIo1ToIo2() { expectSuccess() ); - verify(rdsProxy.client(), times(3)).describeDBInstances(any(DescribeDbInstancesRequest.class)); + verify(rdsProxy.client(), times(4)).describeDBInstances(any(DescribeDbInstancesRequest.class)); final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(ModifyDbInstanceRequest.class); verify(rdsProxy.client()).modifyDBInstance(argumentCaptor.capture()); @@ -1883,4 +1979,26 @@ public void handleRequest_EngineLifecycleSupportShouldFail() { expectFailed(HandlerErrorCode.InvalidRequest) ); } + + @ParameterizedTest + @ArgumentsSource(ThrottleExceptionArgumentsProvider.class) + public void handleRequest_ModifyDBInstance_HandleThrottleException( + final Object requestException + ) { + final DBInstance fakeDBInstance = DBInstance.builder().build(); + when(rdsProxy.client().describeDBInstances(any(DescribeDbInstancesRequest.class))) + .thenReturn(DescribeDbInstancesResponse.builder().dbInstances(fakeDBInstance).build()); + + final CallbackContext context = new CallbackContext(); + context.setStorageAllocated(true); + + test_handleRequest_throttle( + expectModifyDBInstanceCall(), + context, + () -> RESOURCE_MODEL_BLDR().build(), + () -> RESOURCE_MODEL_ALTER, + requestException, + CALLBACK_DELAY + ); + } } diff --git a/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/util/ResourceModelHelperTest.java b/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/util/ResourceModelHelperTest.java index 526db4028..4ec505438 100644 --- a/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/util/ResourceModelHelperTest.java +++ b/aws-rds-dbinstance/src/test/java/software/amazon/rds/dbinstance/util/ResourceModelHelperTest.java @@ -1,13 +1,17 @@ package software.amazon.rds.dbinstance.util; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; - +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; - import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; import software.amazon.rds.dbinstance.ResourceModel; +import java.util.stream.Stream; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + public class ResourceModelHelperTest { @Test @@ -298,14 +302,6 @@ public void shouldNotUpdateAfterCreate_whenRestoreFromOracleCDBEESnapshotAndCDBE assertThat(ResourceModelHelper.shouldUpdateAfterCreate(model, "oracle-ee-cdb")).isFalse(); } - @Test - public void getBackupRetentionPeriod_returnsZeroWhenNotSet() { - final ResourceModel model = ResourceModel.builder() - .build(); - - assertThat(ResourceModelHelper.getBackupRetentionPeriod(model)).isEqualTo(0); - } - @Test public void getBackupRetentionPeriod_returnsValueWhenSet() { final ResourceModel model = ResourceModel.builder() @@ -315,15 +311,6 @@ public void getBackupRetentionPeriod_returnsValueWhenSet() { assertThat(ResourceModelHelper.getBackupRetentionPeriod(model)).isEqualTo(10); } - @Test - public void getAutomaticBackupReplicationRegion_returnsNullWhenBackupReplicationIsZero() { - final ResourceModel model = ResourceModel.builder() - .automaticBackupReplicationRegion("eu-west-1") - .build(); - - assertThat(ResourceModelHelper.getAutomaticBackupReplicationRegion(model)).isNull(); - } - @Test public void getAutomaticBackupReplicationRegion_returnsValueWhenBackupReplicationIsZero() { final ResourceModel model = ResourceModel.builder() @@ -379,7 +366,7 @@ public void shouldStartAutomaticBackupReplication_returnsTrueWhenBackupRetention } @Test - public void shouldStartAutomaticBackupReplication_returnsFalseWhenBackupRetentionPeriodChangedFromOneToTwo() { + public void shouldStartAutomaticBackupReplication_returnsTrueWhenBackupRetentionPeriodChangedFromOneToTwo() { final ResourceModel previous = ResourceModel.builder() .automaticBackupReplicationRegion("eu-west-1") .backupRetentionPeriod(1) @@ -390,7 +377,7 @@ public void shouldStartAutomaticBackupReplication_returnsFalseWhenBackupRetentio .backupRetentionPeriod(2) .build(); - assertThat(ResourceModelHelper.shouldStartAutomaticBackupReplication(previous, desired)).isFalse(); + assertThat(ResourceModelHelper.shouldStartAutomaticBackupReplication(previous, desired)).isTrue(); } @Test @@ -458,7 +445,7 @@ public void shouldStopAutomaticBackupReplication_returnsTrueWhenRegionChanged() } @Test - public void shouldStopAutomaticBackupReplication_returnsTrueWhenBackupRetentionPeriodChangedFromValueToNull() { + public void shouldStopAutomaticBackupReplication_returnsFalseWhenBackupRetentionPeriodChangedFromValueToNull() { final ResourceModel previous = ResourceModel.builder() .automaticBackupReplicationRegion("eu-west-1") .backupRetentionPeriod(10) @@ -468,11 +455,11 @@ public void shouldStopAutomaticBackupReplication_returnsTrueWhenBackupRetentionP .automaticBackupReplicationRegion("eu-west-1") .build(); - assertThat(ResourceModelHelper.shouldStopAutomaticBackupReplication(previous, desired)).isTrue(); + assertThat(ResourceModelHelper.shouldStopAutomaticBackupReplication(previous, desired)).isFalse(); } @Test - public void shouldStopAutomaticBackupReplication_returnsFalseWhenBackupRetentionPeriodChangedFromOneToTwo() { + public void shouldStopAutomaticBackupReplication_returnsTrueWhenBackupRetentionPeriodChangedFromOneToTwo() { final ResourceModel previous = ResourceModel.builder() .automaticBackupReplicationRegion("eu-west-1") .backupRetentionPeriod(1) @@ -483,7 +470,7 @@ public void shouldStopAutomaticBackupReplication_returnsFalseWhenBackupRetention .backupRetentionPeriod(2) .build(); - assertThat(ResourceModelHelper.shouldStopAutomaticBackupReplication(previous, desired)).isFalse(); + assertThat(ResourceModelHelper.shouldStopAutomaticBackupReplication(previous, desired)).isTrue(); } @Test @@ -499,4 +486,23 @@ public void shouldStopAutomaticBackupReplication_returnsTrueWhenRegionSetFromVal assertThat(ResourceModelHelper.shouldStopAutomaticBackupReplication(previous, desired)).isTrue(); } + + private static Stream getApplyImmediatelyTestCases() { + return Stream.of( + Arguments.of(null, Boolean.TRUE), + Arguments.of(Boolean.TRUE, Boolean.TRUE), + Arguments.of(Boolean.FALSE, Boolean.FALSE) + ); + } + + @ParameterizedTest() + @MethodSource("getApplyImmediatelyTestCases") + public void test_ShouldApplyImmediately(Boolean input, Boolean expected) { + final ResourceModel model = ResourceModel.builder() + .applyImmediately(input) + .build(); + + Boolean actualValue = ResourceModelHelper.shouldApplyImmediately(model); + Assertions.assertThat(actualValue).isEqualTo(expected); + } } diff --git a/aws-rds-dbparametergroup/docs/README.md b/aws-rds-dbparametergroup/docs/README.md deleted file mode 100644 index 3ec855425..000000000 --- a/aws-rds-dbparametergroup/docs/README.md +++ /dev/null @@ -1,95 +0,0 @@ -# AWS::RDS::DBParameterGroup - -The AWS::RDS::DBParameterGroup resource creates a custom parameter group for an RDS database family - -## Syntax - -To declare this entity in your AWS CloudFormation template, use the following syntax: - -### JSON - -
-{
-    "Type" : "AWS::RDS::DBParameterGroup",
-    "Properties" : {
-        "DBParameterGroupName" : String,
-        "Description" : String,
-        "Family" : String,
-        "Parameters" : Map,
-        "Tags" : [ Tag, ... ]
-    }
-}
-
- -### YAML - -
-Type: AWS::RDS::DBParameterGroup
-Properties:
-    DBParameterGroupName: String
-    Description: String
-    Family: String
-    Parameters: Map
-    Tags: 
-      - Tag
-
- -## Properties - -#### DBParameterGroupName - -Specifies the name of the DB parameter group - -_Required_: No - -_Type_: String - -_Pattern_: ^[a-zA-Z]{1}(?:-?[a-zA-Z0-9])*$ - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### Description - -Provides the customer-specified description for this DB parameter group. - -_Required_: Yes - -_Type_: String - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### Family - -The DB parameter group family name. - -_Required_: Yes - -_Type_: String - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### Parameters - -An array of parameter names and values for the parameter update. - -_Required_: No - -_Type_: Map - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### Tags - -An array of key-value pairs to apply to this resource. - -_Required_: No - -_Type_: List of Tag - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -## Return Values - -### Ref - -When you pass the logical ID of this resource to the intrinsic `Ref` function, Ref returns the DBParameterGroupName. diff --git a/aws-rds-dbparametergroup/docs/tag.md b/aws-rds-dbparametergroup/docs/tag.md deleted file mode 100644 index a7fa40fe0..000000000 --- a/aws-rds-dbparametergroup/docs/tag.md +++ /dev/null @@ -1,51 +0,0 @@ -# AWS::RDS::DBParameterGroup Tag - -A key-value pair to associate with a resource. - -## Syntax - -To declare this entity in your AWS CloudFormation template, use the following syntax: - -### JSON - -
-{
-    "Key" : String,
-    "Value" : String
-}
-
- -### YAML - -
-Key: String
-Value: String
-
- -## Properties - -#### Key - -The key name of the tag. You can specify a value that is 1 to 128 Unicode characters in length and cannot be prefixed with aws:. You can use any of the following characters: the set of Unicode letters, digits, whitespace, _, ., /, =, +, and -. - -_Required_: Yes - -_Type_: String - -_Minimum Length_: 1 - -_Maximum Length_: 128 - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### Value - -The value for the tag. You can specify a value that is 0 to 256 Unicode characters in length and cannot be prefixed with aws:. You can use any of the following characters: the set of Unicode letters, digits, whitespace, _, ., /, =, +, and -. - -_Required_: No - -_Type_: String - -_Maximum Length_: 256 - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) diff --git a/aws-rds-dbparametergroup/pom.xml b/aws-rds-dbparametergroup/pom.xml index 64cfd9daf..fe91946e8 100644 --- a/aws-rds-dbparametergroup/pom.xml +++ b/aws-rds-dbparametergroup/pom.xml @@ -20,23 +20,6 @@ - - - software.amazon.awssdk - aws-query-protocol - 2.20.138 - - - software.amazon.awssdk - rds - 2.25.56 - - - - software.amazon.cloudformation - aws-cloudformation-rpdk-java-plugin - [2.0.0,3.0.0) - org.projectlombok @@ -85,6 +68,11 @@ 1.0 test + + software.amazon.cloudformation + aws-cloudformation-rpdk-java-plugin + [2.0.0,3.0.0) + @@ -169,10 +157,25 @@ 0.8.8 - **/BaseConfiguration* - **/BaseHandler* - **/HandlerWrapper* - **/ResourceModel* + + **/software/amazon/rds/dbparametergroup/BaseConfiguration.class + **/software/amazon/rds/dbparametergroup/BaseHandler.class + **/software/amazon/rds/dbparametergroup/BaseHandlerStd.class + **/software/amazon/rds/dbparametergroup/CallbackContext.class + **/software/amazon/rds/dbparametergroup/ClientProvider.class + **/software/amazon/rds/dbparametergroup/Configuration.class + **/software/amazon/rds/dbparametergroup/CreateHandler.class + **/software/amazon/rds/dbparametergroup/DeleteHandler.class + **/software/amazon/rds/dbparametergroup/HandlerWrapper.class + **/software/amazon/rds/dbparametergroup/HandlerWrapperExecutable.class + **/software/amazon/rds/dbparametergroup/ListHandler.class + **/software/amazon/rds/dbparametergroup/ParameterType.class + **/software/amazon/rds/dbparametergroup/ReadHandler.class + **/software/amazon/rds/dbparametergroup/ResourceModel.class + **/software/amazon/rds/dbparametergroup/Tag.class + **/software/amazon/rds/dbparametergroup/Translator.class + **/software/amazon/rds/dbparametergroup/TypeConfigurationModel.class + **/software/amazon/rds/dbparametergroup/UpdateHandler.class @@ -196,7 +199,7 @@ - PACKAGE + BUNDLE BRANCH @@ -206,7 +209,7 @@ INSTRUCTION COVEREDRATIO - 0.8 + 0.0 diff --git a/aws-rds-dbparametergroup/src/main/java/software/amazon/rds/dbparametergroup/BaseHandlerStd.java b/aws-rds-dbparametergroup/src/main/java/software/amazon/rds/dbparametergroup/BaseHandlerStd.java index 5e5efdb55..9698dc0af 100644 --- a/aws-rds-dbparametergroup/src/main/java/software/amazon/rds/dbparametergroup/BaseHandlerStd.java +++ b/aws-rds-dbparametergroup/src/main/java/software/amazon/rds/dbparametergroup/BaseHandlerStd.java @@ -21,6 +21,7 @@ import com.google.common.collect.Maps; import lombok.NonNull; import software.amazon.awssdk.services.rds.RdsClient; +import software.amazon.awssdk.services.rds.model.DBParameterGroup; import software.amazon.awssdk.services.rds.model.DbParameterGroupAlreadyExistsException; import software.amazon.awssdk.services.rds.model.DbParameterGroupNotFoundException; import software.amazon.awssdk.services.rds.model.DbParameterGroupQuotaExceededException; @@ -573,6 +574,21 @@ protected ProgressEvent describeEngineDefaultPar return progress; } + protected DBParameterGroup fetchDbParameterGroup( + final ProxyClient proxyClient, + final ResourceModel model + ) { + try { + final var response = proxyClient.injectCredentialsAndInvokeV2( + Translator.describeDbParameterGroupsRequest(model), + proxyClient.client()::describeDBParameterGroups + ); + return response.dbParameterGroups().stream().findFirst().orElse(null); + } catch (final DbParameterGroupNotFoundException e) { + return null; + } + } + @VisibleForTesting static Map computeModifiedDBParameters( @NonNull final Map engineDefaultParameters, diff --git a/aws-rds-dbparametergroup/src/main/java/software/amazon/rds/dbparametergroup/CallbackContext.java b/aws-rds-dbparametergroup/src/main/java/software/amazon/rds/dbparametergroup/CallbackContext.java index b62684c4e..bdccb4dfc 100644 --- a/aws-rds-dbparametergroup/src/main/java/software/amazon/rds/dbparametergroup/CallbackContext.java +++ b/aws-rds-dbparametergroup/src/main/java/software/amazon/rds/dbparametergroup/CallbackContext.java @@ -3,6 +3,7 @@ import software.amazon.cloudformation.proxy.StdCallbackContext; import software.amazon.rds.common.handler.TaggingContext; import software.amazon.rds.common.handler.TimestampContext; +import software.amazon.rds.common.util.IdempotencyHelper; import java.time.Duration; import java.time.Instant; @@ -13,7 +14,9 @@ @lombok.Setter @lombok.ToString @lombok.EqualsAndHashCode(callSuper = true) -public class CallbackContext extends StdCallbackContext implements TaggingContext.Provider, TimestampContext.Provider { +public class CallbackContext extends StdCallbackContext implements TaggingContext.Provider, TimestampContext.Provider, + IdempotencyHelper.PreExistenceContext { + private Boolean preExistenceCheckDone; private boolean parametersApplied; private String dbParameterGroupArn; diff --git a/aws-rds-dbparametergroup/src/main/java/software/amazon/rds/dbparametergroup/CreateHandler.java b/aws-rds-dbparametergroup/src/main/java/software/amazon/rds/dbparametergroup/CreateHandler.java index a267dce7e..f408de13f 100644 --- a/aws-rds-dbparametergroup/src/main/java/software/amazon/rds/dbparametergroup/CreateHandler.java +++ b/aws-rds-dbparametergroup/src/main/java/software/amazon/rds/dbparametergroup/CreateHandler.java @@ -14,6 +14,7 @@ import software.amazon.rds.common.handler.HandlerMethod; import software.amazon.rds.common.handler.Tagging; import software.amazon.rds.common.logging.RequestLogger; +import software.amazon.rds.common.util.IdempotencyHelper; import software.amazon.rds.common.util.IdentifierFactory; public class CreateHandler extends BaseHandlerStd { @@ -39,6 +40,7 @@ protected ProgressEvent handleRequest( final CallbackContext callbackContext ) { final ResourceModel desiredModel = request.getDesiredResourceState(); + setDBParameterGroupNameIfEmpty(desiredModel, request); final Tagging.TagSet allTags = Tagging.TagSet.builder() .systemTags(Tagging.translateTagsToSdk(request.getSystemTags())) @@ -49,8 +51,13 @@ protected ProgressEvent handleRequest( final Map desiredParams = request.getDesiredResourceState().getParameters(); return ProgressEvent.progress(desiredModel, callbackContext) - .then(progress -> setDBParameterGroupNameIfEmpty(request, progress)) - .then(progress -> safeCreateDBParameterGroup(proxy, proxyClient, progress, allTags, requestLogger)) + .then(progress -> IdempotencyHelper.safeCreate( + model -> fetchDbParameterGroup(proxyClient, model), + p -> safeCreateDBParameterGroup(proxy, proxyClient, p, allTags, requestLogger), + ResourceModel.TYPE_NAME, + desiredModel.getDBParameterGroupName(), + progress, + requestLogger)) .then(progress -> applyParameters(proxy, proxyClient, progress, desiredParams)) .then(progress -> new ReadHandler().handleRequest(proxy, proxyClient, request, callbackContext, requestLogger)); } @@ -96,17 +103,13 @@ private ProgressEvent createDBParameterGroup( }); } - private ProgressEvent setDBParameterGroupNameIfEmpty( - final ResourceHandlerRequest request, - final ProgressEvent progress - ) { - if (StringUtils.isNullOrEmpty(progress.getResourceModel().getDBParameterGroupName())) { - progress.getResourceModel().setDBParameterGroupName(groupIdentifierFactory.newIdentifier() + private void setDBParameterGroupNameIfEmpty(ResourceModel model, ResourceHandlerRequest request) { + if (StringUtils.isNullOrEmpty(model.getDBParameterGroupName())) { + model.setDBParameterGroupName(groupIdentifierFactory.newIdentifier() .withStackId(request.getStackId()) .withResourceId(request.getLogicalResourceIdentifier()) .withRequestToken(request.getClientRequestToken()) .toString()); } - return progress; } } diff --git a/aws-rds-dbparametergroup/src/test/java/software/amazon/rds/dbparametergroup/CreateHandlerTest.java b/aws-rds-dbparametergroup/src/test/java/software/amazon/rds/dbparametergroup/CreateHandlerTest.java index 853b448fb..53ccf8756 100644 --- a/aws-rds-dbparametergroup/src/test/java/software/amazon/rds/dbparametergroup/CreateHandlerTest.java +++ b/aws-rds-dbparametergroup/src/test/java/software/amazon/rds/dbparametergroup/CreateHandlerTest.java @@ -42,6 +42,7 @@ import software.amazon.cloudformation.proxy.ProgressEvent; import software.amazon.cloudformation.proxy.ProxyClient; import software.amazon.cloudformation.proxy.ResourceHandlerRequest; +import software.amazon.rds.common.util.IdempotencyHelper; import software.amazon.rds.test.common.core.HandlerName; import software.amazon.rds.test.common.verification.AccessPermissionAlias; import software.amazon.rds.test.common.verification.AccessPermissionFactory; @@ -71,6 +72,7 @@ public void setup() { proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); rdsClient = mock(RdsClient.class); proxyClient = MOCK_PROXY(proxy, rdsClient); + IdempotencyHelper.setBypass(true); } @AfterEach diff --git a/aws-rds-dbshardgroup/.gitignore b/aws-rds-dbshardgroup/.gitignore new file mode 100644 index 000000000..12a8ab9b9 --- /dev/null +++ b/aws-rds-dbshardgroup/.gitignore @@ -0,0 +1,28 @@ +# macOS +.DS_Store +._* + +# Maven outputs +.classpath +/aws-rds-dbshardgroup.zip + +# IntelliJ +*.iml +.idea +out.java +out/ +.settings +.project + +# auto-generated files +target/ +/build/ + +# our logs +rpdk.log + +# contains credentials +sam-tests/ + +# auto-generated sam file +.aws-sam/build.toml diff --git a/aws-rds-dbshardgroup/.rpdk-config b/aws-rds-dbshardgroup/.rpdk-config new file mode 100644 index 000000000..f9d042441 --- /dev/null +++ b/aws-rds-dbshardgroup/.rpdk-config @@ -0,0 +1,22 @@ +{ + "artifact_type": "RESOURCE", + "typeName": "AWS::RDS::DBShardGroup", + "language": "java", + "runtime": "java17", + "entrypoint": "software.amazon.rds.dbshardgroup.HandlerWrapper::handleRequest", + "testEntrypoint": "software.amazon.rds.dbshardgroup.HandlerWrapper::testEntrypoint", + "settings": { + "namespace": [ + "software", + "amazon", + "rds", + "dbshardgroup" + ], + "codegen_template_path": "guided_aws", + "protocolVersion": "2.0.0" + }, + "logProcessorEnabled": "true", + "executableEntrypoint": "software.amazon.rds.dbshardgroup.HandlerWrapperExecutable", + "contractSettings": {}, + "canarySettings": {} +} diff --git a/aws-rds-dbshardgroup/aws-rds-dbshardgroup.json b/aws-rds-dbshardgroup/aws-rds-dbshardgroup.json new file mode 100644 index 000000000..e5e8f8c9f --- /dev/null +++ b/aws-rds-dbshardgroup/aws-rds-dbshardgroup.json @@ -0,0 +1,155 @@ +{ + "typeName": "AWS::RDS::DBShardGroup", + "description": "The AWS::RDS::DBShardGroup resource creates an Amazon Aurora Limitless DB Shard Group.", + "sourceUrl": "https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-rds", + "tagging": { + "cloudFormationSystemTags": true, + "permissions": [ + "rds:AddTagsToResource", + "rds:RemoveTagsFromResource" + ], + "taggable": true, + "tagOnCreate": true, + "tagUpdatable": true, + "tagProperty": "/properties/Tags" + }, + "properties": { + "DBShardGroupResourceId": { + "description": "The Amazon Web Services Region-unique, immutable identifier for the DB shard group.", + "type": "string" + }, + "DBShardGroupIdentifier": { + "description": "The name of the DB shard group.", + "type": "string", + "minLength": 1, + "maxLength": 63 + }, + "DBClusterIdentifier": { + "description": "The name of the primary DB cluster for the DB shard group.", + "type": "string", + "minLength": 1, + "maxLength": 63 + }, + "ComputeRedundancy": { + "description": "Specifies whether to create standby instances for the DB shard group.", + "minimum": 0, + "type": "integer" + }, + "MaxACU": { + "description": "The maximum capacity of the DB shard group in Aurora capacity units (ACUs).", + "type": "number" + }, + "MinACU": { + "description": "The minimum capacity of the DB shard group in Aurora capacity units (ACUs).", + "type": "number" + }, + "PubliclyAccessible": { + "description": "Indicates whether the DB shard group is publicly accessible.", + "type": "boolean" + }, + "Endpoint": { + "description": "The connection endpoint for the DB shard group.", + "type": "string" + }, + "Tags": { + "type": "array", + "maxItems": 50, + "uniqueItems": true, + "insertionOrder": false, + "description": "An array of key-value pairs to apply to this resource.", + "items": { + "$ref": "#/definitions/Tag" + } + } + }, + "definitions": { + "Tag": { + "description": "A key-value pair to associate with a resource.", + "type": "object", + "additionalProperties": false, + "properties": { + "Key": { + "type": "string", + "description": "The key name of the tag. You can specify a value that is 1 to 128 Unicode characters in length and cannot be prefixed with aws:. You can use any of the following characters: the set of Unicode letters, digits, whitespace, _, ., /, =, +, and -. ", + "minLength": 1, + "maxLength": 128 + }, + "Value": { + "type": "string", + "description": "The value for the tag. You can specify a value that is 0 to 256 Unicode characters in length and cannot be prefixed with aws:. You can use any of the following characters: the set of Unicode letters, digits, whitespace, _, ., /, =, +, and -. ", + "minLength": 0, + "maxLength": 256 + } + }, + "required": [ + "Key" + ] + } + }, + "additionalProperties": false, + "propertyTransform": { + "/properties/DBClusterIdentifier": "$lowercase(DBClusterIdentifier)", + "/properties/DBShardGroupIdentifier": "$lowercase(DBShardGroupIdentifier)" + }, + "required": [ + "DBClusterIdentifier", + "MaxACU" + ], + "createOnlyProperties": [ + "/properties/DBClusterIdentifier", + "/properties/DBShardGroupIdentifier", + "/properties/PubliclyAccessible" + ], + "readOnlyProperties": [ + "/properties/DBShardGroupResourceId", + "/properties/Endpoint" + ], + "writeOnlyProperties": [ + "/properties/MinACU" + ], + "primaryIdentifier": [ + "/properties/DBShardGroupIdentifier" + ], + "handlers": { + "create": { + "permissions": [ + "rds:AddTagsToResource", + "rds:CreateDBShardGroup", + "rds:DescribeDBClusters", + "rds:DescribeDBShardGroups", + "rds:ListTagsForResource" + ], + "timeoutInMinutes": 2160 + }, + "read": { + "permissions": [ + "rds:DescribeDBShardGroups", + "rds:ListTagsForResource" + ] + }, + "update": { + "permissions": [ + "rds:AddTagsToResource", + "rds:DescribeDBShardGroups", + "rds:DescribeDBClusters", + "rds:RemoveTagsFromResource", + "rds:ModifyDBShardGroup", + "rds:ListTagsForResource" + ] + }, + "delete": { + "permissions": [ + "rds:DeleteDBShardGroup", + "rds:DescribeDBClusters", + "rds:DescribeDbShardGroups" + ], + "timeoutInMinutes": 2160 + }, + "list": { + "permissions": [ + "rds:DescribeDBShardGroups", + "rds:ListTagsForResource" + ] + } + } +} diff --git a/aws-rds-dbshardgroup/lombok.config b/aws-rds-dbshardgroup/lombok.config new file mode 100644 index 000000000..7a21e8804 --- /dev/null +++ b/aws-rds-dbshardgroup/lombok.config @@ -0,0 +1 @@ +lombok.addLombokGeneratedAnnotation = true diff --git a/aws-rds-dbshardgroup/pom.xml b/aws-rds-dbshardgroup/pom.xml new file mode 100644 index 000000000..2df87657b --- /dev/null +++ b/aws-rds-dbshardgroup/pom.xml @@ -0,0 +1,220 @@ + + + 4.0.0 + + software.amazon.rds.dbshardgroup + aws-rds-dbshardgroup-handler + aws-rds-dbshardgroup-handler + 1.0-SNAPSHOT + jar + + + 17 + 17 + UTF-8 + UTF-8 + 1.18.30 + + + + + + org.apache.commons + commons-lang3 + 3.12.0 + + + software.amazon.rds.common + aws-rds-cfn-common + 1.0 + + + software.amazon.cloudformation + aws-cloudformation-rpdk-java-plugin + [2.0.0,3.0.0) + + + + org.projectlombok + lombok + ${org.projectlombok.version} + provided + + + + org.assertj + assertj-core + 3.22.0 + test + + + + org.junit.jupiter + junit-jupiter + 5.8.2 + test + + + + org.mockito + mockito-core + 4.3.1 + test + + + + org.mockito + mockito-junit-jupiter + 4.3.1 + test + + + software.amazon.rds.common + aws-rds-cfn-test-common + 1.0 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.12.1 + + + -Xlint:all,-options,-processing + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.4 + + false + + + + package + + shade + + + + + + org.codehaus.mojo + exec-maven-plugin + 1.6.0 + + + generate + generate-sources + + exec + + + cfn + generate + ${project.basedir} + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.0.0 + + + add-source + generate-sources + + add-source + + + + ${project.basedir}/target/generated-sources/rpdk + + + + + + + org.apache.maven.plugins + maven-resources-plugin + 2.4 + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M3 + + + org.jacoco + jacoco-maven-plugin + 0.8.8 + + + **/BaseConfiguration* + **/BaseHandler* + **/HandlerWrapper* + **/ResourceModel* + + + + + + prepare-agent + + + + report + test + + report + + + + jacoco-check + + check + + + + + PACKAGE + + + BRANCH + COVEREDRATIO + 0.8 + + + INSTRUCTION + COVEREDRATIO + 0.8 + + + + + + + + + + + + ${project.basedir} + + aws-rds-dbshardgroup.json + + + + + diff --git a/aws-rds-dbshardgroup/resource-role.yaml b/aws-rds-dbshardgroup/resource-role.yaml new file mode 100644 index 000000000..631ae651e --- /dev/null +++ b/aws-rds-dbshardgroup/resource-role.yaml @@ -0,0 +1,46 @@ +AWSTemplateFormatVersion: "2010-09-09" +Description: > + This CloudFormation template creates a role assumed by CloudFormation + during CRUDL operations to mutate resources on behalf of the customer. + +Resources: + ExecutionRole: + Type: AWS::IAM::Role + Properties: + MaxSessionDuration: 43200 + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: resources.cloudformation.amazonaws.com + Action: sts:AssumeRole + Condition: + StringEquals: + aws:SourceAccount: + Ref: AWS::AccountId + StringLike: + aws:SourceArn: + Fn::Sub: arn:${AWS::Partition}:cloudformation:${AWS::Region}:${AWS::AccountId}:type/resource/AWS-RDS-DBShardGroup/* + Path: "/" + Policies: + - PolicyName: ResourceTypePolicy + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - "rds:AddTagsToResource" + - "rds:CreateDBShardGroup" + - "rds:DeleteDBShardGroup" + - "rds:DescribeDBClusters" + - "rds:DescribeDBShardGroups" + - "rds:DescribeDbShardGroups" + - "rds:ListTagsForResource" + - "rds:ModifyDBShardGroup" + - "rds:RemoveTagsFromResource" + Resource: "*" +Outputs: + ExecutionRoleArn: + Value: + Fn::GetAtt: ExecutionRole.Arn diff --git a/aws-rds-dbshardgroup/src/main/java/software/amazon/rds/dbshardgroup/BaseHandlerStd.java b/aws-rds-dbshardgroup/src/main/java/software/amazon/rds/dbshardgroup/BaseHandlerStd.java new file mode 100644 index 000000000..3b7f8fe27 --- /dev/null +++ b/aws-rds-dbshardgroup/src/main/java/software/amazon/rds/dbshardgroup/BaseHandlerStd.java @@ -0,0 +1,276 @@ +package software.amazon.rds.dbshardgroup; + +import com.amazonaws.arn.Arn; +import com.amazonaws.arn.ArnResource; +import java.util.Set; +import java.util.function.BiFunction; +import software.amazon.awssdk.services.rds.RdsClient; +import software.amazon.awssdk.services.rds.model.DBCluster; +import software.amazon.awssdk.services.rds.model.DBShardGroup; +import software.amazon.awssdk.services.rds.model.DbClusterNotFoundException; +import software.amazon.awssdk.services.rds.model.DbShardGroupAlreadyExistsException; +import software.amazon.awssdk.services.rds.model.DbShardGroupNotFoundException; +import software.amazon.awssdk.services.rds.model.InvalidDbClusterStateException; +import software.amazon.awssdk.services.rds.model.InvalidVpcNetworkStateException; +import software.amazon.awssdk.services.rds.model.MaxDbShardGroupLimitReachedException; +import software.amazon.awssdk.services.rds.model.Tag; +import software.amazon.awssdk.services.rds.model.UnsupportedDbEngineVersionException; +import software.amazon.awssdk.utils.ImmutableMap; +import software.amazon.cloudformation.exceptions.CfnNotFoundException; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.HandlerErrorCode; +import software.amazon.cloudformation.proxy.Logger; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.ProxyClient; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; +import software.amazon.cloudformation.proxy.delay.Constant; +import software.amazon.rds.common.error.ErrorRuleSet; +import software.amazon.rds.common.error.ErrorStatus; +import software.amazon.rds.common.handler.Commons; +import software.amazon.rds.common.handler.HandlerConfig; +import software.amazon.rds.common.handler.Tagging; +import software.amazon.rds.common.logging.LoggingProxyClient; +import software.amazon.rds.common.logging.RequestLogger; +import software.amazon.rds.common.printer.FilteredJsonPrinter; + +import java.time.Duration; +import java.util.Collection; + +public abstract class BaseHandlerStd extends BaseHandler { + protected final static HandlerConfig DEFAULT_DB_SHARD_GROUP_HANDLER_CONFIG = HandlerConfig.builder() + .backoff(Constant.of().delay(Duration.ofSeconds(30)).timeout(Duration.ofMinutes(180)).build()) + .build(); + + protected final static HandlerConfig DB_SHARD_GROUP_HANDLER_CONFIG_36H = HandlerConfig.builder() + .backoff(Constant.of().delay(Duration.ofSeconds(30)).timeout(Duration.ofHours(36)).build()) + .build(); + + protected static final ErrorRuleSet DEFAULT_DB_SHARD_GROUP_ERROR_RULE_SET = ErrorRuleSet + .extend(Commons.DEFAULT_ERROR_RULE_SET) + .withErrorClasses(ErrorStatus.failWith(HandlerErrorCode.AlreadyExists), + DbShardGroupAlreadyExistsException.class) + .withErrorClasses(ErrorStatus.failWith(HandlerErrorCode.NotFound), + DbShardGroupNotFoundException.class, + DbClusterNotFoundException.class) + .withErrorClasses(ErrorStatus.failWith(HandlerErrorCode.ServiceLimitExceeded), + MaxDbShardGroupLimitReachedException.class) + .withErrorClasses(ErrorStatus.failWith(HandlerErrorCode.InvalidRequest), + UnsupportedDbEngineVersionException.class) + .withErrorClasses(ErrorStatus.failWith(HandlerErrorCode.ResourceConflict), + InvalidDbClusterStateException.class, + InvalidVpcNetworkStateException.class) + .build(); + + private final FilteredJsonPrinter PARAMETERS_FILTER = new FilteredJsonPrinter(); + + /** + * Custom handler config, mostly to facilitate faster unit test + */ + final HandlerConfig config; + protected RequestLogger requestLogger; + protected static final BiFunction, ResourceModel> NOOP_CALL = (model, proxyClient) -> model; + + public BaseHandlerStd() { + this(HandlerConfig.builder().build()); + } + + BaseHandlerStd(HandlerConfig config) { + this.config = config; + } + + @Override + public final ProgressEvent handleRequest( + final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final CallbackContext callbackContext, + final Logger logger) { + return RequestLogger.handleRequest( + logger, + request, + PARAMETERS_FILTER, + requestLogger -> handleRequest( + proxy, + new LoggingProxyClient<>(requestLogger, proxy.newProxy(new ClientProvider()::getClient)), + request, + callbackContext != null ? callbackContext : new CallbackContext(), + requestLogger + )); + } + + protected abstract ProgressEvent handleRequest( + final AmazonWebServicesClientProxy proxy, + final ProxyClient proxyClient, + final ResourceHandlerRequest request, + final CallbackContext callbackContext); + + protected ProgressEvent handleRequest( + final AmazonWebServicesClientProxy proxy, + final ProxyClient proxyClient, + final ResourceHandlerRequest request, + final CallbackContext context, + final RequestLogger requestLogger + ) { + this.requestLogger = requestLogger; + return handleRequest(proxy, proxyClient, request, context); + } + + protected boolean isDBShardGroupStabilizedAfterMutate( + final ResourceModel model, + final ProxyClient proxyClient + ) { + boolean isDBShardGroupStabilized = isDBShardGroupStabilized(model, proxyClient); + boolean isDBClusterStabilized = isDBClusterStabilized(model, proxyClient); + + requestLogger.log(String.format("isDBShardGroupStabilizedAfterMutate: %b", isDBShardGroupStabilized && isDBClusterStabilized), + ImmutableMap.of("isDBShardGroupStabilized", isDBShardGroupStabilized, + "isDBClusterStabilized", isDBClusterStabilized), + ImmutableMap.of("Description", "isDBShardGroupStabilizedAfterMutate method will be repeatedly" + + " called with a backoff mechanism after the modify call until it returns true. This" + + " process will continue until all included flags are true.")); + + return isDBShardGroupStabilized && isDBClusterStabilized; + } + + protected boolean isDBShardGroupStabilized( + final ResourceModel model, + final ProxyClient proxyClient + ) { + final DBShardGroup dbShardGroup = proxyClient.injectCredentialsAndInvokeV2( + Translator.describeDbShardGroupsRequest(model), + proxyClient.client()::describeDBShardGroups + ).dbShardGroups().get(0); + + return isDBShardGroupAvailable(dbShardGroup); + } + + protected boolean isDBClusterStabilized( + final ResourceModel model, + final ProxyClient proxyClient + ) { + final DBCluster dbCluster = proxyClient.injectCredentialsAndInvokeV2( + Translator.describeDbClustersRequest(model), + proxyClient.client()::describeDBClusters + ).dbClusters().get(0); + + return isDBClusterAvailable(dbCluster); + } + + protected boolean isDBShardGroupAvailable(final DBShardGroup dbShardGroup) { + return ResourceStatus.AVAILABLE.equalsString(dbShardGroup.status()); + } + + protected boolean isDBClusterAvailable(final DBCluster dbCluster) { + return ResourceStatus.AVAILABLE.equalsString(dbCluster.status()); + } + + protected ProgressEvent addTags( + final ProxyClient proxyClient, + final ResourceHandlerRequest request, + final ProgressEvent progress, + final Tagging.TagSet desiredTags + ) { + DBShardGroup dbShardGroup; + try { + dbShardGroup = proxyClient.injectCredentialsAndInvokeV2( + Translator.describeDbShardGroupsRequest(progress.getResourceModel()), proxyClient.client()::describeDBShardGroups + ).dbShardGroups().get(0); + } catch (Exception exception) { + return Commons.handleException(progress, exception, DEFAULT_DB_SHARD_GROUP_ERROR_RULE_SET, requestLogger); + } + + final String arn = assembleArn(request.getAwsPartition(), request.getRegion(), request.getAwsAccountId(), dbShardGroup.dbShardGroupResourceId()); + + try { + Tagging.addTags(proxyClient, arn, Tagging.translateTagsToSdk(desiredTags)); + } catch (Exception exception) { + return Commons.handleException( + progress, + exception, + DEFAULT_DB_SHARD_GROUP_ERROR_RULE_SET.extendWith(Tagging.getUpdateTagsAccessDeniedRuleSet(desiredTags, Tagging.TagSet.emptySet())), + requestLogger + ); + } + + return progress; + } + + protected ProgressEvent updateTags( + final ProxyClient proxyClient, + final ResourceHandlerRequest request, + final ProgressEvent progress, + final Tagging.TagSet previousTags, + final Tagging.TagSet desiredTags + ) { + final Collection effectivePreviousTags = Tagging.translateTagsToSdk(previousTags); + final Collection effectiveDesiredTags = Tagging.translateTagsToSdk(desiredTags); + + final Collection tagsToRemove = Tagging.exclude(effectivePreviousTags, effectiveDesiredTags); + final Collection tagsToAdd = Tagging.exclude(effectiveDesiredTags, effectivePreviousTags); + + if (tagsToAdd.isEmpty() && tagsToRemove.isEmpty()) { + return progress; + } + + final Tagging.TagSet rulesetTagsToAdd = Tagging.exclude(desiredTags, previousTags); + final Tagging.TagSet rulesetTagsToRemove = Tagging.exclude(previousTags, desiredTags); + + DBShardGroup dbShardGroup; + try { + dbShardGroup = proxyClient.injectCredentialsAndInvokeV2( + Translator.describeDbShardGroupsRequest(progress.getResourceModel()), proxyClient.client()::describeDBShardGroups + ).dbShardGroups().get(0); + } catch (Exception exception) { + return Commons.handleException(progress, exception, DEFAULT_DB_SHARD_GROUP_ERROR_RULE_SET, requestLogger); + } + + final String arn = assembleArn(request.getAwsPartition(), request.getRegion(), request.getAwsAccountId(), dbShardGroup.dbShardGroupResourceId()); + + try { + Tagging.removeTags(proxyClient, arn, Tagging.translateTagsToSdk(tagsToRemove)); + Tagging.addTags(proxyClient, arn, Tagging.translateTagsToSdk(tagsToAdd)); + } catch (Exception exception) { + return Commons.handleException( + progress, + exception, + DEFAULT_DB_SHARD_GROUP_ERROR_RULE_SET.extendWith(Tagging.getUpdateTagsAccessDeniedRuleSet(rulesetTagsToAdd, rulesetTagsToRemove)), + requestLogger + ); + } + + return progress; + } + + protected Set getTags( + final ProxyClient proxyClient, + final ResourceHandlerRequest request, + final DBShardGroup dbShardGroup + ) { + String arn = assembleArn(request.getAwsPartition(), request.getRegion(), request.getAwsAccountId(), dbShardGroup.dbShardGroupResourceId()); + return Tagging.listTagsForResource(proxyClient, arn); + } + + private String assembleArn(String partition, String region, String accountId, String dbShardGroupResourceId) { + return Arn.builder() + .withPartition(partition) + .withService("rds") + .withRegion(region) + .withAccountId(accountId) + .withResource(ArnResource.builder() + .withResourceType("shard-group") + .withResource(dbShardGroupResourceId) + .build().toString()) + .build().toString(); + } + + protected DBShardGroup fetchDbShardGroup(final ProxyClient client, final ResourceModel model) { + try { + final var response = client.injectCredentialsAndInvokeV2(Translator.describeDbShardGroupsRequest(model), client.client()::describeDBShardGroups); + if (!response.hasDbShardGroups() || response.dbShardGroups().isEmpty()) { + throw new CfnNotFoundException(ResourceModel.TYPE_NAME, model.getDBShardGroupIdentifier()); + } + return response.dbShardGroups().get(0); + } catch (DbShardGroupNotFoundException e) { + throw new CfnNotFoundException(e); + } + } +} diff --git a/aws-rds-dbshardgroup/src/main/java/software/amazon/rds/dbshardgroup/CallbackContext.java b/aws-rds-dbshardgroup/src/main/java/software/amazon/rds/dbshardgroup/CallbackContext.java new file mode 100644 index 000000000..456a8a4b8 --- /dev/null +++ b/aws-rds-dbshardgroup/src/main/java/software/amazon/rds/dbshardgroup/CallbackContext.java @@ -0,0 +1,41 @@ +package software.amazon.rds.dbshardgroup; + +import software.amazon.cloudformation.proxy.StdCallbackContext; +import software.amazon.rds.common.handler.TaggingContext; +import software.amazon.rds.common.util.IdempotencyHelper; +import software.amazon.rds.common.util.WaiterHelper; + + +@lombok.Getter +@lombok.Setter +@lombok.ToString +@lombok.EqualsAndHashCode(callSuper = true) +public class CallbackContext extends StdCallbackContext implements TaggingContext.Provider, IdempotencyHelper.PreExistenceContext, WaiterHelper.DelayContext { + private Boolean preExistenceCheckDone; + private boolean described; + private boolean updated; + + private TaggingContext taggingContext; + + public CallbackContext() { + super(); + this.taggingContext = new TaggingContext(); + } + + @Override + public TaggingContext getTaggingContext() { + return taggingContext; + } + + public boolean isAddTagsComplete() { + return taggingContext.isAddTagsComplete(); + } + + public void setAddTagsComplete(final boolean addTagsComplete) { + taggingContext.setAddTagsComplete(addTagsComplete); + } + + private String dbClusterIdentifier; + + private int waitTime; +} diff --git a/aws-rds-dbshardgroup/src/main/java/software/amazon/rds/dbshardgroup/ClientProvider.java b/aws-rds-dbshardgroup/src/main/java/software/amazon/rds/dbshardgroup/ClientProvider.java new file mode 100644 index 000000000..b32986d3f --- /dev/null +++ b/aws-rds-dbshardgroup/src/main/java/software/amazon/rds/dbshardgroup/ClientProvider.java @@ -0,0 +1,15 @@ +package software.amazon.rds.dbshardgroup; + +import software.amazon.awssdk.services.rds.RdsClient; +import software.amazon.awssdk.services.rds.RdsClientBuilder; +import software.amazon.rds.common.annotations.ExcludeFromJacocoGeneratedReport; +import software.amazon.rds.common.client.BaseSdkClientProvider; + +public class ClientProvider extends BaseSdkClientProvider { + + @ExcludeFromJacocoGeneratedReport + @Override + public RdsClient getClient() { + return setHttpClient(setUserAgent(RdsClient.builder())).build(); + } +} diff --git a/aws-rds-dbshardgroup/src/main/java/software/amazon/rds/dbshardgroup/Configuration.java b/aws-rds-dbshardgroup/src/main/java/software/amazon/rds/dbshardgroup/Configuration.java new file mode 100644 index 000000000..58d8b8bf4 --- /dev/null +++ b/aws-rds-dbshardgroup/src/main/java/software/amazon/rds/dbshardgroup/Configuration.java @@ -0,0 +1,8 @@ +package software.amazon.rds.dbshardgroup; + +class Configuration extends BaseConfiguration { + + public Configuration() { + super("aws-rds-dbshardgroup.json"); + } +} diff --git a/aws-rds-dbshardgroup/src/main/java/software/amazon/rds/dbshardgroup/CreateHandler.java b/aws-rds-dbshardgroup/src/main/java/software/amazon/rds/dbshardgroup/CreateHandler.java new file mode 100644 index 000000000..6405dab54 --- /dev/null +++ b/aws-rds-dbshardgroup/src/main/java/software/amazon/rds/dbshardgroup/CreateHandler.java @@ -0,0 +1,93 @@ +package software.amazon.rds.dbshardgroup; + +import java.util.function.Function; + +import com.amazonaws.util.StringUtils; +import software.amazon.awssdk.services.rds.RdsClient; +import software.amazon.awssdk.services.rds.model.DBCluster; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.ProxyClient; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; +import software.amazon.rds.common.handler.Commons; +import software.amazon.rds.common.handler.HandlerConfig; +import software.amazon.rds.common.handler.Tagging; +import software.amazon.rds.common.util.IdentifierFactory; + +import java.util.HashSet; + +public class CreateHandler extends BaseHandlerStd { + + public static final String STACK_NAME = "rds"; + public static final String RESOURCE_IDENTIFIER = "dbshardgroup"; + public static final int RESOURCE_ID_MAX_LENGTH = 63; + + public static final IdentifierFactory dbShardGroupIdentifierFactory = new IdentifierFactory( + STACK_NAME, + RESOURCE_IDENTIFIER, + RESOURCE_ID_MAX_LENGTH + ); + + /** Default constructor w/ default backoff */ + public CreateHandler() { + this(DB_SHARD_GROUP_HANDLER_CONFIG_36H); + } + + /** Default constructor w/ custom config */ + public CreateHandler(HandlerConfig config) { + super(config); + } + + protected ProgressEvent handleRequest( + final AmazonWebServicesClientProxy proxy, + final ProxyClient proxyClient, + final ResourceHandlerRequest request, + final CallbackContext callbackContext) { + final Tagging.TagSet allTags = Tagging.TagSet.builder() + .systemTags(Tagging.translateTagsToSdk(request.getSystemTags())) + .stackTags(Tagging.translateTagsToSdk(request.getDesiredResourceTags())) + .resourceTags(new HashSet<>(Translator.translateTagsToSdk(request.getDesiredResourceState().getTags()))) + .build(); + ResourceModel model = request.getDesiredResourceState(); + if (StringUtils.isNullOrEmpty(model.getDBShardGroupIdentifier())) { + model.setDBShardGroupIdentifier( + dbShardGroupIdentifierFactory.newIdentifier() + .withStackId(request.getStackId()) + .withResourceId(request.getLogicalResourceIdentifier()) + .withRequestToken(request.getClientRequestToken()) + .toString()); + } + + return ProgressEvent.progress(request.getDesiredResourceState(), callbackContext) + .then(progress -> Tagging.createWithTaggingFallback(proxy, proxyClient, this::createDbShardGroup, progress, allTags) + .then(p -> Commons.execOnce(p, () -> { + final Tagging.TagSet extraTags = Tagging.TagSet.builder() + .stackTags(allTags.getStackTags()) + .resourceTags(allTags.getResourceTags()) + .build(); + return addTags(proxyClient, request, p, extraTags); + }, CallbackContext::isAddTagsComplete, CallbackContext::setAddTagsComplete)) + ) + .then(progress -> new ReadHandler().handleRequest(proxy, proxyClient, request, callbackContext)); + } + + private ProgressEvent createDbShardGroup( + final AmazonWebServicesClientProxy proxy, + final ProxyClient proxyClient, + final ProgressEvent progress, + final Tagging.TagSet tags + ) { + return proxy.initiate("rds::create-db-shard-group", proxyClient, progress.getResourceModel(), progress.getCallbackContext()) + .translateToServiceRequest((resourceModel) -> Translator.createDbShardGroupRequest(resourceModel, tags)) + .backoffDelay(config.getBackoff()) + .makeServiceCall((createDbShardGroupRequest, proxyInvocation) -> proxyInvocation.injectCredentialsAndInvokeV2(createDbShardGroupRequest, proxyInvocation.client()::createDBShardGroup)) + .stabilize((createRequest, createResponse, proxyInvocation, model, context) -> isDBShardGroupStabilizedAfterMutate(model, proxyInvocation)) + .handleError((deleteRequest, exception, client, resourceModel, ctx) -> Commons.handleException( + ProgressEvent.progress(resourceModel, ctx), + exception, + DEFAULT_DB_SHARD_GROUP_ERROR_RULE_SET, + requestLogger + )) + .progress(); + } +} diff --git a/aws-rds-dbshardgroup/src/main/java/software/amazon/rds/dbshardgroup/DeleteHandler.java b/aws-rds-dbshardgroup/src/main/java/software/amazon/rds/dbshardgroup/DeleteHandler.java new file mode 100644 index 000000000..be0953fae --- /dev/null +++ b/aws-rds-dbshardgroup/src/main/java/software/amazon/rds/dbshardgroup/DeleteHandler.java @@ -0,0 +1,134 @@ +package software.amazon.rds.dbshardgroup; + +import java.util.List; +import java.util.function.Function; +import org.apache.commons.collections.CollectionUtils; +import software.amazon.awssdk.services.rds.RdsClient; +import software.amazon.awssdk.services.rds.model.DBCluster; +import software.amazon.awssdk.services.rds.model.DBShardGroup; +import software.amazon.awssdk.services.rds.model.DbClusterNotFoundException; +import software.amazon.awssdk.services.rds.model.DbShardGroupNotFoundException; +import software.amazon.awssdk.services.rds.model.DescribeDbClustersRequest; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.HandlerErrorCode; +import software.amazon.cloudformation.proxy.OperationStatus; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.ProxyClient; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; +import software.amazon.rds.common.error.ErrorRuleSet; +import software.amazon.rds.common.error.ErrorStatus; +import software.amazon.rds.common.handler.Commons; +import software.amazon.rds.common.handler.HandlerConfig; + +public class DeleteHandler extends BaseHandlerStd { + protected static final ErrorRuleSet DELETE_DB_SHARD_GROUP_ERROR_RULE_SET = ErrorRuleSet + .extend(DEFAULT_DB_SHARD_GROUP_ERROR_RULE_SET) + .withErrorClasses(ErrorStatus.ignore(OperationStatus.IN_PROGRESS), + DbShardGroupNotFoundException.class) + .withErrorClasses(ErrorStatus.ignore(OperationStatus.SUCCESS), + DbClusterNotFoundException.class) + .build(); + + /** Default constructor with default backoff */ + public DeleteHandler() { + this(DB_SHARD_GROUP_HANDLER_CONFIG_36H); + } + + /** Default constructor with custom config */ + public DeleteHandler(HandlerConfig config) { + super(config); + } + + protected ProgressEvent handleRequest( + final AmazonWebServicesClientProxy proxy, + final ProxyClient proxyClient, + final ResourceHandlerRequest request, + final CallbackContext callbackContext) { + + return ProgressEvent.progress(request.getDesiredResourceState(), callbackContext) + .then(progress -> Commons.execOnce(progress, () -> deleteDbShardGroupIfNotInProgress(progress, proxy, request, callbackContext, proxyClient), + CallbackContext::isDescribed, CallbackContext::setDescribed)) + // Stabilize as an independent step for cases where there is an out-of-band delete + .then(progress -> proxy.initiate("rds::delete-db-shard-group-stabilize", proxyClient, request.getDesiredResourceState(), callbackContext) + .translateToServiceRequest(Function.identity()) + .backoffDelay(config.getBackoff()) + .makeServiceCall(NOOP_CALL) + .stabilize((noopRequest, noopResponse, proxyInvocation, model, context) -> isDbShardGroupDeleted(model, proxyInvocation) && isDbClusterStabilizedOrDeleted(proxyInvocation, context)) + .handleError((noopRequest, exception, client, model, context) -> Commons.handleException( + ProgressEvent.progress(model, context), + exception, + DELETE_DB_SHARD_GROUP_ERROR_RULE_SET, + requestLogger + )).progress()) + .then(progress -> ProgressEvent.defaultSuccessHandler(null)); + } + + private ProgressEvent deleteDbShardGroupIfNotInProgress(final ProgressEvent progress, + final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final CallbackContext callbackContext, + final ProxyClient proxyClient) { + try { + final DBShardGroup dbShardGroup = proxyClient.injectCredentialsAndInvokeV2( + Translator.describeDbShardGroupsRequest(request.getDesiredResourceState()), + proxyClient.client()::describeDBShardGroups) + .dbShardGroups().get(0); + callbackContext.setDbClusterIdentifier(dbShardGroup.dbClusterIdentifier()); + if (!ResourceStatus.DELETING.equalsString(dbShardGroup.status())){ + return deleteDbShardGroup(proxy, request, callbackContext, proxyClient); + } else { + return progress; + } + } catch (DbShardGroupNotFoundException e) { + // If the shard group is not found, we exit. + return ProgressEvent.defaultFailureHandler(e, HandlerErrorCode.NotFound); + } + } + + private ProgressEvent deleteDbShardGroup(final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final CallbackContext callbackContext, + final ProxyClient proxyClient) { + return proxy.initiate("rds::delete-db-shard-group", proxyClient, request.getDesiredResourceState(), callbackContext) + .translateToServiceRequest(Translator::deleteDbShardGroupRequest) + .backoffDelay(config.getBackoff()) + .makeServiceCall((deleteDbShardGroupRequest, proxyInvocation) -> proxyInvocation.injectCredentialsAndInvokeV2(deleteDbShardGroupRequest, proxyInvocation.client()::deleteDBShardGroup)) + .handleError((deleteRequest, exception, client, resourceModel, ctx) -> Commons.handleException( + ProgressEvent.progress(resourceModel, ctx), + exception, + DELETE_DB_SHARD_GROUP_ERROR_RULE_SET, + requestLogger + )).progress(); + } + + private boolean isDbShardGroupDeleted(final ResourceModel model, + final ProxyClient proxyClient) { + try { + proxyClient.injectCredentialsAndInvokeV2(Translator.describeDbShardGroupsRequest(model), proxyClient.client()::describeDBShardGroups); + return false; + } catch (DbShardGroupNotFoundException e) { + return true; + } + } + + private boolean isDbClusterStabilizedOrDeleted(final ProxyClient proxyClient, + final CallbackContext callbackContext) { + try { + final List dbClusters = proxyClient.injectCredentialsAndInvokeV2( + DescribeDbClustersRequest.builder() + .dbClusterIdentifier(callbackContext.getDbClusterIdentifier()) + .build(), + proxyClient.client()::describeDBClusters + ).dbClusters(); + + if (CollectionUtils.isEmpty(dbClusters)) { + // For an empty response, we assume the same behavior for a DbClusterNotFoundException + return true; + } else { + return isDBClusterAvailable(dbClusters.get(0)); + } + } catch (DbClusterNotFoundException e) { + return true; + } + } +} diff --git a/aws-rds-dbshardgroup/src/main/java/software/amazon/rds/dbshardgroup/ListHandler.java b/aws-rds-dbshardgroup/src/main/java/software/amazon/rds/dbshardgroup/ListHandler.java new file mode 100644 index 000000000..bac54cce6 --- /dev/null +++ b/aws-rds-dbshardgroup/src/main/java/software/amazon/rds/dbshardgroup/ListHandler.java @@ -0,0 +1,64 @@ +package software.amazon.rds.dbshardgroup; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import software.amazon.awssdk.services.rds.RdsClient; +import software.amazon.awssdk.services.rds.model.DBShardGroup; +import software.amazon.awssdk.services.rds.model.DescribeDbShardGroupsRequest; +import software.amazon.awssdk.services.rds.model.DescribeDbShardGroupsResponse; +import software.amazon.awssdk.services.rds.model.Tag; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.OperationStatus; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.ProxyClient; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; +import software.amazon.rds.common.handler.Commons; +import software.amazon.rds.common.handler.HandlerConfig; + +import java.util.stream.Collectors; +import software.amazon.rds.common.handler.Tagging; + +public class ListHandler extends BaseHandlerStd { + + /** Default constructor w/ default backoff */ + public ListHandler() { + } + + /** Default constructor w/ custom config */ + public ListHandler(HandlerConfig config) { + super(config); + } + + @Override + public ProgressEvent handleRequest( + final AmazonWebServicesClientProxy proxy, + final ProxyClient proxyClient, + final ResourceHandlerRequest request, + final CallbackContext callbackContext + ) { + final DescribeDbShardGroupsRequest describeDbShardGroupsRequest = Translator.describeDbShardGroupsRequest(request.getNextToken()); + DescribeDbShardGroupsResponse describeDbShardGroupsResponse; + List models = new ArrayList<>(); + try { + describeDbShardGroupsResponse = proxy.injectCredentialsAndInvokeV2(describeDbShardGroupsRequest, proxyClient.client()::describeDBShardGroups); + for (DBShardGroup dbShardGroup : describeDbShardGroupsResponse.dbShardGroups()) { + Set tagSet = getTags(proxyClient, request, dbShardGroup); + models.add(Translator.translateDbShardGroupFromSdk(dbShardGroup, tagSet)); + } + } catch (Exception e) { + return Commons.handleException( + ProgressEvent.progress(request.getDesiredResourceState(), callbackContext), + e, + DEFAULT_DB_SHARD_GROUP_ERROR_RULE_SET, + requestLogger + ); + } + return ProgressEvent.builder() + .resourceModels(models) + .nextToken(describeDbShardGroupsResponse.marker()) + .status(OperationStatus.SUCCESS) + .build(); + } +} diff --git a/aws-rds-dbshardgroup/src/main/java/software/amazon/rds/dbshardgroup/ReadHandler.java b/aws-rds-dbshardgroup/src/main/java/software/amazon/rds/dbshardgroup/ReadHandler.java new file mode 100644 index 000000000..53b78989b --- /dev/null +++ b/aws-rds-dbshardgroup/src/main/java/software/amazon/rds/dbshardgroup/ReadHandler.java @@ -0,0 +1,48 @@ +package software.amazon.rds.dbshardgroup; + +import java.util.Set; + +import software.amazon.awssdk.services.rds.RdsClient; +import software.amazon.awssdk.services.rds.model.DBShardGroup; +import software.amazon.awssdk.services.rds.model.DescribeDbShardGroupsRequest; +import software.amazon.awssdk.services.rds.model.Tag; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.ProxyClient; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; +import software.amazon.rds.common.handler.Commons; +import software.amazon.rds.common.handler.HandlerConfig; + +public class ReadHandler extends BaseHandlerStd { + + /** Default constructor w/ default backoff */ + public ReadHandler() { + } + + /** Default constructor w/ custom config */ + public ReadHandler(HandlerConfig config) { + super(config); + } + + protected ProgressEvent handleRequest( + final AmazonWebServicesClientProxy proxy, + final ProxyClient proxyClient, + final ResourceHandlerRequest request, + final CallbackContext callbackContext) { + DescribeDbShardGroupsRequest describeDbShardGroupsRequest = Translator.describeDbShardGroupsRequest(request.getDesiredResourceState()); + ResourceModel model; + try { + DBShardGroup dbShardGroup = proxyClient.injectCredentialsAndInvokeV2(describeDbShardGroupsRequest, proxyClient.client()::describeDBShardGroups).dbShardGroups().get(0); + Set tagSet = getTags(proxyClient, request, dbShardGroup); + model = Translator.translateDbShardGroupFromSdk(dbShardGroup, tagSet); + } catch (Exception e) { + return Commons.handleException( + ProgressEvent.progress(request.getDesiredResourceState(), callbackContext), + e, + DEFAULT_DB_SHARD_GROUP_ERROR_RULE_SET, + requestLogger + ); + } + return ProgressEvent.success(model, callbackContext); + } +} diff --git a/aws-rds-dbshardgroup/src/main/java/software/amazon/rds/dbshardgroup/ResourceStatus.java b/aws-rds-dbshardgroup/src/main/java/software/amazon/rds/dbshardgroup/ResourceStatus.java new file mode 100644 index 000000000..2cc05cc3d --- /dev/null +++ b/aws-rds-dbshardgroup/src/main/java/software/amazon/rds/dbshardgroup/ResourceStatus.java @@ -0,0 +1,25 @@ +package software.amazon.rds.dbshardgroup; + +import software.amazon.awssdk.utils.StringUtils; + +public enum ResourceStatus { + AVAILABLE("available"), + CREATING("creating"), + DELETING("deleting"), + MODIFYING("modifying"); + + private final String value; + + ResourceStatus(final String value) { + this.value = value; + } + + @Override + public String toString() { + return value; + } + + public boolean equalsString(final String other) { + return StringUtils.equals(value, other); + } +} diff --git a/aws-rds-dbshardgroup/src/main/java/software/amazon/rds/dbshardgroup/Translator.java b/aws-rds-dbshardgroup/src/main/java/software/amazon/rds/dbshardgroup/Translator.java new file mode 100644 index 000000000..924bff101 --- /dev/null +++ b/aws-rds-dbshardgroup/src/main/java/software/amazon/rds/dbshardgroup/Translator.java @@ -0,0 +1,109 @@ +package software.amazon.rds.dbshardgroup; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.Optional; +import java.util.stream.Collectors; + +import software.amazon.awssdk.services.rds.model.CreateDbShardGroupRequest; +import software.amazon.awssdk.services.rds.model.DBShardGroup; +import software.amazon.awssdk.services.rds.model.DeleteDbShardGroupRequest; +import software.amazon.awssdk.services.rds.model.DescribeDbClustersRequest; +import software.amazon.awssdk.services.rds.model.DescribeDbShardGroupsRequest; +import software.amazon.awssdk.services.rds.model.ModifyDbShardGroupRequest; +import software.amazon.rds.common.handler.Tagging; + +public class Translator { + + static CreateDbShardGroupRequest createDbShardGroupRequest(final ResourceModel model, final Tagging.TagSet tags) { + return CreateDbShardGroupRequest.builder() + .computeRedundancy(model.getComputeRedundancy()) + .dbClusterIdentifier(model.getDBClusterIdentifier()) + .dbShardGroupIdentifier(model.getDBShardGroupIdentifier()) + .maxACU(model.getMaxACU()) + .minACU(model.getMinACU()) + .publiclyAccessible(model.getPubliclyAccessible()) + .tags(Tagging.translateTagsToSdk(tags)) + .build(); + } + + static ModifyDbShardGroupRequest modifyDbShardGroupRequest(final ResourceModel desiredModel) { + return ModifyDbShardGroupRequest.builder() + .computeRedundancy(desiredModel.getComputeRedundancy()) + .dbShardGroupIdentifier(desiredModel.getDBShardGroupIdentifier()) + .maxACU(desiredModel.getMaxACU()) + .minACU(desiredModel.getMinACU()) + .build(); + } + + static DescribeDbShardGroupsRequest describeDbShardGroupsRequest(final ResourceModel model) { + return DescribeDbShardGroupsRequest.builder() + .dbShardGroupIdentifier(model.getDBShardGroupIdentifier()) + .build(); + } + + static DescribeDbClustersRequest describeDbClustersRequest(final ResourceModel model) { + return DescribeDbClustersRequest.builder() + .dbClusterIdentifier(model.getDBClusterIdentifier()) + .build(); + } + + static DescribeDbShardGroupsRequest describeDbShardGroupsRequest(final String nextToken) { + return DescribeDbShardGroupsRequest.builder() + .marker(nextToken) + .build(); + } + + static DeleteDbShardGroupRequest deleteDbShardGroupRequest(final ResourceModel model) { + return DeleteDbShardGroupRequest.builder() + .dbShardGroupIdentifier(model.getDBShardGroupIdentifier()) + .build(); + } + + public static Map translateTagsToRequest(final Collection tags) { + return Optional.ofNullable(tags).orElse(Collections.emptyList()) + .stream() + .collect(Collectors.toMap(Tag::getKey, Tag::getValue)); + } + + static Set translateTagsToSdk( + final Collection tags + ) { + return Optional.ofNullable(tags).orElse(Collections.emptySet()) + .stream() + .map(tag -> software.amazon.awssdk.services.rds.model.Tag.builder() + .key(tag.getKey()) + .value(tag.getValue()) + .build()) + .collect(Collectors.toSet()); + } + + static Set translateTagsFromSdk( + final Collection tags + ) { + return Optional.ofNullable(tags).orElse(Collections.emptySet()) + .stream() + .map(tag -> software.amazon.rds.dbshardgroup.Tag.builder() + .key(tag.key()) + .value(tag.value()) + .build()) + .collect(Collectors.toSet()); + } + + static ResourceModel translateDbShardGroupFromSdk(final DBShardGroup dbShardGroup, final Set tags) { + return ResourceModel.builder() + .dBShardGroupResourceId(dbShardGroup.dbShardGroupResourceId()) + .dBShardGroupIdentifier(dbShardGroup.dbShardGroupIdentifier()) + .dBClusterIdentifier(dbShardGroup.dbClusterIdentifier()) + .computeRedundancy(dbShardGroup.computeRedundancy()) + .maxACU(dbShardGroup.maxACU()) + // TODO: Return minACU when describeDBShardGroup includes value +// .minACU(dbShardGroup.minACU()) + .publiclyAccessible(dbShardGroup.publiclyAccessible()) + .tags(translateTagsFromSdk(tags)) + .endpoint(dbShardGroup.endpoint()) + .build(); + } +} diff --git a/aws-rds-dbshardgroup/src/main/java/software/amazon/rds/dbshardgroup/UpdateHandler.java b/aws-rds-dbshardgroup/src/main/java/software/amazon/rds/dbshardgroup/UpdateHandler.java new file mode 100644 index 000000000..5dbfb2c48 --- /dev/null +++ b/aws-rds-dbshardgroup/src/main/java/software/amazon/rds/dbshardgroup/UpdateHandler.java @@ -0,0 +1,77 @@ +package software.amazon.rds.dbshardgroup; + +import java.util.function.Function; +import software.amazon.awssdk.services.rds.RdsClient; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.ProxyClient; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; +import software.amazon.rds.common.handler.Commons; +import software.amazon.rds.common.handler.HandlerConfig; +import software.amazon.rds.common.handler.Tagging; +import software.amazon.rds.common.util.WaiterHelper; + +import java.util.HashSet; + +public class UpdateHandler extends BaseHandlerStd { + static final int POST_MODIFY_DELAY_SEC = 300; + static final int CALLBACK_DELAY = 6; + + /** Default constructor w/ default backoff */ + public UpdateHandler() { + } + + /** Default constructor w/ custom config */ + public UpdateHandler(HandlerConfig config) { + super(config); + } + + protected ProgressEvent handleRequest(final AmazonWebServicesClientProxy proxy, + final ProxyClient proxyClient, + final ResourceHandlerRequest request, + final CallbackContext callbackContext) { + final Tagging.TagSet previousTags = Tagging.TagSet.builder() + .systemTags(Tagging.translateTagsToSdk(request.getPreviousSystemTags())) + .stackTags(Tagging.translateTagsToSdk(request.getPreviousResourceTags())) + .resourceTags(new HashSet<>(Translator.translateTagsToSdk(request.getPreviousResourceState().getTags()))) + .build(); + + final Tagging.TagSet desiredTags = Tagging.TagSet.builder() + .systemTags(Tagging.translateTagsToSdk(request.getSystemTags())) + .stackTags(Tagging.translateTagsToSdk(request.getDesiredResourceTags())) + .resourceTags(new HashSet<>(Translator.translateTagsToSdk(request.getDesiredResourceState().getTags()))) + .build(); + + ResourceModel desiredModel = request.getDesiredResourceState(); + + return ProgressEvent.progress(desiredModel, callbackContext) + .then(progress -> Commons.execOnce( + progress, + () -> proxy.initiate("rds::modify-db-shard-group", proxyClient, request.getDesiredResourceState(), callbackContext) + .translateToServiceRequest(Translator::modifyDbShardGroupRequest) + .backoffDelay(config.getBackoff()) + .makeServiceCall((modifyDbShardGroupRequest, proxyInvocation) -> proxyInvocation.injectCredentialsAndInvokeV2(modifyDbShardGroupRequest, proxyClient.client()::modifyDBShardGroup)) + .handleError((describeRequest, exception, client, resourceModel, ctx) -> Commons.handleException( + ProgressEvent.progress(resourceModel, ctx), + exception, + DEFAULT_DB_SHARD_GROUP_ERROR_RULE_SET, + requestLogger)) + .progress(), CallbackContext::isUpdated, CallbackContext::setUpdated)) + .then(progress -> Commons.execOnce(progress, () -> updateTags(proxyClient, request, progress, previousTags, desiredTags), CallbackContext::isAddTagsComplete, CallbackContext::setAddTagsComplete)) + // There is a lag between the modifyDbShardGroup request call and the shard group state moving to "modifying", so we introduce a fixed delay prior to stabilization + .then((progress) -> WaiterHelper.delay(progress, POST_MODIFY_DELAY_SEC, CALLBACK_DELAY)) + .then(progress -> proxy.initiate("rds::update-db-shard-group-stabilize", proxyClient, request.getDesiredResourceState(), callbackContext) + .translateToServiceRequest(Function.identity()) + .backoffDelay(config.getBackoff()) + .makeServiceCall(NOOP_CALL) + .stabilize((noopRequest, noopResponse, proxyInvocation, model, context) -> isDBShardGroupStabilizedAfterMutate(model, proxyInvocation)) + .handleError((deleteRequest, exception, client, resourceModel, ctx) -> Commons.handleException( + ProgressEvent.progress(resourceModel, ctx), + exception, + DEFAULT_DB_SHARD_GROUP_ERROR_RULE_SET, + requestLogger + )) + .progress()) + .then(progress -> new ReadHandler().handleRequest(proxy, proxyClient, request, callbackContext)); + } +} diff --git a/aws-rds-dbshardgroup/src/resources/log4j2.xml b/aws-rds-dbshardgroup/src/resources/log4j2.xml new file mode 100644 index 000000000..5657dafe5 --- /dev/null +++ b/aws-rds-dbshardgroup/src/resources/log4j2.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/aws-rds-dbshardgroup/src/test/java/software/amazon/rds/dbshardgroup/AbstractHandlerTest.java b/aws-rds-dbshardgroup/src/test/java/software/amazon/rds/dbshardgroup/AbstractHandlerTest.java new file mode 100644 index 000000000..c7fcd0dc2 --- /dev/null +++ b/aws-rds-dbshardgroup/src/test/java/software/amazon/rds/dbshardgroup/AbstractHandlerTest.java @@ -0,0 +1,167 @@ +package software.amazon.rds.dbshardgroup; + +import java.time.Duration; +import java.util.Set; +import java.util.UUID; +import java.util.function.Supplier; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableSet; +import software.amazon.awssdk.services.rds.RdsClient; +import software.amazon.awssdk.services.rds.model.DBShardGroup; +import software.amazon.awssdk.services.rds.model.DescribeDbShardGroupsRequest; +import software.amazon.awssdk.services.rds.model.DescribeDbShardGroupsResponse; +import software.amazon.cloudformation.proxy.*; +import software.amazon.cloudformation.proxy.delay.Constant; +import software.amazon.rds.common.handler.HandlerConfig; +import software.amazon.rds.common.handler.Tagging; +import software.amazon.rds.common.logging.RequestLogger; +import software.amazon.rds.common.printer.FilteredJsonPrinter; +import software.amazon.rds.test.common.core.AbstractTestBase; + +public abstract class AbstractHandlerTest extends AbstractTestBase { + + protected static final String LOGICAL_RESOURCE_IDENTIFIER = "dbshardgroup"; + protected static final String CLIENT_REQUEST_TOKEN = UUID.randomUUID().toString(); + protected static final String STACK_ID = UUID.randomUUID().toString(); + + protected static final Credentials MOCK_CREDENTIALS; + protected static final LoggerProxy logger; + static final Set TAG_LIST; + static final Set TAG_LIST_EMPTY; + static final Set TAG_LIST_ALTER; + static final Tagging.TagSet TAG_SET; + + static { + MOCK_CREDENTIALS = new Credentials("accessKey", "secretKey", "token"); + logger = new LoggerProxy(); + + TAG_LIST_EMPTY = ImmutableSet.of(); + + TAG_LIST = ImmutableSet.of( + Tag.builder().key("foo").value("bar").build() + ); + + TAG_LIST_ALTER = ImmutableSet.of( + Tag.builder().key("bar").value("baz").build(), + Tag.builder().key("fizz").value("buzz").build() + ); + + TAG_SET = Tagging.TagSet.builder() + .systemTags(ImmutableSet.of( + software.amazon.awssdk.services.rds.model.Tag.builder().key("system-tag-1").value("system-tag-value1").build(), + software.amazon.awssdk.services.rds.model.Tag.builder().key("system-tag-2").value("system-tag-value2").build(), + software.amazon.awssdk.services.rds.model.Tag.builder().key("system-tag-3").value("system-tag-value3").build() + )).stackTags(ImmutableSet.of( + software.amazon.awssdk.services.rds.model.Tag.builder().key("stack-tag-1").value("stack-tag-value1").build(), + software.amazon.awssdk.services.rds.model.Tag.builder().key("stack-tag-2").value("stack-tag-value2").build(), + software.amazon.awssdk.services.rds.model.Tag.builder().key("stack-tag-3").value("stack-tag-value3").build() + )).resourceTags(ImmutableSet.of( + software.amazon.awssdk.services.rds.model.Tag.builder().key("resource-tag-1").value("resource-tag-value1").build(), + software.amazon.awssdk.services.rds.model.Tag.builder().key("resource-tag-2").value("resource-tag-value2").build(), + software.amazon.awssdk.services.rds.model.Tag.builder().key("resource-tag-3").value("resource-tag-value3").build() + )).build(); + } + + static final String DB_SHARD_GROUP_IDENTIFIER = "testDbShardGroup"; + static final String DB_SHARD_GROUP_RESOURCE_ID = "testDbShardGroupId"; + static final String DB_CLUSTER_IDENTIFIER = "testDbCluster"; + + static final String ACCOUNT_ID = "123456789012"; + static final String REGION = "us-east-1"; + static final String PARTITION = "aws"; + static final String DB_SHARD_GROUP_ARN = "arn:aws:rds:us-east-1:123456789012:shard-group:testDbShardGroupId"; + static final int COMPUTE_REDUNDANCY = 1; + static final Double MAX_ACU = 3.5; + static final Double MAX_ACU_ALTER = 5d; + + static final ResourceModel RESOURCE_MODEL = ResourceModel.builder() + .dBShardGroupIdentifier(DB_SHARD_GROUP_IDENTIFIER) + .dBClusterIdentifier(DB_CLUSTER_IDENTIFIER) + .dBShardGroupResourceId(DB_SHARD_GROUP_RESOURCE_ID) + .computeRedundancy(COMPUTE_REDUNDANCY) + .maxACU(MAX_ACU) + .publiclyAccessible(false) + .build(); + + static final ResourceModel RESOURCE_MODEL_NO_IDENT = ResourceModel.builder() + .dBClusterIdentifier(DB_CLUSTER_IDENTIFIER) + .dBShardGroupResourceId(DB_SHARD_GROUP_RESOURCE_ID) + .computeRedundancy(COMPUTE_REDUNDANCY) + .maxACU(MAX_ACU) + .publiclyAccessible(false) + .build(); + + static final DBShardGroup DB_SHARD_GROUP = DBShardGroup.builder() + .dbShardGroupIdentifier(DB_SHARD_GROUP_IDENTIFIER) + .dbClusterIdentifier(DB_CLUSTER_IDENTIFIER) + .dbShardGroupResourceId(DB_SHARD_GROUP_RESOURCE_ID) + .computeRedundancy(COMPUTE_REDUNDANCY) + .maxACU(MAX_ACU) + .publiclyAccessible(false) + .build(); + + protected static final DBShardGroup DB_SHARD_GROUP_AVAILABLE = DB_SHARD_GROUP.toBuilder() + .status(ResourceStatus.AVAILABLE.toString()) + .build(); + + protected static final DBShardGroup DB_SHARD_GROUP_CREATING = DB_SHARD_GROUP.toBuilder() + .status(ResourceStatus.CREATING.toString()) + .build(); + + protected static final DBShardGroup DB_SHARD_GROUP_MODIFYING = DB_SHARD_GROUP.toBuilder() + .status(ResourceStatus.MODIFYING.toString()) + .build(); + + protected static final DBShardGroup DB_SHARD_GROUP_DELETING = DB_SHARD_GROUP.toBuilder() + .status(ResourceStatus.DELETING.toString()) + .build(); + + // use an accelerated backoff for faster unit testing + protected final HandlerConfig TEST_HANDLER_CONFIG = HandlerConfig.builder() + .probingEnabled(false) + .backoff(Constant.of().delay(Duration.ofMillis(1)) + .timeout(Duration.ofSeconds(120)) + .build()) + .build(); + + static ProxyClient MOCK_PROXY(final AmazonWebServicesClientProxy proxy, final ClientT client) { + return new BaseProxyClient<>(proxy, client); + } + + protected abstract BaseHandlerStd getHandler(); + + protected abstract AmazonWebServicesClientProxy getProxy(); + + protected abstract ProxyClient getProxyClient(); + + @Override + protected String getLogicalResourceIdentifier() { + return LOGICAL_RESOURCE_IDENTIFIER; + } + + @Override + protected String newClientRequestToken() { + return CLIENT_REQUEST_TOKEN; + } + + protected String newStackId() { + return STACK_ID; + } + + @Override + protected void expectResourceSupply(Supplier supplier) { + when(getProxyClient().client().describeDBShardGroups(any(DescribeDbShardGroupsRequest.class))) + .then((req) -> + DescribeDbShardGroupsResponse.builder() + .dbShardGroups(supplier.get()) + .build()); + } + + @Override + protected ProgressEvent invokeHandleRequest(ResourceHandlerRequest request, CallbackContext context) { + return getHandler().handleRequest(getProxy(), getProxyClient(), request, context, new RequestLogger(logger, request, new FilteredJsonPrinter())); + } +} diff --git a/aws-rds-dbshardgroup/src/test/java/software/amazon/rds/dbshardgroup/BaseProxyClient.java b/aws-rds-dbshardgroup/src/test/java/software/amazon/rds/dbshardgroup/BaseProxyClient.java new file mode 100644 index 000000000..39b7ad5dd --- /dev/null +++ b/aws-rds-dbshardgroup/src/test/java/software/amazon/rds/dbshardgroup/BaseProxyClient.java @@ -0,0 +1,65 @@ +package software.amazon.rds.dbshardgroup; + +import software.amazon.awssdk.awscore.AwsRequest; +import software.amazon.awssdk.awscore.AwsResponse; +import software.amazon.awssdk.core.ResponseBytes; +import software.amazon.awssdk.core.ResponseInputStream; +import software.amazon.awssdk.core.pagination.sync.SdkIterable; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.ProxyClient; + +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; + +public class BaseProxyClient implements ProxyClient { + + protected final AmazonWebServicesClientProxy proxy; + protected final ClientT client; + + public BaseProxyClient( + final AmazonWebServicesClientProxy proxy, + final ClientT client + ) { + this.proxy = proxy; + this.client = client; + } + + @Override + public ResponseT injectCredentialsAndInvokeV2(RequestT request, + Function requestFunction) { + return proxy.injectCredentialsAndInvokeV2(request, requestFunction); + } + + @Override + public CompletableFuture injectCredentialsAndInvokeV2Async( + RequestT request, + Function> requestFunction) { + throw new UnsupportedOperationException(); + } + + @Override + public > IterableT injectCredentialsAndInvokeIterableV2( + RequestT request, + Function requestFunction) { + return proxy.injectCredentialsAndInvokeIterableV2(request, requestFunction); + } + + @Override + public ResponseInputStream injectCredentialsAndInvokeV2InputStream( + RequestT request, + Function> requestFunction) { + throw new UnsupportedOperationException(); + } + + @Override + public ResponseBytes injectCredentialsAndInvokeV2Bytes( + RequestT request, + Function> requestFunction) { + throw new UnsupportedOperationException(); + } + + @Override + public ClientT client() { + return client; + } +} diff --git a/aws-rds-dbshardgroup/src/test/java/software/amazon/rds/dbshardgroup/CreateHandlerTest.java b/aws-rds-dbshardgroup/src/test/java/software/amazon/rds/dbshardgroup/CreateHandlerTest.java new file mode 100644 index 000000000..6fc3cd19e --- /dev/null +++ b/aws-rds-dbshardgroup/src/test/java/software/amazon/rds/dbshardgroup/CreateHandlerTest.java @@ -0,0 +1,301 @@ +package software.amazon.rds.dbshardgroup; + +import java.time.Duration; +import java.util.Collections; +import java.util.Objects; + +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import lombok.Getter; +import org.mockito.ArgumentMatchers; +import software.amazon.awssdk.services.rds.RdsClient; +import software.amazon.awssdk.services.rds.model.CreateDbShardGroupRequest; +import software.amazon.awssdk.services.rds.model.CreateDbShardGroupResponse; +import software.amazon.awssdk.services.rds.model.DBCluster; +import software.amazon.awssdk.services.rds.model.DBShardGroup; +import software.amazon.awssdk.services.rds.model.DbShardGroupAlreadyExistsException; +import software.amazon.awssdk.services.rds.model.DbShardGroupNotFoundException; +import software.amazon.awssdk.services.rds.model.DescribeDbClustersRequest; +import software.amazon.awssdk.services.rds.model.DescribeDbClustersResponse; +import software.amazon.awssdk.services.rds.model.DescribeDbShardGroupsRequest; +import software.amazon.awssdk.services.rds.model.ListTagsForResourceRequest; +import software.amazon.awssdk.services.rds.model.ListTagsForResourceResponse; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.HandlerErrorCode; +import software.amazon.cloudformation.proxy.ProxyClient; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +import static software.amazon.rds.dbshardgroup.CreateHandler.STACK_NAME; +import static software.amazon.rds.dbshardgroup.CreateHandler.dbShardGroupIdentifierFactory; + +@ExtendWith(MockitoExtension.class) +public class CreateHandlerTest extends AbstractHandlerTest { + + @Mock + @Getter + private AmazonWebServicesClientProxy proxy; + + @Mock + @Getter + private ProxyClient proxyClient; + + @Mock + RdsClient rdsClient; + + @Getter + private CreateHandler handler; + + @BeforeEach + public void setup() { + handler = new CreateHandler(TEST_HANDLER_CONFIG); + proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); + rdsClient = mock(RdsClient.class); + proxyClient = MOCK_PROXY(proxy, rdsClient); + } + + @AfterEach + public void tear_down() { + verify(rdsClient, atLeastOnce()).serviceName(); + verifyNoMoreInteractions(rdsClient); + } + + @Test + public void handleRequest_SimpleSuccess() { + ResourceHandlerRequest.ResourceHandlerRequestBuilder requestBuilder = ResourceHandlerRequest.builder(); + requestBuilder.region(REGION); + requestBuilder.awsAccountId(ACCOUNT_ID); + requestBuilder.awsPartition(PARTITION); + + when(proxyClient.client().createDBShardGroup(any(CreateDbShardGroupRequest.class))) + .thenReturn(CreateDbShardGroupResponse.builder() + .dbShardGroupIdentifier(DB_SHARD_GROUP_AVAILABLE.dbShardGroupIdentifier()) + .dbClusterIdentifier(DB_SHARD_GROUP_AVAILABLE.dbClusterIdentifier()) + .computeRedundancy(DB_SHARD_GROUP_AVAILABLE.computeRedundancy()) + .maxACU(DB_SHARD_GROUP_AVAILABLE.maxACU()) + .publiclyAccessible(DB_SHARD_GROUP_AVAILABLE.publiclyAccessible()) + .build()); + when(proxyClient.client().describeDBClusters(any(DescribeDbClustersRequest.class))) + .thenReturn(DescribeDbClustersResponse.builder().dbClusters( + DBCluster.builder() + .status(String.valueOf(ResourceStatus.AVAILABLE)) + .build() + ) + .build() + ); + when(proxyClient.client().listTagsForResource(any(ListTagsForResourceRequest.class))) + .thenReturn(ListTagsForResourceResponse.builder() + .build()); + + final CallbackContext context = new CallbackContext(); + + test_handleRequest_base( + context, + requestBuilder, + () -> DB_SHARD_GROUP_AVAILABLE, + null, + () -> RESOURCE_MODEL, + expectSuccess() + ); + + verify(proxyClient.client(), times(1)).createDBShardGroup( + ArgumentMatchers.argThat(req -> + Objects.equals(DB_SHARD_GROUP_CREATING.dbShardGroupIdentifier(), req.dbShardGroupIdentifier()) + ) + ); + verify(proxyClient.client(), times(2)).describeDBShardGroups( + ArgumentMatchers.argThat(req -> + Objects.equals(DB_SHARD_GROUP_CREATING.dbShardGroupIdentifier(), req.dbShardGroupIdentifier()) + ) + ); + verify(proxyClient.client(), times(1)).describeDBClusters( + ArgumentMatchers.argThat(req -> + Objects.equals(DB_SHARD_GROUP_CREATING.dbClusterIdentifier(), req.dbClusterIdentifier()) + ) + ); + verify(proxyClient.client(), times(1)).listTagsForResource( + ArgumentMatchers.argThat( + req -> DB_SHARD_GROUP_ARN.equalsIgnoreCase(req.resourceName())) + ); + } + + @Test + public void handleRequest_NoDbShardGroupIdentifier() { + ResourceHandlerRequest.ResourceHandlerRequestBuilder requestBuilder = ResourceHandlerRequest.builder(); + requestBuilder.region(REGION); + requestBuilder.awsAccountId(ACCOUNT_ID); + requestBuilder.awsPartition(PARTITION); + + String dbShardGroupIdentifier = dbShardGroupIdentifierFactory.newIdentifier() + .withStackId(STACK_ID) + .withResourceId(LOGICAL_RESOURCE_IDENTIFIER) + .withRequestToken(CLIENT_REQUEST_TOKEN) + .toString(); + + when(proxyClient.client().createDBShardGroup(any(CreateDbShardGroupRequest.class))) + .thenReturn(CreateDbShardGroupResponse.builder() + .dbShardGroupIdentifier(dbShardGroupIdentifier) + .dbClusterIdentifier(DB_SHARD_GROUP_AVAILABLE.dbClusterIdentifier()) + .computeRedundancy(DB_SHARD_GROUP_AVAILABLE.computeRedundancy()) + .maxACU(DB_SHARD_GROUP_AVAILABLE.maxACU()) + .publiclyAccessible(DB_SHARD_GROUP_AVAILABLE.publiclyAccessible()) + .build()); + when(proxyClient.client().describeDBClusters(any(DescribeDbClustersRequest.class))) + .thenReturn(DescribeDbClustersResponse.builder().dbClusters( + DBCluster.builder() + .status(String.valueOf(ResourceStatus.AVAILABLE)) + .build() + ) + .build() + ); + when(proxyClient.client().listTagsForResource(any(ListTagsForResourceRequest.class))) + .thenReturn(ListTagsForResourceResponse.builder() + .build()); + + final CallbackContext context = new CallbackContext(); + + test_handleRequest_base( + context, + requestBuilder, + () -> DB_SHARD_GROUP_AVAILABLE, + null, + () -> RESOURCE_MODEL_NO_IDENT, + expectSuccess() + ); + + verify(proxyClient.client(), times(1)).createDBShardGroup( + ArgumentMatchers.argThat(req -> + Objects.equals(dbShardGroupIdentifier, req.dbShardGroupIdentifier()) + ) + ); + verify(proxyClient.client(), times(2)).describeDBShardGroups( + ArgumentMatchers.argThat(req -> + Objects.equals(dbShardGroupIdentifier, req.dbShardGroupIdentifier()) + ) + ); + verify(proxyClient.client(), times(1)).describeDBClusters( + ArgumentMatchers.argThat(req -> + Objects.equals(DB_SHARD_GROUP_CREATING.dbClusterIdentifier(), req.dbClusterIdentifier()) + ) + ); + verify(proxyClient.client(), times(1)).listTagsForResource( + ArgumentMatchers.argThat( + req -> DB_SHARD_GROUP_ARN.equalsIgnoreCase(req.resourceName())) + ); + } + + @Test + public void handleRequest_Stabilize() { + ResourceHandlerRequest.ResourceHandlerRequestBuilder requestBuilder = ResourceHandlerRequest.builder(); + requestBuilder.region(REGION); + requestBuilder.awsAccountId(ACCOUNT_ID); + requestBuilder.awsPartition(PARTITION); + + when(proxyClient.client().createDBShardGroup(any(CreateDbShardGroupRequest.class))) + .thenReturn(CreateDbShardGroupResponse.builder() + .dbShardGroupIdentifier(DB_SHARD_GROUP_AVAILABLE.dbShardGroupIdentifier()) + .dbClusterIdentifier(DB_SHARD_GROUP_AVAILABLE.dbClusterIdentifier()) + .computeRedundancy(DB_SHARD_GROUP_AVAILABLE.computeRedundancy()) + .maxACU(DB_SHARD_GROUP_AVAILABLE.maxACU()) + .publiclyAccessible(DB_SHARD_GROUP_AVAILABLE.publiclyAccessible()) + .build()); + when(proxyClient.client().describeDBClusters(any(DescribeDbClustersRequest.class))) + .thenReturn(DescribeDbClustersResponse.builder().dbClusters( + DBCluster.builder() + .status(String.valueOf(ResourceStatus.MODIFYING)) + .build() + ) + .build() + ).thenReturn(DescribeDbClustersResponse.builder().dbClusters( + DBCluster.builder() + .status(String.valueOf(ResourceStatus.AVAILABLE)) + .build() + ) + .build() + ); + when(proxyClient.client().listTagsForResource(any(ListTagsForResourceRequest.class))) + .thenReturn(ListTagsForResourceResponse.builder() + .build()); + + final CallbackContext context = new CallbackContext(); + final Queue transitions = new ConcurrentLinkedQueue<>(); + transitions.add(DB_SHARD_GROUP_CREATING); + transitions.add(DB_SHARD_GROUP_AVAILABLE); + transitions.add(DB_SHARD_GROUP_AVAILABLE); + transitions.add(DB_SHARD_GROUP_MODIFYING); + + test_handleRequest_base( + context, + requestBuilder, + () -> { + if (transitions.size() > 0) { + return transitions.remove(); + } + return DB_SHARD_GROUP_AVAILABLE; + }, + null, + () -> RESOURCE_MODEL, + expectSuccess() + ); + + verify(proxyClient.client(), times(1)).createDBShardGroup( + ArgumentMatchers.argThat(req -> + Objects.equals(DB_SHARD_GROUP_CREATING.dbShardGroupIdentifier(), req.dbShardGroupIdentifier()) + ) + ); + verify(proxyClient.client(), times(3)).describeDBShardGroups( + ArgumentMatchers.argThat(req -> + Objects.equals(DB_SHARD_GROUP_CREATING.dbShardGroupIdentifier(), req.dbShardGroupIdentifier()) + ) + ); + verify(proxyClient.client(), times(2)).describeDBClusters( + ArgumentMatchers.argThat(req -> + Objects.equals(DB_SHARD_GROUP_CREATING.dbClusterIdentifier(), req.dbClusterIdentifier()) + ) + ); + verify(proxyClient.client(), times(1)).listTagsForResource( + ArgumentMatchers.argThat( + req -> DB_SHARD_GROUP_ARN.equalsIgnoreCase(req.resourceName())) + ); + } + + @Test + public void handleRequest_CreateDbShardGroupException() { + ResourceHandlerRequest.ResourceHandlerRequestBuilder requestBuilder = ResourceHandlerRequest.builder(); + requestBuilder.region(REGION); + requestBuilder.awsAccountId(ACCOUNT_ID); + requestBuilder.awsPartition(PARTITION); + + when(proxyClient.client().createDBShardGroup(any(CreateDbShardGroupRequest.class))) + .thenThrow(DbShardGroupAlreadyExistsException.builder().message("error").build()); + + final CallbackContext context = new CallbackContext(); + + test_handleRequest_base( + context, + requestBuilder, + null, + null, + () -> RESOURCE_MODEL, + expectFailed(HandlerErrorCode.AlreadyExists) + ); + + verify(proxyClient.client(), times(1)).createDBShardGroup( + ArgumentMatchers.argThat(req -> + Objects.equals(DB_SHARD_GROUP_CREATING.dbShardGroupIdentifier(), req.dbShardGroupIdentifier()) + ) + ); + } +} diff --git a/aws-rds-dbshardgroup/src/test/java/software/amazon/rds/dbshardgroup/DeleteHandlerTest.java b/aws-rds-dbshardgroup/src/test/java/software/amazon/rds/dbshardgroup/DeleteHandlerTest.java new file mode 100644 index 000000000..081812b7d --- /dev/null +++ b/aws-rds-dbshardgroup/src/test/java/software/amazon/rds/dbshardgroup/DeleteHandlerTest.java @@ -0,0 +1,206 @@ +package software.amazon.rds.dbshardgroup; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Objects; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +import lombok.Getter; +import org.mockito.ArgumentMatchers; +import software.amazon.awssdk.services.rds.RdsClient; +import software.amazon.awssdk.services.rds.model.*; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.HandlerErrorCode; +import software.amazon.cloudformation.proxy.ProxyClient; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.mockito.Mockito.times; + +@ExtendWith(MockitoExtension.class) +public class DeleteHandlerTest extends AbstractHandlerTest { + + @Mock + @Getter + private AmazonWebServicesClientProxy proxy; + + @Mock + @Getter + private ProxyClient proxyClient; + + @Mock + RdsClient rdsClient; + + @Getter + private DeleteHandler handler; + private boolean expectClientInvocation; + + @BeforeEach + public void setup() { + handler = new DeleteHandler(TEST_HANDLER_CONFIG); + proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); + rdsClient = mock(RdsClient.class); + proxyClient = MOCK_PROXY(proxy, rdsClient); + expectClientInvocation = true; + } + + @AfterEach + public void tear_down() { + if (expectClientInvocation){ + verify(rdsClient, atLeastOnce()).serviceName(); + } + verifyNoMoreInteractions(rdsClient); + } + + @Test + public void handleRequest_SimpleSuccess() { + when(proxyClient.client().deleteDBShardGroup(any(DeleteDbShardGroupRequest.class))) + .thenReturn(DeleteDbShardGroupResponse.builder() + .dbShardGroupIdentifier(DB_SHARD_GROUP_AVAILABLE.dbShardGroupIdentifier()) + .dbClusterIdentifier(DB_SHARD_GROUP_AVAILABLE.dbClusterIdentifier()) + .computeRedundancy(DB_SHARD_GROUP_AVAILABLE.computeRedundancy()) + .maxACU(DB_SHARD_GROUP_AVAILABLE.maxACU()) + .publiclyAccessible(DB_SHARD_GROUP_AVAILABLE.publiclyAccessible()) + .build()); + when(proxyClient.client().describeDBClusters(any(DescribeDbClustersRequest.class))) + .thenReturn(DescribeDbClustersResponse.builder().dbClusters( + DBCluster.builder() + .status(String.valueOf(ResourceStatus.AVAILABLE)) + .build() + ) + .build() + ); + + final CallbackContext context = new CallbackContext(); + + final Queue transitions = new ConcurrentLinkedQueue<>(); + transitions.add(DB_SHARD_GROUP_AVAILABLE); + transitions.add(DB_SHARD_GROUP_AVAILABLE); + + test_handleRequest_base( + context, + () -> { + if (transitions.size() > 0) { + return transitions.remove(); + } + throw DbShardGroupNotFoundException.builder().message("db shard group not found").build(); + }, + () -> RESOURCE_MODEL, + expectSuccess() + ); + + verify(proxyClient.client(), times(1)).deleteDBShardGroup( + ArgumentMatchers.any(DeleteDbShardGroupRequest.class) + ); + verify(proxyClient.client(), times(3)).describeDBShardGroups( + ArgumentMatchers.argThat(req -> + Objects.equals(DB_SHARD_GROUP_AVAILABLE.dbShardGroupIdentifier(), req.dbShardGroupIdentifier()) + ) + ); + verify(proxyClient.client(), times(1)).describeDBClusters( + ArgumentMatchers.argThat(req -> + Objects.equals(DB_SHARD_GROUP_AVAILABLE.dbClusterIdentifier(), req.dbClusterIdentifier()) + ) + ); + } + + @Test + public void handleRequest_ShardGroupDeleting() { + when(proxyClient.client().describeDBClusters(any(DescribeDbClustersRequest.class))) + .thenThrow(DbClusterNotFoundException.builder().message("db cluster not found").build()); + + final CallbackContext context = new CallbackContext(); + + final Queue transitions = new ConcurrentLinkedQueue<>(); + transitions.add(DB_SHARD_GROUP_DELETING); + transitions.add(DB_SHARD_GROUP_DELETING); + + test_handleRequest_base( + context, + () -> { + if (transitions.size() > 0) { + return transitions.remove(); + } + throw DbShardGroupNotFoundException.builder().message("db shard group not found").build(); + }, + () -> RESOURCE_MODEL, + expectSuccess() + ); + + verify(proxyClient.client(), times(3)).describeDBShardGroups( + ArgumentMatchers.argThat(req -> + Objects.equals(DB_SHARD_GROUP_DELETING.dbShardGroupIdentifier(), req.dbShardGroupIdentifier()) + ) + ); + verify(proxyClient.client(), times(1)).describeDBClusters( + ArgumentMatchers.argThat(req -> + Objects.equals(DB_SHARD_GROUP_DELETING.dbClusterIdentifier(), req.dbClusterIdentifier()) + ) + ); + } + + @Test + public void handleRequest_ShardGroupDeleting_ClusterEmptyResponse() { + when(proxyClient.client().describeDBClusters(any(DescribeDbClustersRequest.class))) + .thenReturn(DescribeDbClustersResponse.builder().dbClusters(Collections.emptyList()).build()); + + final CallbackContext context = new CallbackContext(); + + final Queue transitions = new ConcurrentLinkedQueue<>(); + transitions.add(DB_SHARD_GROUP_DELETING); + transitions.add(DB_SHARD_GROUP_DELETING); + + test_handleRequest_base( + context, + () -> { + if (transitions.size() > 0) { + return transitions.remove(); + } + throw DbShardGroupNotFoundException.builder().message("db shard group not found").build(); + }, + () -> RESOURCE_MODEL, + expectSuccess() + ); + + verify(proxyClient.client(), times(3)).describeDBShardGroups( + ArgumentMatchers.argThat(req -> + Objects.equals(DB_SHARD_GROUP_DELETING.dbShardGroupIdentifier(), req.dbShardGroupIdentifier()) + ) + ); + verify(proxyClient.client(), times(1)).describeDBClusters( + ArgumentMatchers.argThat(req -> + Objects.equals(DB_SHARD_GROUP_DELETING.dbClusterIdentifier(), req.dbClusterIdentifier()) + ) + ); + } + + @Test + public void handleRequest_ShardGroupDeleted() { + expectClientInvocation = false; + final CallbackContext context = new CallbackContext(); + + test_handleRequest_base( + context, + () -> { + throw DbShardGroupNotFoundException.builder().message("db shard group not found").build(); + }, + () -> RESOURCE_MODEL, + expectFailed(HandlerErrorCode.NotFound) + ); + + verify(proxyClient.client(), times(1)).describeDBShardGroups( + ArgumentMatchers.argThat(req -> + Objects.equals(DB_SHARD_GROUP_DELETING.dbShardGroupIdentifier(), req.dbShardGroupIdentifier()) + ) + ); + } +} diff --git a/aws-rds-dbshardgroup/src/test/java/software/amazon/rds/dbshardgroup/ListHandlerTest.java b/aws-rds-dbshardgroup/src/test/java/software/amazon/rds/dbshardgroup/ListHandlerTest.java new file mode 100644 index 000000000..061ca5932 --- /dev/null +++ b/aws-rds-dbshardgroup/src/test/java/software/amazon/rds/dbshardgroup/ListHandlerTest.java @@ -0,0 +1,104 @@ +package software.amazon.rds.dbshardgroup; + +import java.util.Collections; +import java.time.Duration; + +import lombok.Getter; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentMatchers; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import software.amazon.awssdk.services.rds.RdsClient; +import software.amazon.awssdk.services.rds.model.DescribeDbShardGroupsRequest; +import software.amazon.awssdk.services.rds.model.DescribeDbShardGroupsResponse; +import software.amazon.awssdk.services.rds.model.ListTagsForResourceRequest; +import software.amazon.awssdk.services.rds.model.ListTagsForResourceResponse; +import software.amazon.cloudformation.proxy.*; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.mockito.Mockito.times; + +@ExtendWith(MockitoExtension.class) +public class ListHandlerTest extends AbstractHandlerTest { + + @Mock + @Getter + private AmazonWebServicesClientProxy proxy; + + @Mock + @Getter + private ProxyClient proxyClient; + + @Mock + RdsClient rdsClient; + + @Getter + private ListHandler handler; + + private boolean expectServiceInvocation; + + @BeforeEach + public void setup() { + handler = new ListHandler(TEST_HANDLER_CONFIG); + proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); + rdsClient = mock(RdsClient.class); + proxyClient = MOCK_PROXY(proxy, rdsClient); + expectServiceInvocation = true; + } + + @AfterEach + public void tear_down() { + if (expectServiceInvocation) { + verify(rdsClient, atLeastOnce()).serviceName(); + } + verifyNoMoreInteractions(rdsClient); + } + + @Test + public void handleRequest_SimpleSuccess() { + expectServiceInvocation = false; + + ResourceHandlerRequest.ResourceHandlerRequestBuilder requestBuilder = ResourceHandlerRequest.builder(); + requestBuilder.region(REGION); + requestBuilder.awsAccountId(ACCOUNT_ID); + requestBuilder.awsPartition(PARTITION); + + when(proxyClient.client().describeDBShardGroups(any(DescribeDbShardGroupsRequest.class))) + .thenReturn(DescribeDbShardGroupsResponse.builder() + .dbShardGroups(DB_SHARD_GROUP_AVAILABLE) + .marker("marker2") + .build()); + when(proxyClient.client().listTagsForResource(any(ListTagsForResourceRequest.class))) + .thenReturn(ListTagsForResourceResponse.builder() + .build()); + + final ProgressEvent response = test_handleRequest_base( + new CallbackContext(), + requestBuilder, + null, + null, + () -> RESOURCE_MODEL, + expectSuccess() + ); + + assertThat(response.getResourceModel()).isNull(); + assertThat(response.getResourceModels().size()).isEqualTo(1); + assertThat(response.getResourceModels().stream().anyMatch(model -> + Translator.translateDbShardGroupFromSdk(DB_SHARD_GROUP_AVAILABLE, Collections.emptySet()).equals(model))).isTrue(); + assertThat(response.getNextToken()).isEqualTo("marker2"); + + + + verify(proxyClient.client(), times(1)).describeDBShardGroups(any(DescribeDbShardGroupsRequest.class)); + verify(proxyClient.client(), times(1)) + .listTagsForResource( + ArgumentMatchers.argThat( + req -> DB_SHARD_GROUP_ARN.equalsIgnoreCase(req.resourceName())) + ); + } +} diff --git a/aws-rds-dbshardgroup/src/test/java/software/amazon/rds/dbshardgroup/ReadHandlerTest.java b/aws-rds-dbshardgroup/src/test/java/software/amazon/rds/dbshardgroup/ReadHandlerTest.java new file mode 100644 index 000000000..0211830e3 --- /dev/null +++ b/aws-rds-dbshardgroup/src/test/java/software/amazon/rds/dbshardgroup/ReadHandlerTest.java @@ -0,0 +1,92 @@ +package software.amazon.rds.dbshardgroup; + +import java.time.Duration; + +import lombok.Getter; +import org.mockito.ArgumentMatchers; +import software.amazon.awssdk.services.rds.RdsClient; +import software.amazon.awssdk.services.rds.model.DescribeDbShardGroupsRequest; +import software.amazon.awssdk.services.rds.model.ListTagsForResourceRequest; +import software.amazon.awssdk.services.rds.model.ListTagsForResourceResponse; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.ProxyClient; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; + +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class ReadHandlerTest extends AbstractHandlerTest { + + @Mock + @Getter + private AmazonWebServicesClientProxy proxy; + + @Mock + @Getter + private ProxyClient proxyClient; + + @Mock + RdsClient rdsClient; + + @Getter + private ReadHandler handler; + + private boolean expectServiceInvocation; + + @BeforeEach + public void setup() { + handler = new ReadHandler(TEST_HANDLER_CONFIG); + proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); + rdsClient = mock(RdsClient.class); + proxyClient = MOCK_PROXY(proxy, rdsClient); + expectServiceInvocation = true; + } + + @AfterEach + public void tear_down() { + if (expectServiceInvocation) { + verify(rdsClient, atLeastOnce()).serviceName(); + } + verifyNoMoreInteractions(rdsClient); + } + + @Test + public void handleRequest_SimpleSuccess() { + expectServiceInvocation = false; + + ResourceHandlerRequest.ResourceHandlerRequestBuilder requestBuilder = ResourceHandlerRequest.builder(); + requestBuilder.region(REGION); + requestBuilder.awsAccountId(ACCOUNT_ID); + requestBuilder.awsPartition(PARTITION); + + when(proxyClient.client().listTagsForResource(any(ListTagsForResourceRequest.class))) + .thenReturn(ListTagsForResourceResponse.builder() + .build()); + + test_handleRequest_base( + new CallbackContext(), + requestBuilder, + () -> DB_SHARD_GROUP_AVAILABLE, + null, + () -> RESOURCE_MODEL, + expectSuccess() + ); + + verify(proxyClient.client(), times(1)) + .describeDBShardGroups( + ArgumentMatchers.argThat( + req -> DB_SHARD_GROUP_IDENTIFIER.equals(req.dbShardGroupIdentifier())) + ); + verify(proxyClient.client(), times(1)) + .listTagsForResource( + ArgumentMatchers.argThat( + req -> DB_SHARD_GROUP_ARN.equalsIgnoreCase(req.resourceName())) + ); + } +} diff --git a/aws-rds-dbshardgroup/src/test/java/software/amazon/rds/dbshardgroup/UpdateHandlerTest.java b/aws-rds-dbshardgroup/src/test/java/software/amazon/rds/dbshardgroup/UpdateHandlerTest.java new file mode 100644 index 000000000..801979afa --- /dev/null +++ b/aws-rds-dbshardgroup/src/test/java/software/amazon/rds/dbshardgroup/UpdateHandlerTest.java @@ -0,0 +1,234 @@ +package software.amazon.rds.dbshardgroup; + +import java.time.Duration; +import java.util.Objects; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +import lombok.Getter; +import org.mockito.ArgumentMatchers; +import software.amazon.awssdk.services.rds.RdsClient; +import software.amazon.awssdk.services.rds.model.*; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.HandlerErrorCode; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.ProxyClient; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.mockito.Mockito.times; + +@ExtendWith(MockitoExtension.class) +public class UpdateHandlerTest extends AbstractHandlerTest { + + @Mock + @Getter + private AmazonWebServicesClientProxy proxy; + + @Mock + @Getter + private ProxyClient proxyClient; + + @Mock + RdsClient rdsClient; + + @Getter + private UpdateHandler handler; + + @BeforeEach + public void setup() { + handler = new UpdateHandler(TEST_HANDLER_CONFIG); + proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); + rdsClient = mock(RdsClient.class); + proxyClient = MOCK_PROXY(proxy, rdsClient); + } + + @AfterEach + public void tear_down() { + verify(rdsClient, atLeastOnce()).serviceName(); + verifyNoMoreInteractions(rdsClient); + } + + @Test + public void handleRequest_SimpleSuccess() { + ResourceHandlerRequest.ResourceHandlerRequestBuilder requestBuilder = ResourceHandlerRequest.builder(); + requestBuilder.region(REGION); + requestBuilder.awsAccountId(ACCOUNT_ID); + requestBuilder.awsPartition(PARTITION); + requestBuilder.previousResourceTags(Translator.translateTagsToRequest(TAG_LIST)); + requestBuilder.desiredResourceTags(Translator.translateTagsToRequest(TAG_LIST_ALTER)); + + when(proxyClient.client().removeTagsFromResource(any(RemoveTagsFromResourceRequest.class))) + .thenReturn(RemoveTagsFromResourceResponse.builder().build()); + when(proxyClient.client().addTagsToResource(any(AddTagsToResourceRequest.class))) + .thenReturn(AddTagsToResourceResponse.builder().build()); + when(proxyClient.client().modifyDBShardGroup(any(ModifyDbShardGroupRequest.class))) + .thenReturn(ModifyDbShardGroupResponse.builder().build()); + when(proxyClient.client().describeDBClusters(any(DescribeDbClustersRequest.class))) + .thenReturn(DescribeDbClustersResponse.builder().dbClusters( + DBCluster.builder() + .status(String.valueOf(ResourceStatus.AVAILABLE)) + .build() + ) + .build() + ); + when(proxyClient.client().listTagsForResource(any(ListTagsForResourceRequest.class))) + .thenReturn(ListTagsForResourceResponse.builder() + .build()); + + Queue transitions = new ConcurrentLinkedQueue<>(); + transitions.add(DB_SHARD_GROUP_MODIFYING); + + ProgressEvent progressEvent = ProgressEvent.builder() + .callbackContext(new CallbackContext()).build(); + + progressEvent.getCallbackContext().setWaitTime(301); + + test_handleRequest_base( + progressEvent.getCallbackContext(), + requestBuilder, + () -> { + if (transitions.size() > 0) { + return transitions.remove(); + } + return DB_SHARD_GROUP_AVAILABLE.toBuilder() + .maxACU(MAX_ACU_ALTER) + .build(); + }, + () -> RESOURCE_MODEL.toBuilder() + .build(), + () -> RESOURCE_MODEL.toBuilder() + .maxACU(MAX_ACU_ALTER) + .build(), + expectSuccess() + ); + + verify(proxyClient.client(), times(1)).modifyDBShardGroup(any(ModifyDbShardGroupRequest.class)); + verify(proxyClient.client(), times(3)).describeDBShardGroups(any(DescribeDbShardGroupsRequest.class)); + verify(proxyClient.client(), times(1)).removeTagsFromResource(any(RemoveTagsFromResourceRequest.class)); + verify(proxyClient.client(), times(1)).addTagsToResource(any(AddTagsToResourceRequest.class)); + verify(proxyClient.client(), times(1)).describeDBClusters( + ArgumentMatchers.argThat(req -> + Objects.equals(DB_SHARD_GROUP_CREATING.dbClusterIdentifier(), req.dbClusterIdentifier()) + ) + ); + verify(proxyClient.client(), times(1)).listTagsForResource( + ArgumentMatchers.argThat( + req -> DB_SHARD_GROUP_ARN.equalsIgnoreCase(req.resourceName())) + ); + } + + @Test + public void handleRequest_Stabilize() { + ResourceHandlerRequest.ResourceHandlerRequestBuilder requestBuilder = ResourceHandlerRequest.builder(); + requestBuilder.region(REGION); + requestBuilder.awsAccountId(ACCOUNT_ID); + requestBuilder.awsPartition(PARTITION); + requestBuilder.previousResourceTags(Translator.translateTagsToRequest(TAG_LIST)); + requestBuilder.desiredResourceTags(Translator.translateTagsToRequest(TAG_LIST_ALTER)); + + when(proxyClient.client().removeTagsFromResource(any(RemoveTagsFromResourceRequest.class))) + .thenReturn(RemoveTagsFromResourceResponse.builder().build()); + when(proxyClient.client().addTagsToResource(any(AddTagsToResourceRequest.class))) + .thenReturn(AddTagsToResourceResponse.builder().build()); + when(proxyClient.client().modifyDBShardGroup(any(ModifyDbShardGroupRequest.class))) + .thenReturn(ModifyDbShardGroupResponse.builder().build()); + when(proxyClient.client().describeDBClusters(any(DescribeDbClustersRequest.class))) + .thenReturn(DescribeDbClustersResponse.builder().dbClusters( + DBCluster.builder() + .status(String.valueOf(ResourceStatus.MODIFYING)) + .build() + ) + .build() + ).thenReturn(DescribeDbClustersResponse.builder().dbClusters( + DBCluster.builder() + .status(String.valueOf(ResourceStatus.AVAILABLE)) + .build() + ) + .build() + ); + when(proxyClient.client().listTagsForResource(any(ListTagsForResourceRequest.class))) + .thenReturn(ListTagsForResourceResponse.builder() + .build()); + + Queue transitions = new ConcurrentLinkedQueue<>(); + transitions.add(DB_SHARD_GROUP_MODIFYING); + transitions.add(DB_SHARD_GROUP_MODIFYING); + transitions.add(DB_SHARD_GROUP_MODIFYING); + + ProgressEvent progressEvent = ProgressEvent.builder() + .callbackContext(new CallbackContext()).build(); + + progressEvent.getCallbackContext().setWaitTime(301); + + test_handleRequest_base( + progressEvent.getCallbackContext(), + requestBuilder, + () -> { + if (transitions.size() > 0) { + return transitions.remove(); + } + return DB_SHARD_GROUP_AVAILABLE.toBuilder() + .maxACU(MAX_ACU_ALTER) + .build(); + }, + () -> RESOURCE_MODEL.toBuilder() + .build(), + () -> RESOURCE_MODEL.toBuilder() + .maxACU(MAX_ACU_ALTER) + .build(), + expectSuccess() + ); + + verify(proxyClient.client(), times(1)).modifyDBShardGroup(any(ModifyDbShardGroupRequest.class)); + verify(proxyClient.client(), times(5)).describeDBShardGroups(any(DescribeDbShardGroupsRequest.class)); + verify(proxyClient.client(), times(1)).removeTagsFromResource(any(RemoveTagsFromResourceRequest.class)); + verify(proxyClient.client(), times(1)).addTagsToResource(any(AddTagsToResourceRequest.class)); + verify(proxyClient.client(), times(3)).describeDBClusters( + ArgumentMatchers.argThat(req -> + Objects.equals(DB_SHARD_GROUP_CREATING.dbClusterIdentifier(), req.dbClusterIdentifier()) + ) + ); + verify(proxyClient.client(), times(1)).listTagsForResource( + ArgumentMatchers.argThat( + req -> DB_SHARD_GROUP_ARN.equalsIgnoreCase(req.resourceName())) + ); + } + + @Test + public void handleRequest_Exception() { + ResourceHandlerRequest.ResourceHandlerRequestBuilder requestBuilder = ResourceHandlerRequest.builder(); + requestBuilder.region(REGION); + requestBuilder.awsAccountId(ACCOUNT_ID); + requestBuilder.awsPartition(PARTITION); + requestBuilder.previousResourceTags(Translator.translateTagsToRequest(TAG_LIST)); + requestBuilder.desiredResourceTags(Translator.translateTagsToRequest(TAG_LIST_ALTER)); + + when(proxyClient.client().modifyDBShardGroup(any(ModifyDbShardGroupRequest.class))) + .thenThrow(DbShardGroupNotFoundException.builder().message("error").build()); + + ProgressEvent progressEvent = ProgressEvent.builder() + .callbackContext(new CallbackContext()).build(); + + test_handleRequest_base( + progressEvent.getCallbackContext(), + requestBuilder, + null, + () -> RESOURCE_MODEL.toBuilder() + .build(), + () -> RESOURCE_MODEL.toBuilder() + .maxACU(MAX_ACU_ALTER) + .build(), + expectFailed(HandlerErrorCode.NotFound) + ); + + verify(proxyClient.client(), times(1)).modifyDBShardGroup(any(ModifyDbShardGroupRequest.class)); + } +} diff --git a/aws-rds-dbshardgroup/template.yml b/aws-rds-dbshardgroup/template.yml new file mode 100644 index 000000000..b081cdc8d --- /dev/null +++ b/aws-rds-dbshardgroup/template.yml @@ -0,0 +1,23 @@ +AWSTemplateFormatVersion: "2010-09-09" +Transform: AWS::Serverless-2016-10-31 +Description: AWS SAM template for the AWS::RDS::DBShardGroup resource type + +Globals: + Function: + Timeout: 180 # docker start-up times can be long for SAM CLI + MemorySize: 512 + +Resources: + TypeFunction: + Type: AWS::Serverless::Function + Properties: + Handler: software.amazon.rds.dbshardgroup.HandlerWrapper::handleRequest + Runtime: java17 + CodeUri: ./target/aws-rds-dbshardgroup-handler-1.0-SNAPSHOT.jar + + TestEntrypoint: + Type: AWS::Serverless::Function + Properties: + Handler: software.amazon.rds.dbshardgroup.HandlerWrapper::testEntrypoint + Runtime: java17 + CodeUri: ./target/aws-rds-dbshardgroup-handler-1.0-SNAPSHOT.jar diff --git a/aws-rds-dbsubnetgroup/docs/README.md b/aws-rds-dbsubnetgroup/docs/README.md deleted file mode 100644 index 0dc39f1f6..000000000 --- a/aws-rds-dbsubnetgroup/docs/README.md +++ /dev/null @@ -1,76 +0,0 @@ -# AWS::RDS::DBSubnetGroup - -The AWS::RDS::DBSubnetGroup resource creates a database subnet group. Subnet groups must contain at least two subnets in two different Availability Zones in the same region. - -## Syntax - -To declare this entity in your AWS CloudFormation template, use the following syntax: - -### JSON - -
-{
-    "Type" : "AWS::RDS::DBSubnetGroup",
-    "Properties" : {
-        "DBSubnetGroupDescription" : String,
-        "DBSubnetGroupName" : String,
-        "SubnetIds" : [ String, ... ],
-        "Tags" : [ Tag, ... ]
-    }
-}
-
- -### YAML - -
-Type: AWS::RDS::DBSubnetGroup
-Properties:
-    DBSubnetGroupDescription: String
-    DBSubnetGroupName: String
-    SubnetIds: 
-      - String
-    Tags: 
-      - Tag
-
- -## Properties - -#### DBSubnetGroupDescription - -_Required_: Yes - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### DBSubnetGroupName - -_Required_: No - -_Type_: String - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### SubnetIds - -_Required_: Yes - -_Type_: List of String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### Tags - -An array of key-value pairs to apply to this resource. - -_Required_: No - -_Type_: List of Tag - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -## Return Values - -### Ref - -When you pass the logical ID of this resource to the intrinsic `Ref` function, Ref returns the DBSubnetGroupName. diff --git a/aws-rds-dbsubnetgroup/docs/tag.md b/aws-rds-dbsubnetgroup/docs/tag.md deleted file mode 100644 index 690803e7c..000000000 --- a/aws-rds-dbsubnetgroup/docs/tag.md +++ /dev/null @@ -1,51 +0,0 @@ -# AWS::RDS::DBSubnetGroup Tag - -A key-value pair to associate with a resource. - -## Syntax - -To declare this entity in your AWS CloudFormation template, use the following syntax: - -### JSON - -
-{
-    "Key" : String,
-    "Value" : String
-}
-
- -### YAML - -
-Key: String
-Value: String
-
- -## Properties - -#### Key - -The key name of the tag. You can specify a value that is 1 to 128 Unicode characters in length and cannot be prefixed with aws:. You can use any of the following characters: the set of Unicode letters, digits, whitespace, _, ., /, =, +, and -. - -_Required_: Yes - -_Type_: String - -_Minimum Length_: 1 - -_Maximum Length_: 128 - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### Value - -The value for the tag. You can specify a value that is 0 to 256 Unicode characters in length and cannot be prefixed with aws:. You can use any of the following characters: the set of Unicode letters, digits, whitespace, _, ., /, =, +, and -. - -_Required_: No - -_Type_: String - -_Maximum Length_: 256 - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) diff --git a/aws-rds-dbsubnetgroup/pom.xml b/aws-rds-dbsubnetgroup/pom.xml index 92e179b25..381eca36c 100644 --- a/aws-rds-dbsubnetgroup/pom.xml +++ b/aws-rds-dbsubnetgroup/pom.xml @@ -1,8 +1,8 @@ + xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 software.amazon.rds.dbsubnetgroup @@ -20,23 +20,6 @@ - - - software.amazon.awssdk - aws-query-protocol - 2.20.138 - - - software.amazon.awssdk - rds - 2.25.56 - - - - software.amazon.cloudformation - aws-cloudformation-rpdk-java-plugin - [2.0.0, 3.0.0) - org.projectlombok @@ -85,6 +68,11 @@ 1.0 test + + software.amazon.cloudformation + aws-cloudformation-rpdk-java-plugin + [2.0.0,3.0.0) + @@ -169,11 +157,24 @@ 0.8.8 - **/BaseConfiguration* - **/BaseHandler* - **/Configuration* - **/HandlerWrapper* - **/ResourceModel* + + **/software/amazon/rds/dbsubnetgroup/BaseConfiguration.class + **/software/amazon/rds/dbsubnetgroup/BaseHandler.class + **/software/amazon/rds/dbsubnetgroup/BaseHandlerStd.class + **/software/amazon/rds/dbsubnetgroup/CallbackContext.class + **/software/amazon/rds/dbsubnetgroup/ClientProvider.class + **/software/amazon/rds/dbsubnetgroup/Configuration.class + **/software/amazon/rds/dbsubnetgroup/CreateHandler.class + **/software/amazon/rds/dbsubnetgroup/DeleteHandler.class + **/software/amazon/rds/dbsubnetgroup/HandlerWrapper.class + **/software/amazon/rds/dbsubnetgroup/HandlerWrapperExecutable.class + **/software/amazon/rds/dbsubnetgroup/ListHandler.class + **/software/amazon/rds/dbsubnetgroup/ReadHandler.class + **/software/amazon/rds/dbsubnetgroup/ResourceModel.class + **/software/amazon/rds/dbsubnetgroup/Tag.class + **/software/amazon/rds/dbsubnetgroup/Translator.class + **/software/amazon/rds/dbsubnetgroup/TypeConfigurationModel.class + **/software/amazon/rds/dbsubnetgroup/UpdateHandler.class @@ -197,7 +198,7 @@ - PACKAGE + BUNDLE BRANCH @@ -207,7 +208,7 @@ INSTRUCTION COVEREDRATIO - 0.8 + 0.0 diff --git a/aws-rds-dbsubnetgroup/src/main/java/software/amazon/rds/dbsubnetgroup/BaseHandlerStd.java b/aws-rds-dbsubnetgroup/src/main/java/software/amazon/rds/dbsubnetgroup/BaseHandlerStd.java index 24cb8802e..15f0a0dec 100644 --- a/aws-rds-dbsubnetgroup/src/main/java/software/amazon/rds/dbsubnetgroup/BaseHandlerStd.java +++ b/aws-rds-dbsubnetgroup/src/main/java/software/amazon/rds/dbsubnetgroup/BaseHandlerStd.java @@ -1,6 +1,7 @@ package software.amazon.rds.dbsubnetgroup; import software.amazon.awssdk.services.rds.RdsClient; +import software.amazon.awssdk.services.rds.model.DBSubnetGroup; import software.amazon.awssdk.services.rds.model.DbSubnetGroupAlreadyExistsException; import software.amazon.awssdk.services.rds.model.DbSubnetGroupDoesNotCoverEnoughAZsException; import software.amazon.awssdk.services.rds.model.DbSubnetGroupNotFoundException; @@ -8,6 +9,7 @@ import software.amazon.awssdk.services.rds.model.InvalidDbSubnetGroupStateException; import software.amazon.awssdk.services.rds.model.InvalidSubnetException; import software.amazon.awssdk.services.rds.model.Tag; +import software.amazon.cloudformation.exceptions.CfnNotFoundException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.HandlerErrorCode; import software.amazon.cloudformation.proxy.Logger; @@ -119,15 +121,25 @@ private void resourceStabilizationTime(final CallbackContext callbackContext) { protected boolean isDeleted(final ResourceModel model, final ProxyClient proxyClient) { try { - proxyClient.injectCredentialsAndInvokeV2( - Translator.describeDbSubnetGroupsRequest(model), - proxyClient.client()::describeDBSubnetGroups); + fetchDbSubnetGroup(proxyClient, model); return false; - } catch (DbSubnetGroupNotFoundException e) { + } catch (CfnNotFoundException e) { return true; } } + protected DBSubnetGroup fetchDbSubnetGroup(final ProxyClient client, final ResourceModel model) { + try { + final var response = client.injectCredentialsAndInvokeV2(Translator.describeDbSubnetGroupsRequest(model), client.client()::describeDBSubnetGroups); + if (!response.hasDbSubnetGroups() || response.dbSubnetGroups().isEmpty()) { + throw new CfnNotFoundException(ResourceModel.TYPE_NAME, model.getDBSubnetGroupName()); + } + return response.dbSubnetGroups().get(0); + } catch (DbSubnetGroupNotFoundException e) { + throw new CfnNotFoundException(e); + } + } + protected ProgressEvent updateTags( final ProxyClient rdsProxyClient, final ProgressEvent progress, diff --git a/aws-rds-dbsubnetgroup/src/main/java/software/amazon/rds/dbsubnetgroup/CallbackContext.java b/aws-rds-dbsubnetgroup/src/main/java/software/amazon/rds/dbsubnetgroup/CallbackContext.java index 4a3419386..429c7635c 100644 --- a/aws-rds-dbsubnetgroup/src/main/java/software/amazon/rds/dbsubnetgroup/CallbackContext.java +++ b/aws-rds-dbsubnetgroup/src/main/java/software/amazon/rds/dbsubnetgroup/CallbackContext.java @@ -3,6 +3,7 @@ import software.amazon.cloudformation.proxy.StdCallbackContext; import software.amazon.rds.common.handler.TaggingContext; import software.amazon.rds.common.handler.TimestampContext; +import software.amazon.rds.common.util.IdempotencyHelper; import java.time.Duration; import java.time.Instant; @@ -13,7 +14,8 @@ @lombok.Setter @lombok.ToString @lombok.EqualsAndHashCode(callSuper = true) -public class CallbackContext extends StdCallbackContext implements TaggingContext.Provider, TimestampContext.Provider { +public class CallbackContext extends StdCallbackContext implements TaggingContext.Provider, TimestampContext.Provider, IdempotencyHelper.PreExistenceContext { + private Boolean preExistenceCheckDone; private String dbSubnetGroupArn; private Map timestamps; diff --git a/aws-rds-dbsubnetgroup/src/main/java/software/amazon/rds/dbsubnetgroup/CreateHandler.java b/aws-rds-dbsubnetgroup/src/main/java/software/amazon/rds/dbsubnetgroup/CreateHandler.java index 8f5dc883d..817391d73 100644 --- a/aws-rds-dbsubnetgroup/src/main/java/software/amazon/rds/dbsubnetgroup/CreateHandler.java +++ b/aws-rds-dbsubnetgroup/src/main/java/software/amazon/rds/dbsubnetgroup/CreateHandler.java @@ -11,6 +11,7 @@ import software.amazon.rds.common.handler.Commons; import software.amazon.rds.common.handler.HandlerConfig; import software.amazon.rds.common.handler.Tagging; +import software.amazon.rds.common.util.IdempotencyHelper; import software.amazon.rds.common.util.IdentifierFactory; public class CreateHandler extends BaseHandlerStd { @@ -45,7 +46,10 @@ protected ProgressEvent handleRequest( return ProgressEvent.progress(request.getDesiredResourceState(), callbackContext) .then(progress -> setDbSubnetGroupNameIfEmpty(request, progress)) - .then(progress -> safeCreateDbSubnetGroup(proxy, proxyClient, progress, allTags)) + .then(progress -> IdempotencyHelper.safeCreate( + m -> fetchDbSubnetGroup(proxyClient, m), + p -> safeCreateDbSubnetGroup(proxy, proxyClient, p, allTags), + ResourceModel.TYPE_NAME, request.getDesiredResourceState().getDBSubnetGroupName(), progress, requestLogger)) .then(progress -> new ReadHandler().handleRequest(proxy, request, callbackContext, proxyClient, requestLogger)); } diff --git a/aws-rds-dbsubnetgroup/src/test/java/software/amazon/rds/dbsubnetgroup/CreateHandlerTest.java b/aws-rds-dbsubnetgroup/src/test/java/software/amazon/rds/dbsubnetgroup/CreateHandlerTest.java index 4fbcd3291..02c3f38c0 100644 --- a/aws-rds-dbsubnetgroup/src/test/java/software/amazon/rds/dbsubnetgroup/CreateHandlerTest.java +++ b/aws-rds-dbsubnetgroup/src/test/java/software/amazon/rds/dbsubnetgroup/CreateHandlerTest.java @@ -41,6 +41,7 @@ import software.amazon.cloudformation.proxy.ResourceHandlerRequest; import software.amazon.rds.common.handler.HandlerConfig; import software.amazon.rds.common.logging.RequestLogger; +import software.amazon.rds.common.util.IdempotencyHelper; import software.amazon.rds.test.common.core.HandlerName; @ExtendWith(MockitoExtension.class) @@ -75,6 +76,7 @@ public void setup() { rds = mock(RdsClient.class); proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); proxyRdsClient = MOCK_PROXY(proxy, rds); + IdempotencyHelper.setBypass(true); } @AfterEach diff --git a/aws-rds-eventsubscription/docs/README.md b/aws-rds-eventsubscription/docs/README.md deleted file mode 100644 index 6d5b25d76..000000000 --- a/aws-rds-eventsubscription/docs/README.md +++ /dev/null @@ -1,121 +0,0 @@ -# AWS::RDS::EventSubscription - -The AWS::RDS::EventSubscription resource allows you to receive notifications for Amazon Relational Database Service events through the Amazon Simple Notification Service (Amazon SNS). For more information, see Using Amazon RDS Event Notification in the Amazon RDS User Guide. - -## Syntax - -To declare this entity in your AWS CloudFormation template, use the following syntax: - -### JSON - -
-{
-    "Type" : "AWS::RDS::EventSubscription",
-    "Properties" : {
-        "Tags" : [ Tag, ... ],
-        "SubscriptionName" : String,
-        "Enabled" : Boolean,
-        "EventCategories" : [ String, ... ],
-        "SnsTopicArn" : String,
-        "SourceIds" : [ String, ... ],
-        "SourceType" : String
-    }
-}
-
- -### YAML - -
-Type: AWS::RDS::EventSubscription
-Properties:
-    Tags: 
-      - Tag
-    SubscriptionName: String
-    Enabled: Boolean
-    EventCategories: 
-      - String
-    SnsTopicArn: String
-    SourceIds: 
-      - String
-    SourceType: String
-
- -## Properties - -#### Tags - -An array of key-value pairs to apply to this resource. - -_Required_: No - -_Type_: List of Tag - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### SubscriptionName - -The name of the subscription. - -_Required_: No - -_Type_: String - -_Maximum Length_: 255 - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### Enabled - -A Boolean value; set to true to activate the subscription, set to false to create the subscription but not active it. - -_Required_: No - -_Type_: Boolean - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### EventCategories - -A list of event categories for a SourceType that you want to subscribe to. You can see a list of the categories for a given SourceType in the Events topic in the Amazon RDS User Guide or by using the DescribeEventCategories action. - -_Required_: No - -_Type_: List of String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### SnsTopicArn - -The Amazon Resource Name (ARN) of the SNS topic created for event notification. The ARN is created by Amazon SNS when you create a topic and subscribe to it. - -_Required_: Yes - -_Type_: String - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### SourceIds - -The list of identifiers of the event sources for which events will be returned. If not specified, then all sources are included in the response. An identifier must begin with a letter and must contain only ASCII letters, digits, and hyphens; it cannot end with a hyphen or contain two consecutive hyphens. - -_Required_: No - -_Type_: List of String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### SourceType - -The type of source that will be generating the events. For example, if you want to be notified of events generated by a DB instance, you would set this parameter to db-instance. if this value is not specified, all events are returned. - -_Required_: No - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -## Return Values - -### Ref - -When you pass the logical ID of this resource to the intrinsic `Ref` function, Ref returns the SubscriptionName. diff --git a/aws-rds-eventsubscription/docs/tag.md b/aws-rds-eventsubscription/docs/tag.md deleted file mode 100644 index 5ca7e0d16..000000000 --- a/aws-rds-eventsubscription/docs/tag.md +++ /dev/null @@ -1,51 +0,0 @@ -# AWS::RDS::EventSubscription Tag - -A key-value pair to associate with a resource. - -## Syntax - -To declare this entity in your AWS CloudFormation template, use the following syntax: - -### JSON - -
-{
-    "Key" : String,
-    "Value" : String
-}
-
- -### YAML - -
-Key: String
-Value: String
-
- -## Properties - -#### Key - -The key name of the tag. You can specify a value that is 1 to 128 Unicode characters in length and cannot be prefixed with aws:. You can use any of the following characters: the set of Unicode letters, digits, whitespace, _, ., /, =, +, and -. - -_Required_: Yes - -_Type_: String - -_Minimum Length_: 1 - -_Maximum Length_: 128 - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### Value - -The value for the tag. You can specify a value that is 0 to 256 Unicode characters in length and cannot be prefixed with aws:. You can use any of the following characters: the set of Unicode letters, digits, whitespace, _, ., /, =, +, and -. - -_Required_: No - -_Type_: String - -_Maximum Length_: 256 - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) diff --git a/aws-rds-eventsubscription/pom.xml b/aws-rds-eventsubscription/pom.xml index 738b7c735..819885a48 100644 --- a/aws-rds-eventsubscription/pom.xml +++ b/aws-rds-eventsubscription/pom.xml @@ -20,28 +20,16 @@ - - - software.amazon.awssdk - aws-query-protocol - 2.20.138 - software.amazon.rds.common aws-rds-cfn-common 1.0 compile - - software.amazon.awssdk - rds - 2.25.56 - - software.amazon.cloudformation aws-cloudformation-rpdk-java-plugin - [2.0.0, 3.0.0) + [2.0.0,3.0.0) @@ -169,11 +157,24 @@ 0.8.8 - **/BaseConfiguration* - **/BaseHandler* - **/HandlerWrapper* - **/ResourceModel* - **/Configuration* + + **/software/amazon/rds/eventsubscription/BaseConfiguration.class + **/software/amazon/rds/eventsubscription/BaseHandler.class + **/software/amazon/rds/eventsubscription/BaseHandlerStd.class + **/software/amazon/rds/eventsubscription/CallbackContext.class + **/software/amazon/rds/eventsubscription/ClientProvider.class + **/software/amazon/rds/eventsubscription/Configuration.class + **/software/amazon/rds/eventsubscription/CreateHandler.class + **/software/amazon/rds/eventsubscription/DeleteHandler.class + **/software/amazon/rds/eventsubscription/HandlerWrapper.class + **/software/amazon/rds/eventsubscription/HandlerWrapperExecutable.class + **/software/amazon/rds/eventsubscription/ListHandler.class + **/software/amazon/rds/eventsubscription/ReadHandler.class + **/software/amazon/rds/eventsubscription/ResourceModel.class + **/software/amazon/rds/eventsubscription/Tag.class + **/software/amazon/rds/eventsubscription/Translator.class + **/software/amazon/rds/eventsubscription/TypeConfigurationModel.class + **/software/amazon/rds/eventsubscription/UpdateHandler.class @@ -197,7 +198,7 @@ - PACKAGE + BUNDLE BRANCH @@ -207,7 +208,7 @@ INSTRUCTION COVEREDRATIO - 0.8 + 0.0 diff --git a/aws-rds-eventsubscription/src/main/java/software/amazon/rds/eventsubscription/BaseHandlerStd.java b/aws-rds-eventsubscription/src/main/java/software/amazon/rds/eventsubscription/BaseHandlerStd.java index 581cc36a1..863237dc2 100644 --- a/aws-rds-eventsubscription/src/main/java/software/amazon/rds/eventsubscription/BaseHandlerStd.java +++ b/aws-rds-eventsubscription/src/main/java/software/amazon/rds/eventsubscription/BaseHandlerStd.java @@ -6,6 +6,7 @@ import java.util.function.Function; import software.amazon.awssdk.services.rds.RdsClient; +import software.amazon.awssdk.services.rds.model.EventSubscription; import software.amazon.awssdk.services.rds.model.EventSubscriptionQuotaExceededException; import software.amazon.awssdk.services.rds.model.InvalidEventSubscriptionStateException; import software.amazon.awssdk.services.rds.model.SnsTopicArnNotFoundException; @@ -13,6 +14,7 @@ import software.amazon.awssdk.services.rds.model.SubscriptionAlreadyExistException; import software.amazon.awssdk.services.rds.model.SubscriptionNotFoundException; import software.amazon.awssdk.services.rds.model.Tag; +import software.amazon.cloudformation.exceptions.CfnNotFoundException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.HandlerErrorCode; import software.amazon.cloudformation.proxy.Logger; @@ -67,7 +69,7 @@ public final ProgressEvent handleRequest( requestLogger -> handleRequest( proxy, new LoggingProxyClient<>(requestLogger, proxy.newProxy(new ClientProvider()::getClient)), request, - callbackContext != null ? callbackContext : new CallbackContext() + callbackContext != null ? callbackContext : new CallbackContext(), requestLogger )); } @@ -198,4 +200,16 @@ protected ProgressEvent fetchEventSubscriptionAr return ProgressEvent.progress(resourceModel, context); }); } + + protected EventSubscription fetchEventSubscription(final ProxyClient client, final ResourceModel model) { + try { + final var response = client.injectCredentialsAndInvokeV2(Translator.describeEventSubscriptionsRequest(model), client.client()::describeEventSubscriptions); + if (!response.hasEventSubscriptionsList() || response.eventSubscriptionsList().isEmpty()) { + throw new CfnNotFoundException(ResourceModel.TYPE_NAME, model.getSubscriptionName()); + } + return response.eventSubscriptionsList().get(0); + } catch (SubscriptionNotFoundException e) { + throw new CfnNotFoundException(e); + } + } } diff --git a/aws-rds-eventsubscription/src/main/java/software/amazon/rds/eventsubscription/CallbackContext.java b/aws-rds-eventsubscription/src/main/java/software/amazon/rds/eventsubscription/CallbackContext.java index fbe3bd68a..35f9eca83 100644 --- a/aws-rds-eventsubscription/src/main/java/software/amazon/rds/eventsubscription/CallbackContext.java +++ b/aws-rds-eventsubscription/src/main/java/software/amazon/rds/eventsubscription/CallbackContext.java @@ -3,6 +3,7 @@ import software.amazon.cloudformation.proxy.StdCallbackContext; import software.amazon.rds.common.handler.TaggingContext; import software.amazon.rds.common.handler.TimestampContext; +import software.amazon.rds.common.util.IdempotencyHelper; import java.time.Duration; import java.time.Instant; @@ -13,7 +14,8 @@ @lombok.Setter @lombok.ToString @lombok.EqualsAndHashCode(callSuper = true) -public class CallbackContext extends StdCallbackContext implements TaggingContext.Provider, TimestampContext.Provider { +public class CallbackContext extends StdCallbackContext implements TaggingContext.Provider, TimestampContext.Provider, IdempotencyHelper.PreExistenceContext { + private Boolean preExistenceCheckDone; private String eventSubscriptionArn; private Map timestamps; diff --git a/aws-rds-eventsubscription/src/main/java/software/amazon/rds/eventsubscription/CreateHandler.java b/aws-rds-eventsubscription/src/main/java/software/amazon/rds/eventsubscription/CreateHandler.java index ec3f61366..33fd4b72a 100644 --- a/aws-rds-eventsubscription/src/main/java/software/amazon/rds/eventsubscription/CreateHandler.java +++ b/aws-rds-eventsubscription/src/main/java/software/amazon/rds/eventsubscription/CreateHandler.java @@ -10,6 +10,7 @@ import software.amazon.cloudformation.proxy.ResourceHandlerRequest; import software.amazon.rds.common.handler.Commons; import software.amazon.rds.common.handler.Tagging; +import software.amazon.rds.common.util.IdempotencyHelper; import software.amazon.rds.common.util.IdentifierFactory; public class CreateHandler extends BaseHandlerStd { @@ -37,7 +38,10 @@ protected ProgressEvent handleRequest( return ProgressEvent.progress(model, callbackContext) .then(progress -> setEnabledDefaultValue(progress)) .then(progress -> setEventSubscriptionNameIfEmpty(request, progress)) - .then(progress -> safeCreateEventSubscription(proxy, proxyClient, progress, allTags)) + .then(progress -> IdempotencyHelper.safeCreate( + m -> fetchEventSubscription(proxyClient, m), + p -> safeCreateEventSubscription(proxy, proxyClient, p, allTags), + ResourceModel.TYPE_NAME, model.getSubscriptionName(), progress, requestLogger)) .then(progress -> new ReadHandler().handleRequest(proxy, proxyClient, request, callbackContext)); } diff --git a/aws-rds-eventsubscription/src/test/java/software/amazon/rds/eventsubscription/CreateHandlerTest.java b/aws-rds-eventsubscription/src/test/java/software/amazon/rds/eventsubscription/CreateHandlerTest.java index 37a8767a4..3f5b0665c 100644 --- a/aws-rds-eventsubscription/src/test/java/software/amazon/rds/eventsubscription/CreateHandlerTest.java +++ b/aws-rds-eventsubscription/src/test/java/software/amazon/rds/eventsubscription/CreateHandlerTest.java @@ -39,6 +39,7 @@ import software.amazon.cloudformation.proxy.ProgressEvent; import software.amazon.cloudformation.proxy.ProxyClient; import software.amazon.cloudformation.proxy.ResourceHandlerRequest; +import software.amazon.rds.common.util.IdempotencyHelper; import software.amazon.rds.test.common.core.HandlerName; @ExtendWith(MockitoExtension.class) @@ -66,6 +67,7 @@ public void setup() { proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); rds = mock(RdsClient.class); proxyRdsClient = MOCK_PROXY(proxy, rds); + IdempotencyHelper.setBypass(true); } @AfterEach diff --git a/aws-rds-globalcluster/aws-rds-globalcluster.json b/aws-rds-globalcluster/aws-rds-globalcluster.json index abd295996..042678504 100644 --- a/aws-rds-globalcluster/aws-rds-globalcluster.json +++ b/aws-rds-globalcluster/aws-rds-globalcluster.json @@ -41,9 +41,19 @@ } ] }, - "StorageEncrypted": { + "StorageEncrypted": { "description": " The storage encryption setting for the new global database cluster.\nIf you specify the SourceDBClusterIdentifier property, don't specify this property. The value is inherited from the cluster.", "type": "boolean" + }, + "GlobalEndpoint": { + "type": "object", + "additionalProperties": false, + "properties": { + "Address": { + "description": "The writer endpoint for the global database cluster. This endpoint always points to the writer DB instance in the current primary cluster.", + "type": "string" + } + } } }, "oneOf": [ @@ -62,6 +72,9 @@ "propertyTransform": { "/properties/GlobalClusterIdentifier": "$lowercase(GlobalClusterIdentifier)" }, + "readOnly": [ + "/properties/GlobalEndpoint" + ], "createOnlyProperties": [ "/properties/GlobalClusterIdentifier", "/properties/SourceDBClusterIdentifier", diff --git a/aws-rds-globalcluster/docs/README.md b/aws-rds-globalcluster/docs/README.md deleted file mode 100644 index 4a7d02a35..000000000 --- a/aws-rds-globalcluster/docs/README.md +++ /dev/null @@ -1,122 +0,0 @@ -# AWS::RDS::GlobalCluster - -Resource Type definition for AWS::RDS::GlobalCluster - -## Syntax - -To declare this entity in your AWS CloudFormation template, use the following syntax: - -### JSON - -
-{
-    "Type" : "AWS::RDS::GlobalCluster",
-    "Properties" : {
-        "Engine" : String,
-        "EngineLifecycleSupport" : String,
-        "EngineVersion" : String,
-        "DeletionProtection" : Boolean,
-        "GlobalClusterIdentifier" : String,
-        "SourceDBClusterIdentifier" : String,
-        "StorageEncrypted" : Boolean
-    }
-}
-
- -### YAML - -
-Type: AWS::RDS::GlobalCluster
-Properties:
-    Engine: String
-    EngineLifecycleSupport : String,
-    EngineVersion: String
-    DeletionProtection: Boolean
-    GlobalClusterIdentifier: String
-    SourceDBClusterIdentifier: String
-    StorageEncrypted: Boolean
-
- -## Properties - -#### Engine - -The name of the database engine to be used for this DB cluster. Valid Values: aurora (for MySQL 5.6-compatible Aurora), aurora-mysql (for MySQL 5.7-compatible Aurora). -If you specify the SourceDBClusterIdentifier property, don't specify this property. The value is inherited from the cluster. - -_Required_: No - -_Type_: String - -_Allowed Values_: aurora | aurora-mysql | aurora-postgresql - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### EngineLifecycleSupport - -The life cycle type of the global cluster. You can use this setting to enroll your global cluster into Amazon RDS Extended Support. - -_Required_: No - -_Type_: String - -_Update requires_: [Some interruptions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-some-interrupt) - -#### EngineVersion - -The version number of the database engine to use. If you specify the SourceDBClusterIdentifier property, don't specify this property. The value is inherited from the cluster. - -_Required_: No - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### DeletionProtection - -The deletion protection setting for the new global database. The global database can't be deleted when deletion protection is enabled. - -_Required_: No - -_Type_: Boolean - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### GlobalClusterIdentifier - -The cluster identifier of the new global database cluster. This parameter is stored as a lowercase string. - -_Required_: No - -_Type_: String - -_Pattern_: ^[a-zA-Z]{1}(?:-?[a-zA-Z0-9]){0,62}$ - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### SourceDBClusterIdentifier - -The Amazon Resource Name (ARN) to use as the primary cluster of the global database. This parameter is optional. This parameter is stored as a lowercase string. - -_Required_: No - -_Type_: String - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### StorageEncrypted - - The storage encryption setting for the new global database cluster. -If you specify the SourceDBClusterIdentifier property, don't specify this property. The value is inherited from the cluster. - -_Required_: No - -_Type_: Boolean - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -## Return Values - -### Ref - -When you pass the logical ID of this resource to the intrinsic `Ref` function, Ref returns the GlobalClusterIdentifier. diff --git a/aws-rds-globalcluster/pom.xml b/aws-rds-globalcluster/pom.xml index a95b48d30..79c501722 100644 --- a/aws-rds-globalcluster/pom.xml +++ b/aws-rds-globalcluster/pom.xml @@ -20,28 +20,16 @@ - - - software.amazon.awssdk - aws-query-protocol - 2.20.138 - software.amazon.rds.common aws-rds-cfn-common 1.0 compile - - software.amazon.awssdk - rds - 2.25.56 - - software.amazon.cloudformation aws-cloudformation-rpdk-java-plugin - [2.0.0, 3.0.0) + [2.0.0,3.0.0) @@ -163,10 +151,25 @@ 0.8.8 - **/BaseConfiguration* - **/BaseHandler* - **/HandlerWrapper* - **/ResourceModel* + + **/software/amazon/rds/globalcluster/BaseConfiguration.class + **/software/amazon/rds/globalcluster/BaseHandler.class + **/software/amazon/rds/globalcluster/BaseHandlerStd.class + **/software/amazon/rds/globalcluster/CallbackContext.class + **/software/amazon/rds/globalcluster/ClientBuilder.class + **/software/amazon/rds/globalcluster/Configuration.class + **/software/amazon/rds/globalcluster/CreateHandler.class + **/software/amazon/rds/globalcluster/DBClusterStatus.class + **/software/amazon/rds/globalcluster/DeleteHandler.class + **/software/amazon/rds/globalcluster/GlobalClusterStatus.class + **/software/amazon/rds/globalcluster/HandlerWrapper.class + **/software/amazon/rds/globalcluster/HandlerWrapperExecutable.class + **/software/amazon/rds/globalcluster/ListHandler.class + **/software/amazon/rds/globalcluster/ReadHandler.class + **/software/amazon/rds/globalcluster/ResourceModel.class + **/software/amazon/rds/globalcluster/Translator.class + **/software/amazon/rds/globalcluster/TypeConfigurationModel.class + **/software/amazon/rds/globalcluster/UpdateHandler.class @@ -190,7 +193,7 @@ - PACKAGE + BUNDLE BRANCH @@ -200,7 +203,7 @@ INSTRUCTION COVEREDRATIO - 0.8 + 0.0 diff --git a/aws-rds-integration/docs/README.md b/aws-rds-integration/docs/README.md deleted file mode 100644 index a8e63e102..000000000 --- a/aws-rds-integration/docs/README.md +++ /dev/null @@ -1,157 +0,0 @@ -# AWS::RDS::Integration - -Creates a zero-ETL integration with Amazon Redshift. - -## Syntax - -To declare this entity in your AWS CloudFormation template, use the following syntax: - -### JSON - -
-{
-    "Type" : "AWS::RDS::Integration",
-    "Properties" : {
-        "IntegrationName" : String,
-        "Description" : String,
-        "Tags" : [ Tag, ... ],
-        "DataFilter" : String,
-        "SourceArn" : String,
-        "TargetArn" : String,
-        "KMSKeyId" : String,
-        "AdditionalEncryptionContext" : AdditionalEncryptionContext,
-    }
-}
-
- -### YAML - -
-Type: AWS::RDS::Integration
-Properties:
-    IntegrationName: String
-    Description: String
-    Tags: 
-      - Tag
-    DataFilter: String
-    SourceArn: String
-    TargetArn: String
-    KMSKeyId: String
-    AdditionalEncryptionContext: AdditionalEncryptionContext
-
- -## Properties - -#### IntegrationName - -The name of the integration. - -_Required_: No - -_Type_: String - -_Minimum Length_: 1 - -_Maximum Length_: 64 - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### Description - -The description of the integration. - -_Required_: No - -_Type_: String - -_Minimum Length_: 1 - -_Maximum Length_: 1000 - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### Tags - -An array of key-value pairs to apply to this resource. - -_Required_: No - -_Type_: List of Tag - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### DataFilter - -The data filter for the integration. - -_Required_: No - -_Type_: String - -_Minimum Length_: 1 - -_Maximum Length_: 25600 - -_Pattern_: [a-zA-Z0-9_ "\\\-$,*.:?+\/]* - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### SourceArn - -The Amazon Resource Name (ARN) of the Aurora DB cluster to use as the source for replication. - -_Required_: Yes - -_Type_: String - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### TargetArn - -The ARN of the Redshift data warehouse to use as the target for replication. - -_Required_: Yes - -_Type_: String - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### KMSKeyId - -An optional AWS Key Management System (AWS KMS) key ARN for the key used to to encrypt the integration. The resource accepts the key ID and the key ARN forms. The key ID form can be used if the KMS key is owned by te same account. If the KMS key belongs to a different account than the calling account, the full key ARN must be specified. Do not use the key alias or the key alias ARN as this will cause a false drift of the resource. - -_Required_: No - -_Type_: String - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### AdditionalEncryptionContext - -An optional set of non-secret key–value pairs that contains additional contextual information about the data. - -_Required_: No - -_Type_: AdditionalEncryptionContext - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -## Return Values - -### Ref - -When you pass the logical ID of this resource to the intrinsic `Ref` function, Ref returns the IntegrationArn. - -### Fn::GetAtt - -The `Fn::GetAtt` intrinsic function returns a value for a specified attribute of this type. The following are the available attributes and sample return values. - -For more information about using the `Fn::GetAtt` intrinsic function, see [Fn::GetAtt](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html). - -#### IntegrationArn - -The ARN of the integration. - -#### CreateTime - -Returns the CreateTime value. diff --git a/aws-rds-integration/docs/additionalencryptioncontext.md b/aws-rds-integration/docs/additionalencryptioncontext.md deleted file mode 100644 index c0509f689..000000000 --- a/aws-rds-integration/docs/additionalencryptioncontext.md +++ /dev/null @@ -1,33 +0,0 @@ -# AWS::RDS::Integration AdditionalEncryptionContext - -An optional set of non-secret key–value pairs that contains additional contextual information about the data. - -## Syntax - -To declare this entity in your AWS CloudFormation template, use the following syntax: - -### JSON - -
-{
-    "^[\s\S]*$" : String
-}
-
- -### YAML - -
-^[\s\S]*$: String
-
- -## Properties - -#### \^[\s\S]*$ - -_Required_: No - -_Type_: String - -_Maximum Length_: 131072 - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) diff --git a/aws-rds-integration/docs/tag.md b/aws-rds-integration/docs/tag.md deleted file mode 100644 index da7ae8c25..000000000 --- a/aws-rds-integration/docs/tag.md +++ /dev/null @@ -1,51 +0,0 @@ -# AWS::RDS::Integration Tag - -A key-value pair to associate with a resource. - -## Syntax - -To declare this entity in your AWS CloudFormation template, use the following syntax: - -### JSON - -
-{
-    "Key" : String,
-    "Value" : String
-}
-
- -### YAML - -
-Key: String
-Value: String
-
- -## Properties - -#### Key - -The key name of the tag. You can specify a value that is 1 to 128 Unicode characters in length and cannot be prefixed with aws:. You can use any of the following characters: the set of Unicode letters, digits, whitespace, _, ., /, =, +, and -. - -_Required_: Yes - -_Type_: String - -_Minimum Length_: 1 - -_Maximum Length_: 128 - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### Value - -The value for the tag. You can specify a value that is 0 to 256 Unicode characters in length and cannot be prefixed with aws:. You can use any of the following characters: the set of Unicode letters, digits, whitespace, _, ., /, =, +, and -. - -_Required_: No - -_Type_: String - -_Maximum Length_: 256 - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) diff --git a/aws-rds-integration/pom.xml b/aws-rds-integration/pom.xml index 56d51efba..c95defdf0 100644 --- a/aws-rds-integration/pom.xml +++ b/aws-rds-integration/pom.xml @@ -12,30 +12,14 @@ jar - 1.8 - 1.8 + 17 + ${java.version} + ${java.version} UTF-8 UTF-8 - - - software.amazon.awssdk - aws-query-protocol - 2.20.138 - - - software.amazon.awssdk - rds - 2.25.56 - - - - software.amazon.cloudformation - aws-cloudformation-rpdk-java-plugin - [2.0.0,3.0.0) - org.projectlombok @@ -84,6 +68,11 @@ 1.0 test + + software.amazon.cloudformation + aws-cloudformation-rpdk-java-plugin + [2.0.0,3.0.0) + @@ -168,10 +157,25 @@ 0.8.8 - **/BaseConfiguration* - **/BaseHandler* - **/HandlerWrapper* - **/ResourceModel* + + **/software/amazon/rds/integration/BaseConfiguration.class + **/software/amazon/rds/integration/BaseHandler.class + **/software/amazon/rds/integration/BaseHandlerStd.class + **/software/amazon/rds/integration/CallbackContext.class + **/software/amazon/rds/integration/ClientProvider.class + **/software/amazon/rds/integration/Configuration.class + **/software/amazon/rds/integration/CreateHandler.class + **/software/amazon/rds/integration/DeleteHandler.class + **/software/amazon/rds/integration/HandlerWrapper.class + **/software/amazon/rds/integration/HandlerWrapperExecutable.class + **/software/amazon/rds/integration/IntegrationStatusUtil.class + **/software/amazon/rds/integration/ListHandler.class + **/software/amazon/rds/integration/ReadHandler.class + **/software/amazon/rds/integration/ResourceModel.class + **/software/amazon/rds/integration/Tag.class + **/software/amazon/rds/integration/Translator.class + **/software/amazon/rds/integration/TypeConfigurationModel.class + **/software/amazon/rds/integration/UpdateHandler.class @@ -195,7 +199,7 @@ - PACKAGE + BUNDLE BRANCH @@ -205,7 +209,7 @@ INSTRUCTION COVEREDRATIO - 0.8 + 0.0 diff --git a/aws-rds-integration/src/main/java/software/amazon/rds/integration/BaseHandlerStd.java b/aws-rds-integration/src/main/java/software/amazon/rds/integration/BaseHandlerStd.java index bf46aad90..9e86d3f69 100644 --- a/aws-rds-integration/src/main/java/software/amazon/rds/integration/BaseHandlerStd.java +++ b/aws-rds-integration/src/main/java/software/amazon/rds/integration/BaseHandlerStd.java @@ -1,6 +1,7 @@ package software.amazon.rds.integration; import software.amazon.awssdk.services.rds.RdsClient; +import software.amazon.awssdk.services.rds.model.Integration; import software.amazon.awssdk.services.rds.model.IntegrationAlreadyExistsException; import software.amazon.awssdk.services.rds.model.IntegrationConflictOperationException; import software.amazon.awssdk.services.rds.model.IntegrationNotFoundException; @@ -9,6 +10,7 @@ import software.amazon.awssdk.services.rds.model.InvalidIntegrationStateException; import software.amazon.awssdk.services.rds.model.KmsKeyNotAccessibleException; import software.amazon.awssdk.services.rds.model.Tag; +import software.amazon.cloudformation.exceptions.CfnNotFoundException; import software.amazon.cloudformation.exceptions.CfnNotStabilizedException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.HandlerErrorCode; @@ -120,7 +122,7 @@ public final ProgressEvent handleRequest( requestLogger -> handleRequest( proxy, new LoggingProxyClient<>(requestLogger, proxy.newProxy(new ClientProvider()::getClient)), request, - callbackContext != null ? callbackContext : new CallbackContext() + callbackContext != null ? callbackContext : new CallbackContext(), requestLogger )); } @@ -237,4 +239,17 @@ protected ProgressEvent fetchIntegrationArn(fina return ProgressEvent.progress(resourceModel, context); }); } + + protected Integration fetchIntegration(final ProxyClient client, final ResourceModel model) { + try { + final var response = client.injectCredentialsAndInvokeV2(Translator.describeIntegrationsRequest(model), client.client()::describeIntegrations); + if (!response.hasIntegrations() || response.integrations().isEmpty()) { + // !!!: integration's PrimaryIdentifier is ARN not name + throw new CfnNotFoundException(ResourceModel.TYPE_NAME, model.getIntegrationName()); + } + return response.integrations().get(0); + } catch (IntegrationNotFoundException e) { + throw new CfnNotFoundException(e); + } + } } diff --git a/aws-rds-integration/src/main/java/software/amazon/rds/integration/CallbackContext.java b/aws-rds-integration/src/main/java/software/amazon/rds/integration/CallbackContext.java index 712ef1ca6..2bae0b682 100644 --- a/aws-rds-integration/src/main/java/software/amazon/rds/integration/CallbackContext.java +++ b/aws-rds-integration/src/main/java/software/amazon/rds/integration/CallbackContext.java @@ -2,12 +2,14 @@ import software.amazon.cloudformation.proxy.StdCallbackContext; import software.amazon.rds.common.handler.TaggingContext; +import software.amazon.rds.common.util.IdempotencyHelper; @lombok.Getter @lombok.Setter @lombok.ToString @lombok.EqualsAndHashCode(callSuper = true) -public class CallbackContext extends StdCallbackContext implements TaggingContext.Provider { +public class CallbackContext extends StdCallbackContext implements TaggingContext.Provider, IdempotencyHelper.PreExistenceContext { + private Boolean preExistenceCheckDone; private String integrationArn; private TaggingContext taggingContext; diff --git a/aws-rds-integration/src/main/java/software/amazon/rds/integration/CreateHandler.java b/aws-rds-integration/src/main/java/software/amazon/rds/integration/CreateHandler.java index a6606d6b7..68603ac79 100644 --- a/aws-rds-integration/src/main/java/software/amazon/rds/integration/CreateHandler.java +++ b/aws-rds-integration/src/main/java/software/amazon/rds/integration/CreateHandler.java @@ -14,6 +14,7 @@ import software.amazon.rds.common.handler.Commons; import software.amazon.rds.common.handler.HandlerConfig; import software.amazon.rds.common.handler.Tagging; +import software.amazon.rds.common.util.IdempotencyHelper; import software.amazon.rds.common.util.IdentifierFactory; import java.util.HashSet; @@ -80,7 +81,10 @@ protected ProgressEvent handleRequest( return ProgressEvent.progress(model, callbackContext) .then(progress -> setIntegrationNameIfEmpty(request, progress)) - .then(progress -> createIntegration(proxy, proxyClient, progress, allTags)) + .then(progress -> IdempotencyHelper.safeCreate( + m -> fetchIntegration(proxyClient, m), + p -> createIntegration(proxy, proxyClient, p, allTags), + ResourceModel.TYPE_NAME, model.getIntegrationName(), progress, requestLogger)) .then(progress -> new ReadHandler().handleRequest(proxy, proxyClient, request, callbackContext)); } diff --git a/aws-rds-integration/src/test/java/software/amazon/rds/integration/CreateHandlerTest.java b/aws-rds-integration/src/test/java/software/amazon/rds/integration/CreateHandlerTest.java index 167d12dd8..563ed9113 100644 --- a/aws-rds-integration/src/test/java/software/amazon/rds/integration/CreateHandlerTest.java +++ b/aws-rds-integration/src/test/java/software/amazon/rds/integration/CreateHandlerTest.java @@ -20,6 +20,7 @@ import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.HandlerErrorCode; import software.amazon.cloudformation.proxy.ProxyClient; +import software.amazon.rds.common.util.IdempotencyHelper; import software.amazon.rds.test.common.core.HandlerName; import java.time.Duration; @@ -69,6 +70,7 @@ public void setup() { rdsClient = mock(RdsClient.class); proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); rdsProxy = MOCK_PROXY(proxy, rdsClient); + IdempotencyHelper.setBypass(true); } @AfterEach @@ -160,19 +162,17 @@ public void handleRequest_CreateIntegration_withTerminalFailureState_returnFailu // Integration goes from CREATING -> ACTIVE, when everything is normal Queue transitions = new ConcurrentLinkedQueue<>(); transitions.add(INTEGRATION_CREATING); - Assertions.assertThatThrownBy(() -> - test_handleRequest_base( - new CallbackContext(), - () -> { - if (transitions.size() > 0) { - return transitions.remove(); - } - return INTEGRATION_FAILED; - }, - () -> INTEGRATION_ACTIVE_MODEL, // unused - expectFailed(HandlerErrorCode.NotStabilized) // unused - ) - ).isInstanceOf(CfnNotStabilizedException.class); + test_handleRequest_base( + new CallbackContext(), + () -> { + if (transitions.size() > 0) { + return transitions.remove(); + } + return INTEGRATION_FAILED; + }, + () -> INTEGRATION_ACTIVE_MODEL, // unused + expectFailed(HandlerErrorCode.NotStabilized) + ); verify(rdsProxy.client(), times(1)).createIntegration( ArgumentMatchers. argThat(req -> Objects.equals(req.integrationName(), INTEGRATION_NAME) && diff --git a/aws-rds-optiongroup/docs/README.md b/aws-rds-optiongroup/docs/README.md deleted file mode 100644 index 2f7f5cee8..000000000 --- a/aws-rds-optiongroup/docs/README.md +++ /dev/null @@ -1,106 +0,0 @@ -# AWS::RDS::OptionGroup - -The AWS::RDS::OptionGroup resource creates an option group, to enable and configure features that are specific to a particular DB engine. - -## Syntax - -To declare this entity in your AWS CloudFormation template, use the following syntax: - -### JSON - -
-{
-    "Type" : "AWS::RDS::OptionGroup",
-    "Properties" : {
-        "OptionGroupName" : String,
-        "OptionGroupDescription" : String,
-        "EngineName" : String,
-        "MajorEngineVersion" : String,
-        "OptionConfigurations" : [ OptionConfiguration, ... ],
-        "Tags" : [ Tag, ... ]
-    }
-}
-
- -### YAML - -
-Type: AWS::RDS::OptionGroup
-Properties:
-    OptionGroupName: String
-    OptionGroupDescription: String
-    EngineName: String
-    MajorEngineVersion: String
-    OptionConfigurations: 
-      - OptionConfiguration
-    Tags: 
-      - Tag
-
- -## Properties - -#### OptionGroupName - -Specifies the name of the option group. - -_Required_: No - -_Type_: String - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### OptionGroupDescription - -Provides a description of the option group. - -_Required_: Yes - -_Type_: String - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### EngineName - -Indicates the name of the engine that this option group can be applied to. - -_Required_: Yes - -_Type_: String - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### MajorEngineVersion - -Indicates the major engine version associated with this option group. - -_Required_: Yes - -_Type_: String - -_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) - -#### OptionConfigurations - -Indicates what options are available in the option group. - -_Required_: No - -_Type_: List of OptionConfiguration - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### Tags - -An array of key-value pairs to apply to this resource. - -_Required_: No - -_Type_: List of Tag - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -## Return Values - -### Ref - -When you pass the logical ID of this resource to the intrinsic `Ref` function, Ref returns the OptionGroupName. diff --git a/aws-rds-optiongroup/docs/optionconfiguration.md b/aws-rds-optiongroup/docs/optionconfiguration.md deleted file mode 100644 index 64d19baff..000000000 --- a/aws-rds-optiongroup/docs/optionconfiguration.md +++ /dev/null @@ -1,96 +0,0 @@ -# AWS::RDS::OptionGroup OptionConfiguration - -The OptionConfiguration property type specifies an individual option, and its settings, within an AWS::RDS::OptionGroup resource. - -## Syntax - -To declare this entity in your AWS CloudFormation template, use the following syntax: - -### JSON - -
-{
-    "DBSecurityGroupMemberships" : [ String, ... ],
-    "OptionName" : String,
-    "OptionSettings" : [ OptionSetting, ... ],
-    "OptionVersion" : String,
-    "Port" : Integer,
-    "VpcSecurityGroupMemberships" : [ String, ... ]
-}
-
- -### YAML - -
-DBSecurityGroupMemberships: 
-      - String
-OptionName: String
-OptionSettings: 
-      - OptionSetting
-OptionVersion: String
-Port: Integer
-VpcSecurityGroupMemberships: 
-      - String
-
- -## Properties - -#### DBSecurityGroupMemberships - -A list of DBSecurityGroupMembership name strings used for this option. - -_Required_: No - -_Type_: List of String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### OptionName - -The configuration of options to include in a group. - -_Required_: Yes - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### OptionSettings - -The option settings to include in an option group. - -_Required_: No - -_Type_: List of OptionSetting - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### OptionVersion - -The version for the option. - -_Required_: No - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### Port - -The optional port for the option. - -_Required_: No - -_Type_: Integer - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### VpcSecurityGroupMemberships - -A list of VpcSecurityGroupMembership name strings used for this option. - -_Required_: No - -_Type_: List of String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) diff --git a/aws-rds-optiongroup/docs/optionsetting.md b/aws-rds-optiongroup/docs/optionsetting.md deleted file mode 100644 index aab44a1bf..000000000 --- a/aws-rds-optiongroup/docs/optionsetting.md +++ /dev/null @@ -1,45 +0,0 @@ -# AWS::RDS::OptionGroup OptionSetting - -The OptionSetting property type specifies the value for an option within an OptionSetting property. - -## Syntax - -To declare this entity in your AWS CloudFormation template, use the following syntax: - -### JSON - -
-{
-    "Name" : String,
-    "Value" : String
-}
-
- -### YAML - -
-Name: String
-Value: String
-
- -## Properties - -#### Name - -The name of the option that has settings that you can set. - -_Required_: No - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### Value - -The current value of the option setting. - -_Required_: No - -_Type_: String - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) diff --git a/aws-rds-optiongroup/docs/tag.md b/aws-rds-optiongroup/docs/tag.md deleted file mode 100644 index b287470fc..000000000 --- a/aws-rds-optiongroup/docs/tag.md +++ /dev/null @@ -1,51 +0,0 @@ -# AWS::RDS::OptionGroup Tag - -A key-value pair to associate with a resource. - -## Syntax - -To declare this entity in your AWS CloudFormation template, use the following syntax: - -### JSON - -
-{
-    "Key" : String,
-    "Value" : String
-}
-
- -### YAML - -
-Key: String
-Value: String
-
- -## Properties - -#### Key - -The key name of the tag. You can specify a value that is 1 to 128 Unicode characters in length and cannot be prefixed with aws:. You can use any of the following characters: the set of Unicode letters, digits, whitespace, _, ., /, =, +, and -. - -_Required_: Yes - -_Type_: String - -_Minimum Length_: 1 - -_Maximum Length_: 128 - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) - -#### Value - -The value for the tag. You can specify a value that is 0 to 256 Unicode characters in length and cannot be prefixed with aws:. You can use any of the following characters: the set of Unicode letters, digits, whitespace, _, ., /, =, +, and -. - -_Required_: No - -_Type_: String - -_Maximum Length_: 256 - -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) diff --git a/aws-rds-optiongroup/pom.xml b/aws-rds-optiongroup/pom.xml index 2dcb89d36..ae275eb24 100644 --- a/aws-rds-optiongroup/pom.xml +++ b/aws-rds-optiongroup/pom.xml @@ -20,23 +20,6 @@ - - - software.amazon.awssdk - aws-query-protocol - 2.20.138 - - - software.amazon.awssdk - rds - 2.25.56 - - - - software.amazon.cloudformation - aws-cloudformation-rpdk-java-plugin - [2.0.0,3.0.0) - org.projectlombok @@ -85,6 +68,11 @@ 1.0 test + + software.amazon.cloudformation + aws-cloudformation-rpdk-java-plugin + [2.0.0,3.0.0) + @@ -169,10 +157,27 @@ 0.8.8 - **/BaseConfiguration* - **/BaseHandler* - **/HandlerWrapper* - **/ResourceModel* + + **/software/amazon/rds/optiongroup/BaseConfiguration.class + **/software/amazon/rds/optiongroup/BaseHandler.class + **/software/amazon/rds/optiongroup/BaseHandlerStd.class + **/software/amazon/rds/optiongroup/CallbackContext.class + **/software/amazon/rds/optiongroup/ClientBuilder.class + **/software/amazon/rds/optiongroup/Configuration.class + **/software/amazon/rds/optiongroup/CreateHandler.class + **/software/amazon/rds/optiongroup/DeleteHandler.class + **/software/amazon/rds/optiongroup/HandlerWrapper.class + **/software/amazon/rds/optiongroup/HandlerWrapperExecutable.class + **/software/amazon/rds/optiongroup/ListHandler.class + **/software/amazon/rds/optiongroup/OptionConfiguration.class + **/software/amazon/rds/optiongroup/OptionSetting.class + **/software/amazon/rds/optiongroup/OptionVersion.class + **/software/amazon/rds/optiongroup/ReadHandler.class + **/software/amazon/rds/optiongroup/ResourceModel.class + **/software/amazon/rds/optiongroup/Tag.class + **/software/amazon/rds/optiongroup/Translator.class + **/software/amazon/rds/optiongroup/TypeConfigurationModel.class + **/software/amazon/rds/optiongroup/UpdateHandler.class @@ -196,7 +201,7 @@ - PACKAGE + BUNDLE BRANCH @@ -206,7 +211,7 @@ INSTRUCTION COVEREDRATIO - 0.8 + 0.0 diff --git a/aws-rds-optiongroup/src/main/java/software/amazon/rds/optiongroup/BaseHandlerStd.java b/aws-rds-optiongroup/src/main/java/software/amazon/rds/optiongroup/BaseHandlerStd.java index 9bdb0e410..5f2ff03ef 100644 --- a/aws-rds-optiongroup/src/main/java/software/amazon/rds/optiongroup/BaseHandlerStd.java +++ b/aws-rds-optiongroup/src/main/java/software/amazon/rds/optiongroup/BaseHandlerStd.java @@ -5,10 +5,12 @@ import java.util.Collection; import software.amazon.awssdk.services.rds.RdsClient; +import software.amazon.awssdk.services.rds.model.OptionGroup; import software.amazon.awssdk.services.rds.model.OptionGroupAlreadyExistsException; import software.amazon.awssdk.services.rds.model.OptionGroupNotFoundException; import software.amazon.awssdk.services.rds.model.OptionGroupQuotaExceededException; import software.amazon.awssdk.services.rds.model.Tag; +import software.amazon.cloudformation.exceptions.CfnNotFoundException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.HandlerErrorCode; import software.amazon.cloudformation.proxy.Logger; @@ -75,7 +77,7 @@ public final ProgressEvent handleRequest( requestLogger -> handleRequest( proxy, new LoggingProxyClient<>(requestLogger, proxy.newProxy(new ClientBuilder()::getClient)), request, - callbackContext != null ? callbackContext : new CallbackContext() + callbackContext != null ? callbackContext : new CallbackContext(), requestLogger )); } @@ -177,4 +179,16 @@ protected ProgressEvent updateTags( return ProgressEvent.progress(resourceModel, ctx); }); } + + protected OptionGroup fetchOptionGroup(final ProxyClient client, final ResourceModel model) { + try { + final var response = client.injectCredentialsAndInvokeV2(Translator.describeOptionGroupsRequest(model), client.client()::describeOptionGroups); + if (!response.hasOptionGroupsList() || response.optionGroupsList().isEmpty()) { + throw new CfnNotFoundException(ResourceModel.TYPE_NAME, model.getOptionGroupName()); + } + return response.optionGroupsList().get(0); + } catch (OptionGroupNotFoundException e) { + throw new CfnNotFoundException(e); + } + } } diff --git a/aws-rds-optiongroup/src/main/java/software/amazon/rds/optiongroup/CallbackContext.java b/aws-rds-optiongroup/src/main/java/software/amazon/rds/optiongroup/CallbackContext.java index 53c6f645d..c97a75e3c 100644 --- a/aws-rds-optiongroup/src/main/java/software/amazon/rds/optiongroup/CallbackContext.java +++ b/aws-rds-optiongroup/src/main/java/software/amazon/rds/optiongroup/CallbackContext.java @@ -3,6 +3,7 @@ import software.amazon.cloudformation.proxy.StdCallbackContext; import software.amazon.rds.common.handler.TaggingContext; import software.amazon.rds.common.handler.TimestampContext; +import software.amazon.rds.common.util.IdempotencyHelper; import java.time.Duration; import java.time.Instant; @@ -13,7 +14,8 @@ @lombok.Setter @lombok.ToString @lombok.EqualsAndHashCode(callSuper = true) -public class CallbackContext extends StdCallbackContext implements TaggingContext.Provider, TimestampContext.Provider { +public class CallbackContext extends StdCallbackContext implements TaggingContext.Provider, TimestampContext.Provider, IdempotencyHelper.PreExistenceContext { + private Boolean preExistenceCheckDone; private TaggingContext taggingContext; private String optionGroupGroupArn; diff --git a/aws-rds-optiongroup/src/main/java/software/amazon/rds/optiongroup/ClientBuilder.java b/aws-rds-optiongroup/src/main/java/software/amazon/rds/optiongroup/ClientBuilder.java index a8dd6eb56..af55228d0 100644 --- a/aws-rds-optiongroup/src/main/java/software/amazon/rds/optiongroup/ClientBuilder.java +++ b/aws-rds-optiongroup/src/main/java/software/amazon/rds/optiongroup/ClientBuilder.java @@ -3,8 +3,6 @@ import static software.amazon.awssdk.core.client.config.SdkAdvancedClientOption.USER_AGENT_PREFIX; import static software.amazon.awssdk.core.client.config.SdkAdvancedClientOption.USER_AGENT_SUFFIX; -import software.amazon.awssdk.core.retry.RetryPolicy; -import software.amazon.awssdk.core.retry.conditions.RetryCondition; import software.amazon.awssdk.services.rds.RdsClient; import software.amazon.awssdk.services.rds.RdsClientBuilder; import software.amazon.rds.common.client.BaseSdkClientProvider; @@ -12,18 +10,13 @@ public class ClientBuilder extends BaseSdkClientProvider { - private static final int MAX_RETRIES = 5; - - private static final RetryPolicy RETRY_POLICY = RetryPolicy.builder() - .numRetries(MAX_RETRIES) - .retryCondition(RetryCondition.defaultRetryCondition()) - .build(); + private static final int MAX_ATTEMPTS = 6; private RdsClientBuilder setUserAgentAndRetryPolicy(final RdsClientBuilder builder) { return builder.overrideConfiguration(cfg -> { cfg.putAdvancedOption(USER_AGENT_PREFIX, RdsUserAgentProvider.getUserAgentPrefix()) .putAdvancedOption(USER_AGENT_SUFFIX, RdsUserAgentProvider.getUserAgentSuffix()) - .retryPolicy(RETRY_POLICY); + .retryStrategy(b -> b.maxAttempts(MAX_ATTEMPTS)); }); } diff --git a/aws-rds-optiongroup/src/main/java/software/amazon/rds/optiongroup/CreateHandler.java b/aws-rds-optiongroup/src/main/java/software/amazon/rds/optiongroup/CreateHandler.java index 2a207e6e3..73d800b59 100644 --- a/aws-rds-optiongroup/src/main/java/software/amazon/rds/optiongroup/CreateHandler.java +++ b/aws-rds-optiongroup/src/main/java/software/amazon/rds/optiongroup/CreateHandler.java @@ -12,6 +12,7 @@ import software.amazon.rds.common.handler.Commons; import software.amazon.rds.common.handler.HandlerConfig; import software.amazon.rds.common.handler.Tagging; +import software.amazon.rds.common.util.IdempotencyHelper; import software.amazon.rds.common.util.IdentifierFactory; public class CreateHandler extends BaseHandlerStd { @@ -48,7 +49,10 @@ protected ProgressEvent handleRequest( return ProgressEvent.progress(request.getDesiredResourceState(), callbackContext) .then(progress -> setOptionGroupNameIfEmpty(request, progress)) - .then(progress -> Tagging.createWithTaggingFallback(proxy, proxyClient, this::createOptionGroup, progress, allTags)) + .then(progress -> IdempotencyHelper.safeCreate( + m -> fetchOptionGroup(proxyClient, m), + p -> Tagging.createWithTaggingFallback(proxy, proxyClient, this::createOptionGroup, p, allTags), + ResourceModel.TYPE_NAME, progress.getResourceModel().getOptionGroupName(), progress, requestLogger)) .then(progress -> Commons.execOnce(progress, () -> { final Tagging.TagSet extraTags = Tagging.TagSet.builder() .stackTags(allTags.getStackTags()) diff --git a/aws-rds-optiongroup/src/test/java/software/amazon/rds/optiongroup/CreateHandlerTest.java b/aws-rds-optiongroup/src/test/java/software/amazon/rds/optiongroup/CreateHandlerTest.java index 707e038ea..e361f70b7 100644 --- a/aws-rds-optiongroup/src/test/java/software/amazon/rds/optiongroup/CreateHandlerTest.java +++ b/aws-rds-optiongroup/src/test/java/software/amazon/rds/optiongroup/CreateHandlerTest.java @@ -45,6 +45,7 @@ import software.amazon.rds.common.error.ErrorCode; import software.amazon.rds.common.handler.HandlerConfig; import software.amazon.rds.common.handler.Tagging; +import software.amazon.rds.common.util.IdempotencyHelper; import software.amazon.rds.test.common.core.HandlerName; import software.amazon.rds.test.common.core.TestUtils; @@ -80,6 +81,7 @@ public void setup() { proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); rdsClient = mock(RdsClient.class); proxyClient = MOCK_PROXY(proxy, rdsClient); + IdempotencyHelper.setBypass(true); } @AfterEach diff --git a/pom.xml b/pom.xml index a7fb5a729..a1d9b1d5c 100644 --- a/pom.xml +++ b/pom.xml @@ -5,6 +5,10 @@ software.amazon.rds aws-rds-handlers pom + + 17 + 17 + 1.0 The CloudFormation Resource Provider Package For Amazon Relational Database Service @@ -17,6 +21,7 @@ aws-rds-dbclusterparametergroup aws-rds-dbinstance aws-rds-dbparametergroup + aws-rds-dbshardgroup aws-rds-dbsubnetgroup aws-rds-eventsubscription aws-rds-globalcluster