diff --git a/.devops/performance-test-pipelines.yml b/.devops/performance-test-pipelines.yml new file mode 100644 index 0000000..62ecf20 --- /dev/null +++ b/.devops/performance-test-pipelines.yml @@ -0,0 +1,51 @@ +# azure-pipelines.yml +trigger: none + +parameters: + - name: "ENVIRONMENT" + displayName: "Environment" + type: string + values: + - "dev" + - "uat" + default: "uat" + - name: "TEST_TYPE" + displayName: "Test type" + type: string + values: + - "load" + - "smoke" + - "soak" + - "spike" + - "stress" + - "constant" + - # TODO: add your values + default: "constant" + - name: "SCRIPT" + displayName: "Script name" + type: string + values: + - yourscriptname # TODO: add your script +variables: + ${{ if eq(parameters['ENVIRONMENT'], 'dev') }}: + poolImage: 'pagopa-dev-loadtest-linux' + API_SUBSCRIPTION_KEY: $(DEV_API_SUBSCRIPTION_KEY) + ${{ if eq(parameters['ENVIRONMENT'], 'uat') }}: + poolImage: 'pagopa-uat-loadtest-linux' + API_SUBSCRIPTION_KEY: $(UAT_API_SUBSCRIPTION_KEY) + + +pool: + name: $(poolImage) + +steps: + - script: | + cd ./performance-test/src + docker pull grafana/k6 + displayName: Pull k6 image + + # TODO: add your env params + - script: | + cd ./performance-test + sh ./run_performance_test.sh ${{ parameters.ENVIRONMENT }} ${{ parameters.TEST_TYPE }} ${{ parameters.SCRIPT }} afmcalculatork6 $(API_SUBSCRIPTION_KEY) + displayName: Run k6 ${{ parameters.SCRIPT }} on ${{ parameters.ENVIRONMENT }} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7b8ed3e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,1404 @@ +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +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 = none +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 + +[*.csv] +indent_style = tab +ij_csv_keep_indents_on_empty_lines = true +ij_csv_wrap_long_lines = false + +[*.dart] +max_line_length = 80 + +[*.feature] +indent_size = 2 +ij_gherkin_keep_indents_on_empty_lines = false + +[*.java] +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 = none +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 = 5 +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 = never +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_suffix = EJB +ij_java_entity_eb_suffix = Bean +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_pk_class = java.lang.String +ij_java_entity_vo_suffix = VO +ij_java_enum_constants_wrap = off +ij_java_extends_keyword_wrap = off +ij_java_extends_list_wrap = off +ij_java_field_annotation_wrap = split_into_lines +ij_java_finally_on_new_line = false +ij_java_for_brace_force = never +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_if_brace_force = never +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 = false +ij_java_keep_simple_classes_in_one_line = false +ij_java_keep_simple_lambdas_in_one_line = false +ij_java_keep_simple_methods_in_one_line = false +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_message_dd_suffix = EJB +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 = 3 +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_packages_to_use_import_on_demand = java.awt.*,javax.swing.* +ij_java_parameter_annotation_wrap = off +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_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_session_dd_suffix = EJB +ij_java_session_eb_suffix = Bean +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_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_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_subclass_name_suffix = Impl +ij_java_ternary_operation_signs_on_next_line = false +ij_java_ternary_operation_wrap = off +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 = never +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 + +[*.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 + +[*.scala] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 2 +ij_scala_align_composite_pattern = true +ij_scala_align_extends_with = 0 +ij_scala_align_group_field_declarations = false +ij_scala_align_if_else = false +ij_scala_align_in_columns_case_branch = false +ij_scala_align_multiline_binary_operation = false +ij_scala_align_multiline_chained_methods = false +ij_scala_align_multiline_for = true +ij_scala_align_multiline_parameters = true +ij_scala_align_multiline_parameters_in_calls = false +ij_scala_align_multiline_parenthesized_expression = false +ij_scala_align_parameter_types_in_multiline_declarations = 0 +ij_scala_align_tuple_elements = false +ij_scala_alternate_continuation_indent_for_params = 4 +ij_scala_binary_operation_wrap = off +ij_scala_blank_lines_after_anonymous_class_header = 0 +ij_scala_blank_lines_after_class_header = 0 +ij_scala_blank_lines_after_imports = 1 +ij_scala_blank_lines_after_package = 1 +ij_scala_blank_lines_around_class = 1 +ij_scala_blank_lines_around_class_in_inner_scopes = 0 +ij_scala_blank_lines_around_field = 0 +ij_scala_blank_lines_around_field_in_inner_scopes = 0 +ij_scala_blank_lines_around_field_in_interface = 0 +ij_scala_blank_lines_around_method = 1 +ij_scala_blank_lines_around_method_in_inner_scopes = 1 +ij_scala_blank_lines_around_method_in_interface = 1 +ij_scala_blank_lines_before_class_end = 0 +ij_scala_blank_lines_before_imports = 1 +ij_scala_blank_lines_before_method_body = 0 +ij_scala_blank_lines_before_package = 0 +ij_scala_block_brace_style = end_of_line +ij_scala_block_comment_add_space = false +ij_scala_block_comment_at_first_column = true +ij_scala_call_parameters_new_line_after_lparen = 0 +ij_scala_call_parameters_right_paren_on_new_line = false +ij_scala_call_parameters_wrap = off +ij_scala_case_clause_brace_force = never +ij_scala_catch_on_new_line = false +ij_scala_class_annotation_wrap = split_into_lines +ij_scala_class_brace_style = end_of_line +ij_scala_closure_brace_force = never +ij_scala_do_not_align_block_expr_params = true +ij_scala_do_not_indent_case_clause_body = false +ij_scala_do_not_indent_tuples_close_brace = true +ij_scala_do_while_brace_force = never +ij_scala_else_on_new_line = false +ij_scala_enable_scaladoc_formatting = true +ij_scala_enforce_functional_syntax_for_unit = true +ij_scala_extends_keyword_wrap = off +ij_scala_extends_list_wrap = off +ij_scala_field_annotation_wrap = split_into_lines +ij_scala_finally_brace_force = never +ij_scala_finally_on_new_line = false +ij_scala_for_brace_force = never +ij_scala_for_statement_wrap = off +ij_scala_formatter = 0 +ij_scala_if_brace_force = never +ij_scala_implicit_value_class_suffix = Ops +ij_scala_indent_braced_function_args = true +ij_scala_indent_case_from_switch = true +ij_scala_indent_first_parameter = true +ij_scala_indent_first_parameter_clause = false +ij_scala_indent_type_arguments = true +ij_scala_indent_type_parameters = true +ij_scala_indent_yield_after_one_line_enumerators = true +ij_scala_keep_blank_lines_before_right_brace = 2 +ij_scala_keep_blank_lines_in_code = 2 +ij_scala_keep_blank_lines_in_declarations = 2 +ij_scala_keep_comments_on_same_line = true +ij_scala_keep_first_column_comment = false +ij_scala_keep_indents_on_empty_lines = false +ij_scala_keep_line_breaks = true +ij_scala_keep_one_line_lambdas_in_arg_list = false +ij_scala_keep_simple_blocks_in_one_line = false +ij_scala_keep_simple_methods_in_one_line = false +ij_scala_keep_xml_formatting = false +ij_scala_line_comment_add_space = false +ij_scala_line_comment_at_first_column = true +ij_scala_method_annotation_wrap = split_into_lines +ij_scala_method_brace_force = never +ij_scala_method_brace_style = end_of_line +ij_scala_method_call_chain_wrap = off +ij_scala_method_parameters_new_line_after_left_paren = false +ij_scala_method_parameters_right_paren_on_new_line = false +ij_scala_method_parameters_wrap = off +ij_scala_modifier_list_wrap = false +ij_scala_multiline_string_align_dangling_closing_quotes = false +ij_scala_multiline_string_closing_quotes_on_new_line = false +ij_scala_multiline_string_insert_margin_on_enter = true +ij_scala_multiline_string_margin_char = | +ij_scala_multiline_string_margin_indent = 2 +ij_scala_multiline_string_opening_quotes_on_new_line = true +ij_scala_multiline_string_process_margin_on_copy_paste = true +ij_scala_new_line_after_case_clause_arrow_when_multiline_body = false +ij_scala_newline_after_annotations = false +ij_scala_not_continuation_indent_for_params = false +ij_scala_parameter_annotation_wrap = off +ij_scala_parentheses_expression_new_line_after_left_paren = false +ij_scala_parentheses_expression_right_paren_on_new_line = false +ij_scala_place_closure_parameters_on_new_line = false +ij_scala_place_self_type_on_new_line = true +ij_scala_prefer_parameters_wrap = false +ij_scala_preserve_space_after_method_declaration_name = false +ij_scala_reformat_on_compile = false +ij_scala_replace_case_arrow_with_unicode_char = false +ij_scala_replace_for_generator_arrow_with_unicode_char = false +ij_scala_replace_lambda_with_greek_letter = false +ij_scala_replace_map_arrow_with_unicode_char = false +ij_scala_scalafmt_fallback_to_default_settings = false +ij_scala_scalafmt_reformat_on_files_save = false +ij_scala_scalafmt_show_invalid_code_warnings = true +ij_scala_scalafmt_use_intellij_formatter_for_range_format = true +ij_scala_sd_align_exception_comments = true +ij_scala_sd_align_list_item_content = true +ij_scala_sd_align_other_tags_comments = true +ij_scala_sd_align_parameters_comments = true +ij_scala_sd_align_return_comments = true +ij_scala_sd_blank_line_after_parameters_comments = false +ij_scala_sd_blank_line_after_return_comments = false +ij_scala_sd_blank_line_before_parameters = false +ij_scala_sd_blank_line_before_tags = true +ij_scala_sd_blank_line_between_parameters = false +ij_scala_sd_keep_blank_lines_between_tags = false +ij_scala_sd_preserve_spaces_in_tags = false +ij_scala_space_after_comma = true +ij_scala_space_after_for_semicolon = true +ij_scala_space_after_modifiers_constructor = false +ij_scala_space_after_type_colon = true +ij_scala_space_before_brace_method_call = true +ij_scala_space_before_class_left_brace = true +ij_scala_space_before_for_parentheses = true +ij_scala_space_before_if_parentheses = true +ij_scala_space_before_infix_like_method_parentheses = false +ij_scala_space_before_infix_method_call_parentheses = false +ij_scala_space_before_infix_operator_like_method_call_parentheses = true +ij_scala_space_before_method_call_parentheses = false +ij_scala_space_before_method_left_brace = true +ij_scala_space_before_method_parentheses = false +ij_scala_space_before_type_colon = false +ij_scala_space_before_type_parameter_in_def_list = false +ij_scala_space_before_type_parameter_leading_context_bound_colon = false +ij_scala_space_before_type_parameter_leading_context_bound_colon_hk = true +ij_scala_space_before_type_parameter_list = false +ij_scala_space_before_type_parameter_rest_context_bound_colons = true +ij_scala_space_before_while_parentheses = true +ij_scala_space_inside_closure_braces = true +ij_scala_space_inside_self_type_braces = true +ij_scala_space_within_empty_method_call_parentheses = false +ij_scala_spaces_around_at_in_patterns = false +ij_scala_spaces_in_imports = false +ij_scala_spaces_in_one_line_blocks = false +ij_scala_spaces_within_brackets = false +ij_scala_spaces_within_for_parentheses = false +ij_scala_spaces_within_if_parentheses = false +ij_scala_spaces_within_method_call_parentheses = false +ij_scala_spaces_within_method_parentheses = false +ij_scala_spaces_within_parentheses = false +ij_scala_spaces_within_while_parentheses = false +ij_scala_special_else_if_treatment = true +ij_scala_trailing_comma_arg_list_enabled = true +ij_scala_trailing_comma_import_selector_enabled = false +ij_scala_trailing_comma_mode = trailing_comma_keep +ij_scala_trailing_comma_params_enabled = true +ij_scala_trailing_comma_pattern_arg_list_enabled = false +ij_scala_trailing_comma_tuple_enabled = false +ij_scala_trailing_comma_tuple_type_enabled = false +ij_scala_trailing_comma_type_params_enabled = false +ij_scala_try_brace_force = never +ij_scala_type_annotation_exclude_constant = true +ij_scala_type_annotation_exclude_in_dialect_sources = true +ij_scala_type_annotation_exclude_in_test_sources = false +ij_scala_type_annotation_exclude_member_of_anonymous_class = false +ij_scala_type_annotation_exclude_member_of_private_class = false +ij_scala_type_annotation_exclude_when_type_is_stable = true +ij_scala_type_annotation_function_parameter = false +ij_scala_type_annotation_implicit_modifier = true +ij_scala_type_annotation_local_definition = false +ij_scala_type_annotation_private_member = false +ij_scala_type_annotation_protected_member = true +ij_scala_type_annotation_public_member = true +ij_scala_type_annotation_structural_type = true +ij_scala_type_annotation_underscore_parameter = false +ij_scala_type_annotation_unit_type = true +ij_scala_use_alternate_continuation_indent_for_params = false +ij_scala_use_scala3_indentation_based_syntax = true +ij_scala_use_scaladoc2_formatting = false +ij_scala_variable_annotation_wrap = off +ij_scala_while_brace_force = never +ij_scala_while_on_new_line = false +ij_scala_wrap_before_with_keyword = false +ij_scala_wrap_first_method_in_call_chain = false +ij_scala_wrap_long_lines = false + +[*.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 + +[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.pom,*.rng,*.tld,*.wadl,*.wsdd,*.wsdl,*.xjb,*.xml,*.xml.tpl,*.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 +ij_xml_use_custom_settings = false + +[{*.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_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_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_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_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 + +[{*.gradle.kts,*.kt,*.kts,*.main.kts,*.space.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 = off +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 = false +ij_kotlin_call_parameters_right_paren_on_new_line = false +ij_kotlin_call_parameters_wrap = off +ij_kotlin_catch_on_new_line = false +ij_kotlin_class_annotation_wrap = split_into_lines +ij_kotlin_continuation_indent_for_chained_calls = true +ij_kotlin_continuation_indent_for_expression_bodies = true +ij_kotlin_continuation_indent_in_argument_lists = true +ij_kotlin_continuation_indent_in_elvis = true +ij_kotlin_continuation_indent_in_if_conditions = true +ij_kotlin_continuation_indent_in_parameter_lists = true +ij_kotlin_continuation_indent_in_supertype_lists = true +ij_kotlin_else_on_new_line = false +ij_kotlin_enum_constants_wrap = off +ij_kotlin_extends_list_wrap = off +ij_kotlin_field_annotation_wrap = split_into_lines +ij_kotlin_finally_on_new_line = false +ij_kotlin_if_rparen_on_new_line = false +ij_kotlin_import_nested_classes = false +ij_kotlin_imports_layout = *,java.**,javax.**,kotlin.**,^ +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 = off +ij_kotlin_method_parameters_new_line_after_left_paren = false +ij_kotlin_method_parameters_right_paren_on_new_line = false +ij_kotlin_method_parameters_wrap = off +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 = 0 +ij_kotlin_wrap_first_method_in_call_chain = false + +[{*.graphqlconfig,*.graphqlrc,*.har,*.jsb2,*.jsb3,*.json,*.json.tpl,.babelrc,.eslintrc,.stylelintrc,bowerrc,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 + +[{*.hcl,*.nomad}] +indent_size = 2 +ij_hcl_array_wrapping = normal +ij_hcl_keep_blank_lines_in_code = 2 +ij_hcl_keep_indents_on_empty_lines = false +ij_hcl_keep_line_breaks = true +ij_hcl_object_wrapping = normal +ij_hcl_property_alignment = 2 +ij_hcl_property_line_commenter_character = 1 +ij_hcl_space_after_comma = true +ij_hcl_space_before_comma = false +ij_hcl_spaces_around_assignment_operators = true +ij_hcl_spaces_within_braces = false +ij_hcl_spaces_within_brackets = false +ij_hcl_wrap_long_lines = false + +[{*.htm,*.html,*.ng,*.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 + +[{*.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 + +[{*.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}] +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 + +[{*.tf,*.tfvars}] +indent_size = 2 +ij_hcl-terraform_array_wrapping = normal +ij_hcl-terraform_keep_blank_lines_in_code = 2 +ij_hcl-terraform_keep_indents_on_empty_lines = false +ij_hcl-terraform_keep_line_breaks = true +ij_hcl-terraform_object_wrapping = normal +ij_hcl-terraform_property_alignment = 2 +ij_hcl-terraform_property_line_commenter_character = 1 +ij_hcl-terraform_space_after_comma = true +ij_hcl-terraform_space_before_comma = false +ij_hcl-terraform_spaces_around_assignment_operators = true +ij_hcl-terraform_spaces_within_braces = false +ij_hcl-terraform_spaces_within_brackets = false +ij_hcl-terraform_wrap_long_lines = false + +[{*.toml,Cargo.lock,Cargo.toml.orig,Gopkg.lock,Pipfile,poetry.lock}] +ij_toml_keep_indents_on_empty_lines = false + +[{*.yaml,*.yml,*playbook.yaml,*playbook.yml,main.yaml,main.yml,pubspec.lock}] +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_sequence_on_new_line = false +ij_yaml_space_before_colon = false +ij_yaml_spaces_within_braces = true +ij_yaml_spaces_within_brackets = true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..308d864 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,36 @@ + + + + + +#### List of Changes + + + +#### Motivation and Context + + + +#### How Has This Been Tested? + + + + + +#### Screenshots (if appropriate): + +#### Types of changes + + + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) + +#### Checklist: + + + + +- [ ] My change requires a change to the documentation. +- [ ] I have updated the documentation accordingly. diff --git a/.github/auto_assign.yml b/.github/auto_assign.yml new file mode 100644 index 0000000..2e0bbe2 --- /dev/null +++ b/.github/auto_assign.yml @@ -0,0 +1,3 @@ +addAssignees: author + +runOnDraft: true diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..7f19e51 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,25 @@ +# release.yml + +changelog: + exclude: + labels: + - ignore-for-release + - skip + authors: + - pagopa-github-bot + categories: + - title: Breaking Changes 🛠 + labels: + - breaking-change + - major + - title: Exciting New Features 🎉 + labels: + - enhancement + - minor + - title: Bug Fixing 🔥 + labels: + - bug + - patch + - title: Other Changes + labels: + - "*" diff --git a/.github/workflows/anchore.yml b/.github/workflows/anchore.yml new file mode 100644 index 0000000..802f626 --- /dev/null +++ b/.github/workflows/anchore.yml @@ -0,0 +1,54 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +# This workflow checks out code, builds an image, performs a container image +# vulnerability scan with Anchore's Grype tool, and integrates the results with GitHub Advanced Security +# code scanning feature. For more information on the Anchore scan action usage +# and parameters, see https://github.com/anchore/scan-action. For more +# information on Anchore's container image scanning tool Grype, see +# https://github.com/anchore/grype +name: Anchore Container Scan + +on: + push: + branches: [ "main" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "main" ] + schedule: + - cron: '00 07 * * *' + +permissions: + contents: read + +env: + DOCKERFILE: Dockerfile + +jobs: + Anchore-Build-Scan: + permissions: + contents: read # for actions/checkout to fetch code + security-events: write # for github/codeql-action/upload-sarif to upload SARIF results + actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status + runs-on: ubuntu-latest + steps: + - name: Checkout the code + uses: actions/checkout@v3 + + - name: Build the Docker image + run: docker build . --file ${{ env.DOCKERFILE }} --tag localbuild/testimage:latest + + - name: Run the Anchore scan action itself with GitHub Advanced Security code scanning integration enabled + uses: anchore/scan-action@v3 + with: + image: "localbuild/testimage:latest" + acs-report-enable: true + fail-build: true + severity-cutoff: "high" + - name: Upload Anchore Scan Report + uses: github/codeql-action/upload-sarif@v2 + if: always() + with: + sarif_file: results.sarif diff --git a/.github/workflows/check_pr.yml b/.github/workflows/check_pr.yml new file mode 100644 index 0000000..4ea9fe4 --- /dev/null +++ b/.github/workflows/check_pr.yml @@ -0,0 +1,180 @@ +name: Check PR + +# Controls when the workflow will run +on: + pull_request: + branches: + - main + types: [ opened, synchronize, labeled, unlabeled, reopened, edited ] + + +permissions: + pull-requests: write + + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + auto_assign: + name: Auto Assign + + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + - name: Assign Me + # You may pin to the exact commit or the version. + uses: kentaro-m/auto-assign-action@v1.2.1 + with: + configuration-path: '.github/auto_assign.yml' + + check_labels: + name: Check Required Labels + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + - name: Verify PR Labels + if: ${{ !contains(github.event.pull_request.labels.*.name, 'major') && !contains(github.event.pull_request.labels.*.name, 'minor') && !contains(github.event.pull_request.labels.*.name, 'patch') && !contains(github.event.pull_request.labels.*.name, 'patch') && !contains(github.event.pull_request.labels.*.name, 'skip') }} + uses: actions/github-script@v6.3.3 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + var comments = await github.rest.issues.listComments({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo + }); + for (const comment of comments.data) { + if (comment.body.includes('This pull request does not contain a valid label')){ + github.rest.issues.deleteComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: comment.id + }) + } + } + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: 'This pull request does not contain a valid label. Please add one of the following labels: `[major, minor, patch, patch, skip]`' + }) + core.setFailed('Missing required labels') + + check_size: + runs-on: ubuntu-latest + name: Check Size + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Check Size + uses: actions/github-script@v6.3.3 + env: + IGNORED_FILES: openapi.json, api-docs.json + BRANCH_NAME: ${{ github.head_ref}} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const additions = context.payload.pull_request.additions || 0 + const deletions = context.payload.pull_request.deletions || 0 + var changes = additions + deletions + console.log('additions: '+additions+' + deletions: '+deletions+ ' = total changes: ' + changes); + + const { IGNORED_FILES, BRANCH_NAME } = process.env + const ignored_files = IGNORED_FILES.trim().split(',').filter(word => word.length > 0); + if (ignored_files.length > 0){ + var ignored = 0 + const execSync = require('child_process').execSync; + for (const file of IGNORED_FILES.trim().split(',')) { + + const ignored_additions_str = execSync('git --no-pager diff --numstat origin/main..origin/'+BRANCH_NAME+' | grep ' + file + ' | cut -f 1', { encoding: 'utf-8' }) + const ignored_deletions_str = execSync('git --no-pager diff --numstat origin/main..origin/'+BRANCH_NAME+' | grep ' + file + ' | cut -f 2', { encoding: 'utf-8' }) + + const ignored_additions = ignored_additions_str.split('\n').map(elem=> parseInt(elem || 0)).reduce( + (accumulator, currentValue) => accumulator + currentValue, + 0); + const ignored_deletions = ignored_deletions_str.split('\n').map(elem=> parseInt(elem || 0)).reduce( + (accumulator, currentValue) => accumulator + currentValue, + 0); + + ignored += ignored_additions + ignored_deletions; + } + changes -= ignored + console.log('ignored lines: ' + ignored + ' , consider changes: ' + changes); + } + + var labels = await github.rest.issues.listLabelsOnIssue({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo + }); + + if (changes <= 400){ + if (labels.data.find(label => label.name == 'size/large')){ + github.rest.issues.removeLabel({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + name: 'size/large' + }) + } + } + + if (changes >= 200){ + if (labels.data.find(label => label.name == 'size/small')){ + github.rest.issues.removeLabel({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + name: 'size/small' + }) + } + } + + var comments = await github.rest.issues.listComments({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo + }); + for (const comment of comments.data) { + if (comment.body.includes('This PR exceeds the recommended size')){ + github.rest.issues.deleteComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: comment.id + }) + } + } + + if (changes < 200){ + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ['size/small'] + }) + } + + if (changes > 400){ + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ['size/large'] + }) + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: 'This PR exceeds the recommended size of 400 lines. Please make sure you are NOT addressing multiple issues with one PR. _Note this PR might be rejected due to its size._' + }) + + } + diff --git a/.github/workflows/code_review.yml b/.github/workflows/code_review.yml new file mode 100644 index 0000000..4778bf5 --- /dev/null +++ b/.github/workflows/code_review.yml @@ -0,0 +1,119 @@ +name: Code Review + +# Controls when the workflow will run +on: + pull_request: + branches: + - main + types: + - opened + - synchronize + - reopened + push: + branches: + - main + + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +env: + PROJECT_KEY: # TODO + +permissions: + id-token: write + contents: read + deployments: write + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + code-review: + name: Code Review + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + - name: Code Review + uses: pagopa/github-actions-template/maven-code-review@v1.10.6 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + sonar_token: ${{ secrets.SONAR_TOKEN }} + project_key: ${{env.PROJECT_KEY}} + java_version: 17 + coverage_exclusions: "**/config/*,**/*Mock*,**/model/**,**/entity/*" + cpd_exclusions: "**/models/**,**/entity/*" + + smoke-test: + name: Smoke Test + runs-on: ubuntu-latest + environment: + name: dev + steps: + - name: Checkout + id: checkout + uses: actions/checkout@1f9a0c22da41e6ebfa534300ef656657ea2c6707 + + - name: Login + id: login + # from https://github.com/Azure/login/commits/master + uses: azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 + with: + client-id: ${{ secrets.CLIENT_ID }} + tenant-id: ${{ secrets.TENANT_ID }} + subscription-id: ${{ secrets.SUBSCRIPTION_ID }} + + - name: Run Service on Docker + shell: bash + id: run_service_docker + run: | + cd ./docker + chmod +x ./run_docker.sh + ./run_docker.sh local + + - name: Run Integration Tests + shell: bash + id: run_integration_test + run: | + export SUBKEY=${{ secrets.SUBKEY }} + export CANARY=${{ inputs.canary }} + export CUCUMBER_PUBLISH_TOKEN=${{ secrets.CUCUMBER_PUBLISH_TOKEN }} + + cd ./integration-test + chmod +x ./run_integration_test.sh + ./run_integration_test.sh local + + + delete_github_deployments: + runs-on: ubuntu-latest + needs: smoke-test + if: ${{ always() }} + steps: + - name: Delete Previous deployments + uses: actions/github-script@v6 + env: + SHA_HEAD: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.sha) || github.sha}} + with: + script: | + const { SHA_HEAD } = process.env + + const deployments = await github.rest.repos.listDeployments({ + owner: context.repo.owner, + repo: context.repo.repo, + sha: SHA_HEAD + }); + await Promise.all( + deployments.data.map(async (deployment) => { + await github.rest.repos.createDeploymentStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: deployment.id, + state: 'inactive' + }); + return github.rest.repos.deleteDeployment({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: deployment.id + }); + }) + ); diff --git a/.github/workflows/create_dashboard.yaml b/.github/workflows/create_dashboard.yaml new file mode 100644 index 0000000..61fa251 --- /dev/null +++ b/.github/workflows/create_dashboard.yaml @@ -0,0 +1,84 @@ +name: Create Dashboard + +# Controls when the workflow will run +on: + push: + branches: + - main + paths: + - 'openapi/**' + - '.github/workflows/create_dashboard.yaml' + - '.opex/**' + + workflow_dispatch: + +permissions: + id-token: write + contents: read + deployments: write + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + dashboard: + # The type of runner that the job will run on + runs-on: ubuntu-22.04 + + strategy: + matrix: + environment: [prod] + environment: + name: ${{ matrix.environment }} + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + - name: Checkout + id: checkout + # from https://github.com/actions/checkout/commits/main + uses: actions/checkout@1f9a0c22da41e6ebfa534300ef656657ea2c6707 + with: + persist-credentials: false + + # from https://github.com/pagopa/opex-dashboard-azure-action/ + - uses: pagopa/opex-dashboard-azure-action@v1.1.2 + with: + environment: ${{ matrix.environment }} + api-name: + config: .opex/env/${{ matrix.environment }}/config.yaml + client-id: ${{ secrets.CLIENT_ID }} + tenant-id: ${{ secrets.TENANT_ID }} + subscription-id: ${{ secrets.SUBSCRIPTION_ID }} + # from https://github.com/pagopa/opex-dashboard-azure-action/pkgs/container/opex-dashboard-azure-action + docker-version: sha256:e4245954566cd3470e1b5527d33bb58ca132ce7493eac01be9e808fd25a11c8d + + delete_github_deployments: + runs-on: ubuntu-latest + needs: dashboard + if: ${{ always() }} + steps: + - name: Delete Previous deployments + uses: actions/github-script@v6 + env: + SHA_HEAD: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.sha) || github.sha}} + with: + script: | + const { SHA_HEAD } = process.env + + const deployments = await github.rest.repos.listDeployments({ + owner: context.repo.owner, + repo: context.repo.repo, + sha: SHA_HEAD + }); + await Promise.all( + deployments.data.map(async (deployment) => { + await github.rest.repos.createDeploymentStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: deployment.id, + state: 'inactive' + }); + return github.rest.repos.deleteDeployment({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: deployment.id + }); + }) + ); diff --git a/.github/workflows/deploy_with_github_runner.yml b/.github/workflows/deploy_with_github_runner.yml new file mode 100644 index 0000000..1bb2804 --- /dev/null +++ b/.github/workflows/deploy_with_github_runner.yml @@ -0,0 +1,119 @@ +name: Deploy on AKS + +on: + workflow_call: + inputs: + environment: + required: true + description: The name of the environment where to deploy + type: string + +env: + APP_NAME: # TODO + + +permissions: + id-token: write + contents: read + +jobs: + create_runner: + name: Create Runner + runs-on: ubuntu-22.04 + environment: + name: ${{ inputs.environment }} + outputs: + runner_name: ${{ steps.create_github_runner.outputs.runner_name }} + steps: + - name: Create GitHub Runner + id: create_github_runner + # from https://github.com/pagopa/eng-github-actions-iac-template/tree/main/azure/github-self-hosted-runner-azure-create-action + uses: pagopa/eng-github-actions-iac-template/azure/github-self-hosted-runner-azure-create-action@main + with: + client_id: ${{ secrets.CLIENT_ID }} + tenant_id: ${{ secrets.TENANT_ID }} + subscription_id: ${{ secrets.SUBSCRIPTION_ID }} + container_app_environment_name: ${{ vars.CONTAINER_APP_ENVIRONMENT_NAME }} + resource_group_name: ${{ vars.CONTAINER_APP_ENVIRONMENT_RESOURCE_GROUP_NAME }} # RG of the runner + pat_token: ${{ secrets.BOT_TOKEN_GITHUB }} + self_hosted_runner_image_tag: "v3.0.0" + + deploy: + needs: [ create_runner ] + runs-on: [ self-hosted, "${{ needs.create_runner.outputs.runner_name }}" ] + name: Deploy on AKS + environment: ${{ inputs.environment }} + steps: + - name: Deploy + uses: pagopa/github-actions-template/aks-deploy@main + with: + branch: ${{ github.ref_name }} + client_id: ${{ secrets.CLIENT_ID }} + subscription_id: ${{ secrets.SUBSCRIPTION_ID }} + tenant_id: ${{ secrets.TENANT_ID }} + env: ${{ inputs.environment }} + namespace: ${{ vars.NAMESPACE }} + cluster_name: ${{ vars.CLUSTER_NAME }} + resource_group: ${{ vars.CLUSTER_RESOURCE_GROUP }} + app_name: ${{ env.APP_NAME }} + helm_upgrade_options: "--debug" + + cleanup_runner: + name: Cleanup Runner + needs: [ create_runner, deploy ] + if: ${{ success() || failure() }} + runs-on: ubuntu-22.04 + environment: ${{ inputs.environment }} + steps: + - name: Cleanup GitHub Runner + id: cleanup_github_runner + # from https://github.com/pagopa/eng-github-actions-iac-template/tree/main/azure/github-self-hosted-runner-azure-cleanup-action + uses: pagopa/eng-github-actions-iac-template/azure/github-self-hosted-runner-azure-cleanup-action@0ee2f58fd46d10ac7f00bce4304b98db3dbdbe9a + with: + client_id: ${{ secrets.CLIENT_ID }} + tenant_id: ${{ secrets.TENANT_ID }} + subscription_id: ${{ secrets.SUBSCRIPTION_ID }} + resource_group_name: ${{ vars.CONTAINER_APP_ENVIRONMENT_RESOURCE_GROUP_NAME }} + runner_name: ${{ needs.create_runner.outputs.runner_name }} + pat_token: ${{ secrets.BOT_TOKEN_GITHUB }} + + update_openapi: + needs: [ deploy ] + runs-on: ubuntu-latest + name: Update OpenAPI + environment: ${{ inputs.environment }} + steps: + - name: Checkout + id: checkout + # from https://github.com/actions/checkout/commits/main + uses: actions/checkout@1f9a0c22da41e6ebfa534300ef656657ea2c6707 + with: + persist-credentials: false + + - name: Setup Terraform + # from https://github.com/hashicorp/setup-terraform/commits/main + uses: hashicorp/setup-terraform@8feba2b913ea459066180f9cb177f58a881cf146 + with: + terraform_version: "1.3.6" + + - name: Login + id: login + # from https://github.com/Azure/login/commits/master + uses: azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 + with: + client-id: ${{ secrets.CLIENT_ID }} + tenant-id: ${{ secrets.TENANT_ID }} + subscription-id: ${{ secrets.SUBSCRIPTION_ID }} + + + - name: Terraform Apply + shell: bash + run: | + cd ./infra + export ARM_CLIENT_ID="${{ secrets.CLIENT_ID }}" + export ARM_SUBSCRIPTION_ID=$(az account show --query id --output tsv) + export ARM_TENANT_ID=$(az account show --query tenantId --output tsv) + export ARM_USE_OIDC=true + export ARM_ACCESS_KEY=$(az storage account keys list --resource-group io-infra-rg --account-name pagopainfraterraform${{inputs.environment}} --query '[0].value' -o tsv) + bash ./terraform.sh init weu-${{ inputs.environment }} + bash ./terraform.sh apply weu-${{ inputs.environment }} -auto-approve diff --git a/.github/workflows/integration_test.yml b/.github/workflows/integration_test.yml new file mode 100644 index 0000000..c405e3e --- /dev/null +++ b/.github/workflows/integration_test.yml @@ -0,0 +1,116 @@ +name: Integration Tests + +on: + schedule: + - cron: '00 08 * * *' + + workflow_dispatch: + inputs: + environment: + required: true + type: choice + description: Select the Environment + options: + - dev + - uat + - prod + canary: + description: 'run the tests on canary version' + required: false + type: boolean + default: false + notify: + required: false + type: boolean + description: 'send the slack notification' + default: true + + +permissions: + id-token: write + contents: read + deployments: write + + +jobs: + integration_test: + name: Test ${{(github.event.inputs == null && 'dev') || inputs.environment }} + runs-on: ubuntu-latest + environment: ${{(github.event.inputs == null && 'dev') || inputs.environment }} + steps: + - name: Checkout + id: checkout + uses: actions/checkout@1f9a0c22da41e6ebfa534300ef656657ea2c6707 + + - name: Login + id: login + # from https://github.com/Azure/login/commits/master + uses: azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 + with: + client-id: ${{ secrets.CLIENT_ID }} + tenant-id: ${{ secrets.TENANT_ID }} + subscription-id: ${{ secrets.SUBSCRIPTION_ID }} + + - name: Run Integration Tests + shell: bash + run: | + export SUBKEY=${{ secrets.SUBKEY }} + export CANARY=${{ inputs.canary }} + export CUCUMBER_PUBLISH_TOKEN=${{ secrets.CUCUMBER_PUBLISH_TOKEN }} + + cd ./integration-test + chmod +x ./run_integration_test.sh + ./run_integration_test.sh ${{( github.event.inputs == null && 'dev') || inputs.environment }} + + notify: + needs: [ integration_test ] + runs-on: ubuntu-latest + name: Notify + if: ${{ always() && inputs.notify == 'true' }} + steps: + - name: Report Status + if: ${{ inputs.notify }} + uses: ravsamhq/notify-slack-action@v2 + with: + status: ${{ needs.integration_test.result }} + token: ${{ secrets.GITHUB_TOKEN }} + notify_when: 'failure,skipped' + notification_title: '<{run_url}|Scheduled Integration Test> has {status_message}' + message_format: '{emoji} <{run_url}|{workflow}> {status_message} in <{repo_url}|{repo}>' + footer: 'Linked to <{workflow_url}| workflow file>' + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + + delete_github_deployments: + runs-on: ubuntu-latest + needs: integration_test + if: ${{ always() }} + steps: + - name: Delete Previous deployments + uses: actions/github-script@v6 + env: + SHA_HEAD: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.sha) || github.sha}} + with: + script: | + const { SHA_HEAD } = process.env + + const deployments = await github.rest.repos.listDeployments({ + owner: context.repo.owner, + repo: context.repo.repo, + sha: SHA_HEAD + }); + await Promise.all( + deployments.data.map(async (deployment) => { + await github.rest.repos.createDeploymentStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: deployment.id, + state: 'inactive' + }); + return github.rest.repos.deleteDeployment({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: deployment.id + }); + }) + ); diff --git a/.github/workflows/release_deploy.yml b/.github/workflows/release_deploy.yml new file mode 100644 index 0000000..73332b7 --- /dev/null +++ b/.github/workflows/release_deploy.yml @@ -0,0 +1,160 @@ +name: Release And Deploy + +# Controls when the workflow will run +on: + merge_group: + branches: [ main ] + pull_request: + types: [ closed ] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + inputs: + environment: + required: true + type: choice + description: Select the Environment + options: + - dev + - uat + - prod + version: + required: false + type: choice + description: Select the version + options: + - patch + - skip_or_promote + - new_release + - breaking_change + + beta: + required: false + type: boolean + description: deploy beta version on AKS + default: false + + +permissions: + packages: write + contents: write + issues: write + id-token: write + actions: read + + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + setup: + name: Setup + runs-on: ubuntu-latest + outputs: + semver: ${{ steps.get_semver.outputs.semver }} + environment: ${{ steps.get_env.outputs.environment }} + steps: + - name: pull request rejected + if: github.event_name == 'pull_request' && github.event.action == 'closed' && github.event.pull_request.merged != true + run: | + echo "❌ PR was closed without a merge" + exit 1 + + # Set Semvar + - run: echo "SEMVER=patch" >> $GITHUB_ENV + + - if: ${{ (github.event.pull_request.merged && contains(github.event.pull_request.labels.*.name, 'skip')) }} + run: echo "SEMVER=skip" >> $GITHUB_ENV + + - if: ${{ (github.event.pull_request.merged && contains(github.event.pull_request.labels.*.name, 'patch')) }} + run: echo "SEMVER=patch" >> $GITHUB_ENV + + - if: ${{ (github.event.pull_request.merged && contains(github.event.pull_request.labels.*.name, 'minor')) }} + run: echo "SEMVER=minor" >> $GITHUB_ENV + + - if: ${{ (github.event.pull_request.merged && contains(github.event.pull_request.labels.*.name, 'major')) }} + run: echo "SEMVER=major" >> $GITHUB_ENV + + # force semver if dev, !=main or skip release + - if: ${{ inputs.version == 'new_release' }} + run: echo "SEMVER=minor" >> $GITHUB_ENV + + - if: ${{ inputs.version == 'breaking_change' }} + run: echo "SEMVER=major" >> $GITHUB_ENV + + - if: ${{ github.ref_name != 'main' }} + run: echo "SEMVER=buildNumber" >> $GITHUB_ENV + + - if: ${{ inputs.version == 'skip_or_promote' }} + run: echo "SEMVER=skip" >> $GITHUB_ENV + + - id: get_semver + name: Set Output + run: echo "semver=${{env.SEMVER}}" >> $GITHUB_OUTPUT + + # Set Environment + - run: echo "ENVIRNOMENT=${{ inputs.environment}}" >> $GITHUB_ENV + + - if: ${{ inputs.environment == null }} + run: echo "ENVIRNOMENT=dev" >> $GITHUB_ENV + + - id: get_env + name: Set Output + run: echo "environment=${{env.ENVIRNOMENT}}" >> $GITHUB_OUTPUT + + + release: + name: Create a New Release + runs-on: ubuntu-latest + needs: [ setup ] + outputs: + version: ${{ steps.release.outputs.version }} + steps: + - name: Make Release + id: release + uses: pagopa/github-actions-template/maven-release@v1.12.0 + with: + semver: ${{ needs.setup.outputs.semver }} + github_token: ${{ secrets.BOT_TOKEN_GITHUB }} + beta: ${{ inputs.beta }} + prerelease: ${{ inputs.environment != 'prod' }} + skip_ci: false + + image: + needs: [ setup, release ] + name: Build and Push Docker Image + runs-on: ubuntu-latest + if: ${{ needs.setup.outputs.semver != 'skip' }} + steps: + - name: Build and Push + id: semver + uses: pagopa/github-actions-template/ghcr-build-push@v1.5.4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + tag: ${{ needs.release.outputs.version }} + + deploy_aks: + name: Deploy on AKS + needs: [ setup, release, image ] + if: ${{ always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }} + uses: ./.github/workflows/deploy_with_github_runner.yml + with: + environment: ${{ needs.setup.outputs.environment }} + secrets: inherit + + notify: + needs: [ setup, release, deploy_aks ] + runs-on: ubuntu-latest + name: Notify + if: always() + steps: + - name: Report Status + if: ${{ needs.setup.outputs.environment == 'prod' }} + uses: ravsamhq/notify-slack-action@v2 + with: + status: ${{ needs.deploy_aks.result }} + token: ${{ secrets.GITHUB_TOKEN }} + notification_title: 'New Release on Production ${{ needs.release.outputs.version }} has {status_message}' + message_format: '{emoji} <{run_url}|{workflow}> {status_message} in <{repo_url}|{repo}>' + footer: 'Linked to <{workflow_url}| workflow file>' + icon_success: ':white_check_mark:' + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} diff --git a/.github/workflows/update_gha.py b/.github/workflows/update_gha.py new file mode 100644 index 0000000..5cd4288 --- /dev/null +++ b/.github/workflows/update_gha.py @@ -0,0 +1,16 @@ +import json +import requests + +url = 'https://api.github.com/repos/pagopa/template-java-spring-microservice/contents/.github/workflows' +url_raw = 'https://raw.githubusercontent.com/pagopa/template-java-spring-microservice/main/' + +response = requests.get(url) + +for item in json.loads(response.text): + path = item["path"] + name = item["name"] + print(name) + response = requests.get(url_raw + path) + fo = open(name, "w") + fo.write(response.text) + fo.close() diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..79d7c43 --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +# IntelliJ +.idea +*.iml + +# VS Code +.vscode + +# Project files +/target/ +**/node_modules +**/.env +.cache_ggshield + +# Helm +/helm/charts/* +**/.terraform/ diff --git a/.identity/00_data.tf b/.identity/00_data.tf new file mode 100644 index 0000000..2a054fb --- /dev/null +++ b/.identity/00_data.tf @@ -0,0 +1,57 @@ +data "azurerm_storage_account" "tf_storage_account"{ + name = "pagopainfraterraform${var.env}" + resource_group_name = "io-infra-rg" +} + +data "azurerm_resource_group" "dashboards" { + name = "dashboards" +} + +data "azurerm_kubernetes_cluster" "aks" { + name = local.aks_cluster.name + resource_group_name = local.aks_cluster.resource_group_name +} + +data "github_organization_teams" "all" { + root_teams_only = true + summary_only = true +} + +data "azurerm_user_assigned_identity" "identity_cd_01" { + name = "${local.prefix}-${var.env_short}-${local.domain}-01-github-cd-identity" + resource_group_name = "${local.prefix}-${var.env_short}-identity-rg" +} + +data "azurerm_key_vault" "key_vault" { + name = "pagopa-${var.env_short}-kv" + resource_group_name = "pagopa-${var.env_short}-sec-rg" +} + +data "azurerm_key_vault" "domain_key_vault" { + name = "pagopa-${var.env_short}-${local.domain}-kv" + resource_group_name = "pagopa-${var.env_short}-${local.domain}-sec-rg" +} + +data "azurerm_resource_group" "apim_resource_group" { + name = "${local.product}-api-rg" +} + +data "azurerm_key_vault_secret" "key_vault_sonar" { + name = "sonar-token" + key_vault_id = data.azurerm_key_vault.key_vault.id +} + +data "azurerm_key_vault_secret" "key_vault_bot_token" { + name = "bot-token-github" + key_vault_id = data.azurerm_key_vault.key_vault.id +} + +data "azurerm_key_vault_secret" "key_vault_cucumber_token" { + name = "cucumber-token" + key_vault_id = data.azurerm_key_vault.key_vault.id +} + +data "azurerm_key_vault_secret" "key_vault_integration_test_subkey" { + name = "integration-test-subkey" + key_vault_id = data.azurerm_key_vault.key_vault.id +} diff --git a/.identity/01_github_environment.tf b/.identity/01_github_environment.tf new file mode 100644 index 0000000..ec4caad --- /dev/null +++ b/.identity/01_github_environment.tf @@ -0,0 +1,79 @@ +resource "github_repository_environment" "github_repository_environment" { + environment = var.env + repository = local.github.repository + # filter teams reviewers from github_organization_teams + # if reviewers_teams is null no reviewers will be configured for environment + dynamic "reviewers" { + for_each = (var.github_repository_environment.reviewers_teams == null || var.env_short != "p" ? [] : [1]) + content { + teams = matchkeys( + data.github_organization_teams.all.teams.*.id, + data.github_organization_teams.all.teams.*.name, + var.github_repository_environment.reviewers_teams + ) + } + } + deployment_branch_policy { + protected_branches = var.github_repository_environment.protected_branches + custom_branch_policies = var.github_repository_environment.custom_branch_policies + } +} + +locals { + env_secrets = { + "CLIENT_ID" : data.azurerm_user_assigned_identity.identity_cd_01.client_id, + "TENANT_ID" : data.azurerm_client_config.current.tenant_id, + "SUBSCRIPTION_ID" : data.azurerm_subscription.current.subscription_id, + "SUBKEY" : data.azurerm_key_vault_secret.key_vault_integration_test_subkey.value, + } + env_variables = { + "CONTAINER_APP_ENVIRONMENT_NAME" : local.container_app_environment.name, + "CONTAINER_APP_ENVIRONMENT_RESOURCE_GROUP_NAME" : local.container_app_environment.resource_group, + "CLUSTER_NAME" : local.aks_cluster.name, + "CLUSTER_RESOURCE_GROUP" : local.aks_cluster.resource_group_name, + "NAMESPACE" : local.domain, + } + repo_secrets = { + "SONAR_TOKEN" : data.azurerm_key_vault_secret.key_vault_sonar.value, + "BOT_TOKEN_GITHUB" : data.azurerm_key_vault_secret.key_vault_bot_token.value, + "CUCUMBER_PUBLISH_TOKEN" : data.azurerm_key_vault_secret.key_vault_cucumber_token.value, + } +} + +############### +# ENV Secrets # +############### + +resource "github_actions_environment_secret" "github_environment_runner_secrets" { + for_each = local.env_secrets + repository = local.github.repository + environment = var.env + secret_name = each.key + plaintext_value = each.value +} + +################# +# ENV Variables # +################# + + +resource "github_actions_environment_variable" "github_environment_runner_variables" { + for_each = local.env_variables + repository = local.github.repository + environment = var.env + variable_name = each.key + value = each.value +} + +############################# +# Secrets of the Repository # +############################# + + +resource "github_actions_secret" "repo_secrets" { + for_each = local.repo_secrets + repository = local.github.repository + secret_name = each.key + plaintext_value = each.value +} + diff --git a/.identity/99_main.tf b/.identity/99_main.tf new file mode 100644 index 0000000..c5eb056 --- /dev/null +++ b/.identity/99_main.tf @@ -0,0 +1,32 @@ +terraform { + required_version = ">=1.3.0" + + required_providers { + azuread = { + source = "hashicorp/azuread" + version = "2.30.0" + } + azurerm = { + source = "hashicorp/azurerm" + version = "3.45.0" + } + github = { + source = "integrations/github" + version = "5.18.3" + } + } + + backend "azurerm" {} +} + +provider "azurerm" { + features {} +} + +provider "github" { + owner = "pagopa" +} + +data "azurerm_subscription" "current" {} + +data "azurerm_client_config" "current" {} diff --git a/.identity/99_variables.tf b/.identity/99_variables.tf new file mode 100644 index 0000000..b57e1b5 --- /dev/null +++ b/.identity/99_variables.tf @@ -0,0 +1,56 @@ +locals { + github = { + org = "pagopa" + repository = "TODO" #TODO + } + + prefix = "pagopa" + domain = "TODO" #TODO + location_short = "weu" + product = "${var.prefix}-${var.env_short}" + + app_name = "github-${local.github.org}-${local.github.repository}-${var.prefix}-${local.domain}-${var.env}-aks" + + aks_cluster = { + name = "${local.product}-${local.location_short}-${var.env}-aks" + resource_group_name = "${local.product}-${local.location_short}-${var.env}-aks-rg" + } + + container_app_environment = { + name = "${local.prefix}-${var.env_short}-${local.location_short}-github-runner-cae", + resource_group = "${local.prefix}-${var.env_short}-${local.location_short}-github-runner-rg", + } +} + +variable "env" { + type = string +} + +variable "env_short" { + type = string +} + +variable "prefix" { + type = string + default = "pagopa" + validation { + condition = ( + length(var.prefix) <= 6 + ) + error_message = "Max length is 6 chars." + } +} + +variable "github_repository_environment" { + type = object({ + protected_branches = bool + custom_branch_policies = bool + reviewers_teams = list(string) + }) + description = "GitHub Continuous Integration roles" + default = { + protected_branches = false + custom_branch_policies = true + reviewers_teams = ["pagopa-team-core"] + } +} diff --git a/.identity/env/dev/backend.ini b/.identity/env/dev/backend.ini new file mode 100644 index 0000000..22e6a99 --- /dev/null +++ b/.identity/env/dev/backend.ini @@ -0,0 +1 @@ +subscription=DEV-pagoPA diff --git a/.identity/env/dev/backend.tfvars b/.identity/env/dev/backend.tfvars new file mode 100644 index 0000000..b5dbac8 --- /dev/null +++ b/.identity/env/dev/backend.tfvars @@ -0,0 +1,4 @@ +resource_group_name = "io-infra-rg" +storage_account_name = "pagopainfraterraformdev" +container_name = "azurermstate" +key = ".tfstate" # TODO diff --git a/.identity/env/dev/terraform.tfvars b/.identity/env/dev/terraform.tfvars new file mode 100644 index 0000000..3345e07 --- /dev/null +++ b/.identity/env/dev/terraform.tfvars @@ -0,0 +1,11 @@ +prefix = "pagopa" +env = "dev" +env_short = "d" + +tags = { + CreatedBy = "Terraform" + Environment = "Dev" + Owner = "pagoPA" + Source = "https://github.com/pagopa/your-repository" # TODO + CostCenter = "TS310 - PAGAMENTI & SERVIZI" +} diff --git a/.identity/env/prod/backend.ini b/.identity/env/prod/backend.ini new file mode 100644 index 0000000..6318425 --- /dev/null +++ b/.identity/env/prod/backend.ini @@ -0,0 +1 @@ +subscription=PROD-pagoPA diff --git a/.identity/env/prod/backend.tfvars b/.identity/env/prod/backend.tfvars new file mode 100644 index 0000000..d8d402c --- /dev/null +++ b/.identity/env/prod/backend.tfvars @@ -0,0 +1,4 @@ +resource_group_name = "io-infra-rg" +storage_account_name = "pagopainfraterraformprod" +container_name = "azurermstate" +key = ".tfstate" # TODO diff --git a/.identity/env/prod/terraform.tfvars b/.identity/env/prod/terraform.tfvars new file mode 100644 index 0000000..ee41cf5 --- /dev/null +++ b/.identity/env/prod/terraform.tfvars @@ -0,0 +1,11 @@ +prefix = "pagopa" +env = "prod" +env_short = "p" + +tags = { + CreatedBy = "Terraform" + Environment = "Prod" + Owner = "pagoPA" + Source = "https://github.com/pagopa/your-repository" # TODO + CostCenter = "TS310 - PAGAMENTI & SERVIZI" +} diff --git a/.identity/env/uat/backend.ini b/.identity/env/uat/backend.ini new file mode 100644 index 0000000..1a01415 --- /dev/null +++ b/.identity/env/uat/backend.ini @@ -0,0 +1 @@ +subscription=UAT-pagoPA diff --git a/.identity/env/uat/backend.tfvars b/.identity/env/uat/backend.tfvars new file mode 100644 index 0000000..502eaf6 --- /dev/null +++ b/.identity/env/uat/backend.tfvars @@ -0,0 +1,4 @@ +resource_group_name = "io-infra-rg" +storage_account_name = "pagopainfraterraformuat" +container_name = "azurermstate" +key = ".tfstate" # TODO diff --git a/.identity/env/uat/terraform.tfvars b/.identity/env/uat/terraform.tfvars new file mode 100644 index 0000000..86ec8fc --- /dev/null +++ b/.identity/env/uat/terraform.tfvars @@ -0,0 +1,11 @@ +prefix = "pagopa" +env = "uat" +env_short = "u" + +tags = { + CreatedBy = "Terraform" + Environment = "Uat" + Owner = "pagoPA" + Source = "https://github.com/pagopa/your-repository" # TODO + CostCenter = "TS310 - PAGAMENTI & SERVIZI" +} diff --git a/.identity/terraform.sh b/.identity/terraform.sh new file mode 100755 index 0000000..02fc806 --- /dev/null +++ b/.identity/terraform.sh @@ -0,0 +1,69 @@ +#!/bin/bash + +set -e + +ACTION=$1 +ENV=$2 +shift 2 +other="$@" +# must be subscription in lower case +subscription="" +BACKEND_CONFIG_PATH="./env/${ENV}/backend.tfvars" + +if [ -z "$ACTION" ]; then + echo "[ERROR] Missed ACTION: init, apply, plan" + exit 0 +fi + +if [ -z "$ENV" ]; then + echo "[ERROR] ENV should be: dev, uat or prod." + exit 0 +fi + +# +# 🏁 Source & init shell +# + +# shellcheck source=/dev/null +source "./env/$ENV/backend.ini" + +# Subscription set +az account set -s "${subscription}" + +# if using cygwin, we have to transcode the WORKDIR +if [[ $WORKDIR == /cygdrive/* ]]; then + WORKDIR=$(cygpath -w $WORKDIR) +fi + +# Helm +export HELM_DEBUG=1 +export TF_VAR_github_token="${GITHUB_TOKEN}" +# TODO set your PAT TOKEN as env var +if [ -z "$GITHUB_TOKEN" ]; then + echo "Error: Set an environment variable named GITHUB_TOKEN with your GitHub PAT Token" + exit 1 +fi + +# +# 🌎 Terraform +# +if echo "init plan apply refresh import output state taint destroy" | grep -w "$ACTION" > /dev/null; then + if [ "$ACTION" = "init" ]; then + echo "[INFO] init tf on ENV: ${ENV}" + terraform "$ACTION" -backend-config="${BACKEND_CONFIG_PATH}" $other + elif [ "$ACTION" = "output" ] || [ "$ACTION" = "state" ] || [ "$ACTION" = "taint" ]; then + # init terraform backend + terraform init -reconfigure -backend-config="${BACKEND_CONFIG_PATH}" + terraform "$ACTION" $other + else + # init terraform backend + echo "[INFO] init tf on ENV: ${ENV}" + terraform init -reconfigure -backend-config="${BACKEND_CONFIG_PATH}" + + echo "[INFO] run tf with: ${ACTION} on ENV: ${ENV} and other: >${other}<" + terraform "${ACTION}" -var-file="./env/${ENV}/terraform.tfvars" -compact-warnings $other + fi +else + echo "[ERROR] ACTION not allowed." + exit 1 +fi diff --git a/.opex/env/prod/backend.ini b/.opex/env/prod/backend.ini new file mode 100644 index 0000000..6318425 --- /dev/null +++ b/.opex/env/prod/backend.ini @@ -0,0 +1 @@ +subscription=PROD-pagoPA diff --git a/.opex/env/prod/backend.tfvars b/.opex/env/prod/backend.tfvars new file mode 100644 index 0000000..ae0e6e4 --- /dev/null +++ b/.opex/env/prod/backend.tfvars @@ -0,0 +1,4 @@ +resource_group_name = "io-infra-rg" +storage_account_name = "pagopainfraterraformprod" +container_name = "azurermstate" +key = "opex..terraform.tfstate" #TODO diff --git a/.opex/env/prod/config.yaml b/.opex/env/prod/config.yaml new file mode 100644 index 0000000..80a9d54 --- /dev/null +++ b/.opex/env/prod/config.yaml @@ -0,0 +1,13 @@ +oa3_spec: ./openapi/openapi.json # If start with http the file would be downloaded from the internet +name: opex_ # TODO +location: West Europe +timespan: 5m # Default, a number or a timespan https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/scalar-data-types/timespan +data_source: /subscriptions/b9fc9419-6097-45fe-9f74-ba0641c91912/resourceGroups/pagopa-p-vnet-rg/providers/Microsoft.Network/applicationGateways/pagopa-p-app-gw +#data_source: /subscriptions/b9fc9419-6097-45fe-9f74-ba0641c91912/resourceGroups/pagopa-p-api-rg/providers/Microsoft.ApiManagement/service/pagopa-p-apim +#resource_type: api-management +action_groups: + - /subscriptions/b9fc9419-6097-45fe-9f74-ba0641c91912/resourceGroups/pagopa-p-monitor-rg/providers/microsoft.insights/actionGroups/PagoPA + - /subscriptions/b9fc9419-6097-45fe-9f74-ba0641c91912/resourceGroups/pagopa-p-monitor-rg/providers/microsoft.insights/actionGroups/SlackPagoPA +overrides: + hosts: # Use these hosts instead of those inside the OpenApi spec + - api.platform.pagopa.it diff --git a/.opex/env/prod/terraform.tfvars b/.opex/env/prod/terraform.tfvars new file mode 100644 index 0000000..1445485 --- /dev/null +++ b/.opex/env/prod/terraform.tfvars @@ -0,0 +1,11 @@ +prefix = "pagopa" +env_short = "p" + + +tags = { + CreatedBy = "Terraform" + Environment = "Prod" + Owner = "pagoPA" + Source = "https://github.com/pagopa/your-repository" # TODO + CostCenter = "TS310 - PAGAMENTI & SERVIZI" +} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..0d2e1f8 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,11 @@ +# 1. `pip install pre-commit` +# 2. `pre-commit install` +# 3. set GITGUARDIAN_API_KEY in your develop environment (get an api key here: https://dashboard.gitguardian.com/workspace/230910/settings/personal/personal-access-tokens) +# more info https://docs.gitguardian.com/internal-repositories-monitoring/integrations/git_hooks/pre_commit +repos: + - repo: https://github.com/gitguardian/ggshield + rev: v1.11.0 + hooks: + - id: ggshield + language_version: python3 + stages: [ commit ] diff --git a/.terraform-version b/.terraform-version new file mode 100644 index 0000000..95b25ae --- /dev/null +++ b/.terraform-version @@ -0,0 +1 @@ +1.3.6 diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..bac36ae --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,4 @@ +# see https://help.github.com/en/articles/about-code-owners#example-of-a-codeowners-file + +* @pagopa/pagopa-tech +# TODO: set your codeowners diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..65e6a12 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +# +# Build +# +FROM maven:3.8.4-jdk-11-slim as buildtime +WORKDIR /build +COPY . . +RUN mvn clean package + + +FROM adoptopenjdk/openjdk11:alpine-jre as builder +COPY --from=buildtime /build/target/*.jar application.jar +RUN java -Djarmode=layertools -jar application.jar extract + + +FROM ghcr.io/pagopa/docker-base-springboot-openjdk11:v1.0.1@sha256:bbbe948e91efa0a3e66d8f308047ec255f64898e7f9250bdb63985efd3a95dbf +ADD --chown=spring:spring https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/download/v1.25.1/opentelemetry-javaagent.jar . + +COPY --chown=spring:spring --from=builder dependencies/ ./ +COPY --chown=spring:spring --from=builder snapshot-dependencies/ ./ +# https://github.com/moby/moby/issues/37965#issuecomment-426853382 +RUN true +COPY --chown=spring:spring --from=builder spring-boot-loader/ ./ +COPY --chown=spring:spring --from=builder application/ ./ + +EXPOSE 8080 + +ENTRYPOINT ["java","-javaagent:opentelemetry-javaagent.jar","--enable-preview","org.springframework.boot.loader.JarLauncher"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..1962476 --- /dev/null +++ b/README.md @@ -0,0 +1,96 @@ +# Template for Java Spring Microservice project + +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=TODO-set-your-id&metric=alert_status)](https://sonarcloud.io/dashboard?id=TODO-set-your-id) +[![Integration Tests](https://github.com/pagopa//actions/workflows/integration_test.yml/badge.svg?branch=main)](https://github.com/pagopa//actions/workflows/integration_test.yml) + +TODO: add a description + +TODO: generate a index with this tool: https://ecotrust-canada.github.io/markdown-toc/ + +TODO: resolve all the TODOs in this template + +--- + +## Api Documentation 📖 + +See the [OpenApi 3 here.](https://editor.swagger.io/?url=https://raw.githubusercontent.com/pagopa//main/openapi/openapi.json) + +--- + +## Technology Stack + +- Java 11 +- Spring Boot +- Spring Web +- Hibernate +- JPA +- ... +- TODO + +--- + +## Start Project Locally 🚀 + +### Prerequisites + +- docker + +### Run docker container + +from `./docker` directory + +`sh ./run_docker.sh local` + +ℹ️ Note: for PagoPa ACR is required the login `az acr login -n ` + +--- + +## Develop Locally 💻 + +### Prerequisites + +- git +- maven +- jdk-11 + +### Run the project + +Start the springboot application with this command: + +`mvn spring-boot:run -Dspring-boot.run.profiles=local` + +### Spring Profiles + +- **local**: to develop locally. +- _default (no profile set)_: The application gets the properties from the environment (for Azure). + +### Testing 🧪 + +#### Unit testing + +To run the **Junit** tests: + +`mvn clean verify` + +#### Integration testing + +From `./integration-test/src` + +1. `yarn install` +2. `yarn test` + +#### Performance testing + +install [k6](https://k6.io/) and then from `./performance-test/src` + +1. `k6 run --env VARS=local.environment.json --env TEST_TYPE=./test-types/load.json main_scenario.js` + +--- + +## Contributors 👥 + +Made with ❤️ by PagoPa S.p.A. + +### Maintainers + +See `CODEOWNERS` file diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..bea5373 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,15 @@ +# Docker Environment 🐳 +`run_docker.sh` is a script to launch the image of this microservice and all the dependencies on Docker. + +## How to use 💻 +You can use `local`, `dev`, `uat` or `prod` images + +Precondition: `az login` + +`sh ./run_docker.sh ` + +--- + +ℹ️ _Note_: for **PagoPa ACR** is **required** the login `az acr login -n ` + +ℹ️ _Note_: If you run the script without the parameter, `local` is used as default. diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..d119b75 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,14 @@ +version: '3.8' + +services: + app: + container_name: 'service' # TODO + image: ${image} + platform: linux/amd64 + build: + dockerfile: Dockerfile + context: ../ + env_file: + - ./.env + ports: + - "8080:8080" diff --git a/docker/run_docker.sh b/docker/run_docker.sh new file mode 100755 index 0000000..5d98b04 --- /dev/null +++ b/docker/run_docker.sh @@ -0,0 +1,62 @@ +#!/bin/bash + +# sh ./run_docker.sh + +ENV=$1 + +if [ -z "$ENV" ] +then + ENV="local" + echo "No environment specified: local is used." +fi + +pip3 install yq + +if [ "$ENV" = "local" ]; then + image="service-local:latest" + ENV="dev" +else + repository=$(yq -r '."microservice-chart".image.repository' ../helm/values-$ENV.yaml) + image="${repository}:latest" +fi +export image=${image} + +FILE=.env +if test -f "$FILE"; then + rm .env +fi +config=$(yq -r '."microservice-chart".envConfig' ../helm/values-$ENV.yaml) +IFS=$'\n' +for line in $(echo "$config" | jq -r '. | to_entries[] | select(.key) | "\(.key)=\(.value)"'); do + echo "$line" >> .env +done + +keyvault=$(yq -r '."microservice-chart".keyvault.name' ../helm/values-$ENV.yaml) +secret=$(yq -r '."microservice-chart".envSecret' ../helm/values-$ENV.yaml) +for line in $(echo "$secret" | jq -r '. | to_entries[] | select(.key) | "\(.key)=\(.value)"'); do + IFS='=' read -r -a array <<< "$line" + response=$(az keyvault secret show --vault-name $keyvault --name "${array[1]}") + value=$(echo "$response" | jq -r '.value') + echo "${array[0]}=$value" >> .env +done + + +stack_name=$(cd .. && basename "$PWD") +docker compose -p "${stack_name}" up -d --remove-orphans --force-recreate --build + + +# waiting the containers +printf 'Waiting for the service' +attempt_counter=0 +max_attempts=50 +until $(curl --output /dev/null --silent --head --fail http://localhost:8080/actuator/info); do + if [ ${attempt_counter} -eq ${max_attempts} ];then + echo "Max attempts reached" + exit 1 + fi + + printf '.' + attempt_counter=$((attempt_counter+1)) + sleep 5 +done +echo 'Service Started' diff --git a/helm/.helmignore b/helm/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/helm/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm/Chart.yaml b/helm/Chart.yaml new file mode 100644 index 0000000..2f15c6e --- /dev/null +++ b/helm/Chart.yaml @@ -0,0 +1,10 @@ +apiVersion: v2 +name: pagopa-afm-calculator +description: Microservice that handles calculation for pagoPA Advanced Fees Management +type: application +version: 0.0.0 +appVersion: 0.0.0 +dependencies: + - name: microservice-chart + version: 5.9.0 + repository: "https://pagopa.github.io/aks-microservice-chart-blueprint" diff --git a/helm/values-dev.yaml b/helm/values-dev.yaml new file mode 100644 index 0000000..c265ccd --- /dev/null +++ b/helm/values-dev.yaml @@ -0,0 +1,120 @@ +microservice-chart: + namespace: "your-namespace" # TODO: set your AKS namespace + nameOverride: "" + fullnameOverride: "" + image: + repository: ghcr.io/pagopa/yourname # TODO + tag: "0.0.0" + pullPolicy: Always + livenessProbe: + httpGet: + path: /actuator/health/liveness + port: 8080 + initialDelaySeconds: 90 + failureThreshold: 6 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /actuator/health/readiness + port: 8080 + initialDelaySeconds: 90 + failureThreshold: 6 + periodSeconds: 10 + deployment: + create: true + service: + create: true + type: ClusterIP + ports: + - 8080 + ingress: + create: true + host: "your.host" # TODO: set the host + path: /your-path-here/(.*) # TODO: set your path + servicePort: 8080 + serviceAccount: + create: false + annotations: { } + name: "" + podAnnotations: { } + podSecurityContext: + seccompProfile: + type: RuntimeDefault + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + capabilities: + drop: + - all + resources: + requests: + memory: "512Mi" + cpu: "0.25" + limits: + memory: "512Mi" + cpu: "0.25" + autoscaling: + enable: true + minReplica: 1 + maxReplica: 10 + pollingInterval: 10 # seconds + cooldownPeriod: 50 # seconds + triggers: + - type: cpu + metadata: + # Required + type: Utilization # Allowed types are 'Utilization' or 'AverageValue' + value: "75" + envConfig: + # TODO: set your name + WEBSITE_SITE_NAME: 'yourProjectName' # required to show cloud role name in application insights + ENV: 'azure-dev' + APP_LOGGING_LEVEL: 'DEBUG' + DEFAULT_LOGGING_LEVEL: 'INFO' + CORS_CONFIGURATION: '{"origins": ["*"], "methods": ["*"]}' + + OTEL_SERVICE_NAME: # TODO + OTEL_RESOURCE_ATTRIBUTES: "deployment.environment=dev" + OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-collector.elastic-system.svc:4317" + OTEL_TRACES_EXPORTER: otlp + OTEL_METRICS_EXPORTER: otlp + OTEL_LOGS_EXPORTER: none + OTEL_TRACES_SAMPLER: "always_on" + envSecret: + # required + APPLICATIONINSIGHTS_CONNECTION_STRING: 'ai-d-connection-string' + OTEL_EXPORTER_OTLP_HEADERS: elastic-apm-secret-token + keyvault: + name: "pagopa-d-name-kv" #TODO + tenantId: "7788edaf-0346-4068-9d79-c868aed15b3d" + nodeSelector: { } + tolerations: [ ] + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: node_type + operator: In + values: + - user + canaryDelivery: + create: true + ingress: + create: true + canary: + type: header + headerName: X-Canary + headerValue: canary + weightPercent: 0 + service: + create: true + deployment: + create: true + image: + repository: ghcr.io/pagopa/yourname # TODO + tag: "0.0.0" + pullPolicy: Always + envConfig: { } + envSecret: { } + diff --git a/helm/values-prod.yaml b/helm/values-prod.yaml new file mode 100644 index 0000000..4c58544 --- /dev/null +++ b/helm/values-prod.yaml @@ -0,0 +1,120 @@ +microservice-chart: + namespace: "your-namespace" # TODO: set your AKS namespace + nameOverride: "" + fullnameOverride: "" + image: + repository: ghcr.io/pagopa/yourname # TODO + tag: "0.0.0" + pullPolicy: Always + livenessProbe: + httpGet: + path: /actuator/health/liveness + port: 8080 + initialDelaySeconds: 90 + failureThreshold: 6 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /actuator/health/readiness + port: 8080 + initialDelaySeconds: 90 + failureThreshold: 6 + periodSeconds: 10 + deployment: + create: true + service: + create: true + type: ClusterIP + ports: + - 8080 + ingress: + create: true + host: "your.host" # TODO: set the host + path: /your-path-here/(.*) # TODO: set your path + servicePort: 8080 + serviceAccount: + create: false + annotations: { } + name: "" + podAnnotations: { } + podSecurityContext: + seccompProfile: + type: RuntimeDefault + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + capabilities: + drop: + - all + resources: + requests: + memory: "512Mi" + cpu: "0.25" + limits: + memory: "512Mi" + cpu: "0.25" + autoscaling: + enable: true + minReplica: 3 + maxReplica: 10 + pollingInterval: 10 # seconds + cooldownPeriod: 50 # seconds + triggers: + - type: cpu + metadata: + # Required + type: Utilization # Allowed types are 'Utilization' or 'AverageValue' + value: "75" + envConfig: + # TODO: set your name + WEBSITE_SITE_NAME: 'yourProjectName' # required to show cloud role name in application insights + ENV: 'azure-prod' + APP_LOGGING_LEVEL: 'INFO' + DEFAULT_LOGGING_LEVEL: 'INFO' + CORS_CONFIGURATION: '{"origins": ["*"], "methods": ["*"]}' + + OTEL_SERVICE_NAME: # TODO + OTEL_RESOURCE_ATTRIBUTES: "deployment.environment=prod" + OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-collector.elastic-system.svc:4317" + OTEL_TRACES_EXPORTER: otlp + OTEL_METRICS_EXPORTER: otlp + OTEL_LOGS_EXPORTER: none + OTEL_TRACES_SAMPLER: "always_on" + envSecret: + # required + APPLICATIONINSIGHTS_CONNECTION_STRING: 'ai-p-connection-string' + OTEL_EXPORTER_OTLP_HEADERS: elastic-apm-secret-token + keyvault: + name: "pagopa-p-name-kv" #TODO + tenantId: "7788edaf-0346-4068-9d79-c868aed15b3d" + nodeSelector: { } + tolerations: [ ] + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: node_type + operator: In + values: + - user + canaryDelivery: + create: true + ingress: + create: true + canary: + type: header + headerName: X-Canary + headerValue: canary + weightPercent: 0 + service: + create: true + deployment: + create: true + image: + repository: ghcr.io/pagopa/yourname # TODO + tag: "0.0.0" + pullPolicy: Always + envConfig: { } + envSecret: { } + diff --git a/helm/values-uat.yaml b/helm/values-uat.yaml new file mode 100644 index 0000000..38a99cc --- /dev/null +++ b/helm/values-uat.yaml @@ -0,0 +1,119 @@ +microservice-chart: + namespace: "your-namespace" # TODO: set your AKS namespace + nameOverride: "" + fullnameOverride: "" + image: + repository: ghcr.io/pagopa/yourname # TODO + tag: "0.0.0" + pullPolicy: Always + livenessProbe: + httpGet: + path: /actuator/health/liveness + port: 8080 + initialDelaySeconds: 90 + failureThreshold: 6 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /actuator/health/readiness + port: 8080 + initialDelaySeconds: 90 + failureThreshold: 6 + periodSeconds: 10 + deployment: + create: true + service: + create: true + type: ClusterIP + ports: + - 8080 + ingress: + create: true + host: "your.host" # TODO: set the host + path: /your-path-here/(.*) # TODO: set your path + servicePort: 8080 + serviceAccount: + create: false + annotations: { } + name: "" + podAnnotations: { } + podSecurityContext: + seccompProfile: + type: RuntimeDefault + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + capabilities: + drop: + - all + resources: + requests: + memory: "512Mi" + cpu: "0.25" + limits: + memory: "512Mi" + cpu: "0.25" + autoscaling: + enable: true + minReplica: 3 + maxReplica: 10 + pollingInterval: 10 # seconds + cooldownPeriod: 50 # seconds + triggers: + - type: cpu + metadata: + # Required + type: Utilization # Allowed types are 'Utilization' or 'AverageValue' + value: "75" + envConfig: + # TODO: set your name + WEBSITE_SITE_NAME: 'yourProjectName' # required to show cloud role name in application insights + ENV: 'azure-uat' + APP_LOGGING_LEVEL: 'DEBUG' + DEFAULT_LOGGING_LEVEL: 'INFO' + CORS_CONFIGURATION: '{"origins": ["*"], "methods": ["*"]}' + OTEL_SERVICE_NAME: # TODO + OTEL_RESOURCE_ATTRIBUTES: "deployment.environment=uat" + OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-collector.elastic-system.svc:4317" + OTEL_TRACES_EXPORTER: otlp + OTEL_METRICS_EXPORTER: otlp + OTEL_LOGS_EXPORTER: none + OTEL_TRACES_SAMPLER: "always_on" + envSecret: + # required + APPLICATIONINSIGHTS_CONNECTION_STRING: 'ai-u-connection-string' + OTEL_EXPORTER_OTLP_HEADERS: elastic-apm-secret-token + keyvault: + name: "pagopa-u-name-kv" #TODO + tenantId: "7788edaf-0346-4068-9d79-c868aed15b3d" + nodeSelector: { } + tolerations: [ ] + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: node_type + operator: In + values: + - user + canaryDelivery: + create: true + ingress: + create: true + canary: + type: header + headerName: X-Canary + headerValue: canary + weightPercent: 0 + service: + create: true + deployment: + create: true + image: + repository: ghcr.io/pagopa/yourname # TODO + tag: "0.0.0" + pullPolicy: Always + envConfig: { } + envSecret: { } + diff --git a/infra/04_apim_api.tf b/infra/04_apim_api.tf new file mode 100644 index 0000000..b24ae4d --- /dev/null +++ b/infra/04_apim_api.tf @@ -0,0 +1,56 @@ +locals { + repo_name = "TODO" # TODO add the name of the repository + + display_name = "TODO" # TODO + description = "TODO" # TODO + path = "TODO" # TODO add your base path + + host = "api.${var.apim_dns_zone_prefix}.${var.external_domain}" + hostname = var.hostname +} + +resource "azurerm_api_management_group" "api_group" { + name = local.apim.product_id + resource_group_name = local.apim.rg + api_management_name = local.apim.name + display_name = local.display_name + description = local.description +} + +resource "azurerm_api_management_api_version_set" "api_version_set" { + name = format("%s-${local.repo_name}", var.env_short) + resource_group_name = local.apim.rg + api_management_name = local.apim.name + display_name = local.display_name + versioning_scheme = "Segment" +} + +module "api_v1" { + source = "git::https://github.com/pagopa/terraform-azurerm-v3.git//api_management_api?ref=v6.7.0" + + name = format("%s-${local.repo_name}", var.env_short) + api_management_name = local.apim.name + resource_group_name = local.apim.rg + product_ids = [local.apim.product_id] + subscription_required = true + + version_set_id = azurerm_api_management_api_version_set.api_version_set.id + api_version = "v1" + + description = local.description + display_name = local.display_name + path = local.path + protocols = ["https"] + + service_url = null + + content_format = "openapi" + content_value = templatefile("../openapi/openapi.json", { + host = local.host + }) + + xml_content = templatefile("./policy/_base_policy.xml", { + hostname = var.hostname + }) +} + diff --git a/infra/99_locals.tf b/infra/99_locals.tf new file mode 100644 index 0000000..5ed42d0 --- /dev/null +++ b/infra/99_locals.tf @@ -0,0 +1,10 @@ +locals { + product = "${var.prefix}-${var.env_short}" + + apim = { + name = "${local.product}-apim" + rg = "${local.product}-api-rg" + product_id = "TODO" # TODO product id to import from pagopa-infra + } +} + diff --git a/infra/99_main.tf b/infra/99_main.tf new file mode 100644 index 0000000..c62f391 --- /dev/null +++ b/infra/99_main.tf @@ -0,0 +1,28 @@ +terraform { + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = ">= 3.30.0" + } + azuread = { + source = "hashicorp/azuread" + version = "2.30.0" + } + azapi = { + source = "Azure/azapi" + version = "= 1.3.0" + } + } + + backend "azurerm" {} +} + +provider "azurerm" { + features {} +} + +provider "azapi" {} + +data "azurerm_subscription" "current" {} + +data "azurerm_client_config" "current" {} diff --git a/infra/99_variables.tf b/infra/99_variables.tf new file mode 100644 index 0000000..4cfc3e4 --- /dev/null +++ b/infra/99_variables.tf @@ -0,0 +1,50 @@ +# general + +variable "prefix" { + type = string + validation { + condition = ( + length(var.prefix) <= 6 + ) + error_message = "Max length is 6 chars." + } +} + +variable "env" { + type = string +} + +variable "env_short" { + type = string + validation { + condition = ( + length(var.env_short) == 1 + ) + error_message = "Length must be 1 chars." + } +} + +variable "tags" { + type = map(any) + default = { + CreatedBy = "Terraform" + } +} + +variable "apim_dns_zone_prefix" { + type = string + default = null + description = "The dns subdomain for apim." +} + +variable "external_domain" { + type = string + default = null + description = "Domain for delegation" +} + +variable "hostname" { + type = string + default = null + description = "Hostname for the API" +} diff --git a/infra/env/weu-dev/backend.ini b/infra/env/weu-dev/backend.ini new file mode 100644 index 0000000..22e6a99 --- /dev/null +++ b/infra/env/weu-dev/backend.ini @@ -0,0 +1 @@ +subscription=DEV-pagoPA diff --git a/infra/env/weu-dev/backend.tfvars b/infra/env/weu-dev/backend.tfvars new file mode 100644 index 0000000..619395b --- /dev/null +++ b/infra/env/weu-dev/backend.tfvars @@ -0,0 +1,4 @@ +resource_group_name = "io-infra-rg" +storage_account_name = "pagopainfraterraformdev" +container_name = "azurermstate" +key = ".infra.tfstate" # TODO diff --git a/infra/env/weu-dev/terraform.tfvars b/infra/env/weu-dev/terraform.tfvars new file mode 100644 index 0000000..63a0705 --- /dev/null +++ b/infra/env/weu-dev/terraform.tfvars @@ -0,0 +1,15 @@ +prefix = "pagopa" +env = "dev" +env_short = "d" + +tags = { + CreatedBy = "Terraform" + Environment = "Dev" + Owner = "pagoPA" + Source = "https://github.com/pagopa/your-repository" # TODO + CostCenter = "TS310 - PAGAMENTI & SERVIZI" +} + +apim_dns_zone_prefix = "dev.platform" +external_domain = "pagopa.it" +hostname = "weudev..internal.dev.platform.pagopa.it" # TODO diff --git a/infra/env/weu-prod/backend.ini b/infra/env/weu-prod/backend.ini new file mode 100644 index 0000000..6318425 --- /dev/null +++ b/infra/env/weu-prod/backend.ini @@ -0,0 +1 @@ +subscription=PROD-pagoPA diff --git a/infra/env/weu-prod/backend.tfvars b/infra/env/weu-prod/backend.tfvars new file mode 100644 index 0000000..dac1727 --- /dev/null +++ b/infra/env/weu-prod/backend.tfvars @@ -0,0 +1,4 @@ +resource_group_name = "io-infra-rg" +storage_account_name = "pagopainfraterraformprod" +container_name = "azurermstate" +key = ".infra.tfstate" # TODO diff --git a/infra/env/weu-prod/terraform.tfvars b/infra/env/weu-prod/terraform.tfvars new file mode 100644 index 0000000..77f85af --- /dev/null +++ b/infra/env/weu-prod/terraform.tfvars @@ -0,0 +1,15 @@ +prefix = "pagopa" +env = "prod" +env_short = "p" + +tags = { + CreatedBy = "Terraform" + Environment = "Prod" + Owner = "pagoPA" + Source = "https://github.com/pagopa/your-repository" # TODO + CostCenter = "TS310 - PAGAMENTI & SERVIZI" +} + +apim_dns_zone_prefix = "platform" +external_domain = "pagopa.it" +hostname = "weuprod..internal.platform.pagopa.it" # TODO diff --git a/infra/env/weu-uat/backend.ini b/infra/env/weu-uat/backend.ini new file mode 100644 index 0000000..1a01415 --- /dev/null +++ b/infra/env/weu-uat/backend.ini @@ -0,0 +1 @@ +subscription=UAT-pagoPA diff --git a/infra/env/weu-uat/backend.tfvars b/infra/env/weu-uat/backend.tfvars new file mode 100644 index 0000000..6f406b1 --- /dev/null +++ b/infra/env/weu-uat/backend.tfvars @@ -0,0 +1,4 @@ +resource_group_name = "io-infra-rg" +storage_account_name = "pagopainfraterraformuat" +container_name = "azurermstate" +key = ".infra.tfstate" # TODO diff --git a/infra/env/weu-uat/terraform.tfvars b/infra/env/weu-uat/terraform.tfvars new file mode 100644 index 0000000..e8160f1 --- /dev/null +++ b/infra/env/weu-uat/terraform.tfvars @@ -0,0 +1,15 @@ +prefix = "pagopa" +env = "uat" +env_short = "u" + +tags = { + CreatedBy = "Terraform" + Environment = "Uat" + Owner = "pagoPA" + Source = "https://github.com/pagopa/your-repository" # TODO + CostCenter = "TS310 - PAGAMENTI & SERVIZI" +} + +apim_dns_zone_prefix = "uat.platform" +external_domain = "pagopa.it" +hostname = "weuuat..internal.uat.platform.pagopa.it" # TODO diff --git a/infra/policy/_base_policy.xml b/infra/policy/_base_policy.xml new file mode 100644 index 0000000..e3b583c --- /dev/null +++ b/infra/policy/_base_policy.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/infra/terraform.sh b/infra/terraform.sh new file mode 100755 index 0000000..b4691c8 --- /dev/null +++ b/infra/terraform.sh @@ -0,0 +1,191 @@ +#!/bin/bash +############################################################ +# Terraform script for managing infrastructure on Azure +# Fingerprint: d2hhdHlvdXdhbnQ/Cg== +############################################################ +# Global variables +# Version format x.y accepted +vers="1.1" +script_name=$(basename "$0") +git_repo="https://raw.githubusercontent.com/pagopa/eng-common-scripts/main/azure/${script_name}" +tmp_file="${script_name}.new" + +# Define functions +function clean_environment() { + rm -rf .terraform* + rm tfplan 2>/dev/null + echo "cleaned!" +} + +function help_usage() { + echo "terraform.sh Version ${vers}" + echo + echo "Usage: ./script.sh [ACTION] [ENV] [OTHER OPTIONS]" + echo "es. ACTION: init, apply, plan, etc." + echo "es. ENV: dev, uat, prod, etc." + echo + echo "Available actions:" + echo " clean Remove .terraform* folders and tfplan files" + echo " help This help" + echo " list List every environment available" + echo " summ Generate summary of Terraform plan" + echo " * any terraform option" +} + +function init_terraform() { + if [ -n "$env" ]; then + terraform init -reconfigure -backend-config="./env/$env/backend.tfvars" + else + echo "ERROR: no env configured!" + exit 1 + fi +} + +function list_env() { + # Check if env directory exists + if [ ! -d "./env" ]; then + echo "No environment directory found" + exit 1 + fi + + # List subdirectories under env directory + env_list=$(ls -d ./env/*/ 2>/dev/null) + + # Check if there are any subdirectories + if [ -z "$env_list" ]; then + echo "No environments found" + exit 1 + fi + + # Print the list of environments + echo "Available environments:" + for env in $env_list; do + env_name=$(echo "$env" | sed 's#./env/##;s#/##') + echo "- $env_name" + done +} + +function other_actions() { + if [ -n "$env" ] && [ -n "$action" ]; then + terraform "$action" -var-file="./env/$env/terraform.tfvars" -compact-warnings $other + else + echo "ERROR: no env or action configured!" + exit 1 + fi +} + +function state_output_taint_actions() { + terraform $action $other +} + +function tfsummary() { + action="plan" + other="-out=tfplan" + other_actions + if [ -n "$(command -v tf-summarize)" ]; then + tf-summarize -separate-tree tfplan && rm tfplan 2>/dev/null + else + echo "tf-summarize is not installed" + fi +} + +function update_script() { + # Check if the repository was cloned successfully + if ! curl -sL "$git_repo" -o "$tmp_file"; then + echo "Error cloning the repository" + rm "$tmp_file" 2>/dev/null + return 1 + fi + + # Check if a newer version exists + remote_vers=$(sed -n '8s/vers="\(.*\)"/\1/p' "$tmp_file") + if [ "$(printf '%s\n' "$vers" "$remote_vers" | sort -V | tail -n 1)" == "$vers" ]; then + echo "The local script version is equal to or newer than the remote version." + rm "$tmp_file" 2>/dev/null + return 0 + fi + + # Check the fingerprint + local_fingerprint=$(sed -n '4p' "$0") + remote_fingerprint=$(sed -n '4p' "$tmp_file") + + if [ "$local_fingerprint" != "$remote_fingerprint" ]; then + echo "The local and remote file fingerprints do not match." + rm "$tmp_file" 2>/dev/null + return 0 + fi + + # Show the current and available versions to the user + echo "Current script version: $vers" + echo "Available script version: $remote_vers" + + # Ask the user if they want to update the script + read -rp "Do you want to update the script to version $remote_vers? (y/n): " answer + + if [ "$answer" == "y" ] || [ "$answer" == "Y" ]; then + # Replace the local script with the updated version + cp "$tmp_file" "$script_name" + chmod +x "$script_name" + rm "$tmp_file" 2>/dev/null + + echo "Script successfully updated to version $remote_vers" + else + echo "Update canceled by the user" + fi + + rm "$tmp_file" 2>/dev/null +} + +# Check arguments number +if [ "$#" -lt 1 ]; then + help_usage + exit 0 +fi + +# Parse arguments +action=$1 +env=$2 +shift 2 +other=$@ + +if [ -n "$env" ]; then + # shellcheck source=/dev/null + source "./env/$env/backend.ini" + if [ -z "$(command -v az)" ]; then + echo "az not found, cannot proceed" + exit 1 + fi + az account set -s "${subscription}" +fi + +# Call appropriate function based on action +case $action in + clean) + clean_environment + ;; + ?|help) + help_usage + ;; + init) + init_terraform + init_terraform "$other" + ;; + list) + list_env + ;; + output|state|taint) + init_terraform + state_output_taint_actions $other + ;; + summ) + init_terraform + tfsummary "$other" + ;; + update) + update_script + ;; + *) + # [ -z "$TF_INIT" ] && terraform init + other_actions "$other" + ;; +esac diff --git a/integration-test/README.md b/integration-test/README.md new file mode 100644 index 0000000..06d38b7 --- /dev/null +++ b/integration-test/README.md @@ -0,0 +1,23 @@ +# Integration Tests + +👀 Integration tests are in `integration-test/src/` folder. See there for more information. + +## How run on Docker 🐳 + +To run the integration tests on docker, you can run from this directory the script: + +``` shell +sh ./run_integration_test.sh +``` + +ℹ️ _Note_: for **PagoPa ACR** is **required** the login `az acr login -n ` + +If you use dev, uat or prod **you test the images on Azure ACR** + +--- +💻 If you want to test your local branch, + +``` shell +sh ./run_integration_test.sh local SUBSCRIPTION-KEY +``` + diff --git a/integration-test/run_integration_test.sh b/integration-test/run_integration_test.sh new file mode 100644 index 0000000..fae61f9 --- /dev/null +++ b/integration-test/run_integration_test.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# example: sh ./run_integration_test.sh + +set -e + +# run integration tests +cd ./src || exit +yarn install +yarn test:"$1" diff --git a/integration-test/src/README.md b/integration-test/src/README.md new file mode 100644 index 0000000..93d85f0 --- /dev/null +++ b/integration-test/src/README.md @@ -0,0 +1,51 @@ +# Integration Test with Cucumber + +## Technology Stack + +- [cucumber js](https://github.com/cucumber/cucumber-js) +- NodeJS v14.17.6 + +## How to start + +- install dependencies: `yarn install` +- run tests: `yarn test` + +if all right you should see something like that : + +```sh +15 scenarios (15 passed) +65 steps (65 passed) +0m09.409s (executing steps: 0m09.349s) +┌──────────────────────────────────────────────────────────────────────────┐ +│ View your Cucumber Report at: │ +│ https://reports.cucumber.io/reports/16ebc4c0-cab6-41f6-9355-f894f9a9601d │ +│ │ +│ This report will self-destruct in 24h. │ +│ Keep reports forever: https://reports.cucumber.io/profile │ +└──────────────────────────────────────────────────────────────────────────┘ +``` + +Click on reporter link to view details . + +### Debug + +To run a single _feature_ or single _Scenario_ typing + +Ex. single _features_ `organizations.feature` + +```sh +npx cucumber-js -r step_definitions features/.feature +``` + +Ex. single _Scenario_ into `.feature` ( add source line ) + +```sh +npx cucumber-js -r step_definitions features/.feature:46 +``` + +### Note + +Remember to start the Backend before start the tests. + +You can configure the host in `./config/.env.local` file. + diff --git a/integration-test/src/config/.env.dev b/integration-test/src/config/.env.dev new file mode 100644 index 0000000..54aaf2f --- /dev/null +++ b/integration-test/src/config/.env.dev @@ -0,0 +1 @@ +APP_HOST=https://host diff --git a/integration-test/src/config/.env.local b/integration-test/src/config/.env.local new file mode 100644 index 0000000..9d01f05 --- /dev/null +++ b/integration-test/src/config/.env.local @@ -0,0 +1 @@ +APP_HOST=http://localhost:8080 diff --git a/integration-test/src/config/.env.prod b/integration-test/src/config/.env.prod new file mode 100644 index 0000000..54aaf2f --- /dev/null +++ b/integration-test/src/config/.env.prod @@ -0,0 +1 @@ +APP_HOST=https://host diff --git a/integration-test/src/config/.env.uat b/integration-test/src/config/.env.uat new file mode 100644 index 0000000..54aaf2f --- /dev/null +++ b/integration-test/src/config/.env.uat @@ -0,0 +1 @@ +APP_HOST=https://host diff --git a/integration-test/src/features/example.feature b/integration-test/src/features/example.feature new file mode 100644 index 0000000..f5fd475 --- /dev/null +++ b/integration-test/src/features/example.feature @@ -0,0 +1,16 @@ +Feature: feat + + Background: + Given some + + Scenario: scen1 + Given initial json + """ + {} + """ + When the client send POST to /yourservice + Then check statusCode is 201 + And check response body is + """ + {} + """ diff --git a/integration-test/src/package.json b/integration-test/src/package.json new file mode 100644 index 0000000..391c883 --- /dev/null +++ b/integration-test/src/package.json @@ -0,0 +1,19 @@ +{ + "name": "your-project-name", // TODO: set your name + "license": "MIT", + "version": "1.0.0", + "scripts": { + "test:local": "dotenv -e ./config/.env.local yarn cucumber", + "test:dev": "dotenv -e ./config/.env.dev yarn cucumber", + "test:uat": "dotenv -e ./config/.env.uat yarn cucumber", + "test:prod": "dotenv -e ./config/.env.prod yarn cucumber", + "cucumber": "npx cucumber-js --publish -r step_definitions" + }, + "dependencies": { + "@cucumber/cucumber": "^8.4.0", + "axios": "^0.27.2", + "dotenv": "^16.0.2", + "dotenv-cli": "^3.2.0", + "npx": "^10.2.2" + } +} diff --git a/integration-test/src/step_definitions/support/common.js b/integration-test/src/step_definitions/support/common.js new file mode 100644 index 0000000..70de50f --- /dev/null +++ b/integration-test/src/step_definitions/support/common.js @@ -0,0 +1,64 @@ +const axios = require("axios"); + +axios.defaults.headers.common['Ocp-Apim-Subscription-Key'] = process.env.SUBKEY // for all requests +if (process.env.CANARY) { + axios.defaults.headers.common['X-Canary'] = 'canary' // for all requests +} + +function get(url) { + return axios.get(url) + .then(res => { + return res; + }) + .catch(error => { + return error.response; + }); +} + +function post(url, body) { + return axios.post(url, body) + .then(res => { + return res; + }) + .catch(error => { + return error.response; + }); +} + +function put(url, body) { + return axios.put(url, body) + .then(res => { + return res; + }) + .catch(error => { + return error.response; + }); +} + +function del(url) { + return axios.delete(url) + .then(res => { + return res; + }) + .catch(error => { + return error.response; + }); +} + +function call(method, url, body) { + if (method === 'GET') { + return get(url) + } + if (method === 'POST') { + return post(url, body) + } + if (method === 'PUT') { + return put(url, body) + } + if (method === 'DELETE') { + return del(url) + } + +} + +module.exports = {get, post, put, del, call} diff --git a/integration-test/src/step_definitions/support/steps.js b/integration-test/src/step_definitions/support/steps.js new file mode 100644 index 0000000..24fdbf2 --- /dev/null +++ b/integration-test/src/step_definitions/support/steps.js @@ -0,0 +1,31 @@ +const {Given, When, Then} = require('@cucumber/cucumber') +const assert = require("assert"); +const {call, post} = require("./common"); +const fs = require("fs"); + +let rawdata = fs.readFileSync('./config/properties.json'); +let properties = JSON.parse(rawdata); +const app_host = properties.app_host; + +let body; +let responseToCheck; + +Given(/^initial json$/, function (payload) { + body = JSON.parse(payload); +}); + +When(/^the client send (GET|POST|PUT|DELETE) to (.*)$/, + async function (method, url) { + responseToCheck = await call(method, afm_host + url, body) + }); + +Then(/^check statusCode is (\d+)$/, function (status) { + assert.strictEqual(responseToCheck.status, status); + +}); + +Then(/^check response body is$/, function (payload) { + console.log(responseToCheck.data) + + assert.deepStrictEqual(responseToCheck.data, JSON.parse(payload)); +}); diff --git a/openapi/generate_openapi.sh b/openapi/generate_openapi.sh new file mode 100755 index 0000000..723c256 --- /dev/null +++ b/openapi/generate_openapi.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +if [[ "$(pwd)" =~ .*"openapi".* ]]; then + cd .. +fi + +mvn test -Dtest=OpenApiGenerationTest + diff --git a/openapi/openapi.json b/openapi/openapi.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/openapi/openapi.json @@ -0,0 +1 @@ +{} diff --git a/performance-test/README.md b/performance-test/README.md new file mode 100644 index 0000000..2cb23a9 --- /dev/null +++ b/performance-test/README.md @@ -0,0 +1,13 @@ +# K6 tests + +This is a set of [k6](https://k6.io) tests. + +To invoke k6 tests use `run_performance_test.sh` script. + +## How to run 🚀 + +Use this command to launch the tests: + +``` shell +sh run_performance_test.sh +``` diff --git a/performance-test/docker-compose.yaml b/performance-test/docker-compose.yaml new file mode 100644 index 0000000..e9205ff --- /dev/null +++ b/performance-test/docker-compose.yaml @@ -0,0 +1,25 @@ +version: '3.3' +services: + k6: + image: grafana/k6 + container_name: k6 + volumes: + - '${PWD}/src:/scripts' + environment: + - API_SUBSCRIPTION_KEY=${sub_key} + - VARS=/scripts/${env}.environment.json + - TEST_TYPE=/scripts/test-types/${type}.json + - K6_OUT=influxdb=http://nginx:8086/${db_name} + command: run /scripts/${script}.js + depends_on: + - nginx + + nginx: + image: nginx + container_name: nginx + volumes: + - '${PWD}/nginx/nginx.conf:/etc/nginx/nginx.conf' + ports: + - "8086:8086" + - "80:80" + diff --git a/performance-test/nginx/nginx.conf b/performance-test/nginx/nginx.conf new file mode 100644 index 0000000..b78a4fe --- /dev/null +++ b/performance-test/nginx/nginx.conf @@ -0,0 +1,17 @@ +events { + worker_connections 1024; +} + +http { + server { + listen 8086; + location / { + proxy_pass https://api.dev.platform.pagopa.it/shared/influxdb/v1/; + proxy_http_version 1.1; + proxy_set_header Host api.dev.platform.pagopa.it; + proxy_pass_request_headers on; + client_max_body_size 0; + } + } +} + diff --git a/performance-test/run_performance_test.sh b/performance-test/run_performance_test.sh new file mode 100644 index 0000000..9454815 --- /dev/null +++ b/performance-test/run_performance_test.sh @@ -0,0 +1,44 @@ +# sh run_performance_test.sh + +ENVIRONMENT=$1 +TYPE=$2 +SCRIPT=$3 +DB_NAME=$4 +API_SUBSCRIPTION_KEY=$5 + +if [ -z "$ENVIRONMENT" ] +then + echo "No env specified: sh run_performance_test.sh " + exit 1 +fi + +if [ -z "$TYPE" ] +then + echo "No test type specified: sh run_performance_test.sh " + exit 1 +fi +if [ -z "$SCRIPT" ] +then + echo "No script name specified: sh run_performance_test.sh " + exit 1 +fi + +if [ -z "$DB_NAME" ] +then + DB_NAME="k6" + echo "No DB name specified: 'k6' is used." +fi + +export env=${ENVIRONMENT} +export type=${TYPE} +export script=${SCRIPT} +export db_name=${DB_NAME} +export sub_key=${API_SUBSCRIPTION_KEY} + +docker rm nginx +docker rm k6 + +stack_name=$(cd .. && basename "$PWD") +docker compose -p "${stack_name}-k6" up -d --remove-orphans --force-recreate --build +docker logs -f k6 +docker stop nginx diff --git a/performance-test/src/README.md b/performance-test/src/README.md new file mode 100644 index 0000000..a12dec7 --- /dev/null +++ b/performance-test/src/README.md @@ -0,0 +1,20 @@ +# Performance Tests with k6 + +[k6](https://k6.io/) is a load testing tool. 👀 +See [here](https://k6.io/docs/get-started/installation/) to install it. + +## How to Run 🚀 + +To run k6 tests use the command: + +``` shell +k6 run --env VARS=local.environment.json --env TEST_TYPE=./test-types/load.json --env API_SUBSCRIPTION_KEY= .js +``` + +where + +- _VARS_ is a environment file +- _TEST_TYPE_ is a file in `/test-types` folder +- _API_SUBSCRIPTION_KEY_ is your sub-key + +`.js` is the scenario to run with k6 diff --git a/performance-test/src/local.environment.json b/performance-test/src/local.environment.json new file mode 100644 index 0000000..eb3a701 --- /dev/null +++ b/performance-test/src/local.environment.json @@ -0,0 +1,9 @@ +{ + "environment": [ + { + "env": "local", + "host": "http://localhost:8080", + "basePath": "" + } + ] +} diff --git a/performance-test/src/main_scanario.js b/performance-test/src/main_scanario.js new file mode 100644 index 0000000..b2cba5f --- /dev/null +++ b/performance-test/src/main_scanario.js @@ -0,0 +1,74 @@ +import http from 'k6/http'; +import {check} from 'k6'; +import {SharedArray} from 'k6/data'; + +export let options = JSON.parse(open(__ENV.TEST_TYPE)); + +// read configuration +// note: SharedArray can currently only be constructed inside init code +// according to https://k6.io/docs/javascript-api/k6-data/sharedarray +const varsArray = new SharedArray('vars', function () { + return JSON.parse(open(`./${__ENV.VARS}`)).environment; +}); +// workaround to use shared array (only array should be used) +const vars = varsArray[0]; +const rootUrl = `${vars.host}/${vars.basePath}`; + +export function setup() { + // Before All + // setup code (once) + // The setup code runs, setting up the test environment (optional) and generating data + // used to reuse code for the same VU + const params = { + headers: { + 'Content-Type': 'application/json' + }, + }; + const response = example(rootUrl, params); + + // precondition is moved to default fn because in this stage + // __VU is always 0 and cannot be used to create env properly +} + +function precondition() { + // no pre conditions +} + +function postcondition() { + + // Delete the new entity created +} + +export default function () { + + // Create a new spontaneous payment. + let tag = { + gpsMethod: "tag", + }; + + let url = `${rootUrl}/${creditor_institution_code}/spontaneouspayments`; + + let payload = JSON.stringify( + {} + ); + + let params = { + headers: { + 'Content-Type': 'application/json' + }, + }; + + let r = http.post(url, payload, params); + + check(r, { + 'check status is 201': (_r) => r.status === 201, + }, tag); + + postcondition(); + +} + +export function teardown(data) { + // After All + // teardown code +} diff --git a/performance-test/src/modules/client.js b/performance-test/src/modules/client.js new file mode 100644 index 0000000..219d6b7 --- /dev/null +++ b/performance-test/src/modules/client.js @@ -0,0 +1,9 @@ +import http from 'k6/http'; + +export function example(rootUrl, params) { + const url = `${rootUrl}/` + + const payload = {} + + return http.post(url, JSON.stringify(payload), params); +} diff --git a/performance-test/src/test-types/constant.json b/performance-test/src/test-types/constant.json new file mode 100644 index 0000000..d1458c2 --- /dev/null +++ b/performance-test/src/test-types/constant.json @@ -0,0 +1,13 @@ +{ + "discardResponseBodies": true, + "scenarios": { + "contacts": { + "executor": "constant-arrival-rate", + "duration": "3m", + "rate": 25, + "timeUnit": "1s", + "preAllocatedVUs": 100, + "maxVUs": 1000 + } + } +} diff --git a/performance-test/src/test-types/load.json b/performance-test/src/test-types/load.json new file mode 100644 index 0000000..a178140 --- /dev/null +++ b/performance-test/src/test-types/load.json @@ -0,0 +1,35 @@ +{ + "summaryTrendStats": [ + "avg", + "min", + "med", + "max", + "p(95)", + "p(99)", + "p(99.99)", + "count" + ], + "stages": [ + { + "duration": "5m", + "target": 100 + }, + { + "duration": "10m", + "target": 100 + }, + { + "duration": "5m", + "target": 0 + } + ], + "thresholds": { + "http_req_failed": [ + "rate<0.001" + ], + "http_req_duration": [ + "p(99)<1500" + ] + } +} + diff --git a/performance-test/src/test-types/smoke.json b/performance-test/src/test-types/smoke.json new file mode 100644 index 0000000..3d7d376 --- /dev/null +++ b/performance-test/src/test-types/smoke.json @@ -0,0 +1,13 @@ +{ + "vus": 1, + "duration": "1m", + "thresholds": { + "http_req_failed": [ + "rate<0.001" + ], + "http_req_duration": [ + "p(99)<1500" + ] + } +} + diff --git a/performance-test/src/test-types/soak.json b/performance-test/src/test-types/soak.json new file mode 100644 index 0000000..d566c28 --- /dev/null +++ b/performance-test/src/test-types/soak.json @@ -0,0 +1,35 @@ +{ + "summaryTrendStats": [ + "avg", + "min", + "med", + "max", + "p(95)", + "p(99)", + "p(99.99)", + "count" + ], + "stages": [ + { + "duration": "2m", + "target": 400 + }, + { + "duration": "3h56m", + "target": 400 + }, + { + "duration": "2m", + "target": 0 + } + ], + "thresholds": { + "http_req_failed": [ + "rate<0.001" + ], + "http_req_duration": [ + "p(99)<1500" + ] + } +} + diff --git a/performance-test/src/test-types/spike.json b/performance-test/src/test-types/spike.json new file mode 100644 index 0000000..5eb414d --- /dev/null +++ b/performance-test/src/test-types/spike.json @@ -0,0 +1,50 @@ +{ + "summaryTrendStats": [ + "avg", + "min", + "med", + "max", + "p(95)", + "p(99)", + "p(99.99)", + "count" + ], + "stages": [ + { + "duration": "10s", + "target": 100 + }, + { + "duration": "1m", + "target": 100 + }, + { + "duration": "10s", + "target": 1400 + }, + { + "duration": "3m", + "target": 1400 + }, + { + "duration": "10s", + "target": 100 + }, + { + "duration": "3m", + "target": 100 + }, + { + "duration": "10s", + "target": 0 + } + ], + "thresholds": { + "http_req_failed": [ + "rate<0.001" + ], + "http_req_duration": [ + "p(99)<1500" + ] + } +} diff --git a/performance-test/src/test-types/stress.json b/performance-test/src/test-types/stress.json new file mode 100644 index 0000000..9559863 --- /dev/null +++ b/performance-test/src/test-types/stress.json @@ -0,0 +1,58 @@ +{ + "summaryTrendStats": [ + "avg", + "min", + "med", + "max", + "p(95)", + "p(99)", + "p(99.99)", + "count" + ], + "stages": [ + { + "duration": "2m", + "target": 100 + }, + { + "duration": "5m", + "target": 100 + }, + { + "duration": "2m", + "target": 200 + }, + { + "duration": "5m", + "target": 200 + }, + { + "duration": "2m", + "target": 300 + }, + { + "duration": "5m", + "target": 300 + }, + { + "duration": "2m", + "target": 400 + }, + { + "duration": "5m", + "target": 400 + }, + { + "duration": "10m", + "target": 0 + } + ], + "thresholds": { + "http_req_failed": [ + "rate<0.001" + ], + "http_req_duration": [ + "p(99)<1500" + ] + } +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..1a42a2b --- /dev/null +++ b/pom.xml @@ -0,0 +1,175 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.2.3 + + + it.gov.pagopa + microservice + 0.0.0 + Your Name + Your description + + + 17 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-validation + + + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-actuator + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + org.springframework.data + spring-data-jpa + + + + org.springframework.boot + spring-boot-starter-cache + + + com.github.ben-manes.caffeine + caffeine + + + + + org.springdoc + springdoc-openapi-ui + 1.6.11 + + + + com.h2database + h2 + runtime + + + + org.hibernate.orm + hibernate-core + 6.1.3.Final + + + + org.springframework.cloud + spring-cloud-starter-openfeign + 4.0.3 + + + + + org.modelmapper + modelmapper + 3.1.0 + + + org.projectlombok + lombok + true + + + + junit + junit + test + + + co.elastic.logging + logback-ecs-encoder + 1.5.0 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.jacoco + jacoco-maven-plugin + 0.8.7 + + + + + + + + prepare-agent + + + + report + test + + report + + + + + + org.sonarsource.scanner.maven + sonar-maven-plugin + 3.3.0.603 + + + verify + + sonar + + + + + + + + src/test/resources + true + + + + + diff --git a/postman-collection/README.md b/postman-collection/README.md new file mode 100644 index 0000000..eb3a499 --- /dev/null +++ b/postman-collection/README.md @@ -0,0 +1,15 @@ +# Postman Collection + +In this directory you find the postman collection and the environments to use. + +## Run with newman 🚀 + +You can run the collection +with [newman](https://learning.postman.com/docs/running-collections/using-newman-cli/installing-running-newman/) + +`sh run_newman.sh` + +## Import in Postman + +To import the collection and the environment follow +this [guide](https://learning.postman.com/docs/getting-started/importing-and-exporting-data/) diff --git a/postman-collection/run_newman.sh b/postman-collection/run_newman.sh new file mode 100644 index 0000000..2e28dbb --- /dev/null +++ b/postman-collection/run_newman.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +# install newman if not present +if [ $(npm list -g | grep -c newman) -eq 0 ]; then + npm install -g newman +fi + +# run the collection +# TODO: add your files and edit the following command +newman run yourfile.postman_collection.json \ + --environment=local.postman_environment.json \ + --reporters cli,junit \ + --reporter-junit-export Results/results-TEST.xml diff --git a/sops.sh b/sops.sh new file mode 100755 index 0000000..dbf1ec7 --- /dev/null +++ b/sops.sh @@ -0,0 +1,72 @@ +#!/bin/bash + +# Function to display usage information +display_usage() { + echo "Usage: $0 " + echo " : 'enc' or 'dec'" + echo " : 'dev', 'uat', or 'prod'" + echo " : path to the file" + echo "Example: $0 enc dev /path/to/file" +} + +# Function to validate action +validate_action() { + if [ "$1" != "enc" ] && [ "$1" != "dec" ]; then + echo "Error: Action must be 'enc' or 'dec'" + exit 1 + fi +} + +# Function to validate environment +validate_environment() { + if [ "$1" != "dev" ] && [ "$1" != "uat" ] && [ "$1" != "prod" ]; then + echo "Error: Environment must be 'dev', 'uat', or 'prod'" + exit 1 + fi +} + +# Function to validate filepath +validate_filepath() { + if [ ! -f "$1" ]; then + echo "Error: File '$1' not found" + exit 1 + fi +} + +# Main script starts here +main() { + # Validate number of arguments + if [ "$#" -ne 3 ]; then + echo "Error: Incorrect number of arguments" + display_usage + exit 1 + fi + + # Assign arguments to variables + action="$1" + environment="$2" + filepath="$3" + + # Validate action, environment, and filepath + validate_action "$action" + validate_environment "$environment" + validate_filepath "$filepath" + + env_short=$(echo "$environment" | cut -c1) + + # TODO set your kv + azure_kv_url=$(az keyvault key show --name pagopa-"$env_short"-TODO-sops-key --vault-name pagopa-"$env_short"-TODO-kv --query key.kid | sed 's/"//g') + + if [ "$action" == "enc" ]; then + sops --encrypt --azure-kv "$azure_kv_url" --input-type dotenv --output-type dotenv ./"$filepath" > ./"$filepath".encrypted + fi; + + if [ "$action" == "dec" ]; then + sops --decrypt --azure-kv "$azure_kv_url" --input-type dotenv --output-type dotenv ./"$filepath" > "$(echo "./$filepath" | sed 's/\.encrypted$//')" + fi; + + echo 'done' +} + +# Call the main function +main "$@" diff --git a/src/main/java/it/gov/pagopa/microservice/Application.java b/src/main/java/it/gov/pagopa/microservice/Application.java new file mode 100644 index 0000000..465b6cb --- /dev/null +++ b/src/main/java/it/gov/pagopa/microservice/Application.java @@ -0,0 +1,13 @@ +package it.gov.pagopa.microservice; // TODO: refactor the package + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + +} diff --git a/src/main/java/it/gov/pagopa/microservice/config/LoggingAspect.java b/src/main/java/it/gov/pagopa/microservice/config/LoggingAspect.java new file mode 100644 index 0000000..8990632 --- /dev/null +++ b/src/main/java/it/gov/pagopa/microservice/config/LoggingAspect.java @@ -0,0 +1,162 @@ +package it.gov.pagopa.microservice.config; + +import it.gov.pagopa.microservice.exception.AppError; +import it.gov.pagopa.microservice.model.ProblemJson; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.CodeSignature; +import org.slf4j.MDC; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import static it.gov.pagopa.microservice.util.CommonUtility.deNull; + + +@Aspect +@Component +@Slf4j +public class LoggingAspect { + + public static final String START_TIME = "startTime"; + public static final String METHOD = "method"; + public static final String STATUS = "status"; + public static final String CODE = "httpCode"; + public static final String RESPONSE_TIME = "responseTime"; + public static final String FAULT_CODE = "faultCode"; + public static final String FAULT_DETAIL = "faultDetail"; + public static final String REQUEST_ID = "requestId"; + public static final String OPERATION_ID = "operationId"; + public static final String ARGS = "args"; + + @Autowired + HttpServletRequest httRequest; + + @Autowired + HttpServletResponse httpResponse; + + @Value("${info.application.name}") + private String name; + + @Value("${info.application.version}") + private String version; + + @Value("${info.properties.environment}") + private String environment; + + + @Pointcut("@within(org.springframework.web.bind.annotation.RestController)") + public void restController() { + // all rest controllers + } + + @Pointcut("@within(org.springframework.stereotype.Repository)") + public void repository() { + // all repository methods + } + + @Pointcut("@within(org.springframework.stereotype.Service)") + public void service() { + // all service methods + } + + /** + * Log essential info of application during the startup. + */ + @PostConstruct + public void logStartup() { + log.info("-> Starting {} version {} - environment {}", name, version, environment); + } + + @Around(value = "restController()") + public Object logApiInvocation(ProceedingJoinPoint joinPoint) throws Throwable { + MDC.put(METHOD, joinPoint.getSignature().getName()); + MDC.put(START_TIME, String.valueOf(System.currentTimeMillis())); + MDC.put(OPERATION_ID, UUID.randomUUID().toString()); + if(MDC.get(REQUEST_ID) == null) { + var requestId = UUID.randomUUID().toString(); + MDC.put(REQUEST_ID, requestId); + } + Map params = getParams(joinPoint); + MDC.put(ARGS, params.toString()); + + log.info("Invoking API operation {} - args: {}", joinPoint.getSignature().getName(), params); + + Object result = joinPoint.proceed(); + + MDC.put(STATUS, "OK"); + MDC.put(CODE, String.valueOf(httpResponse.getStatus())); + MDC.put(RESPONSE_TIME, getExecutionTime()); + log.info("Successful API operation {} - result: {}", joinPoint.getSignature().getName(), result); + MDC.remove(STATUS); + MDC.remove(CODE); + MDC.remove(RESPONSE_TIME); + MDC.remove(START_TIME); + return result; + } + + @AfterReturning(value = "execution(* *..exception.ErrorHandler.*(..))", returning = "result") + public void trowingApiInvocation(JoinPoint joinPoint, ResponseEntity result) { + MDC.put(STATUS, "KO"); + MDC.put(CODE, String.valueOf(result.getStatusCodeValue())); + MDC.put(RESPONSE_TIME, getExecutionTime()); + MDC.put(FAULT_CODE, getTitle(result)); + MDC.put(FAULT_DETAIL, getDetail(result)); + log.info("Failed API operation {} - error: {}", MDC.get(METHOD), result); + MDC.clear(); + } + + @Around(value = "repository() || service()") + public Object logTrace(ProceedingJoinPoint joinPoint) throws Throwable { + Map params = getParams(joinPoint); + log.debug("Call method {} - args: {}", joinPoint.getSignature().toShortString(), params); + Object result = joinPoint.proceed(); + log.debug("Return method {} - result: {}", joinPoint.getSignature().toShortString(), result); + return result; + } + + private static String getDetail(ResponseEntity result) { + if(result != null && result.getBody() != null && result.getBody().getDetail() != null) { + return result.getBody().getDetail(); + } else return AppError.UNKNOWN.getDetails(); + } + + private static String getTitle(ResponseEntity result) { + if(result != null && result.getBody() != null && result.getBody().getTitle() != null) { + return result.getBody().getTitle(); + } else return AppError.UNKNOWN.getTitle(); + } + + public static String getExecutionTime() { + String startTime = MDC.get(START_TIME); + if(startTime != null) { + long endTime = System.currentTimeMillis(); + long executionTime = endTime - Long.parseLong(startTime); + return String.valueOf(executionTime); + } + return "-"; + } + + private static Map getParams(ProceedingJoinPoint joinPoint) { + CodeSignature codeSignature = (CodeSignature) joinPoint.getSignature(); + Map params = new HashMap<>(); + int i = 0; + for (var paramName : codeSignature.getParameterNames()) { + params.put(paramName, deNull(joinPoint.getArgs()[i++])); + } + return params; + } +} diff --git a/src/main/java/it/gov/pagopa/microservice/config/MappingsConfiguration.java b/src/main/java/it/gov/pagopa/microservice/config/MappingsConfiguration.java new file mode 100644 index 0000000..b403822 --- /dev/null +++ b/src/main/java/it/gov/pagopa/microservice/config/MappingsConfiguration.java @@ -0,0 +1,19 @@ +package it.gov.pagopa.microservice.config; + + +import org.modelmapper.ModelMapper; +import org.modelmapper.convention.MatchingStrategies; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class MappingsConfiguration { + + @Bean + ModelMapper modelMapper() { + ModelMapper mapper = new ModelMapper(); + mapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT); + return mapper; + } + +} diff --git a/src/main/java/it/gov/pagopa/microservice/config/OpenApiConfig.java b/src/main/java/it/gov/pagopa/microservice/config/OpenApiConfig.java new file mode 100644 index 0000000..6b7ec18 --- /dev/null +++ b/src/main/java/it/gov/pagopa/microservice/config/OpenApiConfig.java @@ -0,0 +1,129 @@ +package it.gov.pagopa.microservice.config; + +import static it.gov.pagopa.microservice.util.Constants.HEADER_REQUEST_ID; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Paths; +import io.swagger.v3.oas.models.headers.Header; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.media.StringSchema; +import io.swagger.v3.oas.models.parameters.Parameter; +import io.swagger.v3.oas.models.responses.ApiResponses; +import io.swagger.v3.oas.models.security.SecurityScheme; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import org.springdoc.core.customizers.OpenApiCustomiser; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class OpenApiConfig { + + @Bean + public OpenAPI customOpenAPI( + @Value("${info.application.artifactId}") String appName, + @Value("${info.application.description}") String appDescription, + @Value("${info.application.version}") String appVersion) { + return new OpenAPI() + .components( + new Components() + .addSecuritySchemes( + "ApiKey", + new SecurityScheme() + .type(SecurityScheme.Type.APIKEY) + .description("The API key to access this function app.") + .name("Ocp-Apim-Subscription-Key") + .in(SecurityScheme.In.HEADER))) + .info( + new Info() + .title(appName) + .version(appVersion) + .description(appDescription) + .termsOfService("https://www.pagopa.gov.it/")); + } + + @Bean + public OpenApiCustomiser sortOperationsAlphabetically() { + return openApi -> { + Paths paths = + openApi + .getPaths() + .entrySet() + .stream() + .sorted(Map.Entry.comparingByKey()) + .collect( + Paths::new, + (map, item) -> map.addPathItem(item.getKey(), item.getValue()), + Paths::putAll); + + paths.forEach( + (key, value) -> + value + .readOperations() + .forEach( + operation -> { + var responses = + operation + .getResponses() + .entrySet() + .stream() + .sorted(Map.Entry.comparingByKey()) + .collect( + ApiResponses::new, + (map, item) -> + map.addApiResponse(item.getKey(), item.getValue()), + ApiResponses::putAll); + operation.setResponses(responses); + })); + openApi.setPaths(paths); + }; + } + + @Bean + public OpenApiCustomiser addCommonHeaders() { + return openApi -> + openApi + .getPaths() + .forEach( + (key, value) -> { + + // add Request-ID as request header + var header = + Optional.ofNullable(value.getParameters()) + .orElse(Collections.emptyList()) + .parallelStream() + .filter(Objects::nonNull) + .anyMatch(elem -> HEADER_REQUEST_ID.equals(elem.getName())); + if (!header) { + value.addParametersItem( + new Parameter() + .in("header") + .name(HEADER_REQUEST_ID) + .schema(new StringSchema()) + .description( + "This header identifies the call, if not passed it is self-generated. This ID is returned in the response.")); + } + + // add Request-ID as response header + value + .readOperations() + .forEach( + operation -> + operation + .getResponses() + .values() + .forEach( + response -> + response.addHeaderObject( + HEADER_REQUEST_ID, + new Header() + .schema(new StringSchema()) + .description( + "This header identifies the call")))); + }); + } +} diff --git a/src/main/java/it/gov/pagopa/microservice/config/RequestFilter.java b/src/main/java/it/gov/pagopa/microservice/config/RequestFilter.java new file mode 100644 index 0000000..63726e3 --- /dev/null +++ b/src/main/java/it/gov/pagopa/microservice/config/RequestFilter.java @@ -0,0 +1,58 @@ +package it.gov.pagopa.microservice.config; + +import static it.gov.pagopa.microservice.util.Constants.HEADER_REQUEST_ID; + +import java.io.IOException; +import java.util.UUID; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.MDC; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +@Component +@Order(Ordered.HIGHEST_PRECEDENCE) +@Slf4j +public class RequestFilter implements Filter { + + /** + * Get the request ID from the custom header "X-Request-Id" if present, otherwise it generates + * one. Set the X-Request-Id value in the {@code response} and in the MDC + * + * @param request http request + * @param response http response + * @param chain next filter + * @throws IOException if an I/O error occurs during this filter's processing of the request + * @throws ServletException if the processing fails for any other reason + */ + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + try { + HttpServletRequest httRequest = (HttpServletRequest) request; + + // get requestId from header or generate one + String requestId = httRequest.getHeader(HEADER_REQUEST_ID); + if (requestId == null || requestId.isEmpty()) { + requestId = UUID.randomUUID().toString(); + } + + // set requestId in MDC + MDC.put("requestId", requestId); + + // set requestId in the response header + ((HttpServletResponse) response).setHeader(HEADER_REQUEST_ID, requestId); + chain.doFilter(request, response); + } finally { + MDC.clear(); + } + } + +} diff --git a/src/main/java/it/gov/pagopa/microservice/config/ResponseValidator.java b/src/main/java/it/gov/pagopa/microservice/config/ResponseValidator.java new file mode 100644 index 0000000..67cc3af --- /dev/null +++ b/src/main/java/it/gov/pagopa/microservice/config/ResponseValidator.java @@ -0,0 +1,52 @@ +package it.gov.pagopa.microservice.config; + +import it.gov.pagopa.microservice.exception.AppException; +import java.util.Set; +import javax.validation.ConstraintViolation; +import javax.validation.Validator; +import org.apache.commons.lang3.StringUtils; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; + +@Aspect +@Component +public class ResponseValidator { + + @Autowired + private Validator validator; + + + /** + * This method validates the response annotated with the {@link javax.validation.constraints} + * + * @param joinPoint not used + * @param result the response to validate + */ + // TODO: set your package + @AfterReturning(pointcut = "execution(* it.gov.pagopa.microservice.controller.*.*(..))", returning = "result") + public void validateResponse(JoinPoint joinPoint, Object result) { + if (result instanceof ResponseEntity) { + validateResponse((ResponseEntity) result); + } + } + + private void validateResponse(ResponseEntity response) { + if (response.getBody() != null) { + Set> validationResults = validator.validate(response.getBody()); + + if (!validationResults.isEmpty()) { + var sb = new StringBuilder(); + for (ConstraintViolation error : validationResults) { + sb.append(error.getPropertyPath()).append(" ").append(error.getMessage()).append(". "); + } + var msg = StringUtils.chop(sb.toString()); + throw new AppException(HttpStatus.INTERNAL_SERVER_ERROR, "Invalid response", msg); + } + } + } +} diff --git a/src/main/java/it/gov/pagopa/microservice/config/WebMvcConfiguration.java b/src/main/java/it/gov/pagopa/microservice/config/WebMvcConfiguration.java new file mode 100644 index 0000000..c61db33 --- /dev/null +++ b/src/main/java/it/gov/pagopa/microservice/config/WebMvcConfiguration.java @@ -0,0 +1,30 @@ +package it.gov.pagopa.microservice.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import it.gov.pagopa.microservice.model.AppCorsConfiguration; +import lombok.SneakyThrows; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + + +@Configuration +public class WebMvcConfiguration implements WebMvcConfigurer { + + @Value("${cors.configuration}") + private String corsConfiguration; + + + @SneakyThrows + @Override + public void addCorsMappings(CorsRegistry registry) { + AppCorsConfiguration appCorsConfiguration = new ObjectMapper().readValue(corsConfiguration, + AppCorsConfiguration.class); + registry.addMapping("/**") + .allowedOrigins(appCorsConfiguration.getOrigins()) + .allowedMethods(appCorsConfiguration.getMethods()); + } +} + + diff --git a/src/main/java/it/gov/pagopa/microservice/controller/HomeController.java b/src/main/java/it/gov/pagopa/microservice/controller/HomeController.java new file mode 100644 index 0000000..70b6dd1 --- /dev/null +++ b/src/main/java/it/gov/pagopa/microservice/controller/HomeController.java @@ -0,0 +1,30 @@ +package it.gov.pagopa.microservice.controller; + +import io.swagger.v3.oas.annotations.Hidden; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.view.RedirectView; + +@RestController +@Validated +public class HomeController { + + @Value("${server.servlet.context-path}") + String basePath; + + + /** + * @return redirect to Swagger page documentation + */ + @Hidden + @GetMapping("") + public RedirectView home() { + if (!basePath.endsWith("/")) { + basePath += "/"; + } + return new RedirectView(basePath + "swagger-ui.html"); + } + +} diff --git a/src/main/java/it/gov/pagopa/microservice/exception/AppError.java b/src/main/java/it/gov/pagopa/microservice/exception/AppError.java new file mode 100644 index 0000000..d9ccb08 --- /dev/null +++ b/src/main/java/it/gov/pagopa/microservice/exception/AppError.java @@ -0,0 +1,30 @@ +package it.gov.pagopa.microservice.exception; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + + +@Getter +public enum AppError { + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Internal Server Error", "Something was wrong"), + BAD_REQUEST(HttpStatus.INTERNAL_SERVER_ERROR, "Bad Request", "%s"), + UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "Unauthorized", "Error during authentication"), + FORBIDDEN(HttpStatus.FORBIDDEN, "Forbidden", "This method is forbidden"), + RESPONSE_NOT_READABLE(HttpStatus.BAD_GATEWAY, "Response Not Readable", "The response body is not readable"), + + UNKNOWN(null, null, null); + + + public final HttpStatus httpStatus; + public final String title; + public final String details; + + + AppError(HttpStatus httpStatus, String title, String details) { + this.httpStatus = httpStatus; + this.title = title; + this.details = details; + } +} + + diff --git a/src/main/java/it/gov/pagopa/microservice/exception/AppException.java b/src/main/java/it/gov/pagopa/microservice/exception/AppException.java new file mode 100644 index 0000000..e16fd36 --- /dev/null +++ b/src/main/java/it/gov/pagopa/microservice/exception/AppException.java @@ -0,0 +1,87 @@ +package it.gov.pagopa.microservice.exception; + +import java.util.Formatter; +import javax.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.springframework.http.HttpStatus; +import org.springframework.validation.annotation.Validated; + +/** + * Custom exception. + *

See {@link ErrorHandler} + */ +@EqualsAndHashCode(callSuper = true) +@Value +@Validated +public class AppException extends RuntimeException { + + /** + * title returned to the response when this exception occurred + */ + String title; + + /** + * http status returned to the response when this exception occurred + */ + HttpStatus httpStatus; + + /** + * @param httpStatus HTTP status returned to the response + * @param title title returned to the response when this exception occurred + * @param message the detail message returend to the response + * @param cause The cause of this {@link AppException} + */ + public AppException(@NotNull HttpStatus httpStatus, @NotNull String title, + @NotNull String message, Throwable cause) { + super(message, cause); + this.title = title; + this.httpStatus = httpStatus; + } + + /** + * @param httpStatus HTTP status returned to the response + * @param title title returned to the response when this exception occurred + * @param message the detail message returend to the response + */ + public AppException(@NotNull HttpStatus httpStatus, @NotNull String title, @NotNull String message) { + super(message); + this.title = title; + this.httpStatus = httpStatus; + } + + + /** + * @param appError Response template returned to the response + * @param args {@link Formatter} replaces the placeholders in "details" string of + * {@link AppError} with the arguments. If there are more arguments than format + * specifiers, the extra arguments are ignored. + */ + public AppException(@NotNull AppError appError, Object... args) { + super(formatDetails(appError, args)); + this.httpStatus = appError.httpStatus; + this.title = appError.title; + } + + /** + * @param appError Response template returned to the response + * @param cause The cause of this {@link AppException} + * @param args Arguments for the details of {@link AppError} replaced by the + * {@link Formatter}. If there are more arguments than format specifiers, the + * extra arguments are ignored. + */ + public AppException(@NotNull AppError appError, Throwable cause, Object... args) { + super(formatDetails(appError, args), cause); + this.httpStatus = appError.httpStatus; + this.title = appError.title; + } + + private static String formatDetails(AppError appError, Object[] args) { + return String.format(appError.details, args); + } + + @Override + public String toString() { + return "AppException(" + httpStatus + ", " + title + ")" + super.toString(); + } +} diff --git a/src/main/java/it/gov/pagopa/microservice/exception/ErrorHandler.java b/src/main/java/it/gov/pagopa/microservice/exception/ErrorHandler.java new file mode 100644 index 0000000..3fd7e49 --- /dev/null +++ b/src/main/java/it/gov/pagopa/microservice/exception/ErrorHandler.java @@ -0,0 +1,220 @@ +package it.gov.pagopa.microservice.exception; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import feign.FeignException; +import it.gov.pagopa.microservice.model.ProblemJson; +import java.util.ArrayList; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.exception.ConstraintViolationException; +import org.springframework.beans.TypeMismatchException; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +/** + * All Exceptions are handled by this class + */ +@ControllerAdvice +@Slf4j +public class ErrorHandler extends ResponseEntityExceptionHandler { + + /** + * Handle if the input request is not a valid JSON + * + * @param ex {@link HttpMessageNotReadableException} exception raised + * @param headers of the response + * @param status of the response + * @param request from frontend + * @return a {@link ProblemJson} as response with the cause and with a 400 as HTTP status + */ + @Override + public ResponseEntity handleHttpMessageNotReadable(HttpMessageNotReadableException ex, + HttpHeaders headers, HttpStatus status, WebRequest request) { + log.warn("Input not readable: ", ex); + var errorResponse = ProblemJson.builder() + .status(HttpStatus.BAD_REQUEST.value()) + .title(AppError.BAD_REQUEST.getTitle()) + .detail("Invalid input format") + .build(); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + /** + * Handle if missing some request parameters in the request + * + * @param ex {@link MissingServletRequestParameterException} exception raised + * @param headers of the response + * @param status of the response + * @param request from frontend + * @return a {@link ProblemJson} as response with the cause and with a 400 as HTTP status + */ + @Override + public ResponseEntity handleMissingServletRequestParameter( + MissingServletRequestParameterException ex, HttpHeaders headers, HttpStatus status, + WebRequest request) { + log.warn("Missing request parameter: ", ex); + var errorResponse = ProblemJson.builder() + .status(HttpStatus.BAD_REQUEST.value()) + .title(AppError.BAD_REQUEST.getTitle()) + .detail(ex.getMessage()) + .build(); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + + /** + * Customize the response for TypeMismatchException. + * + * @param ex the exception + * @param headers the headers to be written to the response + * @param status the selected response status + * @param request the current request + * @return a {@code ResponseEntity} instance + */ + @Override + protected ResponseEntity handleTypeMismatch(TypeMismatchException ex, HttpHeaders headers, + HttpStatus status, WebRequest request) { + log.warn("Type mismatch: ", ex); + var errorResponse = ProblemJson.builder() + .status(HttpStatus.BAD_REQUEST.value()) + .title(AppError.BAD_REQUEST.getTitle()) + .detail(String.format("Invalid value %s for property %s", ex.getValue(), + ((MethodArgumentTypeMismatchException) ex).getName())) + .build(); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + /** + * Handle if validation constraints are unsatisfied + * + * @param ex {@link MethodArgumentNotValidException} exception raised + * @param headers of the response + * @param status of the response + * @param request from frontend + * @return a {@link ProblemJson} as response with the cause and with a 400 as HTTP status + */ + @Override + protected ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException ex, + HttpHeaders headers, HttpStatus status, WebRequest request) { + List details = new ArrayList<>(); + for (FieldError error : ex.getBindingResult().getFieldErrors()) { + details.add(error.getField() + ": " + error.getDefaultMessage()); + } + var detailsMessage = String.join(", ", details); + log.warn("Input not valid: " + detailsMessage); + var errorResponse = ProblemJson.builder() + .status(HttpStatus.BAD_REQUEST.value()) + .title(AppError.BAD_REQUEST.getTitle()) + .detail(detailsMessage) + .build(); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler({javax.validation.ConstraintViolationException.class}) + public ResponseEntity handleConstraintViolationException( + final javax.validation.ConstraintViolationException ex, final WebRequest request) { + log.warn("Validation Error raised:", ex); + var errorResponse = ProblemJson.builder() + .status(HttpStatus.BAD_REQUEST.value()) + .title(AppError.BAD_REQUEST.getTitle()) + .detail(ex.getMessage()) + .build(); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + + /** + * Handle if a {@link FeignException} is raised + * + * @param ex {@link FeignException} exception raised + * @param request from frontend + * @return a {@link ProblemJson} as response with the cause and with an appropriated HTTP status + */ + @ExceptionHandler({FeignException.class}) + public ResponseEntity handleFeignException(final FeignException ex, final WebRequest request) { + log.warn("FeignException raised: ", ex); + + ProblemJson problem; + if(ex.responseBody().isPresent()) { + var body = new String(ex.responseBody().get().array(), StandardCharsets.UTF_8); + try { + problem = new ObjectMapper().readValue(body, ProblemJson.class); + } catch (JsonProcessingException e) { + problem = ProblemJson.builder() + .status(HttpStatus.BAD_GATEWAY.value()) + .title(AppError.RESPONSE_NOT_READABLE.getTitle()) + .detail(AppError.RESPONSE_NOT_READABLE.getDetails()) + .build(); + } + } else { + problem = ProblemJson.builder() + .status(HttpStatus.BAD_GATEWAY.value()) + .title("No Response Body") + .detail("Error with external dependency") + .build(); + } + + return new ResponseEntity<>(problem, HttpStatus.valueOf(problem.getStatus())); + } + + + /** + * Handle if a {@link AppException} is raised + * + * @param ex {@link AppException} exception raised + * @param request from frontend + * @return a {@link ProblemJson} as response with the cause and with an appropriated HTTP status + */ + @ExceptionHandler({AppException.class}) + public ResponseEntity handleAppException(final AppException ex, + final WebRequest request) { + if (ex.getCause() != null) { + log.warn("App Exception raised: " + ex.getMessage() + "\nCause of the App Exception: ", + ex.getCause()); + } else { + log.warn("App Exception raised: ", ex); + } + var errorResponse = ProblemJson.builder() + .status(ex.getHttpStatus().value()) + .title(ex.getTitle()) + .detail(ex.getMessage()) + .build(); + return new ResponseEntity<>(errorResponse, ex.getHttpStatus()); + } + + + /** + * Handle if a {@link Exception} is raised + * + * @param ex {@link Exception} exception raised + * @param request from frontend + * @return a {@link ProblemJson} as response with the cause and with 500 as HTTP status + */ + @ExceptionHandler({Exception.class}) + public ResponseEntity handleGenericException(final Exception ex, + final WebRequest request) { + log.error("Generic Exception raised:", ex); + var errorResponse = ProblemJson.builder() + .status(HttpStatus.INTERNAL_SERVER_ERROR.value()) + .title(AppError.INTERNAL_SERVER_ERROR.getTitle()) + .detail(ex.getMessage()) + .build(); + return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); + } +} diff --git a/src/main/java/it/gov/pagopa/microservice/model/AppCorsConfiguration.java b/src/main/java/it/gov/pagopa/microservice/model/AppCorsConfiguration.java new file mode 100644 index 0000000..8d99b89 --- /dev/null +++ b/src/main/java/it/gov/pagopa/microservice/model/AppCorsConfiguration.java @@ -0,0 +1,23 @@ +package it.gov.pagopa.microservice.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Data +@Builder(toBuilder = true) +@NoArgsConstructor +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@ToString +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class AppCorsConfiguration { + + private String[] origins; + private String[] methods; +} diff --git a/src/main/java/it/gov/pagopa/microservice/model/AppInfo.java b/src/main/java/it/gov/pagopa/microservice/model/AppInfo.java new file mode 100644 index 0000000..d381de0 --- /dev/null +++ b/src/main/java/it/gov/pagopa/microservice/model/AppInfo.java @@ -0,0 +1,22 @@ +package it.gov.pagopa.microservice.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Data +@Builder(toBuilder = true) +@NoArgsConstructor +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@ToString +@JsonIgnoreProperties(ignoreUnknown = true) +public class AppInfo { + + private String name; + private String version; + private String environment; +} diff --git a/src/main/java/it/gov/pagopa/microservice/model/ProblemJson.java b/src/main/java/it/gov/pagopa/microservice/model/ProblemJson.java new file mode 100644 index 0000000..db11a37 --- /dev/null +++ b/src/main/java/it/gov/pagopa/microservice/model/ProblemJson.java @@ -0,0 +1,40 @@ +package it.gov.pagopa.microservice.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * Object returned as response in case of an error. + *

See {@link it.pagopa.microservice.exception.ErrorHandler} + */ +@Data +@Builder(toBuilder = true) +@NoArgsConstructor +@AllArgsConstructor +@ToString +@JsonIgnoreProperties(ignoreUnknown = true) +public class ProblemJson { + + @JsonProperty("title") + @Schema(description = "A short, summary of the problem type. Written in english and readable for engineers (usually not suited for non technical stakeholders and not localized); example: Service Unavailable") + private String title; + + @JsonProperty("status") + @Schema(example = "200", description = "The HTTP status code generated by the origin server for this occurrence of the problem.") + @Min(100) + @Max(600) + private Integer status; + + @JsonProperty("detail") + @Schema(example = "There was an error processing the request", description = "A human readable explanation specific to this occurrence of the problem.") + private String detail; + +} diff --git a/src/main/java/it/gov/pagopa/microservice/util/CommonUtility.java b/src/main/java/it/gov/pagopa/microservice/util/CommonUtility.java new file mode 100644 index 0000000..f080c47 --- /dev/null +++ b/src/main/java/it/gov/pagopa/microservice/util/CommonUtility.java @@ -0,0 +1,55 @@ +package it.gov.pagopa.microservice.util; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.util.Calendar; +import java.util.List; +import java.util.Optional; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class CommonUtility { + + + /** + * @param value value to deNullify. + * @return return empty string if value is null + */ + public static String deNull(String value) { + return Optional.ofNullable(value).orElse(""); + } + + /** + * @param value value to deNullify. + * @return return empty string if value is null + */ + public static String deNull(Object value) { + return Optional.ofNullable(value).orElse("").toString(); + } + + /** + * @param value value to deNullify. + * @return return false if value is null + */ + public static Boolean deNull(Boolean value) { + return Optional.ofNullable(value).orElse(false); + } + + /** + * @param headers header of the CSV file + * @param rows data of the CSV file + * @return byte array of the CSV using commas (;) as separator + */ + public static byte[] createCsv(List headers, List> rows) { + var csv = new StringBuilder(); + csv.append(String.join(";", headers)); + rows.forEach(row -> csv.append(System.lineSeparator()).append(String.join(";", row))); + return csv.toString().getBytes(); + } + + public static long getTimelapse(long startTime) { + return Calendar.getInstance().getTimeInMillis() - startTime; + } + + +} diff --git a/src/main/java/it/gov/pagopa/microservice/util/Constants.java b/src/main/java/it/gov/pagopa/microservice/util/Constants.java new file mode 100644 index 0000000..e5812c7 --- /dev/null +++ b/src/main/java/it/gov/pagopa/microservice/util/Constants.java @@ -0,0 +1,11 @@ +package it.gov.pagopa.microservice.util; + +import lombok.experimental.UtilityClass; + +@UtilityClass +public class Constants { + + + public static final String HEADER_REQUEST_ID = "X-Request-Id"; + +} diff --git a/src/main/resources/application-local.properties b/src/main/resources/application-local.properties new file mode 100644 index 0000000..1e89894 --- /dev/null +++ b/src/main/resources/application-local.properties @@ -0,0 +1,7 @@ +# Info +info.properties.environment=local +# Logging +logging.level.root=INFO +logging.level.it.gov.pagopa=DEBUG +# CORS configuration +cors.configuration={"origins": ["*"], "methods": ["*"]} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..5ad4771 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,25 @@ +# Info +info.application.artifactId=@project.artifactId@ +info.application.version=@project.version@ +info.application.description=@project.description@ +info.properties.environment=${ENV:azure} +# Actuator +management.endpoints.web.exposure.include=health,info +management.endpoints.jmx.exposure.include=health,info +management.info.env.enabled=true +management.endpoint.health.probes.enabled=true +management.health.livenessState.enabled=true +management.health.readinessState.enabled=true +# Openapi +springdoc.writer-with-order-by-keys=true +springdoc.writer-with-default-pretty-printer=true +# Server +# TODO: set your base path +server.servlet.context-path=/ +server.port=8080 +# Logging +logging.level.root=${DEFAULT_LOGGING_LEVEL:INFO} +logging.level.it.gov.pagopa=${APP_LOGGING_LEVEL:INFO} +# CORS configuration +cors.configuration=${CORS_CONFIGURATION:'{"origins": ["*"], "methods": ["*"]}'} + diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..f7beb7e --- /dev/null +++ b/src/main/resources/logback-spring.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + %clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m %clr(%mdc){magenta}%n%wEx + + + + + + + + + + + + + + ${OTEL_SERVICE_NAME} + ${ECS_SERVICE_VERSION} + ${ENV} + + + + + + + + + diff --git a/src/test/java/it/gov/pagopa/microservice/ApplicationTest.java b/src/test/java/it/gov/pagopa/microservice/ApplicationTest.java new file mode 100644 index 0000000..80c6e10 --- /dev/null +++ b/src/test/java/it/gov/pagopa/microservice/ApplicationTest.java @@ -0,0 +1,16 @@ +package it.gov.pagopa.microservice; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ApplicationTest { + + @Test + void contextLoads() { + // check only if the context is loaded + assertTrue(true); + } +} diff --git a/src/test/java/it/gov/pagopa/microservice/OpenApiGenerationTest.java b/src/test/java/it/gov/pagopa/microservice/OpenApiGenerationTest.java new file mode 100644 index 0000000..07f521b --- /dev/null +++ b/src/test/java/it/gov/pagopa/microservice/OpenApiGenerationTest.java @@ -0,0 +1,47 @@ +package it.gov.pagopa.microservice; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +@SpringBootTest(classes = Application.class) +@AutoConfigureMockMvc +class OpenApiGenerationTest { + + @Autowired ObjectMapper objectMapper; + + @Autowired private MockMvc mvc; + + @Test + void swaggerSpringPlugin() throws Exception { + mvc.perform(MockMvcRequestBuilders.get("/v3/api-docs").accept(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().is2xxSuccessful()) + .andDo( + (result) -> { + assertNotNull(result); + assertNotNull(result.getResponse()); + final String content = result.getResponse().getContentAsString(); + assertFalse(content.isBlank()); + assertFalse(content.contains("${"), "Generated swagger contains placeholders"); + Object swagger = + objectMapper.readValue(result.getResponse().getContentAsString(), Object.class); + String formatted = + objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(swagger); + Path basePath = Paths.get("openapi/"); + Files.createDirectories(basePath); + Files.write(basePath.resolve("openapi.json"), formatted.getBytes()); + }); + } +} diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties new file mode 100644 index 0000000..cd4c92f --- /dev/null +++ b/src/test/resources/application.properties @@ -0,0 +1,7 @@ +# Info +info.properties.environment=test +# logging +logging.level.root=INFO +logging.level.it.gov.pagopa=INFO +# CORS configuration +cors.configuration={"origins": ["*"], "methods": ["*"]}