diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 000000000..f21bf4c48
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,621 @@
+[*]
+charset = utf-8
+end_of_line = lf
+ij_formatter_off_tag = @formatter:off,<#if
+ij_formatter_on_tag = @formatter:on, #if
+ij_formatter_tags_enabled = true
+ij_smart_tabs = false
+ij_visual_guides =
+ij_wrap_on_typing = 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_new_line_in_record_component = false
+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_field_with_annotations = 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_blank_lines_between_record_components = 0
+ij_java_block_brace_style = end_of_line
+ij_java_block_comment_add_space = false
+ij_java_block_comment_at_first_column = true
+ij_java_builder_methods =
+ij_java_call_parameters_new_line_after_left_paren = false
+ij_java_call_parameters_right_paren_on_new_line = false
+ij_java_call_parameters_wrap = off
+ij_java_case_statement_on_separate_line = true
+ij_java_catch_on_new_line = false
+ij_java_class_annotation_wrap = split_into_lines
+ij_java_class_brace_style = end_of_line
+ij_java_class_count_to_use_import_on_demand = 5
+ij_java_class_names_in_javadoc = 1
+ij_java_deconstruction_list_wrap = normal
+ij_java_delete_unused_module_imports = false
+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_prefix =
+ij_java_entity_dd_suffix = EJB
+ij_java_entity_eb_prefix =
+ij_java_entity_eb_suffix = Bean
+ij_java_entity_hi_prefix =
+ij_java_entity_hi_suffix = Home
+ij_java_entity_lhi_prefix = Local
+ij_java_entity_lhi_suffix = Home
+ij_java_entity_li_prefix = Local
+ij_java_entity_li_suffix =
+ij_java_entity_pk_class = java.lang.String
+ij_java_entity_ri_prefix =
+ij_java_entity_ri_suffix =
+ij_java_entity_vo_prefix =
+ij_java_entity_vo_suffix = VO
+ij_java_enum_constants_wrap = off
+ij_java_enum_field_annotation_wrap = off
+ij_java_extends_keyword_wrap = off
+ij_java_extends_list_wrap = off
+ij_java_field_annotation_wrap = split_into_lines
+ij_java_field_name_prefix =
+ij_java_field_name_suffix =
+ij_java_filter_class_prefix =
+ij_java_filter_class_suffix =
+ij_java_filter_dd_prefix =
+ij_java_filter_dd_suffix =
+ij_java_finally_on_new_line = false
+ij_java_for_brace_force = 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_generate_use_type_annotation_before_type = true
+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_on_demand_import_from_same_package_first = true
+ij_java_layout_static_imports_separately = true
+ij_java_line_comment_add_space = false
+ij_java_line_comment_add_space_on_reformat = false
+ij_java_line_comment_at_first_column = true
+ij_java_listener_class_prefix =
+ij_java_listener_class_suffix =
+ij_java_local_variable_name_prefix =
+ij_java_local_variable_name_suffix =
+ij_java_message_dd_prefix =
+ij_java_message_dd_suffix = EJB
+ij_java_message_eb_prefix =
+ij_java_message_eb_suffix = Bean
+ij_java_method_annotation_wrap = split_into_lines
+ij_java_method_brace_style = end_of_line
+ij_java_method_call_chain_wrap = off
+ij_java_method_parameters_new_line_after_left_paren = false
+ij_java_method_parameters_right_paren_on_new_line = false
+ij_java_method_parameters_wrap = off
+ij_java_modifier_list_wrap = false
+ij_java_multi_catch_types_wrap = normal
+ij_java_names_count_to_use_import_on_demand = 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_new_line_when_body_is_presented = false
+ij_java_packages_to_use_import_on_demand = java.awt.*, javax.swing.*
+ij_java_parameter_annotation_wrap = off
+ij_java_parameter_name_prefix =
+ij_java_parameter_name_suffix =
+ij_java_parentheses_expression_new_line_after_left_paren = false
+ij_java_parentheses_expression_right_paren_on_new_line = false
+ij_java_place_assignment_sign_on_next_line = false
+ij_java_prefer_longer_names = true
+ij_java_prefer_parameters_wrap = false
+ij_java_preserve_module_imports = true
+ij_java_record_components_wrap = normal
+ij_java_repeat_annotations =
+ij_java_repeat_synchronized = true
+ij_java_replace_instanceof_and_cast = false
+ij_java_replace_null_check = true
+ij_java_replace_sum_lambda_with_method_ref = true
+ij_java_resource_list_new_line_after_left_paren = false
+ij_java_resource_list_right_paren_on_new_line = false
+ij_java_resource_list_wrap = off
+ij_java_rparen_on_new_line_in_annotation = false
+ij_java_rparen_on_new_line_in_deconstruction_pattern = true
+ij_java_rparen_on_new_line_in_record_header = false
+ij_java_servlet_class_prefix =
+ij_java_servlet_class_suffix =
+ij_java_servlet_dd_prefix =
+ij_java_servlet_dd_suffix =
+ij_java_session_dd_prefix =
+ij_java_session_dd_suffix = EJB
+ij_java_session_eb_prefix =
+ij_java_session_eb_suffix = Bean
+ij_java_session_hi_prefix =
+ij_java_session_hi_suffix = Home
+ij_java_session_lhi_prefix = Local
+ij_java_session_lhi_suffix = Home
+ij_java_session_li_prefix = Local
+ij_java_session_li_suffix =
+ij_java_session_ri_prefix =
+ij_java_session_ri_suffix =
+ij_java_session_si_prefix =
+ij_java_session_si_suffix = Service
+ij_java_space_after_closing_angle_bracket_in_type_argument = false
+ij_java_space_after_colon = true
+ij_java_space_after_comma = true
+ij_java_space_after_comma_in_type_arguments = true
+ij_java_space_after_for_semicolon = true
+ij_java_space_after_quest = true
+ij_java_space_after_type_cast = true
+ij_java_space_before_annotation_array_initializer_left_brace = false
+ij_java_space_before_annotation_parameter_list = false
+ij_java_space_before_array_initializer_left_brace = false
+ij_java_space_before_catch_keyword = true
+ij_java_space_before_catch_left_brace = true
+ij_java_space_before_catch_parentheses = true
+ij_java_space_before_class_left_brace = true
+ij_java_space_before_colon = true
+ij_java_space_before_colon_in_foreach = true
+ij_java_space_before_comma = false
+ij_java_space_before_deconstruction_list = false
+ij_java_space_before_do_left_brace = true
+ij_java_space_before_else_keyword = true
+ij_java_space_before_else_left_brace = true
+ij_java_space_before_finally_keyword = true
+ij_java_space_before_finally_left_brace = true
+ij_java_space_before_for_left_brace = true
+ij_java_space_before_for_parentheses = true
+ij_java_space_before_for_semicolon = false
+ij_java_space_before_if_left_brace = true
+ij_java_space_before_if_parentheses = true
+ij_java_space_before_method_call_parentheses = false
+ij_java_space_before_method_left_brace = true
+ij_java_space_before_method_parentheses = false
+ij_java_space_before_opening_angle_bracket_in_type_parameter = false
+ij_java_space_before_quest = true
+ij_java_space_before_switch_left_brace = true
+ij_java_space_before_switch_parentheses = true
+ij_java_space_before_synchronized_left_brace = true
+ij_java_space_before_synchronized_parentheses = true
+ij_java_space_before_try_left_brace = true
+ij_java_space_before_try_parentheses = true
+ij_java_space_before_type_parameter_list = false
+ij_java_space_before_while_keyword = true
+ij_java_space_before_while_left_brace = true
+ij_java_space_before_while_parentheses = true
+ij_java_space_inside_one_line_enum_braces = false
+ij_java_space_within_empty_array_initializer_braces = false
+ij_java_space_within_empty_method_call_parentheses = false
+ij_java_space_within_empty_method_parentheses = false
+ij_java_spaces_around_additive_operators = true
+ij_java_spaces_around_annotation_eq = true
+ij_java_spaces_around_assignment_operators = true
+ij_java_spaces_around_bitwise_operators = true
+ij_java_spaces_around_equality_operators = true
+ij_java_spaces_around_lambda_arrow = true
+ij_java_spaces_around_logical_operators = true
+ij_java_spaces_around_method_ref_dbl_colon = false
+ij_java_spaces_around_multiplicative_operators = true
+ij_java_spaces_around_relational_operators = true
+ij_java_spaces_around_shift_operators = true
+ij_java_spaces_around_type_bounds_in_type_parameters = true
+ij_java_spaces_around_unary_operator = false
+ij_java_spaces_inside_block_braces_when_body_is_present = false
+ij_java_spaces_within_angle_brackets = false
+ij_java_spaces_within_annotation_parentheses = false
+ij_java_spaces_within_array_initializer_braces = false
+ij_java_spaces_within_braces = false
+ij_java_spaces_within_brackets = false
+ij_java_spaces_within_cast_parentheses = false
+ij_java_spaces_within_catch_parentheses = false
+ij_java_spaces_within_deconstruction_list = false
+ij_java_spaces_within_for_parentheses = false
+ij_java_spaces_within_if_parentheses = false
+ij_java_spaces_within_method_call_parentheses = false
+ij_java_spaces_within_method_parentheses = false
+ij_java_spaces_within_parentheses = false
+ij_java_spaces_within_record_header = false
+ij_java_spaces_within_switch_parentheses = false
+ij_java_spaces_within_synchronized_parentheses = false
+ij_java_spaces_within_try_parentheses = false
+ij_java_spaces_within_while_parentheses = false
+ij_java_special_else_if_treatment = true
+ij_java_static_field_name_prefix =
+ij_java_static_field_name_suffix =
+ij_java_subclass_name_prefix =
+ij_java_subclass_name_suffix = Impl
+ij_java_switch_expressions_wrap = normal
+ij_java_ternary_operation_signs_on_next_line = false
+ij_java_ternary_operation_wrap = off
+ij_java_test_name_prefix =
+ij_java_test_name_suffix = Test
+ij_java_throws_keyword_wrap = off
+ij_java_throws_list_wrap = off
+ij_java_use_external_annotations = false
+ij_java_use_fq_class_names = false
+ij_java_use_relative_indents = false
+ij_java_use_single_class_imports = true
+ij_java_variable_annotation_wrap = off
+ij_java_visibility = public
+ij_java_while_brace_force = 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
+ij_java_wrap_semicolon_after_call_chain = false
+
+[.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
+
+[{*.gant,*.groovy,*.gy}]
+indent_style = tab
+max_line_length = 180
+ij_smart_tabs = true
+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 = false
+ij_groovy_align_multiline_parameters_in_calls = false
+ij_groovy_align_multiline_resources = true
+ij_groovy_align_multiline_ternary_operation = true
+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 = on_every_item
+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 = $*, |, |, com.cloudogu.**, |, com.spring.**, io.micronaut.**, |, javax.**, java.**, jakarta.**, groovy.**, |, *
+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 = 1
+ij_groovy_keep_blank_lines_in_code = 1
+ij_groovy_keep_blank_lines_in_declarations = 1
+ij_groovy_keep_control_statement_in_one_line = true
+ij_groovy_keep_first_column_comment = false
+ij_groovy_keep_indents_on_empty_lines = false
+ij_groovy_keep_line_breaks = false
+ij_groovy_keep_multiple_expressions_in_one_line = false
+ij_groovy_keep_simple_blocks_in_one_line = true
+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 = normal
+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 = true
+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 = true
+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 = true
+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 = normal
+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
+
+[{*.kt,*.kts}]
+ij_kotlin_align_in_columns_case_branch = false
+ij_kotlin_align_multiline_binary_operation = false
+ij_kotlin_align_multiline_extends_list = false
+ij_kotlin_align_multiline_method_parentheses = false
+ij_kotlin_align_multiline_parameters = true
+ij_kotlin_align_multiline_parameters_in_calls = false
+ij_kotlin_allow_trailing_comma = false
+ij_kotlin_allow_trailing_comma_collection_literal_expression = false
+ij_kotlin_allow_trailing_comma_context_receiver_list = true
+ij_kotlin_allow_trailing_comma_destructuring_declaration = true
+ij_kotlin_allow_trailing_comma_function_literal = true
+ij_kotlin_allow_trailing_comma_indices = false
+ij_kotlin_allow_trailing_comma_on_call_site = false
+ij_kotlin_allow_trailing_comma_type_argument_list = false
+ij_kotlin_allow_trailing_comma_type_parameter_list = true
+ij_kotlin_allow_trailing_comma_value_argument_list = false
+ij_kotlin_allow_trailing_comma_value_parameter_list = true
+ij_kotlin_allow_trailing_comma_when_entry = true
+ij_kotlin_assignment_wrap = normal
+ij_kotlin_blank_lines_after_class_header = 0
+ij_kotlin_blank_lines_around_block_when_branches = 0
+ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1
+ij_kotlin_block_comment_add_space = false
+ij_kotlin_block_comment_at_first_column = true
+ij_kotlin_call_parameters_new_line_after_left_paren = true
+ij_kotlin_call_parameters_right_paren_on_new_line = true
+ij_kotlin_call_parameters_wrap = on_every_item
+ij_kotlin_catch_on_new_line = false
+ij_kotlin_class_annotation_wrap = split_into_lines
+ij_kotlin_continuation_indent_for_chained_calls = false
+ij_kotlin_continuation_indent_for_expression_bodies = false
+ij_kotlin_continuation_indent_in_argument_lists = false
+ij_kotlin_continuation_indent_in_elvis = false
+ij_kotlin_continuation_indent_in_if_conditions = false
+ij_kotlin_continuation_indent_in_parameter_lists = false
+ij_kotlin_continuation_indent_in_supertype_lists = false
+ij_kotlin_else_on_new_line = false
+ij_kotlin_enum_constants_wrap = off
+ij_kotlin_extends_list_wrap = normal
+ij_kotlin_field_annotation_wrap = split_into_lines
+ij_kotlin_finally_on_new_line = false
+ij_kotlin_if_rparen_on_new_line = true
+ij_kotlin_import_nested_classes = false
+ij_kotlin_imports_layout = *, java.**, javax.**, kotlin.**, ^
+ij_kotlin_indent_before_arrow_on_new_line = true
+ij_kotlin_insert_whitespaces_in_simple_one_line_method = true
+ij_kotlin_keep_blank_lines_before_right_brace = 2
+ij_kotlin_keep_blank_lines_in_code = 2
+ij_kotlin_keep_blank_lines_in_declarations = 2
+ij_kotlin_keep_first_column_comment = true
+ij_kotlin_keep_indents_on_empty_lines = false
+ij_kotlin_keep_line_breaks = true
+ij_kotlin_lbrace_on_next_line = false
+ij_kotlin_line_break_after_multiline_when_entry = true
+ij_kotlin_line_comment_add_space = false
+ij_kotlin_line_comment_add_space_on_reformat = false
+ij_kotlin_line_comment_at_first_column = true
+ij_kotlin_method_annotation_wrap = split_into_lines
+ij_kotlin_method_call_chain_wrap = normal
+ij_kotlin_method_parameters_new_line_after_left_paren = true
+ij_kotlin_method_parameters_right_paren_on_new_line = true
+ij_kotlin_method_parameters_wrap = on_every_item
+ij_kotlin_name_count_to_use_star_import = 5
+ij_kotlin_name_count_to_use_star_import_for_members = 3
+ij_kotlin_packages_to_use_import_on_demand = java.util.*, kotlinx.android.synthetic.**, io.ktor.**
+ij_kotlin_parameter_annotation_wrap = off
+ij_kotlin_space_after_comma = true
+ij_kotlin_space_after_extend_colon = true
+ij_kotlin_space_after_type_colon = true
+ij_kotlin_space_before_catch_parentheses = true
+ij_kotlin_space_before_comma = false
+ij_kotlin_space_before_extend_colon = true
+ij_kotlin_space_before_for_parentheses = true
+ij_kotlin_space_before_if_parentheses = true
+ij_kotlin_space_before_lambda_arrow = true
+ij_kotlin_space_before_type_colon = false
+ij_kotlin_space_before_when_parentheses = true
+ij_kotlin_space_before_while_parentheses = true
+ij_kotlin_spaces_around_additive_operators = true
+ij_kotlin_spaces_around_assignment_operators = true
+ij_kotlin_spaces_around_elvis = true
+ij_kotlin_spaces_around_equality_operators = true
+ij_kotlin_spaces_around_function_type_arrow = true
+ij_kotlin_spaces_around_logical_operators = true
+ij_kotlin_spaces_around_multiplicative_operators = true
+ij_kotlin_spaces_around_range = false
+ij_kotlin_spaces_around_relational_operators = true
+ij_kotlin_spaces_around_unary_operator = false
+ij_kotlin_spaces_around_when_arrow = true
+ij_kotlin_variable_annotation_wrap = off
+ij_kotlin_while_on_new_line = false
+ij_kotlin_wrap_elvis_expressions = 1
+ij_kotlin_wrap_expression_body_functions = 1
+ij_kotlin_wrap_first_method_in_call_chain = false
\ No newline at end of file
diff --git a/compiler.groovy b/compiler.groovy
index 8f0a827d4..4791e21e9 100644
--- a/compiler.groovy
+++ b/compiler.groovy
@@ -1,4 +1,4 @@
withConfig(configuration) {
- ast(groovy.transform.CompileStatic)
- ast(groovy.transform.TypeChecked)
-}
+ ast(groovy.transform.CompileStatic)
+ ast(groovy.transform.TypeChecked)
+}
\ No newline at end of file
diff --git a/docs/code-format/Project_Default.xml b/docs/code-format/Project_Default.xml
new file mode 100644
index 000000000..7c2495709
--- /dev/null
+++ b/docs/code-format/Project_Default.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/code-format/code-format.md b/docs/code-format/code-format.md
new file mode 100644
index 000000000..3ca0f2a5a
--- /dev/null
+++ b/docs/code-format/code-format.md
@@ -0,0 +1,28 @@
+# Code Formatting Guide
+
+When you open a `.editorconfig` file in IntelliJ IDEA for the first time, you'll see an "Enable EditorConfig support"
+button at the top of the editor. Simply click this button to activate EditorConfig support.
+
+Alternatively, you can enable it manually:
+
+1. Go to Settings/Preferences
+2. Search for "EditorConfig"
+3. Check "Enable EditorConfig support"
+
+Once enabled, the project's `.editorconfig` file will be automatically recognized and its rules will be applied to your
+code.
+
+## Auto-Format on Commit
+
+To ensure consistent code formatting across the project, you can configure IntelliJ to automatically format code before
+committing:
+
+
+
+## CodeNarc Inspection Profile
+
+To use custom CodeNarc rules in IntelliJ:
+
+
+
+and load Project_Default.xml
\ No newline at end of file
diff --git a/docs/code-format/format-commit.png b/docs/code-format/format-commit.png
new file mode 100644
index 000000000..e54c429e0
Binary files /dev/null and b/docs/code-format/format-commit.png differ
diff --git a/docs/code-format/formatsave.png b/docs/code-format/formatsave.png
new file mode 100644
index 000000000..a9ccde041
Binary files /dev/null and b/docs/code-format/formatsave.png differ
diff --git a/docs/code-format/loadCodeNarcProfile.png b/docs/code-format/loadCodeNarcProfile.png
new file mode 100644
index 000000000..b6433ec0e
Binary files /dev/null and b/docs/code-format/loadCodeNarcProfile.png differ
diff --git a/scripts/jenkins/pluginCheck.groovy b/scripts/jenkins/pluginCheck.groovy
index c8f77c309..745e0027a 100644
--- a/scripts/jenkins/pluginCheck.groovy
+++ b/scripts/jenkins/pluginCheck.groovy
@@ -4,21 +4,19 @@ List expected = []
String input = "${PLUGIN_LIST}"
input.split(",").toList().stream()
- .filter { it.split(":")[0] != "prometheus" } // prometheus does not support dynamic loading and will be installed when restarting.
- .collect()
- .each { it -> expected.add(it.substring(0, it.indexOf(":"))) }
+ .filter { it.split(":")[0] != "prometheus" } // prometheus does not support dynamic loading and will be installed when restarting.
+ .collect()
+ .each { it -> expected.add(it.substring(0, it.indexOf(":"))) }
Jenkins.instance.pluginManager.failedPlugins.each {
- available.add(it.name)
+ available.add(it.name)
}
Jenkins.instance.pluginManager.plugins.each {
- available.add(it.shortName)
+ available.add(it.shortName)
}
-available.each { p ->
- if (expected.find { (it == p) })
- plugins.add(p)
+available.each { p -> if (expected.find { (it == p) }) plugins.add(p)
}
def commons = plugins.intersect(expected)
diff --git a/src/main/groovy/com/cloudogu/gitops/Application.groovy b/src/main/groovy/com/cloudogu/gitops/Application.groovy
index 6e395bebb..177b85623 100644
--- a/src/main/groovy/com/cloudogu/gitops/Application.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/Application.groovy
@@ -2,67 +2,66 @@ package com.cloudogu.gitops
import com.cloudogu.gitops.config.Config
import com.cloudogu.gitops.utils.TemplatingEngine
+
+import jakarta.inject.Singleton
+import groovy.util.logging.Slf4j
+
import freemarker.template.Configuration
import freemarker.template.DefaultObjectWrapperBuilder
-import groovy.util.logging.Slf4j
-import jakarta.inject.Singleton
@Slf4j
@Singleton
class Application {
- final List features
- final Config config
+ final List features
+ final Config config
- Application(Config config,
- List features
- ) {
- this.config = config
- // Order is important. Enforced by @Order-Annotation on the Singletons
- this.features = features
- }
+ Application(Config config,
+ List features) {
+ this.config = config
+ // Order is important. Enforced by @Order-Annotation on the Singletons
+ this.features = features
+ }
- def start() {
- log.debug("Starting Application")
+ def start() {
+ log.debug("Starting Application")
- setNamespaceListToConfig(config)
+ setNamespaceListToConfig(config)
- features.forEach(feature -> {
- feature.validate()
- })
- features.forEach(feature -> {
- feature.install()
- })
- log.debug("Application finished")
- }
+ features.forEach(feature -> {
+ feature.validate()
+ })
+ features.forEach(feature -> {
+ feature.install()
+ })
+ log.debug("Application finished")
+ }
- List getFeatures() {
- return features
- }
+ List getFeatures() {
+ return features
+ }
- void setNamespaceListToConfig(Config config) {
- LinkedHashSet dedicatedNamespaces = new LinkedHashSet<>()
- LinkedHashSet tenantNamespaces = new LinkedHashSet<>()
- def engine = new TemplatingEngine()
+ void setNamespaceListToConfig(Config config) {
+ LinkedHashSet dedicatedNamespaces = new LinkedHashSet<>()
+ LinkedHashSet tenantNamespaces = new LinkedHashSet<>()
+ def engine = new TemplatingEngine()
- config.content.namespaces.each { String ns ->
- tenantNamespaces.add(engine.template(ns, [
- config : config,
- // Allow for using static classes inside the templates
- statics: new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_32).build().getStaticModels()
- ]))
- }
- config.content.namespaces = tenantNamespaces.toList()
+ config.content.namespaces.each { String ns ->
+ tenantNamespaces.add(engine.template(ns, [config : config,
+ // Allow for using static classes inside the templates
+ statics: new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_32).build().getStaticModels()]))
+ }
+ config.content.namespaces = tenantNamespaces.toList()
- //iterates over all FeatureWithImages and gets their namespaces
- dedicatedNamespaces.addAll(this.features
- .collect { it.activeNamespaceFromFeature }
- .findAll { it }
- .unique()
- .collect { "${it}".toString() })
+ //iterates over all FeatureWithImages and gets their namespaces
+ dedicatedNamespaces.addAll(this.features
+ .collect { it.activeNamespaceFromFeature }
+ .findAll { it }
+ .unique()
+ .collect { "${it}".toString() })
- config.application.namespaces.dedicatedNamespaces = dedicatedNamespaces
- config.application.namespaces.tenantNamespaces = tenantNamespaces
- log.debug("Active namespaces retrieved: {}", config.application.namespaces.activeNamespaces)
- }
+ config.application.namespaces.dedicatedNamespaces = dedicatedNamespaces
+ config.application.namespaces.tenantNamespaces = tenantNamespaces
+ log.debug("Active namespaces retrieved: {}", config.application.namespaces.activeNamespaces)
+ }
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/Feature.groovy b/src/main/groovy/com/cloudogu/gitops/Feature.groovy
index 857359d75..aaa97c0e5 100644
--- a/src/main/groovy/com/cloudogu/gitops/Feature.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/Feature.groovy
@@ -1,5 +1,7 @@
package com.cloudogu.gitops
+import static com.cloudogu.gitops.features.deployment.DeploymentStrategy.RepoType
+
import com.cloudogu.gitops.config.Config
import com.cloudogu.gitops.features.deployment.DeploymentStrategy
import com.cloudogu.gitops.features.git.GitHandler
@@ -7,29 +9,27 @@ import com.cloudogu.gitops.utils.AirGappedUtils
import com.cloudogu.gitops.utils.FileSystemUtils
import com.cloudogu.gitops.utils.MapUtils
import com.cloudogu.gitops.utils.TemplatingEngine
-import freemarker.template.Configuration
-import freemarker.template.DefaultObjectWrapperBuilder
-import groovy.util.logging.Slf4j
-import groovy.yaml.YamlSlurper
-import java.nio.file.Files
import java.nio.file.Path
+import groovy.util.logging.Slf4j
+import groovy.yaml.YamlSlurper
-import static com.cloudogu.gitops.features.deployment.DeploymentStrategy.*
+import freemarker.template.Configuration
+import freemarker.template.DefaultObjectWrapperBuilder
/**
* A single tool to be deployed by GOP.
- *
+ *
* Typically, this is a helm chart (see {@link com.cloudogu.gitops.features.deployment.DeploymentStrategy} and
* {@code downloadHelmCharts.sh}) with its own section in the config
* (see {@link com.cloudogu.gitops.config.schema.Schema#features}).
- *
+ *
* In the config, features typically set their default helm chart coordinates and provide options to
*
* configure images
* overwrite default helm values
*
- *
+ *
* In addition to their own config, features react to several generic GOP config options.
* Here are some typical examples:
*
@@ -39,136 +39,131 @@ import static com.cloudogu.gitops.features.deployment.DeploymentStrategy.*
* Install with Resource requests + limits: {@link com.cloudogu.gitops.config.schema.Schema.ApplicationSchema#podResources}
* Install without CRDs: {@link com.cloudogu.gitops.config.schema.Schema.ApplicationSchema#skipCrds}
* For apps with UI: Setting {@link com.cloudogu.gitops.config.schema.Schema.ApplicationSchema#username} and {@link com.cloudogu.gitops.config.schema.Schema.ApplicationSchema#password}
- *
- */
+ * */
@Slf4j
abstract class Feature {
- protected FileSystemUtils fileSystemUtils
- protected DeploymentStrategy deployer
- protected AirGappedUtils airGappedUtils
- protected GitHandler gitHandler
- protected Map helmValuesTemplateData = [:]
-
- protected void addHelmValuesData(String key, Object value) {
- this.helmValuesTemplateData[key] = value
- }
-
- boolean install() {
- if (isEnabled()) {
- log.info("Installing Feature ${getClass().getSimpleName()}")
-
- if (this instanceof FeatureWithImage) {
- (this as FeatureWithImage).createImagePullSecret()
- }
-
- enable()
- return true
- } else {
- log.debug("Feature ${getClass().getSimpleName()} is disabled")
- disable()
- return false
- }
- }
-
- String getActiveNamespaceFromFeature() {
- //using reflection to get all subclasses implementing a own namespace
- if (this.metaClass.hasProperty(this, 'namespace')) {
- return isEnabled() ? this.getProperty('namespace') : null
- }
- return null
- }
-
- static Map templateToMap(String filePath, Map parameters) {
- def hydratedString = new TemplatingEngine().template(new File(filePath), parameters)
-
- if (hydratedString.trim().isEmpty()) {
- // Otherwise YamlSlurper returns an empty array, whereas we expect a Map
- return [:]
- }
- return new YamlSlurper().parseText(hydratedString) as Map
- }
-
- protected void deployHelmChart(
- String featureName,
- String releaseName,
- String namespace,
- Config.HelmConfigWithValues helmConfig,
- String helmValuesTemplatePath,
- Config config
- ) {
- String repoURL = helmConfig.repoURL
- String chartOrPath = helmConfig.chart
- String version = helmConfig.version
- RepoType repoType = RepoType.HELM
-
- this.addHelmValuesData("config", config)
- this.addHelmValuesData("statics", new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_32).build().getStaticModels())
-
- /* If we get a helmValuesTemplatePath we render the Template with the given Data.
- * Some Features might not use a values template and thus passing no helmValuesTemplatePath, in that
- * case we simply treat helmValuesTemplateData directly as helmValuesData */
- Map helmValuesData = this.helmValuesTemplateData
- if (helmValuesTemplatePath) {
- log.debug("got helm_value_path, rendering values template")
- helmValuesData = templateToMap(helmValuesTemplatePath, this.helmValuesTemplateData)
- }
-
- helmValuesData = MapUtils.deepMerge(helmConfig.values, helmValuesData)
- Path tempValuesPath = this.fileSystemUtils.writeTempFile(helmValuesData)
-
- if (config.application.mirrorRepos) {
- log.debug("Using a local, mirrored git repo as deployment source for feature ${featureName}")
-
- String repoNamespaceAndName = this.airGappedUtils.mirrorHelmRepoToGit(helmConfig)
- repoURL = this.gitHandler.resourcesScm.repoUrl(repoNamespaceAndName)
- chartOrPath = '.'
- repoType = RepoType.GIT
- version = new YamlSlurper()
- .parse(Path.of("${config.application.localHelmChartFolder}/${helmConfig.chart}",
- 'Chart.yaml'))['version']
- }
-
- log.debug("Starting deployment of feature ${featureName} from ${repoURL}.")
- log.debug("helm values used: ${helmValuesData}")
-
- this.deployer.deployFeature(
- repoURL,
- featureName,
- chartOrPath,
- version,
- namespace,
- releaseName,
- tempValuesPath,
- repoType)
- }
-
- abstract boolean isEnabled()
-
-
-
- /*
- * Hooks for enabling or disabling a feature. Both optional, because not always needed.
- */
- protected void enable() {}
- protected void disable() {}
-
- /*
- * Hook for special feature validation. Optional.
- * Feature should throw RuntimeException to stop immediately.
- */
- protected void validate() { }
-
- /**
- * Hook for preConfigInit. Optional.
- * Feature should throw RuntimeException to stop immediately.
- */
- void preConfigInit(Config configToSet) { }
-
- /**
- * Hook for postConfigInit. Optional.
- * Feature should throw RuntimeException to stop immediately.
- */
- void postConfigInit(Config configToSet) { }
+ protected FileSystemUtils fileSystemUtils
+ protected DeploymentStrategy deployer
+ protected AirGappedUtils airGappedUtils
+ protected GitHandler gitHandler
+ protected Map helmValuesTemplateData = [:]
+
+ protected void addHelmValuesData(String key, Object value) {
+ this.helmValuesTemplateData[key] = value
+ }
+
+ boolean install() {
+ if (isEnabled()) {
+ log.info("Installing Feature ${getClass().getSimpleName()}")
+
+ if (this instanceof FeatureWithImage) {
+ (this as FeatureWithImage).createImagePullSecret()
+ }
+
+ enable()
+ return true
+ } else {
+ log.debug("Feature ${getClass().getSimpleName()} is disabled")
+ disable()
+ return false
+ }
+ }
+
+ String getActiveNamespaceFromFeature() {
+ //using reflection to get all subclasses implementing a own namespace
+ if (this.metaClass.hasProperty(this, 'namespace')) {
+ return isEnabled() ? this.getProperty('namespace') : null
+ }
+ return null
+ }
+
+ static Map templateToMap(String filePath, Map parameters) {
+ def hydratedString = new TemplatingEngine().template(new File(filePath), parameters)
+
+ if (hydratedString.trim().isEmpty()) {
+ // Otherwise YamlSlurper returns an empty array, whereas we expect a Map
+ return [:]
+ }
+ return new YamlSlurper().parseText(hydratedString) as Map
+ }
+
+ protected void deployHelmChart(String featureName,
+ String releaseName,
+ String namespace,
+ Config.HelmConfigWithValues helmConfig,
+ String helmValuesTemplatePath,
+ Config config) {
+ String repoURL = helmConfig.repoURL
+ String chartOrPath = helmConfig.chart
+ String version = helmConfig.version
+ RepoType repoType = RepoType.HELM
+
+ this.addHelmValuesData("config", config)
+ this.addHelmValuesData("statics", new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_32).build().getStaticModels())
+
+ /* If we get a helmValuesTemplatePath we render the Template with the given Data.
+ * Some Features might not use a values template and thus passing no helmValuesTemplatePath, in that
+ * case we simply treat helmValuesTemplateData directly as helmValuesData */
+ Map helmValuesData = this.helmValuesTemplateData
+ if (helmValuesTemplatePath) {
+ log.debug("got helm_value_path, rendering values template")
+ helmValuesData = templateToMap(helmValuesTemplatePath, this.helmValuesTemplateData)
+ }
+
+ helmValuesData = MapUtils.deepMerge(helmConfig.values, helmValuesData)
+ Path tempValuesPath = this.fileSystemUtils.writeTempFile(helmValuesData)
+
+ if (config.application.mirrorRepos) {
+ log.debug("Using a local, mirrored git repo as deployment source for feature ${featureName}")
+
+ String repoNamespaceAndName = this.airGappedUtils.mirrorHelmRepoToGit(helmConfig)
+ repoURL = this.gitHandler.resourcesScm.repoUrl(repoNamespaceAndName)
+ chartOrPath = '.'
+ repoType = RepoType.GIT
+ version = new YamlSlurper()
+ .parse(Path.of("${config.application.localHelmChartFolder}/${helmConfig.chart}",
+ 'Chart.yaml'))['version']
+ }
+
+ log.debug("Starting deployment of feature ${featureName} from ${repoURL}.")
+ log.debug("helm values used: ${helmValuesData}")
+
+ this.deployer.deployFeature(repoURL,
+ featureName,
+ chartOrPath,
+ version,
+ namespace,
+ releaseName,
+ tempValuesPath,
+ repoType)
+ }
+
+ abstract boolean isEnabled()
+
+ /*
+ * Hooks for enabling or disabling a feature. Both optional, because not always needed.
+ */
+
+ protected void enable() {}
+
+ protected void disable() {}
+
+ /*
+ * Hook for special feature validation. Optional.
+ * Feature should throw RuntimeException to stop immediately.
+ */
+
+ protected void validate() {}
+
+ /**
+ * Hook for preConfigInit. Optional.
+ * Feature should throw RuntimeException to stop immediately.*/
+ void preConfigInit(Config configToSet) {}
+
+ /**
+ * Hook for postConfigInit. Optional.
+ * Feature should throw RuntimeException to stop immediately.*/
+ void postConfigInit(Config configToSet) {}
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/FeatureWithImage.groovy b/src/main/groovy/com/cloudogu/gitops/FeatureWithImage.groovy
index e7d487b6b..5f0f2f135 100644
--- a/src/main/groovy/com/cloudogu/gitops/FeatureWithImage.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/FeatureWithImage.groovy
@@ -2,30 +2,32 @@ package com.cloudogu.gitops
import com.cloudogu.gitops.config.Config
import com.cloudogu.gitops.kubernetes.api.K8sClient
+
import org.slf4j.Logger
import org.slf4j.LoggerFactory
/**
- * A feature that relies on container images running inside the kubernetes cluster.
- */
+ * A feature that relies on container images running inside the kubernetes cluster.*/
trait FeatureWithImage {
- final Logger log = LoggerFactory.getLogger(this.class)
-
- void createImagePullSecret() {
- if (config.registry.createImagePullSecrets) {
-
- log.trace("Creating image pull secret 'proxy-registry' in namespace ${this.namespace}")
- String url = config.registry.proxyUrl ?: config.registry.url
- String user = config.registry.proxyUsername ?: config.registry.readOnlyUsername ?: config.registry.username
- String password = config.registry.proxyPassword ?: config.registry.readOnlyPassword ?: config.registry.password
-
- k8sClient.createNamespace(this.namespace)
- k8sClient.createImagePullSecret('proxy-registry', namespace, url, user, password)
- }
- }
-
- abstract String getNamespace()
- abstract K8sClient getK8sClient()
- abstract Config getConfig()
+ final Logger log = LoggerFactory.getLogger(this.class)
+
+ void createImagePullSecret() {
+ if (config.registry.createImagePullSecrets) {
+
+ log.trace("Creating image pull secret 'proxy-registry' in namespace ${this.namespace}")
+ String url = config.registry.proxyUrl ?: config.registry.url
+ String user = config.registry.proxyUsername ?: config.registry.readOnlyUsername ?: config.registry.username
+ String password = config.registry.proxyPassword ?: config.registry.readOnlyPassword ?: config.registry.password
+
+ k8sClient.createNamespace(this.namespace)
+ k8sClient.createImagePullSecret('proxy-registry', namespace, url, user, password)
+ }
+ }
+
+ abstract String getNamespace()
+
+ abstract K8sClient getK8sClient()
+
+ abstract Config getConfig()
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/cli/GenerateJsonSchema.groovy b/src/main/groovy/com/cloudogu/gitops/cli/GenerateJsonSchema.groovy
index 75b4ad8b5..96bf2a581 100644
--- a/src/main/groovy/com/cloudogu/gitops/cli/GenerateJsonSchema.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/cli/GenerateJsonSchema.groovy
@@ -1,9 +1,12 @@
package com.cloudogu.gitops.cli
import com.cloudogu.gitops.config.schema.JsonSchemaGenerator
+
+import io.micronaut.context.ApplicationContext
+
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.node.ObjectNode
-import io.micronaut.context.ApplicationContext
+
/**
* Generates the JSON Config for the configuration file and prints it to docs/configuration.schema.json.
* Passing '-' as parameter prints the schema to stdout
@@ -12,16 +15,16 @@ import io.micronaut.context.ApplicationContext
* @see com.cloudogu.gitops.config.Config
*/
class GenerateJsonSchema {
- static void main(String[] args) {
- ObjectNode jsonSchema = ApplicationContext.run().getBean(JsonSchemaGenerator).createSchema()
- def prettyJson = new ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(jsonSchema)
+ static void main(String[] args) {
+ ObjectNode jsonSchema = ApplicationContext.run().getBean(JsonSchemaGenerator).createSchema()
+ def prettyJson = new ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(jsonSchema)
- if (args.length > 0 && args[0] == "-") {
- println(prettyJson)
- } else {
- def schemaFile = 'docs/configuration.schema.json'
- new File(schemaFile).setText(prettyJson)
- println "Wrote schema to file ${schemaFile}"
- }
- }
-}
+ if (args.length > 0 && args[0] == "-") {
+ println(prettyJson)
+ } else {
+ def schemaFile = 'docs/configuration.schema.json'
+ new File(schemaFile).setText(prettyJson)
+ println "Wrote schema to file ${schemaFile}"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCli.groovy b/src/main/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCli.groovy
index e7a45c4f1..df078bff8 100644
--- a/src/main/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCli.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCli.groovy
@@ -1,11 +1,8 @@
package com.cloudogu.gitops.cli
-import ch.qos.logback.classic.Level
-import ch.qos.logback.classic.Logger
-import ch.qos.logback.classic.LoggerContext
-import ch.qos.logback.classic.encoder.PatternLayoutEncoder
-import ch.qos.logback.classic.spi.ILoggingEvent
-import ch.qos.logback.core.ConsoleAppender
+import static com.cloudogu.gitops.config.ConfigConstants.APP_NAME
+import static com.cloudogu.gitops.utils.MapUtils.deepMerge
+
import com.cloudogu.gitops.Application
import com.cloudogu.gitops.Feature
import com.cloudogu.gitops.config.ApplicationConfigurator
@@ -13,18 +10,24 @@ import com.cloudogu.gitops.config.CommonFeatureConfig
import com.cloudogu.gitops.config.Config
import com.cloudogu.gitops.config.schema.JsonSchemaValidator
import com.cloudogu.gitops.destroy.Destroyer
+import com.cloudogu.gitops.kubernetes.api.K8sClient
import com.cloudogu.gitops.utils.CommandExecutor
import com.cloudogu.gitops.utils.FileSystemUtils
-import com.cloudogu.gitops.kubernetes.api.K8sClient
+
+import io.micronaut.context.ApplicationContext
+
import groovy.util.logging.Slf4j
import groovy.yaml.YamlSlurper
-import io.micronaut.context.ApplicationContext
+
+import ch.qos.logback.classic.Level
+import ch.qos.logback.classic.Logger
+import ch.qos.logback.classic.LoggerContext
+import ch.qos.logback.classic.encoder.PatternLayoutEncoder
+import ch.qos.logback.classic.spi.ILoggingEvent
+import ch.qos.logback.core.ConsoleAppender
import org.slf4j.LoggerFactory
import picocli.CommandLine
-import static com.cloudogu.gitops.config.ConfigConstants.APP_NAME
-import static com.cloudogu.gitops.utils.MapUtils.deepMerge
-
/**
* Provides the entrypoint to the application as well as all config parameters.
* When changing parameters, make sure to update the Config for the config file as well
@@ -34,221 +37,219 @@ import static com.cloudogu.gitops.utils.MapUtils.deepMerge
@Slf4j
class GitopsPlaygroundCli {
- K8sClient k8sClient
- ApplicationConfigurator applicationConfigurator
-
- GitopsPlaygroundCli(K8sClient k8sClient = new K8sClient(new CommandExecutor(), new FileSystemUtils(), null),
- ApplicationConfigurator applicationConfigurator = new ApplicationConfigurator()) {
- this.k8sClient = k8sClient
- this.applicationConfigurator = applicationConfigurator
- }
-
- ReturnCode run(String[] args) {
- setLogging(args)
-
- log.debug("Reading initial CLI params")
- def cliParams = new Config()
- new CommandLine(cliParams).parseArgs(args)
-
- if (cliParams.application.usageHelpRequested) {
- // if help is requested picocli help is used and printed by execute automatically
- new CommandLine(cliParams).execute(args)
- return ReturnCode.SUCCESS
- }
-
- def version = createVersionOutput()
- if (cliParams.application.versionInfoRequested) {
- println version
- return ReturnCode.SUCCESS
- }
-
- def context = createApplicationContext()
- Application app = context.getBean(Application)
-
- def config = readConfigs(args)
- runHook(app, 'preConfigInit', config)
-
- if (config.application.outputConfigFile) {
- println(config.toYaml(false))
- return ReturnCode.SUCCESS
- }
-
- // Set internal values in config after help/version/output because these should work without connecting to k8s
- // eg a simple docker run .. --help should not fail with connection refused
- config = applicationConfigurator.initConfig(config)
- log.debug("Actual config: ${config.toYaml(true)}")
- runHook(app, 'postConfigInit', config)
-
- context = createApplicationContext()
- register(config, context)
-
- if (config.application.destroy) {
- log.info version
- if (!confirm("Destroying gitops playground in kubernetes cluster '${k8sClient.currentContext}'.", config)) {
- return ReturnCode.NOT_CONFIRMED
- }
-
- Destroyer destroyer = context.getBean(Destroyer)
- destroyer.destroy()
- } else {
- log.info version
- if (!confirm("Applying gitops playground to kubernetes cluster '${k8sClient.currentContext}'.", config)) {
- return ReturnCode.NOT_CONFIRMED
- }
- app = context.getBean(Application)
- app.start()
-
- printWelcomeScreen()
- }
-
- return ReturnCode.SUCCESS
- }
-
- protected String createVersionOutput() {
- def versionName = Version.NAME.replace('\\n', '\n')
-
- if (versionName.trim().startsWith('(')) {
- // When there is no git tag, print commit without parentheses
- versionName = versionName.trim()
- .replace('(', '')
- .replace(')', '')
- }
- return "${APP_NAME} ${versionName}"
- }
-
- /** Can be used as a hook by child classes */
- @SuppressWarnings('GrMethodMayBeStatic')
- // static methods cannot be overridden
- protected void register(Config config, ApplicationContext context) {
- context.registerSingleton(config)
- }
-
- private static boolean confirm(String message, Config config) {
- if (config.application.yes) {
- return true
- }
-
- log.info("\n${message}\nContinue? y/n [n]")
-
- def input = System.in.newReader().readLine()
-
- return input == 'y'
- }
-
- /** Can be used as a hook by tests */
- protected ApplicationContext createApplicationContext() {
- ApplicationContext.run()
- }
-
- private void setLogging(String[] args) {
- Logger logger = (Logger) LoggerFactory.getLogger("com.cloudogu.gitops")
- if (args.contains('--trace') || args.contains('-x')) {
- log.info("Setting loglevel to trace")
- logger.setLevel(Level.TRACE)
- // log levels can be set via picocli.trace sys env - defaults to 'WARN'
- System.setProperty("picocli.trace", "DEBUG")
- } else if (args.contains('--debug') || args.contains('-d')) {
- System.setProperty("picocli.trace", "INFO")
- logger.setLevel(Level.DEBUG)
- log.info("Setting loglevel to debug")
- } else {
- setSimpleLogPattern()
- }
- }
-
- /**
- * Changes log pattern to a simpler one, to reduce noise for normal users
- */
- void setSimpleLogPattern() {
- LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory()
- def rootLogger = loggerContext.getLogger(Logger.ROOT_LOGGER_NAME)
- def defaultPattern = ((rootLogger.getAppender('STDOUT') as ConsoleAppender)
- .getEncoder() as PatternLayoutEncoder).pattern
-
- // Avoid duplicate output by existing appender
- rootLogger.detachAppender('STDOUT')
- PatternLayoutEncoder encoder = new PatternLayoutEncoder()
- // Remove less relevant details from log pattern
- encoder.setPattern(defaultPattern
- .replaceAll(" \\S*%thread\\S* ", " ")
- .replaceAll(" \\S*%logger\\S* ", " "))
- encoder.setContext(loggerContext)
- encoder.start()
- ConsoleAppender appender = new ConsoleAppender<>()
- appender.setName('STDOUT')
- appender.setContext(loggerContext)
- appender.setEncoder(encoder)
- appender.start()
- rootLogger.addAppender(appender)
- }
-
- private Config readConfigs(String[] args) {
- def cliParams = new Config()
- new CommandLine(cliParams).parseArgs(args)
-
- // first evaluate profile for setting predefined values e.g. examples, if applicable
- Config profileConfig = extractProfile(cliParams)
-
- Boolean contentExamples = cliParams.content.examples || profileConfig.content.examples
- Boolean multiTenancyExamples = cliParams.content.multitenancyExamples || profileConfig.content.multitenancyExamples
-
- List configFile = []
- List configMap = []
- Map contentExamplesFile = [:]
- Map multiTenancyContentExamplesFile = [:]
-
- for(String configFileItem : cliParams.application.configFiles) {
- log.debug("Reading config file ${configFileItem}")
- configFile.add(validateConfig(new File(configFileItem).text))
- }
-
- for (String configMapItem : cliParams.application.configMaps) {
- log.debug("Reading config map ${configMapItem}")
- def configValues = k8sClient.getConfigMap(configMapItem, 'config.yaml')
- configMap.add(validateConfig(configValues))
- }
-
- if (contentExamples) {
- String contentExamplesConfigPath = "examples/example-apps-via-content-loader/config.yaml"
- log.debug("Adding example-apps-via-content-loader configuration from '${contentExamplesConfigPath}'")
- contentExamplesFile = validateConfig(new File(contentExamplesConfigPath).text)
- }
-
- if (multiTenancyExamples) {
- String multiTenancyContentExamplesConfigPath = "examples/init-multi-tenancy/managementConfig.yaml"
- log.debug("Adding multi tenancy example-apps config loader from '${multiTenancyContentExamplesConfigPath}'")
- multiTenancyContentExamplesFile = validateConfig(new File(multiTenancyContentExamplesConfigPath).text)
- }
-
-
- // Last one takes precedence
- def configPrecedence = [profileConfig.toMap(), multiTenancyContentExamplesFile, contentExamplesFile, configMap, configFile]
- Map mergedConfigs = [:]
- configPrecedence.flatten().each { element ->
- deepMerge(element as Map, mergedConfigs)
- }
-
- // DeepMerge with default Config values to keep the default values defined in Config.groovy
- mergedConfigs = deepMerge(mergedConfigs, new Config().toMap())
-
- log.debug("Writing CLI params into config")
- Config mergedConfig = Config.fromMap(mergedConfigs)
- new CommandLine(mergedConfig).parseArgs(args)
-
- return mergedConfig
- }
-
- static Map validateConfig(String configValues) {
- def map = new YamlSlurper().parseText(configValues)
- if (!(map instanceof Map)) {
- throw new RuntimeException("Could not parse YAML as map: $map")
- }
- JsonSchemaValidator.validate(map as Map)
- return map as Map
- }
-
- void printWelcomeScreen() {
- log.info '''\n
+ K8sClient k8sClient
+ ApplicationConfigurator applicationConfigurator
+
+ GitopsPlaygroundCli(K8sClient k8sClient = new K8sClient(new CommandExecutor(), new FileSystemUtils(), null),
+ ApplicationConfigurator applicationConfigurator = new ApplicationConfigurator()) {
+ this.k8sClient = k8sClient
+ this.applicationConfigurator = applicationConfigurator
+ }
+
+ ReturnCode run(String[] args) {
+ setLogging(args)
+
+ log.debug("Reading initial CLI params")
+ def cliParams = new Config()
+ new CommandLine(cliParams).parseArgs(args)
+
+ if (cliParams.application.usageHelpRequested) {
+ // if help is requested picocli help is used and printed by execute automatically
+ new CommandLine(cliParams).execute(args)
+ return ReturnCode.SUCCESS
+ }
+
+ def version = createVersionOutput()
+ if (cliParams.application.versionInfoRequested) {
+ println version
+ return ReturnCode.SUCCESS
+ }
+
+ def context = createApplicationContext()
+ Application app = context.getBean(Application)
+
+ def config = readConfigs(args)
+ runHook(app, 'preConfigInit', config)
+
+ if (config.application.outputConfigFile) {
+ println(config.toYaml(false))
+ return ReturnCode.SUCCESS
+ }
+
+ // Set internal values in config after help/version/output because these should work without connecting to k8s
+ // eg a simple docker run .. --help should not fail with connection refused
+ config = applicationConfigurator.initConfig(config)
+ log.debug("Actual config: ${config.toYaml(true)}")
+ runHook(app, 'postConfigInit', config)
+
+ context = createApplicationContext()
+ register(config, context)
+
+ if (config.application.destroy) {
+ log.info version
+ if (!confirm("Destroying gitops playground in kubernetes cluster '${k8sClient.currentContext}'.", config)) {
+ return ReturnCode.NOT_CONFIRMED
+ }
+
+ Destroyer destroyer = context.getBean(Destroyer)
+ destroyer.destroy()
+ } else {
+ log.info version
+ if (!confirm("Applying gitops playground to kubernetes cluster '${k8sClient.currentContext}'.", config)) {
+ return ReturnCode.NOT_CONFIRMED
+ }
+ app = context.getBean(Application)
+ app.start()
+
+ printWelcomeScreen()
+ }
+
+ return ReturnCode.SUCCESS
+ }
+
+ protected String createVersionOutput() {
+ def versionName = Version.NAME.replace('\\n', '\n')
+
+ if (versionName.trim().startsWith('(')) {
+ // When there is no git tag, print commit without parentheses
+ versionName = versionName.trim()
+ .replace('(', '')
+ .replace(')', '')
+ }
+ return "${APP_NAME} ${versionName}"
+ }
+
+ /** Can be used as a hook by child classes */
+ @SuppressWarnings('GrMethodMayBeStatic')
+ // static methods cannot be overridden
+ protected void register(Config config, ApplicationContext context) {
+ context.registerSingleton(config)
+ }
+
+ private static boolean confirm(String message, Config config) {
+ if (config.application.yes) {
+ return true
+ }
+
+ log.info("\n${message}\nContinue? y/n [n]")
+
+ def input = System.in.newReader().readLine()
+
+ return input == 'y'
+ }
+
+ /** Can be used as a hook by tests */
+ protected ApplicationContext createApplicationContext() {
+ ApplicationContext.run()
+ }
+
+ private void setLogging(String[] args) {
+ Logger logger = (Logger) LoggerFactory.getLogger("com.cloudogu.gitops")
+ if (args.contains('--trace') || args.contains('-x')) {
+ log.info("Setting loglevel to trace")
+ logger.setLevel(Level.TRACE)
+ // log levels can be set via picocli.trace sys env - defaults to 'WARN'
+ System.setProperty("picocli.trace", "DEBUG")
+ } else if (args.contains('--debug') || args.contains('-d')) {
+ System.setProperty("picocli.trace", "INFO")
+ logger.setLevel(Level.DEBUG)
+ log.info("Setting loglevel to debug")
+ } else {
+ setSimpleLogPattern()
+ }
+ }
+
+ /**
+ * Changes log pattern to a simpler one, to reduce noise for normal users*/
+ void setSimpleLogPattern() {
+ LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory()
+ def rootLogger = loggerContext.getLogger(Logger.ROOT_LOGGER_NAME)
+ def defaultPattern = ((rootLogger.getAppender('STDOUT') as ConsoleAppender)
+ .getEncoder() as PatternLayoutEncoder).pattern
+
+ // Avoid duplicate output by existing appender
+ rootLogger.detachAppender('STDOUT')
+ PatternLayoutEncoder encoder = new PatternLayoutEncoder()
+ // Remove less relevant details from log pattern
+ encoder.setPattern(defaultPattern
+ .replaceAll(" \\S*%thread\\S* ", " ")
+ .replaceAll(" \\S*%logger\\S* ", " "))
+ encoder.setContext(loggerContext)
+ encoder.start()
+ ConsoleAppender appender = new ConsoleAppender<>()
+ appender.setName('STDOUT')
+ appender.setContext(loggerContext)
+ appender.setEncoder(encoder)
+ appender.start()
+ rootLogger.addAppender(appender)
+ }
+
+ private Config readConfigs(String[] args) {
+ def cliParams = new Config()
+ new CommandLine(cliParams).parseArgs(args)
+
+ // first evaluate profile for setting predefined values e.g. examples, if applicable
+ Config profileConfig = extractProfile(cliParams)
+
+ Boolean contentExamples = cliParams.content.examples || profileConfig.content.examples
+ Boolean multiTenancyExamples = cliParams.content.multitenancyExamples || profileConfig.content.multitenancyExamples
+
+ List configFile = []
+ List configMap = []
+ Map contentExamplesFile = [:]
+ Map multiTenancyContentExamplesFile = [:]
+
+ for (String configFileItem : cliParams.application.configFiles) {
+ log.debug("Reading config file ${configFileItem}")
+ configFile.add(validateConfig(new File(configFileItem).text))
+ }
+
+ for (String configMapItem : cliParams.application.configMaps) {
+ log.debug("Reading config map ${configMapItem}")
+ def configValues = k8sClient.getConfigMap(configMapItem, 'config.yaml')
+ configMap.add(validateConfig(configValues))
+ }
+
+ if (contentExamples) {
+ String contentExamplesConfigPath = "examples/example-apps-via-content-loader/config.yaml"
+ log.debug("Adding example-apps-via-content-loader configuration from '${contentExamplesConfigPath}'")
+ contentExamplesFile = validateConfig(new File(contentExamplesConfigPath).text)
+ }
+
+ if (multiTenancyExamples) {
+ String multiTenancyContentExamplesConfigPath = "examples/init-multi-tenancy/managementConfig.yaml"
+ log.debug("Adding multi tenancy example-apps config loader from '${multiTenancyContentExamplesConfigPath}'")
+ multiTenancyContentExamplesFile = validateConfig(new File(multiTenancyContentExamplesConfigPath).text)
+ }
+
+
+ // Last one takes precedence
+ def configPrecedence = [profileConfig.toMap(), multiTenancyContentExamplesFile, contentExamplesFile, configMap, configFile]
+ Map mergedConfigs = [:]
+ configPrecedence.flatten().each { element -> deepMerge(element as Map, mergedConfigs)
+ }
+
+ // DeepMerge with default Config values to keep the default values defined in Config.groovy
+ mergedConfigs = deepMerge(mergedConfigs, new Config().toMap())
+
+ log.debug("Writing CLI params into config")
+ Config mergedConfig = Config.fromMap(mergedConfigs)
+ new CommandLine(mergedConfig).parseArgs(args)
+
+ return mergedConfig
+ }
+
+ static Map validateConfig(String configValues) {
+ def map = new YamlSlurper().parseText(configValues)
+ if (!(map instanceof Map)) {
+ throw new RuntimeException("Could not parse YAML as map: $map")
+ }
+ JsonSchemaValidator.validate(map as Map)
+ return map as Map
+ }
+
+ void printWelcomeScreen() {
+ log.info '''\n
|----------------------------------------------------------------------------------------------|
| Welcome to the GitOps playground by Cloudogu!
|----------------------------------------------------------------------------------------------|
@@ -264,37 +265,37 @@ class GitopsPlaygroundCli {
| Please be aware, Jenkins and Argo CD may take some time to build and deploy all apps.
|----------------------------------------------------------------------------------------------|
'''
- }
-
- static void runHook(Application app, String methodName, def config) {
- ([new CommonFeatureConfig(), *app.features]).each { feature ->
- // Executing only the method if the derived feature class has implemented the passed methodName
- def mm = feature.metaClass.getMetaMethod(methodName, config)
- if (mm && mm.declaringClass.theClass != Feature) {
- log.debug("Executing ${methodName} hook on feature ${feature.class.name}")
- mm.invoke(feature, config)
- }
- }
- }
-
- private static Config extractProfile(Config newConfig) {
-
- String profile = newConfig.application.profile
-
- Config profileConfig = new Config()
- if (profile) {
- String profileName = "src/main/resources/application-${profile}.yaml"
- log.debug("Loading profile '${profileName}'")
- def file
- try {
- file = new File(profileName)
-
- } catch (Exception e) {
- throw new RuntimeException("Profile '${profileName}' does not exist.")
- }
- Map profileFile = validateConfig(file.text)
- profileConfig = Config.fromMap(profileFile)
- }
- return profileConfig
- }
+ }
+
+ static void runHook(Application app, String methodName, def config) {
+ ([new CommonFeatureConfig(), *app.features]).each { feature ->
+ // Executing only the method if the derived feature class has implemented the passed methodName
+ def mm = feature.metaClass.getMetaMethod(methodName, config)
+ if (mm && mm.declaringClass.theClass != Feature) {
+ log.debug("Executing ${methodName} hook on feature ${feature.class.name}")
+ mm.invoke(feature, config)
+ }
+ }
+ }
+
+ private static Config extractProfile(Config newConfig) {
+
+ String profile = newConfig.application.profile
+
+ Config profileConfig = new Config()
+ if (profile) {
+ String profileName = "src/main/resources/application-${profile}.yaml"
+ log.debug("Loading profile '${profileName}'")
+ def file
+ try {
+ file = new File(profileName)
+
+ } catch (Exception e) {
+ throw new RuntimeException("Profile '${profileName}' does not exist.")
+ }
+ Map profileFile = validateConfig(file.text)
+ profileConfig = Config.fromMap(profileFile)
+ }
+ return profileConfig
+ }
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCliMain.groovy b/src/main/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCliMain.groovy
index 4c3baa21a..d34f10dd4 100644
--- a/src/main/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCliMain.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCliMain.groovy
@@ -1,30 +1,29 @@
package com.cloudogu.gitops.cli
-
import groovy.util.logging.Slf4j
@Slf4j
class GitopsPlaygroundCliMain {
- static void main(String[] args) throws Exception {
- new GitopsPlaygroundCliMain().exec(args, GitopsPlaygroundCli.class)
- }
+ static void main(String[] args) throws Exception {
+ new GitopsPlaygroundCliMain().exec(args, GitopsPlaygroundCli.class)
+ }
+
+ @SuppressWarnings('GrMethodMayBeStatic')
+ // Non-static for easier testing and reuse
+ void exec(String[] args, Class extends GitopsPlaygroundCli> commandClass) {
+ GitopsPlaygroundCli app = commandClass.getDeclaredConstructor().newInstance()
- @SuppressWarnings('GrMethodMayBeStatic')
- // Non-static for easier testing and reuse
- void exec(String[] args, Class extends GitopsPlaygroundCli> commandClass) {
- GitopsPlaygroundCli app = commandClass.getDeclaredConstructor().newInstance()
-
- try {
- System.exit(app.run(args).ordinal())
- } catch (RuntimeException e) {
- if (log.isDebugEnabled()) {
- log.error('', e)
- } else {
- log.error(e.message)
- }
- System.exit(ReturnCode.GENERIC_ERROR.ordinal())
- }
- }
+ try {
+ System.exit(app.run(args).ordinal())
+ } catch (RuntimeException e) {
+ if (log.isDebugEnabled()) {
+ log.error('', e)
+ } else {
+ log.error(e.message)
+ }
+ System.exit(ReturnCode.GENERIC_ERROR.ordinal())
+ }
+ }
-}
+}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCliMainScripted.groovy b/src/main/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCliMainScripted.groovy
index e45b2d0a7..2e23240a8 100644
--- a/src/main/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCliMainScripted.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/cli/GitopsPlaygroundCliMainScripted.groovy
@@ -17,11 +17,16 @@ import com.cloudogu.gitops.git.GitRepoFactory
import com.cloudogu.gitops.jenkins.*
import com.cloudogu.gitops.kubernetes.api.HelmClient
import com.cloudogu.gitops.kubernetes.api.K8sClient
-import com.cloudogu.gitops.utils.*
-import groovy.transform.CompileStatic
-import groovy.util.logging.Slf4j
+import com.cloudogu.gitops.utils.AirGappedUtils
+import com.cloudogu.gitops.utils.CommandExecutor
+import com.cloudogu.gitops.utils.FileSystemUtils
+import com.cloudogu.gitops.utils.NetworkingUtils
+
import io.micronaut.context.ApplicationContext
+
import jakarta.inject.Provider
+import groovy.transform.CompileStatic
+import groovy.util.logging.Slf4j
/**
* Micronaut's dependency injection relies on statically compiled class files with seems incompatible with groovy
@@ -30,71 +35,66 @@ import jakarta.inject.Provider
* air-gapped customer envs.
*
* To make this work the dev image gets it's own main() method that explicitly creates instances of the groovy classes.
- * Yes, redundant and not beautiful, but not using dependency injection is worse.
- */
+ * Yes, redundant and not beautiful, but not using dependency injection is worse.*/
@Slf4j
@CompileStatic
class GitopsPlaygroundCliMainScripted {
- static void main(String[] args) throws Exception {
- new GitopsPlaygroundCliMain().exec(args, GitopsPlaygroundCliScripted)
- }
+ static void main(String[] args) throws Exception {
+ new GitopsPlaygroundCliMain().exec(args, GitopsPlaygroundCliScripted)
+ }
- static class GitopsPlaygroundCliScripted extends GitopsPlaygroundCli {
+ static class GitopsPlaygroundCliScripted extends GitopsPlaygroundCli {
- protected void register(Config config, ApplicationContext context) {
- super.register(config, context)
+ protected void register(Config config, ApplicationContext context) {
+ super.register(config, context)
- FileSystemUtils fileSystemUtils = new FileSystemUtils()
- CommandExecutor executor = new CommandExecutor()
- NetworkingUtils networkingUtils = new NetworkingUtils()
+ FileSystemUtils fileSystemUtils = new FileSystemUtils()
+ CommandExecutor executor = new CommandExecutor()
+ NetworkingUtils networkingUtils = new NetworkingUtils()
- K8sClient k8sClient = new K8sClient(executor, fileSystemUtils, new Provider() {
- @Override
- Config get() {
- return config
- }
- })
+ K8sClient k8sClient = new K8sClient(executor, fileSystemUtils, new Provider() {
+ @Override
+ Config get() {
+ return config
+ }
+ })
- HelmClient helmClient = new HelmClient(executor)
- HttpClientFactory httpClientFactory = new HttpClientFactory()
- GitRepoFactory gitRepoFactory = new GitRepoFactory(config, fileSystemUtils)
- HelmStrategy helmStrategy = new HelmStrategy(config, helmClient)
- GitHandler gitHandler = new GitHandler(config, helmStrategy, fileSystemUtils, k8sClient, networkingUtils)
+ HelmClient helmClient = new HelmClient(executor)
+ HttpClientFactory httpClientFactory = new HttpClientFactory()
+ GitRepoFactory gitRepoFactory = new GitRepoFactory(config, fileSystemUtils)
+ HelmStrategy helmStrategy = new HelmStrategy(config, helmClient)
+ GitHandler gitHandler = new GitHandler(config, helmStrategy, fileSystemUtils, k8sClient, networkingUtils)
- JenkinsApiClient jenkinsApiClient = new JenkinsApiClient(config,
- httpClientFactory.okHttpClientJenkins(config))
+ JenkinsApiClient jenkinsApiClient = new JenkinsApiClient(config,
+ httpClientFactory.okHttpClientJenkins(config))
- context.registerSingleton(k8sClient)
+ context.registerSingleton(k8sClient)
- if (config.application.destroy) {
- context.registerSingleton(new Destroyer([
- new ArgoCDDestructionHandler(config, k8sClient, gitRepoFactory, helmClient, fileSystemUtils, gitHandler),
- new ScmmDestructionHandler(config),
- new JenkinsDestructionHandler(new JobManager(jenkinsApiClient), config, new GlobalPropertyManager(jenkinsApiClient)),
- ]))
- } else {
- Deployer deployer = new Deployer(config, new ArgoCdApplicationStrategy(config, fileSystemUtils, gitRepoFactory, gitHandler), helmStrategy)
- AirGappedUtils airGappedUtils = new AirGappedUtils(config, gitRepoFactory, fileSystemUtils, helmClient, gitHandler)
- Jenkins jenkins = new Jenkins(config, executor, fileSystemUtils, new GlobalPropertyManager(jenkinsApiClient),
- new JobManager(jenkinsApiClient), new UserManager(jenkinsApiClient),
- new PrometheusConfigurator(jenkinsApiClient), helmStrategy, k8sClient, networkingUtils, gitHandler)
+ if (config.application.destroy) {
+ context.registerSingleton(new Destroyer([new ArgoCDDestructionHandler(config, k8sClient, gitRepoFactory, helmClient, fileSystemUtils, gitHandler),
+ new ScmmDestructionHandler(config),
+ new JenkinsDestructionHandler(new JobManager(jenkinsApiClient), config, new GlobalPropertyManager(jenkinsApiClient)),]))
+ } else {
+ Deployer deployer = new Deployer(config, new ArgoCdApplicationStrategy(config, fileSystemUtils, gitRepoFactory, gitHandler), helmStrategy)
+ AirGappedUtils airGappedUtils = new AirGappedUtils(config, gitRepoFactory, fileSystemUtils, helmClient, gitHandler)
+ Jenkins jenkins = new Jenkins(config, executor, fileSystemUtils, new GlobalPropertyManager(jenkinsApiClient),
+ new JobManager(jenkinsApiClient), new UserManager(jenkinsApiClient),
+ new PrometheusConfigurator(jenkinsApiClient), helmStrategy, k8sClient, networkingUtils, gitHandler)
- // make sure the order of features is in same order as the @Order values
- context.registerSingleton(new Application(config, [
- new Registry(config, fileSystemUtils, k8sClient, helmStrategy),
- gitHandler,
- jenkins,
- new ArgoCD(config, k8sClient, helmClient, fileSystemUtils, gitRepoFactory, gitHandler),
- new Ingress(config, fileSystemUtils, deployer, k8sClient, airGappedUtils, gitHandler),
- new CertManager(config, fileSystemUtils, deployer, k8sClient, airGappedUtils, gitHandler),
- new Mail(config, fileSystemUtils, deployer, k8sClient, airGappedUtils, gitHandler),
- new Monitoring(config, fileSystemUtils, deployer, k8sClient, airGappedUtils, gitRepoFactory, gitHandler),
- new ExternalSecretsOperator(config, fileSystemUtils, deployer, k8sClient, airGappedUtils, gitHandler),
- new Vault(config, fileSystemUtils, k8sClient, deployer, airGappedUtils, gitHandler),
- new ContentLoader(config, k8sClient, gitRepoFactory, jenkins, gitHandler),
- ]))
- }
- }
- }
-}
+ // make sure the order of features is in same order as the @Order values
+ context.registerSingleton(new Application(config, [new Registry(config, fileSystemUtils, k8sClient, helmStrategy),
+ gitHandler,
+ jenkins,
+ new ArgoCD(config, k8sClient, helmClient, fileSystemUtils, gitRepoFactory, gitHandler),
+ new Ingress(config, fileSystemUtils, deployer, k8sClient, airGappedUtils, gitHandler),
+ new CertManager(config, fileSystemUtils, deployer, k8sClient, airGappedUtils, gitHandler),
+ new Mail(config, fileSystemUtils, deployer, k8sClient, airGappedUtils, gitHandler),
+ new Monitoring(config, fileSystemUtils, deployer, k8sClient, airGappedUtils, gitRepoFactory, gitHandler),
+ new ExternalSecretsOperator(config, fileSystemUtils, deployer, k8sClient, airGappedUtils, gitHandler),
+ new Vault(config, fileSystemUtils, k8sClient, deployer, airGappedUtils, gitHandler),
+ new ContentLoader(config, k8sClient, gitRepoFactory, jenkins, gitHandler),]))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/cli/ReturnCode.groovy b/src/main/groovy/com/cloudogu/gitops/cli/ReturnCode.groovy
index a1d6e7274..26e0a4631 100644
--- a/src/main/groovy/com/cloudogu/gitops/cli/ReturnCode.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/cli/ReturnCode.groovy
@@ -1,3 +1,5 @@
package com.cloudogu.gitops.cli
-enum ReturnCode { SUCCESS, NOT_CONFIRMED, GENERIC_ERROR }
\ No newline at end of file
+enum ReturnCode {
+ SUCCESS, NOT_CONFIRMED, GENERIC_ERROR
+}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/config/ApplicationConfigurator.groovy b/src/main/groovy/com/cloudogu/gitops/config/ApplicationConfigurator.groovy
index a058e09d9..51f9e8a26 100644
--- a/src/main/groovy/com/cloudogu/gitops/config/ApplicationConfigurator.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/config/ApplicationConfigurator.groovy
@@ -1,324 +1,319 @@
package com.cloudogu.gitops.config
import com.cloudogu.gitops.utils.FileSystemUtils
+
import groovy.util.logging.Slf4j
@Slf4j
class ApplicationConfigurator {
- private FileSystemUtils fileSystemUtils
-
- ApplicationConfigurator(FileSystemUtils fileSystemUtils = new FileSystemUtils()) {
- this.fileSystemUtils = fileSystemUtils
- }
-
- /**
- * Sets dynamic fields and validates params
- */
- Config initConfig(Config newConfig) {
-
- addAdditionalApplicationConfig(newConfig)
- addNamePrefix(newConfig)
-
- addScmConfig(newConfig)
-
- addRegistryConfig(newConfig)
-
- addJenkinsConfig(newConfig)
-
- addFeatureConfig(newConfig)
-
- evaluateBaseUrl(newConfig)
-
- setResourceInclusionsCluster(newConfig)
-
- setMultiTenantModeConfig(newConfig)
-
- return newConfig
- }
-
- private void addFeatureConfig(Config newConfig) {
- if (newConfig.features.secrets.vault.mode)
- newConfig.features.secrets.active = true
-
- if (newConfig.features.mail.smtpAddress || newConfig.features.mail.mailServer)
- newConfig.features.mail.active = true
- if (newConfig.features.mail.smtpAddress && newConfig.features.mail.mailServer) {
- newConfig.features.mail.mailServer = false
- log.warn("Enabled both external Mailserver and in-cluster Mailserver! Implicitly deactivating in-cluster mailserver")
- }
-
- if (newConfig.features.ingress.active && !newConfig.application.baseUrl) {
- log.warn("Ingress-controller is activated without baseUrl parameter. Services will not be accessible by hostnames. To avoid this use baseUrl with ingress. ")
- }
- if (newConfig.content.examples) {
- if (!newConfig.registry.active) {
- throw new RuntimeException("content.examples requires either registry.active or registry.url")
- }
- String prefix = newConfig.application.namePrefix
- newConfig.content.namespaces += [prefix + "example-apps-staging", prefix + "example-apps-production"]
- }
- }
-
- private void addNamePrefix(Config newConfig) {
- String namePrefix = newConfig.application.namePrefix
- if (namePrefix) {
- if (!namePrefix.endsWith('-')) {
- newConfig.application.namePrefix = "${namePrefix}-"
- }
- newConfig.application.namePrefixForEnvVars = "${(newConfig.application.namePrefix as String).toUpperCase().replace('-', '_')}"
- }
- }
-
- private void addRegistryConfig(Config newConfig) {
- // Process image pull secrets first, they might even be relevant if no registry is set
- if (newConfig.registry.createImagePullSecrets) {
- String username = newConfig.registry.readOnlyUsername ?: newConfig.registry.username
- String password = newConfig.registry.readOnlyPassword ?: newConfig.registry.password
- if (!username || !password) {
- throw new RuntimeException("createImagePullSecrets needs to be used with either registry username and password or the readOnly variants")
- }
- }
-
- if (newConfig.registry.url) {
- newConfig.registry.internal = false
- newConfig.registry.active = true
- } else if (newConfig.registry.active) {
- /* Internal Docker registry must be on localhost. Otherwise docker will use HTTPS, leading to errors on
- docker push in the example application's Jenkins Jobs.
- Both setting up HTTPS or allowing insecure registry via daemon.json makes the playground difficult to use.
- So, always use localhost.
- Allow overriding the port, in case multiple playground instance run on a single host in different
- k3d clusters. */
- newConfig.registry.internal = true
- newConfig.registry.url = "localhost:${newConfig.registry.internalPort}"
- } else {
- // Registry not active, no need to set the following values
- return
- }
-
- if (newConfig.registry.proxyUrl) {
- newConfig.registry.twoRegistries = true
- if (!newConfig.registry.proxyUsername || !newConfig.registry.proxyPassword) {
- throw new RuntimeException("Proxy URL needs to be used with proxy-username and proxy-password")
- }
- }
- }
-
- private void addAdditionalApplicationConfig(Config newConfig) {
- if (System.getenv("KUBERNETES_SERVICE_HOST")) {
- log.debug("installation is running in kubernetes.")
- newConfig.application.runningInsideK8s = true
- }
- }
-
- private void addScmConfig(Config newConfig) {
- log.debug("Adding additional config for SCM")
-
- if (newConfig.scm.scmManager.url) {
- log.debug("Setting external scmm config")
- newConfig.scm.scmManager.internal = false
- newConfig.scm.scmManager.urlForJenkins = newConfig.scm.scmManager.url
- } else {
- log.debug("Setting configs for internal SCM-Manager")
- // We use the K8s service as default name here, because it is the only option:
- // "scmm.localhost" will not work inside the Pods and k3d-container IP + Port (e.g. 172.x.y.z:9091)
- // will not work on Windows and MacOS.
- newConfig.scm.scmManager.urlForJenkins =
- "http://scmm.${newConfig.application.namePrefix}scm-manager.svc.cluster.local/scm"
-
- // More internal fields are set lazily in ScmManger.groovy (after SCMM is deployed and ports are known)
- }
-
- // We probably could get rid of some of the complexity by refactoring url, host and ingress into a single var
- if (newConfig.application.baseUrl) {
- newConfig.scm.scmManager.ingress = new URL(injectSubdomain("scmm",
- newConfig.application.baseUrl as String, newConfig.application.urlSeparatorHyphen as Boolean)).host
- }
- // When specific user/pw are not set, set them to global values
- if (newConfig.scm.scmManager.password === Config.DEFAULT_ADMIN_PW) {
- newConfig.scm.scmManager.password = newConfig.application.password
- }
- if (newConfig.scm.scmManager.username === Config.DEFAULT_ADMIN_USER) {
- newConfig.scm.scmManager.username = newConfig.application.username
- }
-
-
- }
-
- private void addJenkinsConfig(Config newConfig) {
- log.debug("Adding additional config for Jenkins")
- if (newConfig.jenkins.url) {
- log.debug("Setting external jenkins config")
- newConfig.jenkins.active = true
- newConfig.jenkins.internal = false
- newConfig.jenkins.urlForScm = newConfig.jenkins.url
- } else if (newConfig.jenkins.active) {
- log.debug("Setting configs for internal jenkins")
- // We use the K8s service as default name here, because it is the only option:
- // "jenkins.localhost" will not work inside the Pods and k3d-container IP + Port (e.g. 172.x.y.z:9090)
- // will not work on Windows and MacOS.
- newConfig.jenkins.urlForScm = "http://jenkins.${newConfig.application.namePrefix}jenkins.svc.cluster.local"
-
- // More internal fields are set lazily in Jenkins.groovy (after Jenkins is deployed and ports are known)
- } else {
- // Jenkins not active, no need to set the following values
- return
- }
-
- if (newConfig.application.baseUrl) {
- newConfig.jenkins.ingress = new URL(injectSubdomain("jenkins",
- newConfig.application.baseUrl, newConfig.application.urlSeparatorHyphen)).host
- }
- // When specific user/pw are not set, set them to global values
- if (newConfig.jenkins.username === Config.DEFAULT_ADMIN_USER) {
- newConfig.jenkins.username = newConfig.application.username
- }
- if (newConfig.jenkins.password === Config.DEFAULT_ADMIN_PW) {
- newConfig.jenkins.password = newConfig.application.password
- }
- }
-
- private void evaluateBaseUrl(Config newConfig) {
- String baseUrl = newConfig.application.baseUrl
- if (!baseUrl) {
- return
- }
- log.debug("Base URL set, adapting to individual tools")
- def argocd = newConfig.features.argocd
- def mail = newConfig.features.mail
- def monitoring = newConfig.features.monitoring
- def vault = newConfig.features.secrets.vault
- boolean urlSeparatorHyphen = newConfig.application.urlSeparatorHyphen
-
- if (argocd.active && !argocd.url) {
- argocd.url = injectSubdomain("argocd", baseUrl, urlSeparatorHyphen)
- log.debug("Setting ArgoCD URL ${argocd.url}")
- }
- if (mail.mailServer && !mail.mailUrl) {
- mail.mailUrl = injectSubdomain('mail', baseUrl, urlSeparatorHyphen)
- log.debug("Setting Mail URL ${mail.mailUrl}")
- }
- if (monitoring.active && !monitoring.grafanaUrl) {
- monitoring.grafanaUrl = injectSubdomain('grafana', baseUrl, urlSeparatorHyphen)
- log.debug("Setting Monitoring URL ${monitoring.grafanaUrl}")
- }
- if (newConfig.features.secrets.active && !vault.url) {
- vault.url = injectSubdomain('vault', baseUrl, urlSeparatorHyphen)
- log.debug("Setting Vault URL ${vault.url}")
- }
-
- }
-
- void setMultiTenantModeConfig(Config newConfig) {
- if (newConfig.multiTenant.useDedicatedInstance) {
- if (!newConfig.application.namePrefix) {
- throw new RuntimeException('To enable Central Multi-Tenant mode, you must define a name prefix to distinguish between instances.')
- }
-
- if (!newConfig.features.argocd.operator) {
- newConfig.features.argocd.operator = true
- }
-
- // Removes trailing slash from the input URL to avoid duplicated slashes in further URL handling
- if (newConfig.multiTenant.scmManager.url) {
- String urlString = newConfig.multiTenant.scmManager.url.toString()
- if (urlString.endsWith("/")) {
- urlString = urlString[0..-2]
- }
- newConfig.multiTenant.scmManager.url = urlString
- }
-
- //Disabling Ingress in DedicatedInstances Mode for now.
- //Ingress has to be handled by Cluster, not by this tenant.
- //Ingress has to be handled manually for local dev.
- //See /scripts/local/ for local dev.
- newConfig.features.ingress.active = false
- }
- }
-
- /**
- *
- * @param subdomain , e.g. argocd
- * @param baseUrl e.g. http://localhost:8080
- * @param urlSeparatorHyphen
- * @return e.g. http://argocd.localhost:8080
- */
- private String injectSubdomain(String subdomain, String baseUrl, boolean urlSeparatorHyphen) {
- URL url = new URL(baseUrl)
- String newUrl
-
- if (urlSeparatorHyphen) {
- newUrl = url.getProtocol() + "://" + subdomain + "-" + url.getHost()
- } else {
- newUrl = url.getProtocol() + "://" + subdomain + "." + url.getHost()
- }
- if (url.getPort() != -1) {
- newUrl += ":" + url.getPort()
- }
- newUrl += url.getPath()
- return newUrl
- }
-
- private void setResourceInclusionsCluster(Config configToSet) {
- // Return early if NOT deploying via operator
- if (!configToSet.features.argocd.operator) {
- log.debug("ArgoCD operator is not enabled. Skipping features.argocd.resourceInclusionsCluster setup.")
- return
- }
- log.info("Starting setup of features.argocd.resourceInclusionsCluster for ArgoCD Operator")
-
- if (!isUrlSetAndValid(configToSet)) {
- // If features.argocd.resourceInclusionsClus namespaces = []
-
- @JsonPropertyDescription(CONTENT_REPO_DESCRIPTION)
- List repos = []
-
- @JsonPropertyDescription(CONTENT_VARIABLES_DESCRIPTION)
- Map variables = [:]
-
- @Option(names = ['--content-whitelist'], description = CONTENT_STATICSWHITELIST_ENABLED_DESCRIPTION)
- @JsonPropertyDescription(CONTENT_STATICSWHITELIST_ENABLED_DESCRIPTION)
- Boolean useWhitelist = false
-
- @JsonPropertyDescription(CONTENT_STATICSWHITELIST_DESCRIPTION)
- Set allowedStaticsWhitelist = [
- 'java.lang.String',
- 'java.lang.Integer',
- 'java.lang.Long',
- 'java.lang.Double',
- 'java.lang.Float',
- 'java.lang.Boolean',
- 'java.lang.Math',
- 'com.cloudogu.gitops.utils.DockerImageParser'
- ] as Set
-
- static class ContentRepositorySchema {
- static final String DEFAULT_PATH = '.'
- // This is controversial. Forcing users to explicitly choose a type requires them to understand the concept
- // of types. What would be a good default? The simplest use case ist MIRROR from url to target.
- // COPY and FOLDER_BASED are more advanced use cases. So we choose MIRROR as the default.
- static final ContentRepoType DEFAULT_TYPE = ContentRepoType.MIRROR
-
- @JsonPropertyDescription(CONTENT_REPO_URL_DESCRIPTION)
- String url = ''
-
- @JsonPropertyDescription(CONTENT_REPO_PATH_DESCRIPTION)
- String path = DEFAULT_PATH
-
- @JsonPropertyDescription(CONTENT_REPO_REF_DESCRIPTION)
- String ref = ''
-
- @JsonPropertyDescription(CONTENT_REPO_TARGET_REF_DESCRIPTION)
- String targetRef = ''
-
- @JsonPropertyDescription(CONTENT_REPO_CREDENTIALS_DESCRIPTION)
- Credentials credentials
-
- @JsonPropertyDescription(CONTENT_REPO_TEMPLATING_DESCRIPTION)
- Boolean templating = false
-
- @JsonPropertyDescription(CONTENT_REPO_TYPE_DESCRIPTION)
- ContentRepoType type = DEFAULT_TYPE
-
- @JsonPropertyDescription(CONTENT_REPO_TARGET_DESCRIPTION)
- String target = ''
-
- @JsonPropertyDescription(CONTENT_REPO_TARGET_OVERWRITE_MODE_DESCRIPTION)
- OverwriteMode overwriteMode = OverwriteMode.INIT
- // Defensively use init to not override existing files by default
-
- @JsonPropertyDescription(CONTENT_REPO_CREATE_JENKINS_JOB_DESCRIPTION)
- Boolean createJenkinsJob = false
-
- }
- }
-
- static class HelmConfig {
- @JsonPropertyDescription(HELM_CONFIG_CHART_DESCRIPTION)
- String chart = ''
- @JsonPropertyDescription(HELM_CONFIG_REPO_URL_DESCRIPTION)
- String repoURL = ''
- @JsonPropertyDescription(HELM_CONFIG_VERSION_DESCRIPTION)
- String version = ''
- }
-
- static class HelmConfigWithValues extends HelmConfig {
- @JsonPropertyDescription(HELM_CONFIG_VALUES_DESCRIPTION)
- Map values = [:]
- }
-
- static class RegistrySchema {
- Boolean internal = true
- Boolean twoRegistries = false
-
- @Option(names = ['--registry'], description = REGISTRY_ENABLE_DESCRIPTION)
- @JsonPropertyDescription(REGISTRY_ENABLE_DESCRIPTION)
- Boolean active = false
-
- @Option(names = ['--internal-registry-port'], description = REGISTRY_INTERNAL_PORT_DESCRIPTION)
- @JsonPropertyDescription(REGISTRY_INTERNAL_PORT_DESCRIPTION)
- Integer internalPort = DEFAULT_REGISTRY_PORT
-
- @Option(names = ['--registry-url'], description = REGISTRY_URL_DESCRIPTION)
- @JsonPropertyDescription(REGISTRY_URL_DESCRIPTION)
- String url = ''
-
- @Option(names = ['--registry-path'], description = REGISTRY_PATH_DESCRIPTION)
- @JsonPropertyDescription(REGISTRY_PATH_DESCRIPTION)
- String path = ''
-
- @Option(names = ['--registry-username'], description = REGISTRY_USERNAME_DESCRIPTION)
- @JsonPropertyDescription(REGISTRY_USERNAME_DESCRIPTION)
- String username = ''
-
- @Option(names = ['--registry-password'], description = REGISTRY_PASSWORD_DESCRIPTION)
- @JsonPropertyDescription(REGISTRY_PASSWORD_DESCRIPTION)
- String password = ''
-
- // Alternative: Use different registries, e.g. in air-gapped envs
- // "Proxy" registry for 3rd party images, e.g. base images
- @Option(names = ['--registry-proxy-url'], description = REGISTRY_PROXY_URL_DESCRIPTION)
- @JsonPropertyDescription(REGISTRY_PROXY_URL_DESCRIPTION)
- String proxyUrl = ''
-
- @Option(names = ['--registry-proxy-username'], description = REGISTRY_PROXY_PASSWORD_DESCRIPTION)
- @JsonPropertyDescription(REGISTRY_PROXY_USERNAME_DESCRIPTION)
- String proxyUsername = ''
-
- @Option(names = ['--registry-proxy-password'], description = 'Optional when --registry-proxy-url is set')
- @JsonPropertyDescription(REGISTRY_PROXY_PASSWORD_DESCRIPTION)
- String proxyPassword = ''
-
- // Alternative set of credentials for url, used only for image pull secrets
- @Option(names = ['--registry-username-read-only'], description = REGISTRY_USERNAME_RO_DESCRIPTION)
- @JsonPropertyDescription(REGISTRY_USERNAME_RO_DESCRIPTION)
- String readOnlyUsername = ''
-
- @Option(names = ['--registry-password-read-only'], description = REGISTRY_PASSWORD_RO_DESCRIPTION)
- @JsonPropertyDescription(REGISTRY_PASSWORD_RO_DESCRIPTION)
- String readOnlyPassword = ''
-
- @Option(names = ['--create-image-pull-secrets'], description = REGISTRY_CREATE_IMAGE_PULL_SECRETS_DESCRIPTION)
- @JsonPropertyDescription(REGISTRY_CREATE_IMAGE_PULL_SECRETS_DESCRIPTION)
- Boolean createImagePullSecrets = false
-
- @JsonPropertyDescription(HELM_CONFIG_DESCRIPTION)
- HelmConfigWithValues helm = new HelmConfigWithValues(
- chart: 'docker-registry',
- repoURL: 'https://twuni.github.io/docker-registry.helm',
- version: '3.0.0')
-
- }
-
- static class JenkinsSchema {
- Boolean internal = true
- /* When installing via Docker we have to distinguish jenkins.url (which is a local IP address) from
- the Jenkins URL used by SCMM.
-
- This is the URL configured in SCMM inside the Jenkins Plugin, e.g. at http://scmm.localhost/scm/admin/settings/jenkins
- See addJenkinsConfig() and the comment at scmm.urlForJenkins */
- String urlForScm = ''
- String ingress = ''
- // Bash image used with internal Jenkins only
- String internalBashImage = 'bash:5'
- /* Docker client image, downloaded on internal Jenkins only
- For updating, delete pvc jenkins-docker-client
- When updating, we should not use too recent version, to not break support for LTS distros like debian
- https://docs.docker.com/engine/install/debian/#os-requirements -> oldstable
- For example:
- $ curl -s https://download.docker.com/linux/debian/dists/bullseye/stable/binary-amd64/Packages | grep -EA5 'Package\: docker-ce$' | grep Version | sort | uniq | tail -n1
- Version: 5:27.1.1-1~debian.11~bullseye */
- String internalDockerClientVersion = '27.1.2'
-
- @Option(names = ['--jenkins'], description = JENKINS_ENABLE_DESCRIPTION)
- @JsonPropertyDescription(JENKINS_ENABLE_DESCRIPTION)
- Boolean active = false
-
- @Option(names = ['--jenkins-skip-restart'], description = JENKINS_SKIP_RESTART_DESCRIPTION)
- @JsonPropertyDescription(JENKINS_SKIP_RESTART_DESCRIPTION)
- Boolean skipRestart = false
-
- @Option(names = ['--jenkins-skip-plugins'], description = JENKINS_SKIP_PLUGINS_DESCRIPTION)
- @JsonPropertyDescription(JENKINS_SKIP_PLUGINS_DESCRIPTION)
- Boolean skipPlugins = false
-
- @Option(names = ['--jenkins-url'], description = JENKINS_URL_DESCRIPTION)
- @JsonPropertyDescription(JENKINS_URL_DESCRIPTION)
- String url = ''
-
- @Option(names = ['--jenkins-username'], description = JENKINS_USERNAME_DESCRIPTION)
- @JsonPropertyDescription(JENKINS_USERNAME_DESCRIPTION)
- String username = DEFAULT_ADMIN_USER
-
- @Option(names = ['--jenkins-password'], description = JENKINS_PASSWORD_DESCRIPTION)
- @JsonPropertyDescription(JENKINS_PASSWORD_DESCRIPTION)
- String password = DEFAULT_ADMIN_PW
-
- @Option(names = ['--jenkins-metrics-username'], description = JENKINS_METRICS_USERNAME_DESCRIPTION)
- @JsonPropertyDescription(JENKINS_METRICS_USERNAME_DESCRIPTION)
- String metricsUsername = "metrics"
-
- @Option(names = ['--jenkins-metrics-password'], description = JENKINS_METRICS_PASSWORD_DESCRIPTION)
- @JsonPropertyDescription(JENKINS_METRICS_PASSWORD_DESCRIPTION)
- String metricsPassword = "metrics"
-
- @Option(names = ['--maven-central-mirror'], description = MAVEN_CENTRAL_MIRROR_DESCRIPTION)
- @JsonPropertyDescription(MAVEN_CENTRAL_MIRROR_DESCRIPTION)
- String mavenCentralMirror = ''
-
- @Option(names = ["--jenkins-additional-envs"], description = JENKINS_ADDITIONAL_ENVS_DESCRIPTION, split = ",", required = false)
- @JsonPropertyDescription(JENKINS_ADDITIONAL_ENVS_DESCRIPTION)
- Map additionalEnvs = [:]
-
- @JsonPropertyDescription(HELM_CONFIG_DESCRIPTION)
- HelmConfigWithValues helm = new HelmConfigWithValues(
- chart: 'jenkins',
- repoURL: 'https://charts.jenkins.io',
- version: '5.8.43')
- }
-
- static class ApplicationSchema {
- Boolean runningInsideK8s = false
- String namePrefixForEnvVars = ''
- String internalKubernetesApiUrl = ''
- String localHelmChartFolder = System.getenv('LOCAL_HELM_CHART_FOLDER')
-
- NamespaceSchema namespaces = new NamespaceSchema()
-
- @Option(names = ['--config-file'], description = CONFIG_FILE_DESCRIPTION, split = ',')
- List configFiles = []
-
- @Option(names = ['--config-map'], description = CONFIG_MAP_DESCRIPTION, split = ',')
- List configMaps = []
-
- @Option(names = ['-d', '--debug'], description = DEBUG_DESCRIPTION, scope = ScopeType.INHERIT)
- Boolean debug
-
- @Option(names = ['-x', '--trace'], description = TRACE_DESCRIPTION, scope = ScopeType.INHERIT)
- Boolean trace
-
- @Option(names = ['--output-config-file'], description = OUTPUT_CONFIG_FILE_DESCRIPTION, help = true)
- Boolean outputConfigFile = false
-
- @Option(names = ["-v", "--version"], help = true, description = "Display version and license info")
- Boolean versionInfoRequested = false
-
- // We define or own --version, so we need to define our own help param.
- // The param itself is not used, "usageHelp = true" leads to hel being printed
- @Option(names = ["-h", "--help"], usageHelp = true, description = "Display this help message")
- Boolean usageHelpRequested = false
-
- @Option(names = ['--insecure'], description = INSECURE_DESCRIPTION)
- @JsonPropertyDescription(INSECURE_DESCRIPTION)
- Boolean insecure = false
-
- @Option(names = ['--openshift'], description = OPENSHIFT_DESCRIPTION)
- @JsonPropertyDescription(OPENSHIFT_DESCRIPTION)
- Boolean openshift = false
-
- @Option(names = ['--username'], description = USERNAME_DESCRIPTION)
- @JsonPropertyDescription(USERNAME_DESCRIPTION)
- String username = DEFAULT_ADMIN_USER
-
- @Option(names = ['--password'], description = PASSWORD_DESCRIPTION)
- @JsonPropertyDescription(PASSWORD_DESCRIPTION)
- String password = DEFAULT_ADMIN_PW
+ @JsonPropertyDescription(REGISTRY_DESCRIPTION)
+ @Mixin
+ RegistrySchema registry = new RegistrySchema()
- @Option(names = ['-y', '--yes'], description = PIPE_YES_DESCRIPTION)
- @JsonPropertyDescription(PIPE_YES_DESCRIPTION)
- Boolean yes = false
+ @JsonPropertyDescription(JENKINS_DESCRIPTION)
+ @Mixin
+ JenkinsSchema jenkins = new JenkinsSchema()
+
+ @JsonPropertyDescription(MULTITENANT_DESCRIPTION)
+ @Mixin
+ MultiTenantSchema multiTenant = new MultiTenantSchema()
+
+ @JsonPropertyDescription(SCM_DESCRIPTION)
+ @Mixin
+ ScmTenantSchema scm = new ScmTenantSchema()
+
+ @JsonPropertyDescription(APPLICATION_DESCRIPTION)
+ @Mixin
+ ApplicationSchema application = new ApplicationSchema()
+
+ @JsonPropertyDescription(FEATURES_DESCRIPTION)
+ @Mixin
+ FeaturesSchema features = new FeaturesSchema()
+
+ @JsonPropertyDescription(CONTENT_DESCRIPTION)
+ @Mixin
+ ContentSchema content = new ContentSchema()
+
+ static class ContentSchema {
+ @Option(names = ['--content-examples'], description = CONTENT_EXAMPLES_DESCRIPTION)
+ @JsonPropertyDescription(CONTENT_EXAMPLES_DESCRIPTION)
+ Boolean examples = false
+
+ @Option(names = ['--multi-tenancy-examples'], description = CONTENT_MULTI_TENANCY_EXAMPLES_DESCRIPTION)
+ @JsonPropertyDescription(CONTENT_MULTI_TENANCY_EXAMPLES_DESCRIPTION)
+ Boolean multitenancyExamples = false
+
+ @JsonPropertyDescription(CONTENT_NAMESPACES_DESCRIPTION)
+ List namespaces = []
+
+ @JsonPropertyDescription(CONTENT_REPO_DESCRIPTION)
+ List repos = []
+
+ @JsonPropertyDescription(CONTENT_VARIABLES_DESCRIPTION)
+ Map variables = [:]
+
+ @Option(names = ['--content-whitelist'], description = CONTENT_STATICSWHITELIST_ENABLED_DESCRIPTION)
+ @JsonPropertyDescription(CONTENT_STATICSWHITELIST_ENABLED_DESCRIPTION)
+ Boolean useWhitelist = false
+
+ @JsonPropertyDescription(CONTENT_STATICSWHITELIST_DESCRIPTION)
+ Set allowedStaticsWhitelist = ['java.lang.String',
+ 'java.lang.Integer',
+ 'java.lang.Long',
+ 'java.lang.Double',
+ 'java.lang.Float',
+ 'java.lang.Boolean',
+ 'java.lang.Math',
+ 'com.cloudogu.gitops.utils.DockerImageParser'] as Set
+
+ static class ContentRepositorySchema {
+ static final String DEFAULT_PATH = '.'
+ // This is controversial. Forcing users to explicitly choose a type requires them to understand the concept
+ // of types. What would be a good default? The simplest use case ist MIRROR from url to target.
+ // COPY and FOLDER_BASED are more advanced use cases. So we choose MIRROR as the default.
+ static final ContentRepoType DEFAULT_TYPE = ContentRepoType.MIRROR
+
+ @JsonPropertyDescription(CONTENT_REPO_URL_DESCRIPTION)
+ String url = ''
+
+ @JsonPropertyDescription(CONTENT_REPO_PATH_DESCRIPTION)
+ String path = DEFAULT_PATH
+
+ @JsonPropertyDescription(CONTENT_REPO_REF_DESCRIPTION)
+ String ref = ''
+
+ @JsonPropertyDescription(CONTENT_REPO_TARGET_REF_DESCRIPTION)
+ String targetRef = ''
+
+ @JsonPropertyDescription(CONTENT_REPO_CREDENTIALS_DESCRIPTION)
+ Credentials credentials
+
+ @JsonPropertyDescription(CONTENT_REPO_TEMPLATING_DESCRIPTION)
+ Boolean templating = false
+
+ @JsonPropertyDescription(CONTENT_REPO_TYPE_DESCRIPTION)
+ ContentRepoType type = DEFAULT_TYPE
+
+ @JsonPropertyDescription(CONTENT_REPO_TARGET_DESCRIPTION)
+ String target = ''
+
+ @JsonPropertyDescription(CONTENT_REPO_TARGET_OVERWRITE_MODE_DESCRIPTION)
+ OverwriteMode overwriteMode = OverwriteMode.INIT
+ // Defensively use init to not override existing files by default
+
+ @JsonPropertyDescription(CONTENT_REPO_CREATE_JENKINS_JOB_DESCRIPTION)
+ Boolean createJenkinsJob = false
+
+ }
+ }
+
+ static class HelmConfig {
+ @JsonPropertyDescription(HELM_CONFIG_CHART_DESCRIPTION)
+ String chart = ''
+ @JsonPropertyDescription(HELM_CONFIG_REPO_URL_DESCRIPTION)
+ String repoURL = ''
+ @JsonPropertyDescription(HELM_CONFIG_VERSION_DESCRIPTION)
+ String version = ''
+ }
+
+ static class HelmConfigWithValues extends HelmConfig {
+ @JsonPropertyDescription(HELM_CONFIG_VALUES_DESCRIPTION)
+ Map values = [:]
+ }
+
+ static class RegistrySchema {
+ Boolean internal = true
+ Boolean twoRegistries = false
+
+ @Option(names = ['--registry'], description = REGISTRY_ENABLE_DESCRIPTION)
+ @JsonPropertyDescription(REGISTRY_ENABLE_DESCRIPTION)
+ Boolean active = false
+
+ @Option(names = ['--internal-registry-port'], description = REGISTRY_INTERNAL_PORT_DESCRIPTION)
+ @JsonPropertyDescription(REGISTRY_INTERNAL_PORT_DESCRIPTION)
+ Integer internalPort = DEFAULT_REGISTRY_PORT
+
+ @Option(names = ['--registry-url'], description = REGISTRY_URL_DESCRIPTION)
+ @JsonPropertyDescription(REGISTRY_URL_DESCRIPTION)
+ String url = ''
+
+ @Option(names = ['--registry-path'], description = REGISTRY_PATH_DESCRIPTION)
+ @JsonPropertyDescription(REGISTRY_PATH_DESCRIPTION)
+ String path = ''
+
+ @Option(names = ['--registry-username'], description = REGISTRY_USERNAME_DESCRIPTION)
+ @JsonPropertyDescription(REGISTRY_USERNAME_DESCRIPTION)
+ String username = ''
+
+ @Option(names = ['--registry-password'], description = REGISTRY_PASSWORD_DESCRIPTION)
+ @JsonPropertyDescription(REGISTRY_PASSWORD_DESCRIPTION)
+ String password = ''
+
+ // Alternative: Use different registries, e.g. in air-gapped envs
+ // "Proxy" registry for 3rd party images, e.g. base images
+ @Option(names = ['--registry-proxy-url'], description = REGISTRY_PROXY_URL_DESCRIPTION)
+ @JsonPropertyDescription(REGISTRY_PROXY_URL_DESCRIPTION)
+ String proxyUrl = ''
+
+ @Option(names = ['--registry-proxy-username'], description = REGISTRY_PROXY_PASSWORD_DESCRIPTION)
+ @JsonPropertyDescription(REGISTRY_PROXY_USERNAME_DESCRIPTION)
+ String proxyUsername = ''
+
+ @Option(names = ['--registry-proxy-password'], description = 'Optional when --registry-proxy-url is set')
+ @JsonPropertyDescription(REGISTRY_PROXY_PASSWORD_DESCRIPTION)
+ String proxyPassword = ''
+
+ // Alternative set of credentials for url, used only for image pull secrets
+ @Option(names = ['--registry-username-read-only'], description = REGISTRY_USERNAME_RO_DESCRIPTION)
+ @JsonPropertyDescription(REGISTRY_USERNAME_RO_DESCRIPTION)
+ String readOnlyUsername = ''
+
+ @Option(names = ['--registry-password-read-only'], description = REGISTRY_PASSWORD_RO_DESCRIPTION)
+ @JsonPropertyDescription(REGISTRY_PASSWORD_RO_DESCRIPTION)
+ String readOnlyPassword = ''
+
+ @Option(names = ['--create-image-pull-secrets'], description = REGISTRY_CREATE_IMAGE_PULL_SECRETS_DESCRIPTION)
+ @JsonPropertyDescription(REGISTRY_CREATE_IMAGE_PULL_SECRETS_DESCRIPTION)
+ Boolean createImagePullSecrets = false
+
+ @JsonPropertyDescription(HELM_CONFIG_DESCRIPTION)
+ HelmConfigWithValues helm = new HelmConfigWithValues(chart: 'docker-registry',
+ repoURL: 'https://twuni.github.io/docker-registry.helm',
+ version: '3.0.0')
+
+ }
+
+ static class JenkinsSchema {
+ Boolean internal = true
+ /* When installing via Docker we have to distinguish jenkins.url (which is a local IP address) from
+ the Jenkins URL used by SCMM.
+
+ This is the URL configured in SCMM inside the Jenkins Plugin, e.g. at http://scmm.localhost/scm/admin/settings/jenkins
+ See addJenkinsConfig() and the comment at scmm.urlForJenkins */
+ String urlForScm = ''
+ String ingress = ''
+ // Bash image used with internal Jenkins only
+ String internalBashImage = 'bash:5'
+ /* Docker client image, downloaded on internal Jenkins only
+ For updating, delete pvc jenkins-docker-client
+ When updating, we should not use too recent version, to not break support for LTS distros like debian
+ https://docs.docker.com/engine/install/debian/#os-requirements -> oldstable
+ For example:
+ $ curl -s https://download.docker.com/linux/debian/dists/bullseye/stable/binary-amd64/Packages | grep -EA5 'Package\: docker-ce$' | grep Version | sort | uniq | tail -n1
+ Version: 5:27.1.1-1~debian.11~bullseye */
+ String internalDockerClientVersion = '27.1.2'
+
+ @Option(names = ['--jenkins'], description = JENKINS_ENABLE_DESCRIPTION)
+ @JsonPropertyDescription(JENKINS_ENABLE_DESCRIPTION)
+ Boolean active = false
+
+ @Option(names = ['--jenkins-skip-restart'], description = JENKINS_SKIP_RESTART_DESCRIPTION)
+ @JsonPropertyDescription(JENKINS_SKIP_RESTART_DESCRIPTION)
+ Boolean skipRestart = false
+
+ @Option(names = ['--jenkins-skip-plugins'], description = JENKINS_SKIP_PLUGINS_DESCRIPTION)
+ @JsonPropertyDescription(JENKINS_SKIP_PLUGINS_DESCRIPTION)
+ Boolean skipPlugins = false
+
+ @Option(names = ['--jenkins-url'], description = JENKINS_URL_DESCRIPTION)
+ @JsonPropertyDescription(JENKINS_URL_DESCRIPTION)
+ String url = ''
+
+ @Option(names = ['--jenkins-username'], description = JENKINS_USERNAME_DESCRIPTION)
+ @JsonPropertyDescription(JENKINS_USERNAME_DESCRIPTION)
+ String username = DEFAULT_ADMIN_USER
+
+ @Option(names = ['--jenkins-password'], description = JENKINS_PASSWORD_DESCRIPTION)
+ @JsonPropertyDescription(JENKINS_PASSWORD_DESCRIPTION)
+ String password = DEFAULT_ADMIN_PW
+
+ @Option(names = ['--jenkins-metrics-username'], description = JENKINS_METRICS_USERNAME_DESCRIPTION)
+ @JsonPropertyDescription(JENKINS_METRICS_USERNAME_DESCRIPTION)
+ String metricsUsername = "metrics"
+
+ @Option(names = ['--jenkins-metrics-password'], description = JENKINS_METRICS_PASSWORD_DESCRIPTION)
+ @JsonPropertyDescription(JENKINS_METRICS_PASSWORD_DESCRIPTION)
+ String metricsPassword = "metrics"
+
+ @Option(names = ['--maven-central-mirror'], description = MAVEN_CENTRAL_MIRROR_DESCRIPTION)
+ @JsonPropertyDescription(MAVEN_CENTRAL_MIRROR_DESCRIPTION)
+ String mavenCentralMirror = ''
+
+ @Option(names = ["--jenkins-additional-envs"], description = JENKINS_ADDITIONAL_ENVS_DESCRIPTION, split = ",", required = false)
+ @JsonPropertyDescription(JENKINS_ADDITIONAL_ENVS_DESCRIPTION)
+ Map additionalEnvs = [:]
+
+ @JsonPropertyDescription(HELM_CONFIG_DESCRIPTION)
+ HelmConfigWithValues helm = new HelmConfigWithValues(chart: 'jenkins',
+ repoURL: 'https://charts.jenkins.io',
+ version: '5.8.43')
+ }
+
+ static class ApplicationSchema {
+ Boolean runningInsideK8s = false
+ String namePrefixForEnvVars = ''
+ String internalKubernetesApiUrl = ''
+ String localHelmChartFolder = System.getenv('LOCAL_HELM_CHART_FOLDER')
+
+ NamespaceSchema namespaces = new NamespaceSchema()
+
+ @Option(names = ['--config-file'], description = CONFIG_FILE_DESCRIPTION, split = ',')
+ List configFiles = []
+
+ @Option(names = ['--config-map'], description = CONFIG_MAP_DESCRIPTION, split = ',')
+ List configMaps = []
+
+ @Option(names = ['-d', '--debug'], description = DEBUG_DESCRIPTION, scope = ScopeType.INHERIT)
+ Boolean debug
+
+ @Option(names = ['-x', '--trace'], description = TRACE_DESCRIPTION, scope = ScopeType.INHERIT)
+ Boolean trace
+
+ @Option(names = ['--output-config-file'], description = OUTPUT_CONFIG_FILE_DESCRIPTION, help = true)
+ Boolean outputConfigFile = false
+
+ @Option(names = ["-v", "--version"], help = true, description = "Display version and license info")
+ Boolean versionInfoRequested = false
+
+ // We define or own --version, so we need to define our own help param.
+ // The param itself is not used, "usageHelp = true" leads to hel being printed
+ @Option(names = ["-h", "--help"], usageHelp = true, description = "Display this help message")
+ Boolean usageHelpRequested = false
+
+ @Option(names = ['--insecure'], description = INSECURE_DESCRIPTION)
+ @JsonPropertyDescription(INSECURE_DESCRIPTION)
+ Boolean insecure = false
+
+ @Option(names = ['--openshift'], description = OPENSHIFT_DESCRIPTION)
+ @JsonPropertyDescription(OPENSHIFT_DESCRIPTION)
+ Boolean openshift = false
+
+ @Option(names = ['--username'], description = USERNAME_DESCRIPTION)
+ @JsonPropertyDescription(USERNAME_DESCRIPTION)
+ String username = DEFAULT_ADMIN_USER
+
+ @Option(names = ['--password'], description = PASSWORD_DESCRIPTION)
+ @JsonPropertyDescription(PASSWORD_DESCRIPTION)
+ String password = DEFAULT_ADMIN_PW
- @Option(names = ['--name-prefix'], description = NAME_PREFIX_DESCRIPTION)
- @JsonPropertyDescription(NAME_PREFIX_DESCRIPTION)
- String namePrefix = ''
+ @Option(names = ['-y', '--yes'], description = PIPE_YES_DESCRIPTION)
+ @JsonPropertyDescription(PIPE_YES_DESCRIPTION)
+ Boolean yes = false
- @Option(names = ['--destroy'], description = DESTROY_DESCRIPTION)
- @JsonPropertyDescription(DESTROY_DESCRIPTION)
- Boolean destroy = false
+ @Option(names = ['--name-prefix'], description = NAME_PREFIX_DESCRIPTION)
+ @JsonPropertyDescription(NAME_PREFIX_DESCRIPTION)
+ String namePrefix = ''
- @Option(names = ['--pod-resources'], description = POD_RESOURCES_DESCRIPTION)
- @JsonPropertyDescription(POD_RESOURCES_DESCRIPTION)
- Boolean podResources = false
+ @Option(names = ['--destroy'], description = DESTROY_DESCRIPTION)
+ @JsonPropertyDescription(DESTROY_DESCRIPTION)
+ Boolean destroy = false
- @Option(names = ['--git-name'], description = GIT_NAME_DESCRIPTION)
- @JsonPropertyDescription(GIT_NAME_DESCRIPTION)
- String gitName = 'Cloudogu'
+ @Option(names = ['--pod-resources'], description = POD_RESOURCES_DESCRIPTION)
+ @JsonPropertyDescription(POD_RESOURCES_DESCRIPTION)
+ Boolean podResources = false
- @Option(names = ['--git-email'], description = GIT_EMAIL_DESCRIPTION)
- @JsonPropertyDescription(GIT_EMAIL_DESCRIPTION)
- String gitEmail = 'hello@cloudogu.com'
+ @Option(names = ['--git-name'], description = GIT_NAME_DESCRIPTION)
+ @JsonPropertyDescription(GIT_NAME_DESCRIPTION)
+ String gitName = 'Cloudogu'
- @Option(names = ['--base-url'], description = BASE_URL_DESCRIPTION)
- @JsonPropertyDescription(BASE_URL_DESCRIPTION)
- String baseUrl = ''
+ @Option(names = ['--git-email'], description = GIT_EMAIL_DESCRIPTION)
+ @JsonPropertyDescription(GIT_EMAIL_DESCRIPTION)
+ String gitEmail = 'hello@cloudogu.com'
- @Option(names = ['--url-separator-hyphen'], description = URL_SEPARATOR_HYPHEN_DESCRIPTION)
- @JsonPropertyDescription(URL_SEPARATOR_HYPHEN_DESCRIPTION)
- Boolean urlSeparatorHyphen = false
+ @Option(names = ['--base-url'], description = BASE_URL_DESCRIPTION)
+ @JsonPropertyDescription(BASE_URL_DESCRIPTION)
+ String baseUrl = ''
- @Option(names = ['--mirror-repos'], description = MIRROR_REPOS_DESCRIPTION)
- @JsonPropertyDescription(MIRROR_REPOS_DESCRIPTION)
- Boolean mirrorRepos = false
+ @Option(names = ['--url-separator-hyphen'], description = URL_SEPARATOR_HYPHEN_DESCRIPTION)
+ @JsonPropertyDescription(URL_SEPARATOR_HYPHEN_DESCRIPTION)
+ Boolean urlSeparatorHyphen = false
- @Option(names = ['--skip-crds'], description = SKIP_CRDS_DESCRIPTION)
- @JsonPropertyDescription(SKIP_CRDS_DESCRIPTION)
- Boolean skipCrds = false
+ @Option(names = ['--mirror-repos'], description = MIRROR_REPOS_DESCRIPTION)
+ @JsonPropertyDescription(MIRROR_REPOS_DESCRIPTION)
+ Boolean mirrorRepos = false
- @Option(names = ['--namespace-isolation'], description = NAMESPACE_ISOLATION_DESCRIPTION)
- @JsonPropertyDescription(NAMESPACE_ISOLATION_DESCRIPTION)
- Boolean namespaceIsolation = false
+ @Option(names = ['--skip-crds'], description = SKIP_CRDS_DESCRIPTION)
+ @JsonPropertyDescription(SKIP_CRDS_DESCRIPTION)
+ Boolean skipCrds = false
- @Option(names = ['--netpols'], description = NETPOLS_DESCRIPTION)
- @JsonPropertyDescription(NETPOLS_DESCRIPTION)
- Boolean netpols = false
+ @Option(names = ['--namespace-isolation'], description = NAMESPACE_ISOLATION_DESCRIPTION)
+ @JsonPropertyDescription(NAMESPACE_ISOLATION_DESCRIPTION)
+ Boolean namespaceIsolation = false
- @Option(names = ['--cluster-admin'], description = CLUSTER_ADMIN_DESCRIPTION)
- @JsonPropertyDescription(CLUSTER_ADMIN_DESCRIPTION)
- Boolean clusterAdmin = false
+ @Option(names = ['--netpols'], description = NETPOLS_DESCRIPTION)
+ @JsonPropertyDescription(NETPOLS_DESCRIPTION)
+ Boolean netpols = false
- @Option(names = ["-p", "--profile"], description = APPLICATION_PROFIL)
- String profile
+ @Option(names = ['--cluster-admin'], description = CLUSTER_ADMIN_DESCRIPTION)
+ @JsonPropertyDescription(CLUSTER_ADMIN_DESCRIPTION)
+ Boolean clusterAdmin = false
- static class NamespaceSchema {
- LinkedHashSet dedicatedNamespaces = new LinkedHashSet<>()
- LinkedHashSet tenantNamespaces = new LinkedHashSet<>()
+ @Option(names = ["-p", "--profile"], description = APPLICATION_PROFIL)
+ String profile
- LinkedHashSet getActiveNamespaces() {
- return new LinkedHashSet<>(dedicatedNamespaces + tenantNamespaces)
- }
- }
+ static class NamespaceSchema {
+ LinkedHashSet dedicatedNamespaces = new LinkedHashSet<>()
+ LinkedHashSet tenantNamespaces = new LinkedHashSet<>()
- @JsonIgnore
- String getTenantName() {
- return namePrefix.replaceAll(/-$/, "")
- }
- }
+ LinkedHashSet getActiveNamespaces() {
+ return new LinkedHashSet<>(dedicatedNamespaces + tenantNamespaces)
+ }
+ }
- static class FeaturesSchema {
+ @JsonIgnore
+ String getTenantName() {
+ return namePrefix.replaceAll(/-$/, "")
+ }
+ }
- @Mixin
- @JsonPropertyDescription(ARGOCD_DESCRIPTION)
- ArgoCDSchema argocd = new ArgoCDSchema()
+ static class FeaturesSchema {
- @Mixin
- @JsonPropertyDescription(MAIL_DESCRIPTION)
- MailSchema mail = new MailSchema()
+ @Mixin
+ @JsonPropertyDescription(ARGOCD_DESCRIPTION)
+ ArgoCDSchema argocd = new ArgoCDSchema()
- @Mixin
- @JsonPropertyDescription(MONITORING_DESCRIPTION)
- MonitoringSchema monitoring = new MonitoringSchema()
+ @Mixin
+ @JsonPropertyDescription(MAIL_DESCRIPTION)
+ MailSchema mail = new MailSchema()
- @Mixin
- @JsonPropertyDescription(SECRETS_DESCRIPTION)
- SecretsSchema secrets = new SecretsSchema()
+ @Mixin
+ @JsonPropertyDescription(MONITORING_DESCRIPTION)
+ MonitoringSchema monitoring = new MonitoringSchema()
- @Mixin
- @JsonPropertyDescription(INGRESS_DESCRIPTION)
- IngressSchema ingress = new IngressSchema()
+ @Mixin
+ @JsonPropertyDescription(SECRETS_DESCRIPTION)
+ SecretsSchema secrets = new SecretsSchema()
- @Mixin
- @JsonPropertyDescription(CERTMANAGER_DESCRIPTION)
- CertManagerSchema certManager = new CertManagerSchema()
- }
+ @Mixin
+ @JsonPropertyDescription(INGRESS_DESCRIPTION)
+ IngressSchema ingress = new IngressSchema()
- static class ArgoCDSchema {
- Boolean configOnly = false
+ @Mixin
+ @JsonPropertyDescription(CERTMANAGER_DESCRIPTION)
+ CertManagerSchema certManager = new CertManagerSchema()
+ }
- @Option(names = ['--argocd'], description = ARGOCD_ENABLE_DESCRIPTION)
- @JsonPropertyDescription(ARGOCD_ENABLE_DESCRIPTION)
- Boolean active = false
+ static class ArgoCDSchema {
+ Boolean configOnly = false
- @Option(names = ['--argocd-operator'], description = ARGOCD_OPERATOR_DESCRIPTION)
- @JsonPropertyDescription(ARGOCD_OPERATOR_DESCRIPTION)
- Boolean operator = false
-
- @Option(names = ['--argocd-url'], description = ARGOCD_URL_DESCRIPTION)
- @JsonPropertyDescription(ARGOCD_URL_DESCRIPTION)
- String url = ''
-
- @JsonPropertyDescription(ARGOCD_ENV_DESCRIPTION)
- List> env
-
- @Option(names = ['--argocd-email-from'], description = ARGOCD_EMAIL_FROM_DESCRIPTION)
- @JsonPropertyDescription(ARGOCD_EMAIL_FROM_DESCRIPTION)
- String emailFrom = 'argocd@example.org'
-
- @Option(names = ['--argocd-email-to-user'], description = ARGOCD_EMAIL_TO_USER_DESCRIPTION)
- @JsonPropertyDescription(ARGOCD_EMAIL_TO_USER_DESCRIPTION)
- String emailToUser = 'app-team@example.org'
-
- @Option(names = ['--argocd-email-to-admin'], description = ARGOCD_EMAIL_TO_ADMIN_DESCRIPTION)
- @JsonPropertyDescription(ARGOCD_EMAIL_TO_ADMIN_DESCRIPTION)
- String emailToAdmin = 'infra@example.org'
-
- @Option(names = ['--argocd-resource-inclusions-cluster'], description = ARGOCD_RESOURCE_INCLUSIONS_CLUSTER)
- @JsonPropertyDescription(ARGOCD_RESOURCE_INCLUSIONS_CLUSTER)
- String resourceInclusionsCluster = ''
-
- @Option(names = ['--argocd-namespace'], description = ARGOCD_CUSTOM_NAMESPACE_DESCRIPTION)
- @JsonPropertyDescription(ARGOCD_CUSTOM_NAMESPACE_DESCRIPTION)
- String namespace = 'argocd'
-
- @JsonPropertyDescription(HELM_CONFIG_VALUES_DESCRIPTION)
- Map values = [:]
- }
-
- static class MailSchema {
-
- Boolean active = false
-
- @Option(names = ['--mail'], description = MAILSERVER_ENABLE_DESCRIPTION, scope = ScopeType.INHERIT)
- @JsonPropertyDescription(MAILSERVER_ENABLE_DESCRIPTION)
- Boolean mailServer = false
-
-
- @Option(names = ['--mail-url'], description = MAIL_URL_DESCRIPTION)
- @JsonPropertyDescription(MAIL_URL_DESCRIPTION)
- String mailUrl = ''
-
- @Option(names = ['--smtp-address'], description = SMTP_ADDRESS_DESCRIPTION)
- @JsonPropertyDescription(SMTP_ADDRESS_DESCRIPTION)
- String smtpAddress = ''
-
- @Option(names = ['--smtp-port'], description = SMTP_PORT_DESCRIPTION)
- @JsonPropertyDescription(SMTP_PORT_DESCRIPTION)
- Integer smtpPort = null
-
- @Option(names = ['--smtp-user'], description = SMTP_USER_DESCRIPTION)
- @JsonPropertyDescription(SMTP_USER_DESCRIPTION)
- String smtpUser = ''
-
- @Option(names = ['--smtp-password'], description = SMTP_PASSWORD_DESCRIPTION)
- @JsonPropertyDescription(SMTP_PASSWORD_DESCRIPTION)
- String smtpPassword = ''
-
- @JsonPropertyDescription(HELM_CONFIG_DESCRIPTION)
- @Mixin
- MailHelmSchema helm = new MailHelmSchema(
- chart: 'mailhog',
- repoURL: 'https://codecentric.github.io/helm-charts',
- version: '5.0.1')
-
- static class MailHelmSchema extends HelmConfigWithValues {
- @Option(names = ['--mail-image'], description = HELM_CONFIG_IMAGE_DESCRIPTION)
- @JsonPropertyDescription(HELM_CONFIG_IMAGE_DESCRIPTION)
- String image = 'ghcr.io/cloudogu/mailhog:v1.0.1'
- }
- }
-
- static class MonitoringSchema {
- @Option(names = ['--metrics', '--monitoring'], description = MONITORING_ENABLE_DESCRIPTION)
- @JsonPropertyDescription(MONITORING_ENABLE_DESCRIPTION)
- Boolean active = false
-
- @Option(names = ['--grafana-url'], description = GRAFANA_URL_DESCRIPTION)
- @JsonPropertyDescription(GRAFANA_URL_DESCRIPTION)
- String grafanaUrl = ''
-
- @Option(names = ['--grafana-email-from'], description = GRAFANA_EMAIL_FROM_DESCRIPTION)
- @JsonPropertyDescription(GRAFANA_EMAIL_FROM_DESCRIPTION)
- String grafanaEmailFrom = 'grafana@example.org'
-
- @Option(names = ['--grafana-email-to'], description = GRAFANA_EMAIL_TO_DESCRIPTION)
- @JsonPropertyDescription(GRAFANA_EMAIL_TO_DESCRIPTION)
- String grafanaEmailTo = 'infra@example.org'
-
- @Mixin
- @JsonPropertyDescription(HELM_CONFIG_DESCRIPTION)
- @SuppressWarnings('GroovyAssignabilityCheck')
- // Because of values
- MonitoringHelmSchema helm = new MonitoringHelmSchema(
- chart: 'kube-prometheus-stack',
- repoURL: 'https://prometheus-community.github.io/helm-charts',
- /* When updating this make sure to also test if air-gapped mode still works */
- version: '80.2.2',
- values: [:] // Otherwise values is null 🤷♂️
- )
- static class MonitoringHelmSchema extends HelmConfigWithValues {
- @Option(names = ['--grafana-image'], description = GRAFANA_IMAGE_DESCRIPTION)
- @JsonPropertyDescription(GRAFANA_IMAGE_DESCRIPTION)
- String grafanaImage = ''
-
- @Option(names = ['--grafana-sidecar-image'], description = GRAFANA_SIDECAR_IMAGE_DESCRIPTION)
- @JsonPropertyDescription(GRAFANA_SIDECAR_IMAGE_DESCRIPTION)
- String grafanaSidecarImage = ''
-
- @Option(names = ['--prometheus-image'], description = PROMETHEUS_IMAGE_DESCRIPTION)
- @JsonPropertyDescription(PROMETHEUS_IMAGE_DESCRIPTION)
- String prometheusImage = ''
-
- @Option(names = ['--prometheus-operator-image'], description = PROMETHEUS_OPERATOR_IMAGE_DESCRIPTION)
- @JsonPropertyDescription(PROMETHEUS_OPERATOR_IMAGE_DESCRIPTION)
- String prometheusOperatorImage = ''
-
- @Option(names = ['--prometheus-config-reloader-image'], description = PROMETHEUS_CONFIG_RELOADER_IMAGE_DESCRIPTION)
- @JsonPropertyDescription(PROMETHEUS_CONFIG_RELOADER_IMAGE_DESCRIPTION)
- String prometheusConfigReloaderImage = ''
- }
- }
-
- static class SecretsSchema {
- Boolean active = false
-
- @Mixin
- @JsonPropertyDescription(ESO_DESCRIPTION)
- ESOSchema externalSecrets = new ESOSchema()
-
- @Mixin
- @JsonPropertyDescription(VAULT_DESCRIPTION)
- VaultSchema vault = new VaultSchema()
-
- static class ESOSchema {
-
- @Mixin
- @JsonPropertyDescription(HELM_CONFIG_DESCRIPTION)
- ESOHelmSchema helm = new ESOHelmSchema(
- chart: 'external-secrets',
- repoURL: 'https://charts.external-secrets.io',
- version: '0.9.16'
- )
- static class ESOHelmSchema extends HelmConfigWithValues {
- @Option(names = ['--external-secrets-image'], description = EXTERNAL_SECRETS_IMAGE_DESCRIPTION)
- @JsonPropertyDescription(EXTERNAL_SECRETS_IMAGE_DESCRIPTION)
- String image = ''
-
- @Option(names = ['--external-secrets-certcontroller-image'], description = EXTERNAL_SECRETS_CERT_CONTROLLER_IMAGE_DESCRIPTION)
- @JsonPropertyDescription(EXTERNAL_SECRETS_CERT_CONTROLLER_IMAGE_DESCRIPTION)
- String certControllerImage = ''
-
- @Option(names = ['--external-secrets-webhook-image'], description = EXTERNAL_SECRETS_WEBHOOK_IMAGE_DESCRIPTION)
- @JsonPropertyDescription(EXTERNAL_SECRETS_WEBHOOK_IMAGE_DESCRIPTION)
- String webhookImage = ''
- }
- }
-
- static class VaultSchema {
- @Option(names = ['--vault'], description = VAULT_ENABLE_DESCRIPTION)
- @JsonPropertyDescription(VAULT_ENABLE_DESCRIPTION)
- VaultMode mode
-
- @Option(names = ['--vault-url'], description = VAULT_URL_DESCRIPTION)
- @JsonPropertyDescription(VAULT_URL_DESCRIPTION)
- String url = ''
-
- @Mixin
- @JsonPropertyDescription(HELM_CONFIG_DESCRIPTION)
- VaultHelmSchema helm = new VaultHelmSchema(
- chart: 'vault',
- repoURL: 'https://helm.releases.hashicorp.com',
- version: '0.25.0'
- )
- static class VaultHelmSchema extends HelmConfigWithValues {
- @Option(names = ['--vault-image'], description = VAULT_IMAGE_DESCRIPTION)
- @JsonPropertyDescription(VAULT_IMAGE_DESCRIPTION)
- String image = ''
- }
- }
- }
-
- static class IngressSchema {
-
- @Option(names = ['--ingress'], description = INGRESS_ENABLE_DESCRIPTION)
- @JsonPropertyDescription(INGRESS_ENABLE_DESCRIPTION)
- Boolean active = false
-
- @Mixin
- @JsonPropertyDescription(HELM_CONFIG_DESCRIPTION)
- IngressHelmSchema helm = new IngressHelmSchema(
- chart: 'traefik',
- repoURL: 'https://traefik.github.io/charts',
- version: '39.0.0'
- )
- static class IngressHelmSchema extends HelmConfigWithValues {
- @Option(names = ['--ingress-image'], description = HELM_CONFIG_IMAGE_DESCRIPTION)
- @JsonPropertyDescription(HELM_CONFIG_IMAGE_DESCRIPTION)
- String image = ''
- }
-
- String ingressNamespace = 'ingress'
- }
-
- static class CertManagerSchema {
- @Option(names = ['--cert-manager'], description = CERTMANAGER_ENABLE_DESCRIPTION)
- @JsonPropertyDescription(CERTMANAGER_ENABLE_DESCRIPTION)
- Boolean active = false
-
- @Mixin
- @JsonPropertyDescription(HELM_CONFIG_DESCRIPTION)
- CertManagerHelmSchema helm = new CertManagerHelmSchema(
- chart: 'cert-manager',
- repoURL: 'https://charts.jetstack.io',
- version: '1.16.1'
- )
- static class CertManagerHelmSchema extends HelmConfigWithValues {
-
- @Option(names = ['--cert-manager-image'], description = CERTMANAGER_IMAGE_DESCRIPTION)
- @JsonPropertyDescription(CERTMANAGER_IMAGE_DESCRIPTION)
- String image = ''
-
- @Option(names = ['--cert-manager-webhook-image'], description = CERTMANAGER_WEBHOOK_IMAGE_DESCRIPTION)
- @JsonPropertyDescription(CERTMANAGER_WEBHOOK_IMAGE_DESCRIPTION)
- String webhookImage = ''
-
- @Option(names = ['--cert-manager-cainjector-image'], description = CERTMANAGER_CAINJECTOR_IMAGE_DESCRIPTION)
- @JsonPropertyDescription(CERTMANAGER_CAINJECTOR_IMAGE_DESCRIPTION)
- String cainjectorImage = ''
-
- @Option(names = ['--cert-manager-acme-solver-image'], description = CERTMANAGER_ACME_SOLVER_IMAGE_DESCRIPTION)
- @JsonPropertyDescription(CERTMANAGER_ACME_SOLVER_IMAGE_DESCRIPTION)
- String acmeSolverImage = ''
-
- @Option(names = ['--cert-manager-startup-api-check-image'], description = CERTMANAGER_STARTUP_API_CHECK_IMAGE_DESCRIPTION)
- @JsonPropertyDescription(CERTMANAGER_STARTUP_API_CHECK_IMAGE_DESCRIPTION)
- String startupAPICheckImage = ''
-
- }
- }
-
- static enum ContentRepoType {
- FOLDER_BASED, COPY, MIRROR
- }
-
- static enum VaultMode {
- dev, prod
- }
-
- /**
- * This defines, how customer repos will be updated.
- * See {@link ConfigConstants#CONTENT_REPO_TARGET_OVERWRITE_MODE_DESCRIPTION}
- */
- static enum OverwriteMode {
- INIT, RESET, UPGRADE
- }
-
- private static final ObjectMapper objectMapper = new ObjectMapper()
- .registerModule(new SimpleModule().addSerializer(GString, new JsonSerializer() {
- @Override
- void serialize(GString value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
- jsonGenerator.writeString(value.toString())
- }
- }))
-
- static Config fromMap(Map map) {
- objectMapper.convertValue(map, Config)
- }
-
- Map toMap() {
- objectMapper.convertValue(this, Map)
- }
-
- String toYaml(boolean includeInternals) {
- createYamlMapper(includeInternals)
- .writeValueAsString(this)
- }
-
- private static YAMLMapper createYamlMapper(boolean includeInternals) {
- if (!includeInternals) {
- new YAMLMapper()
- .registerModule(new SimpleModule().setSerializerModifier(new BeanSerializerModifier() {
- @Override
- List changeProperties(SerializationConfig serializationConfig, BeanDescription beanDesc, List beanProperties) {
- beanProperties.findAll { writer -> writer.getAnnotation(JsonPropertyDescription) != null }
- }
- })) as YAMLMapper
- } else {
- new YAMLMapper()
- }
- }
+ @Option(names = ['--argocd'], description = ARGOCD_ENABLE_DESCRIPTION)
+ @JsonPropertyDescription(ARGOCD_ENABLE_DESCRIPTION)
+ Boolean active = false
+
+ @Option(names = ['--argocd-operator'], description = ARGOCD_OPERATOR_DESCRIPTION)
+ @JsonPropertyDescription(ARGOCD_OPERATOR_DESCRIPTION)
+ Boolean operator = false
+
+ @Option(names = ['--argocd-url'], description = ARGOCD_URL_DESCRIPTION)
+ @JsonPropertyDescription(ARGOCD_URL_DESCRIPTION)
+ String url = ''
+
+ @JsonPropertyDescription(ARGOCD_ENV_DESCRIPTION)
+ List> env
+
+ @Option(names = ['--argocd-email-from'], description = ARGOCD_EMAIL_FROM_DESCRIPTION)
+ @JsonPropertyDescription(ARGOCD_EMAIL_FROM_DESCRIPTION)
+ String emailFrom = 'argocd@example.org'
+
+ @Option(names = ['--argocd-email-to-user'], description = ARGOCD_EMAIL_TO_USER_DESCRIPTION)
+ @JsonPropertyDescription(ARGOCD_EMAIL_TO_USER_DESCRIPTION)
+ String emailToUser = 'app-team@example.org'
+
+ @Option(names = ['--argocd-email-to-admin'], description = ARGOCD_EMAIL_TO_ADMIN_DESCRIPTION)
+ @JsonPropertyDescription(ARGOCD_EMAIL_TO_ADMIN_DESCRIPTION)
+ String emailToAdmin = 'infra@example.org'
+
+ @Option(names = ['--argocd-resource-inclusions-cluster'], description = ARGOCD_RESOURCE_INCLUSIONS_CLUSTER)
+ @JsonPropertyDescription(ARGOCD_RESOURCE_INCLUSIONS_CLUSTER)
+ String resourceInclusionsCluster = ''
+
+ @Option(names = ['--argocd-namespace'], description = ARGOCD_CUSTOM_NAMESPACE_DESCRIPTION)
+ @JsonPropertyDescription(ARGOCD_CUSTOM_NAMESPACE_DESCRIPTION)
+ String namespace = 'argocd'
+
+ @JsonPropertyDescription(HELM_CONFIG_VALUES_DESCRIPTION)
+ Map values = [:]
+ }
+
+ static class MailSchema {
+
+ Boolean active = false
+
+ @Option(names = ['--mail'], description = MAILSERVER_ENABLE_DESCRIPTION, scope = ScopeType.INHERIT)
+ @JsonPropertyDescription(MAILSERVER_ENABLE_DESCRIPTION)
+ Boolean mailServer = false
+
+ @Option(names = ['--mail-url'], description = MAIL_URL_DESCRIPTION)
+ @JsonPropertyDescription(MAIL_URL_DESCRIPTION)
+ String mailUrl = ''
+
+ @Option(names = ['--smtp-address'], description = SMTP_ADDRESS_DESCRIPTION)
+ @JsonPropertyDescription(SMTP_ADDRESS_DESCRIPTION)
+ String smtpAddress = ''
+
+ @Option(names = ['--smtp-port'], description = SMTP_PORT_DESCRIPTION)
+ @JsonPropertyDescription(SMTP_PORT_DESCRIPTION)
+ Integer smtpPort = null
+
+ @Option(names = ['--smtp-user'], description = SMTP_USER_DESCRIPTION)
+ @JsonPropertyDescription(SMTP_USER_DESCRIPTION)
+ String smtpUser = ''
+
+ @Option(names = ['--smtp-password'], description = SMTP_PASSWORD_DESCRIPTION)
+ @JsonPropertyDescription(SMTP_PASSWORD_DESCRIPTION)
+ String smtpPassword = ''
+
+ @JsonPropertyDescription(HELM_CONFIG_DESCRIPTION)
+ @Mixin
+ MailHelmSchema helm = new MailHelmSchema(chart: 'mailhog',
+ repoURL: 'https://codecentric.github.io/helm-charts',
+ version: '5.0.1')
+
+ static class MailHelmSchema extends HelmConfigWithValues {
+ @Option(names = ['--mail-image'], description = HELM_CONFIG_IMAGE_DESCRIPTION)
+ @JsonPropertyDescription(HELM_CONFIG_IMAGE_DESCRIPTION)
+ String image = 'ghcr.io/cloudogu/mailhog:v1.0.1'
+ }
+ }
+
+ static class MonitoringSchema {
+ @Option(names = ['--metrics', '--monitoring'], description = MONITORING_ENABLE_DESCRIPTION)
+ @JsonPropertyDescription(MONITORING_ENABLE_DESCRIPTION)
+ Boolean active = false
+
+ @Option(names = ['--grafana-url'], description = GRAFANA_URL_DESCRIPTION)
+ @JsonPropertyDescription(GRAFANA_URL_DESCRIPTION)
+ String grafanaUrl = ''
+
+ @Option(names = ['--grafana-email-from'], description = GRAFANA_EMAIL_FROM_DESCRIPTION)
+ @JsonPropertyDescription(GRAFANA_EMAIL_FROM_DESCRIPTION)
+ String grafanaEmailFrom = 'grafana@example.org'
+
+ @Option(names = ['--grafana-email-to'], description = GRAFANA_EMAIL_TO_DESCRIPTION)
+ @JsonPropertyDescription(GRAFANA_EMAIL_TO_DESCRIPTION)
+ String grafanaEmailTo = 'infra@example.org'
+
+ @Mixin
+ @JsonPropertyDescription(HELM_CONFIG_DESCRIPTION)
+ @SuppressWarnings('GroovyAssignabilityCheck')
+ // Because of values
+ MonitoringHelmSchema helm = new MonitoringHelmSchema(chart: 'kube-prometheus-stack',
+ repoURL: 'https://prometheus-community.github.io/helm-charts',
+ /* When updating this make sure to also test if air-gapped mode still works */
+ version: '80.2.2',
+ values: [:] // Otherwise values is null 🤷♂️
+ )
+ static class MonitoringHelmSchema extends HelmConfigWithValues {
+ @Option(names = ['--grafana-image'], description = GRAFANA_IMAGE_DESCRIPTION)
+ @JsonPropertyDescription(GRAFANA_IMAGE_DESCRIPTION)
+ String grafanaImage = ''
+
+ @Option(names = ['--grafana-sidecar-image'], description = GRAFANA_SIDECAR_IMAGE_DESCRIPTION)
+ @JsonPropertyDescription(GRAFANA_SIDECAR_IMAGE_DESCRIPTION)
+ String grafanaSidecarImage = ''
+
+ @Option(names = ['--prometheus-image'], description = PROMETHEUS_IMAGE_DESCRIPTION)
+ @JsonPropertyDescription(PROMETHEUS_IMAGE_DESCRIPTION)
+ String prometheusImage = ''
+
+ @Option(names = ['--prometheus-operator-image'], description = PROMETHEUS_OPERATOR_IMAGE_DESCRIPTION)
+ @JsonPropertyDescription(PROMETHEUS_OPERATOR_IMAGE_DESCRIPTION)
+ String prometheusOperatorImage = ''
+
+ @Option(names = ['--prometheus-config-reloader-image'], description = PROMETHEUS_CONFIG_RELOADER_IMAGE_DESCRIPTION)
+ @JsonPropertyDescription(PROMETHEUS_CONFIG_RELOADER_IMAGE_DESCRIPTION)
+ String prometheusConfigReloaderImage = ''
+ }
+ }
+
+ static class SecretsSchema {
+ Boolean active = false
+
+ @Mixin
+ @JsonPropertyDescription(ESO_DESCRIPTION)
+ ESOSchema externalSecrets = new ESOSchema()
+
+ @Mixin
+ @JsonPropertyDescription(VAULT_DESCRIPTION)
+ VaultSchema vault = new VaultSchema()
+
+ static class ESOSchema {
+
+ @Mixin
+ @JsonPropertyDescription(HELM_CONFIG_DESCRIPTION)
+ ESOHelmSchema helm = new ESOHelmSchema(chart: 'external-secrets',
+ repoURL: 'https://charts.external-secrets.io',
+ version: '0.9.16')
+ static class ESOHelmSchema extends HelmConfigWithValues {
+ @Option(names = ['--external-secrets-image'], description = EXTERNAL_SECRETS_IMAGE_DESCRIPTION)
+ @JsonPropertyDescription(EXTERNAL_SECRETS_IMAGE_DESCRIPTION)
+ String image = ''
+
+ @Option(names = ['--external-secrets-certcontroller-image'], description = EXTERNAL_SECRETS_CERT_CONTROLLER_IMAGE_DESCRIPTION)
+ @JsonPropertyDescription(EXTERNAL_SECRETS_CERT_CONTROLLER_IMAGE_DESCRIPTION)
+ String certControllerImage = ''
+
+ @Option(names = ['--external-secrets-webhook-image'], description = EXTERNAL_SECRETS_WEBHOOK_IMAGE_DESCRIPTION)
+ @JsonPropertyDescription(EXTERNAL_SECRETS_WEBHOOK_IMAGE_DESCRIPTION)
+ String webhookImage = ''
+ }
+ }
+
+ static class VaultSchema {
+ @Option(names = ['--vault'], description = VAULT_ENABLE_DESCRIPTION)
+ @JsonPropertyDescription(VAULT_ENABLE_DESCRIPTION)
+ VaultMode mode
+
+ @Option(names = ['--vault-url'], description = VAULT_URL_DESCRIPTION)
+ @JsonPropertyDescription(VAULT_URL_DESCRIPTION)
+ String url = ''
+
+ @Mixin
+ @JsonPropertyDescription(HELM_CONFIG_DESCRIPTION)
+ VaultHelmSchema helm = new VaultHelmSchema(chart: 'vault',
+ repoURL: 'https://helm.releases.hashicorp.com',
+ version: '0.25.0')
+ static class VaultHelmSchema extends HelmConfigWithValues {
+ @Option(names = ['--vault-image'], description = VAULT_IMAGE_DESCRIPTION)
+ @JsonPropertyDescription(VAULT_IMAGE_DESCRIPTION)
+ String image = ''
+ }
+ }
+ }
+
+ static class IngressSchema {
+
+ @Option(names = ['--ingress'], description = INGRESS_ENABLE_DESCRIPTION)
+ @JsonPropertyDescription(INGRESS_ENABLE_DESCRIPTION)
+ Boolean active = false
+
+ @Mixin
+ @JsonPropertyDescription(HELM_CONFIG_DESCRIPTION)
+ IngressHelmSchema helm = new IngressHelmSchema(chart: 'traefik',
+ repoURL: 'https://traefik.github.io/charts',
+ version: '39.0.0')
+ static class IngressHelmSchema extends HelmConfigWithValues {
+ @Option(names = ['--ingress-image'], description = HELM_CONFIG_IMAGE_DESCRIPTION)
+ @JsonPropertyDescription(HELM_CONFIG_IMAGE_DESCRIPTION)
+ String image = ''
+ }
+
+ String ingressNamespace = 'ingress'
+ }
+
+ static class CertManagerSchema {
+ @Option(names = ['--cert-manager'], description = CERTMANAGER_ENABLE_DESCRIPTION)
+ @JsonPropertyDescription(CERTMANAGER_ENABLE_DESCRIPTION)
+ Boolean active = false
+
+ @Mixin
+ @JsonPropertyDescription(HELM_CONFIG_DESCRIPTION)
+ CertManagerHelmSchema helm = new CertManagerHelmSchema(chart: 'cert-manager',
+ repoURL: 'https://charts.jetstack.io',
+ version: '1.16.1')
+ static class CertManagerHelmSchema extends HelmConfigWithValues {
+
+ @Option(names = ['--cert-manager-image'], description = CERTMANAGER_IMAGE_DESCRIPTION)
+ @JsonPropertyDescription(CERTMANAGER_IMAGE_DESCRIPTION)
+ String image = ''
+
+ @Option(names = ['--cert-manager-webhook-image'], description = CERTMANAGER_WEBHOOK_IMAGE_DESCRIPTION)
+ @JsonPropertyDescription(CERTMANAGER_WEBHOOK_IMAGE_DESCRIPTION)
+ String webhookImage = ''
+
+ @Option(names = ['--cert-manager-cainjector-image'], description = CERTMANAGER_CAINJECTOR_IMAGE_DESCRIPTION)
+ @JsonPropertyDescription(CERTMANAGER_CAINJECTOR_IMAGE_DESCRIPTION)
+ String cainjectorImage = ''
+
+ @Option(names = ['--cert-manager-acme-solver-image'], description = CERTMANAGER_ACME_SOLVER_IMAGE_DESCRIPTION)
+ @JsonPropertyDescription(CERTMANAGER_ACME_SOLVER_IMAGE_DESCRIPTION)
+ String acmeSolverImage = ''
+
+ @Option(names = ['--cert-manager-startup-api-check-image'], description = CERTMANAGER_STARTUP_API_CHECK_IMAGE_DESCRIPTION)
+ @JsonPropertyDescription(CERTMANAGER_STARTUP_API_CHECK_IMAGE_DESCRIPTION)
+ String startupAPICheckImage = ''
+
+ }
+ }
+
+ static enum ContentRepoType {
+ FOLDER_BASED, COPY, MIRROR
+ }
+
+ static enum VaultMode {
+ dev, prod
+ }
+
+ /**
+ * This defines, how customer repos will be updated.
+ * See {@link ConfigConstants#CONTENT_REPO_TARGET_OVERWRITE_MODE_DESCRIPTION}
+ */
+ static enum OverwriteMode {
+ INIT, RESET, UPGRADE
+ }
+
+ private static final ObjectMapper objectMapper = new ObjectMapper()
+ .registerModule(new SimpleModule().addSerializer(GString, new JsonSerializer() {
+ @Override
+ void serialize(GString value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
+ jsonGenerator.writeString(value.toString())
+ }
+ }))
+
+ static Config fromMap(Map map) {
+ objectMapper.convertValue(map, Config)
+ }
+
+ Map toMap() {
+ objectMapper.convertValue(this, Map)
+ }
+
+ String toYaml(boolean includeInternals) {
+ createYamlMapper(includeInternals)
+ .writeValueAsString(this)
+ }
+
+ private static YAMLMapper createYamlMapper(boolean includeInternals) {
+ if (!includeInternals) {
+ new YAMLMapper()
+ .registerModule(new SimpleModule().setSerializerModifier(new BeanSerializerModifier() {
+ @Override
+ List changeProperties(SerializationConfig serializationConfig, BeanDescription beanDesc, List beanProperties) {
+ beanProperties.findAll { writer -> writer.getAnnotation(JsonPropertyDescription) != null }
+ }
+ })) as YAMLMapper
+ } else {
+ new YAMLMapper()
+ }
+ }
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/config/ConfigConstants.groovy b/src/main/groovy/com/cloudogu/gitops/config/ConfigConstants.groovy
index 14f29c4e4..1c0bfc8f9 100644
--- a/src/main/groovy/com/cloudogu/gitops/config/ConfigConstants.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/config/ConfigConstants.groovy
@@ -2,166 +2,166 @@ package com.cloudogu.gitops.config
interface ConfigConstants {
- public static final String BINARY_NAME = 'apply-ng'
- public static final String APP_NAME = 'gitops-playground (GOP)'
- public static final String APP_DESCRIPTION = 'CLI-tool to deploy gitops-playground.'
-
- // group registry
- String REGISTRY_ENABLE_DESCRIPTION = 'Installs a simple cluster-local registry for demonstration purposes. Warning: Registry does not provide authentication!'
- String REGISTRY_DESCRIPTION = 'Config parameters for Registry'
- String REGISTRY_INTERNAL_PORT_DESCRIPTION = 'Port of registry registry. Ignored when a registry*url params are set'
- String REGISTRY_URL_DESCRIPTION = 'The url of your external registry, used for pushing images'
- String REGISTRY_PATH_DESCRIPTION = 'Optional when registry-url is set'
- String REGISTRY_USERNAME_DESCRIPTION = 'Optional when registry-url is set'
- String REGISTRY_PASSWORD_DESCRIPTION = 'Optional when registry-url is set'
-
- String REGISTRY_PROXY_URL_DESCRIPTION = 'The url of your proxy-registry. Used in pipelines to authorize pull base images. Use in conjunction with petclinic base image. Used in helm charts when create-image-pull-secrets is set. Use in conjunction with helm.*image fields.'
- String REGISTRY_PROXY_USERNAME_DESCRIPTION = 'Use with registry-proxy-url, added to Jenkins as credentials and created as pull secrets, when create-image-pull-secrets is set.'
- String REGISTRY_PROXY_PASSWORD_DESCRIPTION = 'Use with registry-proxy-url, added to Jenkins as credentials and created as pull secrets, when create-image-pull-secrets is set.'
-
- String REGISTRY_USERNAME_RO_DESCRIPTION = 'Optional alternative username for registry-url with read-only permissions that is used when create-image-pull-secrets is set.'
- String REGISTRY_PASSWORD_RO_DESCRIPTION = 'Optional alternative password for registry-url with read-only permissions that is used when create-image-pull-secrets is set.'
- String REGISTRY_CREATE_IMAGE_PULL_SECRETS_DESCRIPTION = 'Create image pull secrets for registry and proxy-registry for all GOP namespaces and helm charts. Uses proxy-username, read-only-username or registry-username (in this order). Use this if your cluster is not auto-provisioned with credentials for your private registries or if you configure individual helm images to be pulled from the proxy-registry that requires authentication.'
-
- String FEATURES_DESCRIPTION = 'Config parameters for features or tools'
-
- String CONTENT_DESCRIPTION = 'Config parameters for content, i.e. end-user or tenant applications as opposed to cluster-resources'
-
- // ContentLoader
- String CONTENT_EXAMPLES_DESCRIPTION = 'Deploy example content: source repos, GitOps repos, Jenkins Job, Argo CD apps/project'
- String CONTENT_MULTI_TENANCY_EXAMPLES_DESCRIPTION = "Deploy multi tenancy example content: source repos, GitOps repos, Jenkins Job, Argo CD apps/project"
-
- String CONTENT_NAMESPACES_DESCRIPTION = 'Additional kubernetes namespaces. These are authorized to Argo CD, supplied with image pull secrets, monitored by prometheus, etc. Namespaces can be templates, e.g. ${config.application.namePrefix}staging'
- String CONTENT_REPO_DESCRIPTION = "ContentLoader repos to push into target environment"
- String CONTENT_REPO_URL_DESCRIPTION = "URL of the content repo. Mandatory for each type."
- String CONTENT_REPO_PATH_DESCRIPTION = "Path within the content repo to process"
- String CONTENT_REPO_REF_DESCRIPTION = "Reference for a specific branch, tag, or commit. Emtpy defaults to default branch of the repo. With type MIRROR: ref must not be a commit hash; Choosing a ref only mirrors the ref but does not delete other branches/tags!"
- String CONTENT_REPO_TARGET_REF_DESCRIPTION = "Reference for a specific branch or tag in the target repo of a MIRROR or COPY repo. If ref is a tag, targetRef is treated as tag as well. Except: targetRef is full ref like refs/heads/my-branch or refs/tags/my-tag. Empty defaults to the source ref."
- String CONTENT_REPO_CREDENTIALS_DESCRIPTION = "Credentials Object to authenticate against content repo. Allows using a K8s Secret"
- String CONTENT_REPO_TEMPLATING_DESCRIPTION = "When true, template all files ending in .ftl within the repo"
- String CONTENT_REPO_TYPE_DESCRIPTION = "ContentLoader Repos can either be:\ncopied (only the files, starting on ref, starting at path within the repo. Requires target)\n, mirrored (FORCE pushes ref or the whole git repo if no ref set). Requires target, does not allow path and template.)\nfolderBased (folder structure is interpreted as repos. That is, root folder becomes namespace in SCM, sub folders become repository names in SCM, files are copied. Requires target.)"
- String CONTENT_REPO_TARGET_DESCRIPTION = "Target repo for the repository in the for of namespace/name. Must contain one slash to separate namespace from name."
- String CONTENT_REPO_TARGET_OVERWRITE_MODE_DESCRIPTION = "This defines, how customer repos will be updated.\nINIT - push only if repo does not exist.\nRESET - delete all files after cloning source - files not in content are deleted\nUPGRADE - clone and copy - existing files will be overwritten, files not in content are kept. For type: MIRROR reset and upgrade have same result: in both cases source repo will be force pushed to target repo."
- String CONTENT_REPO_CREATE_JENKINS_JOB_DESCRIPTION = "If true, creates a Jenkins job, if jenkinsfile exists in one of the content repo's branches."
- String CONTENT_VARIABLES_DESCRIPTION = "Additional variables to use in custom templates."
- String CONTENT_STATICSWHITELIST_ENABLED_DESCRIPTION = 'Enables the whitelist for statics in content templating'
- String CONTENT_STATICSWHITELIST_DESCRIPTION = 'Whitelist for Statics freemarker is allowing in user templates'
-
- // group jenkins
- String JENKINS_ENABLE_DESCRIPTION = 'Installs Jenkins as CI server'
- String JENKINS_SKIP_RESTART_DESCRIPTION = 'Skips restarting Jenkins after plugin installation. Use with caution! If the plugins are not installed up front, the installation will likely fail. The intended use case for this is after the first installation, for config changes only. Do not use on first installation or upgrades.'
- String JENKINS_SKIP_PLUGINS_DESCRIPTION = 'Skips plugin installation. Use with caution! If the plugins are not installed up front, the installation will likely fail. The intended use case for this is after the first installation, for config changes only. Do not use on first installation or upgrades.'
- String JENKINS_DESCRIPTION = 'Config parameters for Jenkins CI/CD Pipeline Server'
- String JENKINS_URL_DESCRIPTION = 'The url of your external jenkins'
- String JENKINS_USERNAME_DESCRIPTION = 'Mandatory when jenkins-url is set'
- String JENKINS_PASSWORD_DESCRIPTION = 'Mandatory when jenkins-url is set'
- String JENKINS_METRICS_USERNAME_DESCRIPTION = 'Mandatory when jenkins-url is set and monitoring enabled'
- String JENKINS_METRICS_PASSWORD_DESCRIPTION = 'Mandatory when jenkins-url is set and monitoring enabled'
- String MAVEN_CENTRAL_MIRROR_DESCRIPTION = 'URL for maven mirror, used by applications built in Jenkins'
- String JENKINS_ADDITIONAL_ENVS_DESCRIPTION = 'Set additional environments to Jenkins'
-
- // group scmm
- String SCM_DESCRIPTION = 'Config parameters for Scm'
- String GIT_NAME_DESCRIPTION = 'Sets git author and committer name used for initial commits'
- String GIT_EMAIL_DESCRIPTION = 'Sets git author and committer email used for initial commits'
-
- //MutliTentant
- String MULTITENANT_DESCRIPTION = 'Multi Tenant Configs'
-
- // group remote
- String INSECURE_DESCRIPTION = 'Sets insecure-mode in cURL which skips cert validation'
-
- // group tool configuration
- String APPLICATION_DESCRIPTION = 'Application configuration parameter for GOP'
- String GRAFANA_IMAGE_DESCRIPTION = 'Sets image for grafana'
- String GRAFANA_SIDECAR_IMAGE_DESCRIPTION = 'Sets image for grafana\'s sidecar'
- String PROMETHEUS_IMAGE_DESCRIPTION = 'Sets image for prometheus'
- String PROMETHEUS_OPERATOR_IMAGE_DESCRIPTION = 'Sets image for prometheus-operator'
- String PROMETHEUS_CONFIG_RELOADER_IMAGE_DESCRIPTION = 'Sets image for prometheus-operator\'s config-reloader'
- String EXTERNAL_SECRETS_IMAGE_DESCRIPTION = 'Sets image for external secrets operator'
- String EXTERNAL_SECRETS_CERT_CONTROLLER_IMAGE_DESCRIPTION = 'Sets image for external secrets operator\'s controller'
- String EXTERNAL_SECRETS_WEBHOOK_IMAGE_DESCRIPTION = 'Sets image for external secrets operator\'s webhook'
- String VAULT_IMAGE_DESCRIPTION = 'Sets image for vault'
- String BASE_URL_DESCRIPTION = 'the external base url (TLD) for all tools, e.g. https://example.com or http://localhost:8080. The individual -url params for argocd, grafana, vault and mailhog take precedence.'
- String URL_SEPARATOR_HYPHEN_DESCRIPTION = 'Use hyphens instead of dots to separate application name from base-url'
- String SKIP_CRDS_DESCRIPTION = 'Skip installation of CRDs. This requires prior installation of CRDs'
- String NAMESPACE_ISOLATION_DESCRIPTION = 'Configure tools to explicitly work with the given namespaces only, and not cluster-wide. This way GOP can be installed without having cluster-admin permissions.'
- String MIRROR_REPOS_DESCRIPTION = 'Changes the sources of deployed tools so they are not pulled from the internet, but are pulled from git and work in air-gapped environments.'
- String NETPOLS_DESCRIPTION = 'Sets Network Policies'
- String CLUSTER_ADMIN_DESCRIPTION = 'Binds ArgoCD controllers to cluster-admin ClusterRole'
- String OPENSHIFT_DESCRIPTION = 'When set, openshift specific resources and configurations are applied'
- String APPLICATION_PROFIL = 'Use predefined profile (full, only-argocd, operator-mandants aso.)'
-
- // group metrics
- String MONITORING_DESCRIPTION = 'Config parameters for the Monitoring system (prometheus)'
- String MONITORING_ENABLE_DESCRIPTION = 'Installs the Kube-Prometheus-Stack. This includes Prometheus, the Prometheus operator, Grafana and some extra resources'
- String GRAFANA_URL_DESCRIPTION = 'Sets url for grafana'
- String GRAFANA_EMAIL_FROM_DESCRIPTION = 'Notifications, define grafana alerts sender email address'
- String GRAFANA_EMAIL_TO_DESCRIPTION = 'Notifications, define grafana alerts recipient email address'
-
- // group vault / secrets
- String SECRETS_DESCRIPTION = 'Config parameters for the secrets management'
- String ESO_DESCRIPTION = 'Config parameters for the external secrets operator'
- String VAULT_DESCRIPTION = 'Config parameters for the secrets-vault'
- String VAULT_ENABLE_DESCRIPTION = "Installs Hashicorp vault and the external secrets operator. Possible values: dev, prod."
- String VAULT_URL_DESCRIPTION = 'Sets url for vault ui'
-
- String MAIL_DESCRIPTION = 'Config parameters for mail servers'
- String MAIL_URL_DESCRIPTION = 'Sets url for the mail server frontend'
- String MAILSERVER_ENABLE_DESCRIPTION = 'Installs a dedicated mail server.'
-
- // group external Mailserver
- String SMTP_ADDRESS_DESCRIPTION = 'Sets smtp port of external Mailserver'
- String SMTP_PORT_DESCRIPTION = 'Sets smtp port of external Mailserver'
- String SMTP_USER_DESCRIPTION = 'Sets smtp username for external Mailserver'
- String SMTP_PASSWORD_DESCRIPTION = 'Sets smtp password of external Mailserver'
-
- // group debug
- String DEBUG_DESCRIPTION = 'Debug output'
- String TRACE_DESCRIPTION = 'Debug + Show each command executed (set -x)'
-
- // group configuration
- String USERNAME_DESCRIPTION = 'Set initial admin username'
- String PASSWORD_DESCRIPTION = 'Set initial admin passwords'
- String PIPE_YES_DESCRIPTION = 'Skip confirmation'
- String NAME_PREFIX_DESCRIPTION = 'Set name-prefix for repos, jobs, namespaces'
- String DESTROY_DESCRIPTION = 'Unroll playground'
- String CONFIG_FILE_DESCRIPTION = 'Config file for the application'
- String CONFIG_MAP_DESCRIPTION = 'Kubernetes configuration map. Should contain a key `config.yaml`.'
- String OUTPUT_CONFIG_FILE_DESCRIPTION = 'Output current config as config file as much as possible'
- String POD_RESOURCES_DESCRIPTION = 'Write kubernetes resource requests and limits on each pod'
-
- // group ArgoCD Operator
- String ARGOCD_DESCRIPTION = 'Config Parameter for the ArgoCD Operator'
- String ARGOCD_ENABLE_DESCRIPTION = 'Install ArgoCD'
- String ARGOCD_URL_DESCRIPTION = 'The URL where argocd is accessible. It has to be the full URL with http:// or https://'
- String ARGOCD_EMAIL_FROM_DESCRIPTION = 'Notifications, define Argo CD sender email address'
- String ARGOCD_EMAIL_TO_USER_DESCRIPTION = 'Notifications, define Argo CD user / app-team recipient email address'
- String ARGOCD_EMAIL_TO_ADMIN_DESCRIPTION = 'Notifications, define Argo CD admin recipient email address'
- String ARGOCD_OPERATOR_DESCRIPTION = 'Install ArgoCD via an already running ArgoCD Operator'
- String ARGOCD_ENV_DESCRIPTION = 'Pass a list of env vars to Argo CD components. Currently only works with operator'
- String ARGOCD_RESOURCE_INCLUSIONS_CLUSTER = 'Internal Kubernetes API Server URL https://IP:PORT (kubernetes.default.svc). Needed in argocd-operator resourceInclusions. Use this parameter if argocd.operator=true and NOT running inside a Pod (remote mode). Full URL needed, for example: https://100.125.0.1:443'
- String ARGOCD_CUSTOM_NAMESPACE_DESCRIPTION= 'Defines the kubernetes namespace for ArgoCD'
- // group example apps
-
- // group ingress-class
- String INGRESS_DESCRIPTION = 'Config parameters for the Ingress Controller'
- String INGRESS_ENABLE_DESCRIPTION = 'Sets and enables Ingress Controller'
-
- // group CERTMANAGER
- String CERTMANAGER_DESCRIPTION = 'Config parameters for the Cert Manager'
- String CERTMANAGER_ENABLE_DESCRIPTION = 'Sets and enables Cert Manager'
- String CERTMANAGER_IMAGE_DESCRIPTION = 'Sets image for Cert Manager'
- String CERTMANAGER_WEBHOOK_IMAGE_DESCRIPTION = 'Sets webhook Image for Cert Manager'
- String CERTMANAGER_CAINJECTOR_IMAGE_DESCRIPTION = 'Sets cainjector Image for Cert Manager'
- String CERTMANAGER_ACME_SOLVER_IMAGE_DESCRIPTION = 'Sets acmeSolver Image for Cert Manager'
- String CERTMANAGER_STARTUP_API_CHECK_IMAGE_DESCRIPTION = 'Sets startupAPICheck Image for Cert Manager'
-
- // group helm
- String HELM_CONFIG_DESCRIPTION = 'Common Config parameters for the Helm package manager: Name of Chart (chart), URl of Helm-Repository (repoURL) and Chart Version (version). Note: These config is intended to obtain the chart from a different source (e.g. in air-gapped envs), not to use a different version of a helm chart. Using a different helm chart or version to the one used in the GOP version will likely cause errors.'
- String HELM_CONFIG_CHART_DESCRIPTION = 'Name of the Helm chart'
- String HELM_CONFIG_REPO_URL_DESCRIPTION = 'Repository url from which the Helm chart should be obtained'
- String HELM_CONFIG_VERSION_DESCRIPTION = 'The version of the Helm chart to be installed'
- String HELM_CONFIG_IMAGE_DESCRIPTION = 'The image of the Helm chart to be installed'
- String HELM_CONFIG_VALUES_DESCRIPTION = 'Helm values of the chart, allows overriding defaults and setting values that are not exposed as explicit configuration'
+ public static final String BINARY_NAME = 'apply-ng'
+ public static final String APP_NAME = 'gitops-playground (GOP)'
+ public static final String APP_DESCRIPTION = 'CLI-tool to deploy gitops-playground.'
+
+ // group registry
+ String REGISTRY_ENABLE_DESCRIPTION = 'Installs a simple cluster-local registry for demonstration purposes. Warning: Registry does not provide authentication!'
+ String REGISTRY_DESCRIPTION = 'Config parameters for Registry'
+ String REGISTRY_INTERNAL_PORT_DESCRIPTION = 'Port of registry registry. Ignored when a registry*url params are set'
+ String REGISTRY_URL_DESCRIPTION = 'The url of your external registry, used for pushing images'
+ String REGISTRY_PATH_DESCRIPTION = 'Optional when registry-url is set'
+ String REGISTRY_USERNAME_DESCRIPTION = 'Optional when registry-url is set'
+ String REGISTRY_PASSWORD_DESCRIPTION = 'Optional when registry-url is set'
+
+ String REGISTRY_PROXY_URL_DESCRIPTION = 'The url of your proxy-registry. Used in pipelines to authorize pull base images. Use in conjunction with petclinic base image. Used in helm charts when create-image-pull-secrets is set. Use in conjunction with helm.*image fields.'
+ String REGISTRY_PROXY_USERNAME_DESCRIPTION = 'Use with registry-proxy-url, added to Jenkins as credentials and created as pull secrets, when create-image-pull-secrets is set.'
+ String REGISTRY_PROXY_PASSWORD_DESCRIPTION = 'Use with registry-proxy-url, added to Jenkins as credentials and created as pull secrets, when create-image-pull-secrets is set.'
+
+ String REGISTRY_USERNAME_RO_DESCRIPTION = 'Optional alternative username for registry-url with read-only permissions that is used when create-image-pull-secrets is set.'
+ String REGISTRY_PASSWORD_RO_DESCRIPTION = 'Optional alternative password for registry-url with read-only permissions that is used when create-image-pull-secrets is set.'
+ String REGISTRY_CREATE_IMAGE_PULL_SECRETS_DESCRIPTION = 'Create image pull secrets for registry and proxy-registry for all GOP namespaces and helm charts. Uses proxy-username, read-only-username or registry-username (in this order). Use this if your cluster is not auto-provisioned with credentials for your private registries or if you configure individual helm images to be pulled from the proxy-registry that requires authentication.'
+
+ String FEATURES_DESCRIPTION = 'Config parameters for features or tools'
+
+ String CONTENT_DESCRIPTION = 'Config parameters for content, i.e. end-user or tenant applications as opposed to cluster-resources'
+
+ // ContentLoader
+ String CONTENT_EXAMPLES_DESCRIPTION = 'Deploy example content: source repos, GitOps repos, Jenkins Job, Argo CD apps/project'
+ String CONTENT_MULTI_TENANCY_EXAMPLES_DESCRIPTION = "Deploy multi tenancy example content: source repos, GitOps repos, Jenkins Job, Argo CD apps/project"
+
+ String CONTENT_NAMESPACES_DESCRIPTION = 'Additional kubernetes namespaces. These are authorized to Argo CD, supplied with image pull secrets, monitored by prometheus, etc. Namespaces can be templates, e.g. ${config.application.namePrefix}staging'
+ String CONTENT_REPO_DESCRIPTION = "ContentLoader repos to push into target environment"
+ String CONTENT_REPO_URL_DESCRIPTION = "URL of the content repo. Mandatory for each type."
+ String CONTENT_REPO_PATH_DESCRIPTION = "Path within the content repo to process"
+ String CONTENT_REPO_REF_DESCRIPTION = "Reference for a specific branch, tag, or commit. Emtpy defaults to default branch of the repo. With type MIRROR: ref must not be a commit hash; Choosing a ref only mirrors the ref but does not delete other branches/tags!"
+ String CONTENT_REPO_TARGET_REF_DESCRIPTION = "Reference for a specific branch or tag in the target repo of a MIRROR or COPY repo. If ref is a tag, targetRef is treated as tag as well. Except: targetRef is full ref like refs/heads/my-branch or refs/tags/my-tag. Empty defaults to the source ref."
+ String CONTENT_REPO_CREDENTIALS_DESCRIPTION = "Credentials Object to authenticate against content repo. Allows using a K8s Secret"
+ String CONTENT_REPO_TEMPLATING_DESCRIPTION = "When true, template all files ending in .ftl within the repo"
+ String CONTENT_REPO_TYPE_DESCRIPTION = "ContentLoader Repos can either be:\ncopied (only the files, starting on ref, starting at path within the repo. Requires target)\n, mirrored (FORCE pushes ref or the whole git repo if no ref set). Requires target, does not allow path and template.)\nfolderBased (folder structure is interpreted as repos. That is, root folder becomes namespace in SCM, sub folders become repository names in SCM, files are copied. Requires target.)"
+ String CONTENT_REPO_TARGET_DESCRIPTION = "Target repo for the repository in the for of namespace/name. Must contain one slash to separate namespace from name."
+ String CONTENT_REPO_TARGET_OVERWRITE_MODE_DESCRIPTION = "This defines, how customer repos will be updated.\nINIT - push only if repo does not exist.\nRESET - delete all files after cloning source - files not in content are deleted\nUPGRADE - clone and copy - existing files will be overwritten, files not in content are kept. For type: MIRROR reset and upgrade have same result: in both cases source repo will be force pushed to target repo."
+ String CONTENT_REPO_CREATE_JENKINS_JOB_DESCRIPTION = "If true, creates a Jenkins job, if jenkinsfile exists in one of the content repo's branches."
+ String CONTENT_VARIABLES_DESCRIPTION = "Additional variables to use in custom templates."
+ String CONTENT_STATICSWHITELIST_ENABLED_DESCRIPTION = 'Enables the whitelist for statics in content templating'
+ String CONTENT_STATICSWHITELIST_DESCRIPTION = 'Whitelist for Statics freemarker is allowing in user templates'
+
+ // group jenkins
+ String JENKINS_ENABLE_DESCRIPTION = 'Installs Jenkins as CI server'
+ String JENKINS_SKIP_RESTART_DESCRIPTION = 'Skips restarting Jenkins after plugin installation. Use with caution! If the plugins are not installed up front, the installation will likely fail. The intended use case for this is after the first installation, for config changes only. Do not use on first installation or upgrades.'
+ String JENKINS_SKIP_PLUGINS_DESCRIPTION = 'Skips plugin installation. Use with caution! If the plugins are not installed up front, the installation will likely fail. The intended use case for this is after the first installation, for config changes only. Do not use on first installation or upgrades.'
+ String JENKINS_DESCRIPTION = 'Config parameters for Jenkins CI/CD Pipeline Server'
+ String JENKINS_URL_DESCRIPTION = 'The url of your external jenkins'
+ String JENKINS_USERNAME_DESCRIPTION = 'Mandatory when jenkins-url is set'
+ String JENKINS_PASSWORD_DESCRIPTION = 'Mandatory when jenkins-url is set'
+ String JENKINS_METRICS_USERNAME_DESCRIPTION = 'Mandatory when jenkins-url is set and monitoring enabled'
+ String JENKINS_METRICS_PASSWORD_DESCRIPTION = 'Mandatory when jenkins-url is set and monitoring enabled'
+ String MAVEN_CENTRAL_MIRROR_DESCRIPTION = 'URL for maven mirror, used by applications built in Jenkins'
+ String JENKINS_ADDITIONAL_ENVS_DESCRIPTION = 'Set additional environments to Jenkins'
+
+ // group scmm
+ String SCM_DESCRIPTION = 'Config parameters for Scm'
+ String GIT_NAME_DESCRIPTION = 'Sets git author and committer name used for initial commits'
+ String GIT_EMAIL_DESCRIPTION = 'Sets git author and committer email used for initial commits'
+
+ //MutliTentant
+ String MULTITENANT_DESCRIPTION = 'Multi Tenant Configs'
+
+ // group remote
+ String INSECURE_DESCRIPTION = 'Sets insecure-mode in cURL which skips cert validation'
+
+ // group tool configuration
+ String APPLICATION_DESCRIPTION = 'Application configuration parameter for GOP'
+ String GRAFANA_IMAGE_DESCRIPTION = 'Sets image for grafana'
+ String GRAFANA_SIDECAR_IMAGE_DESCRIPTION = 'Sets image for grafana\'s sidecar'
+ String PROMETHEUS_IMAGE_DESCRIPTION = 'Sets image for prometheus'
+ String PROMETHEUS_OPERATOR_IMAGE_DESCRIPTION = 'Sets image for prometheus-operator'
+ String PROMETHEUS_CONFIG_RELOADER_IMAGE_DESCRIPTION = 'Sets image for prometheus-operator\'s config-reloader'
+ String EXTERNAL_SECRETS_IMAGE_DESCRIPTION = 'Sets image for external secrets operator'
+ String EXTERNAL_SECRETS_CERT_CONTROLLER_IMAGE_DESCRIPTION = 'Sets image for external secrets operator\'s controller'
+ String EXTERNAL_SECRETS_WEBHOOK_IMAGE_DESCRIPTION = 'Sets image for external secrets operator\'s webhook'
+ String VAULT_IMAGE_DESCRIPTION = 'Sets image for vault'
+ String BASE_URL_DESCRIPTION = 'the external base url (TLD) for all tools, e.g. https://example.com or http://localhost:8080. The individual -url params for argocd, grafana, vault and mailhog take precedence.'
+ String URL_SEPARATOR_HYPHEN_DESCRIPTION = 'Use hyphens instead of dots to separate application name from base-url'
+ String SKIP_CRDS_DESCRIPTION = 'Skip installation of CRDs. This requires prior installation of CRDs'
+ String NAMESPACE_ISOLATION_DESCRIPTION = 'Configure tools to explicitly work with the given namespaces only, and not cluster-wide. This way GOP can be installed without having cluster-admin permissions.'
+ String MIRROR_REPOS_DESCRIPTION = 'Changes the sources of deployed tools so they are not pulled from the internet, but are pulled from git and work in air-gapped environments.'
+ String NETPOLS_DESCRIPTION = 'Sets Network Policies'
+ String CLUSTER_ADMIN_DESCRIPTION = 'Binds ArgoCD controllers to cluster-admin ClusterRole'
+ String OPENSHIFT_DESCRIPTION = 'When set, openshift specific resources and configurations are applied'
+ String APPLICATION_PROFIL = 'Use predefined profile (full, only-argocd, operator-mandants aso.)'
+
+ // group metrics
+ String MONITORING_DESCRIPTION = 'Config parameters for the Monitoring system (prometheus)'
+ String MONITORING_ENABLE_DESCRIPTION = 'Installs the Kube-Prometheus-Stack. This includes Prometheus, the Prometheus operator, Grafana and some extra resources'
+ String GRAFANA_URL_DESCRIPTION = 'Sets url for grafana'
+ String GRAFANA_EMAIL_FROM_DESCRIPTION = 'Notifications, define grafana alerts sender email address'
+ String GRAFANA_EMAIL_TO_DESCRIPTION = 'Notifications, define grafana alerts recipient email address'
+
+ // group vault / secrets
+ String SECRETS_DESCRIPTION = 'Config parameters for the secrets management'
+ String ESO_DESCRIPTION = 'Config parameters for the external secrets operator'
+ String VAULT_DESCRIPTION = 'Config parameters for the secrets-vault'
+ String VAULT_ENABLE_DESCRIPTION = "Installs Hashicorp vault and the external secrets operator. Possible values: dev, prod."
+ String VAULT_URL_DESCRIPTION = 'Sets url for vault ui'
+
+ String MAIL_DESCRIPTION = 'Config parameters for mail servers'
+ String MAIL_URL_DESCRIPTION = 'Sets url for the mail server frontend'
+ String MAILSERVER_ENABLE_DESCRIPTION = 'Installs a dedicated mail server.'
+
+ // group external Mailserver
+ String SMTP_ADDRESS_DESCRIPTION = 'Sets smtp port of external Mailserver'
+ String SMTP_PORT_DESCRIPTION = 'Sets smtp port of external Mailserver'
+ String SMTP_USER_DESCRIPTION = 'Sets smtp username for external Mailserver'
+ String SMTP_PASSWORD_DESCRIPTION = 'Sets smtp password of external Mailserver'
+
+ // group debug
+ String DEBUG_DESCRIPTION = 'Debug output'
+ String TRACE_DESCRIPTION = 'Debug + Show each command executed (set -x)'
+
+ // group configuration
+ String USERNAME_DESCRIPTION = 'Set initial admin username'
+ String PASSWORD_DESCRIPTION = 'Set initial admin passwords'
+ String PIPE_YES_DESCRIPTION = 'Skip confirmation'
+ String NAME_PREFIX_DESCRIPTION = 'Set name-prefix for repos, jobs, namespaces'
+ String DESTROY_DESCRIPTION = 'Unroll playground'
+ String CONFIG_FILE_DESCRIPTION = 'Config file for the application'
+ String CONFIG_MAP_DESCRIPTION = 'Kubernetes configuration map. Should contain a key `config.yaml`.'
+ String OUTPUT_CONFIG_FILE_DESCRIPTION = 'Output current config as config file as much as possible'
+ String POD_RESOURCES_DESCRIPTION = 'Write kubernetes resource requests and limits on each pod'
+
+ // group ArgoCD Operator
+ String ARGOCD_DESCRIPTION = 'Config Parameter for the ArgoCD Operator'
+ String ARGOCD_ENABLE_DESCRIPTION = 'Install ArgoCD'
+ String ARGOCD_URL_DESCRIPTION = 'The URL where argocd is accessible. It has to be the full URL with http:// or https://'
+ String ARGOCD_EMAIL_FROM_DESCRIPTION = 'Notifications, define Argo CD sender email address'
+ String ARGOCD_EMAIL_TO_USER_DESCRIPTION = 'Notifications, define Argo CD user / app-team recipient email address'
+ String ARGOCD_EMAIL_TO_ADMIN_DESCRIPTION = 'Notifications, define Argo CD admin recipient email address'
+ String ARGOCD_OPERATOR_DESCRIPTION = 'Install ArgoCD via an already running ArgoCD Operator'
+ String ARGOCD_ENV_DESCRIPTION = 'Pass a list of env vars to Argo CD components. Currently only works with operator'
+ String ARGOCD_RESOURCE_INCLUSIONS_CLUSTER = 'Internal Kubernetes API Server URL https://IP:PORT (kubernetes.default.svc). Needed in argocd-operator resourceInclusions. Use this parameter if argocd.operator=true and NOT running inside a Pod (remote mode). Full URL needed, for example: https://100.125.0.1:443'
+ String ARGOCD_CUSTOM_NAMESPACE_DESCRIPTION = 'Defines the kubernetes namespace for ArgoCD'
+ // group example apps
+
+ // group ingress-class
+ String INGRESS_DESCRIPTION = 'Config parameters for the Ingress Controller'
+ String INGRESS_ENABLE_DESCRIPTION = 'Sets and enables Ingress Controller'
+
+ // group CERTMANAGER
+ String CERTMANAGER_DESCRIPTION = 'Config parameters for the Cert Manager'
+ String CERTMANAGER_ENABLE_DESCRIPTION = 'Sets and enables Cert Manager'
+ String CERTMANAGER_IMAGE_DESCRIPTION = 'Sets image for Cert Manager'
+ String CERTMANAGER_WEBHOOK_IMAGE_DESCRIPTION = 'Sets webhook Image for Cert Manager'
+ String CERTMANAGER_CAINJECTOR_IMAGE_DESCRIPTION = 'Sets cainjector Image for Cert Manager'
+ String CERTMANAGER_ACME_SOLVER_IMAGE_DESCRIPTION = 'Sets acmeSolver Image for Cert Manager'
+ String CERTMANAGER_STARTUP_API_CHECK_IMAGE_DESCRIPTION = 'Sets startupAPICheck Image for Cert Manager'
+
+ // group helm
+ String HELM_CONFIG_DESCRIPTION = 'Common Config parameters for the Helm package manager: Name of Chart (chart), URl of Helm-Repository (repoURL) and Chart Version (version). Note: These config is intended to obtain the chart from a different source (e.g. in air-gapped envs), not to use a different version of a helm chart. Using a different helm chart or version to the one used in the GOP version will likely cause errors.'
+ String HELM_CONFIG_CHART_DESCRIPTION = 'Name of the Helm chart'
+ String HELM_CONFIG_REPO_URL_DESCRIPTION = 'Repository url from which the Helm chart should be obtained'
+ String HELM_CONFIG_VERSION_DESCRIPTION = 'The version of the Helm chart to be installed'
+ String HELM_CONFIG_IMAGE_DESCRIPTION = 'The image of the Helm chart to be installed'
+ String HELM_CONFIG_VALUES_DESCRIPTION = 'Helm values of the chart, allows overriding defaults and setting values that are not exposed as explicit configuration'
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/config/Credentials.groovy b/src/main/groovy/com/cloudogu/gitops/config/Credentials.groovy
index 0281f78d5..a7933e5e8 100644
--- a/src/main/groovy/com/cloudogu/gitops/config/Credentials.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/config/Credentials.groovy
@@ -1,43 +1,44 @@
package com.cloudogu.gitops.config
-import com.fasterxml.jackson.annotation.JsonIgnore
-import com.fasterxml.jackson.annotation.JsonPropertyDescription
+import static com.cloudogu.gitops.config.ConfigConstants.CONTENT_REPO_CREDENTIALS_DESCRIPTION
+
import groovy.transform.ToString
-import static com.cloudogu.gitops.config.ConfigConstants.CONTENT_REPO_CREDENTIALS_DESCRIPTION
+import com.fasterxml.jackson.annotation.JsonIgnore
+import com.fasterxml.jackson.annotation.JsonPropertyDescription
@ToString
class Credentials {
- @JsonPropertyDescription(CONTENT_REPO_CREDENTIALS_DESCRIPTION)
- String username
- @JsonPropertyDescription(CONTENT_REPO_CREDENTIALS_DESCRIPTION)
- @JsonIgnore
- String password
- @JsonPropertyDescription(CONTENT_REPO_CREDENTIALS_DESCRIPTION)
- String secretNamespace
- @JsonPropertyDescription(CONTENT_REPO_CREDENTIALS_DESCRIPTION)
- String secretName
- @JsonPropertyDescription(CONTENT_REPO_CREDENTIALS_DESCRIPTION)
- String usernameKey = 'username'
- @JsonPropertyDescription(CONTENT_REPO_CREDENTIALS_DESCRIPTION)
- String passwordKey = 'password'
-
- Credentials() {}
-
- Credentials(String username, String password, String secretName = '', String secretNamespace = '', String usernameKey = "username", String passwordKey = 'password') {
- this.username = username
- this.password = password
- this.secretNamespace = secretNamespace
- this.secretName = secretName
- this.usernameKey = usernameKey
- this.passwordKey = passwordKey
- }
-
- Credentials(Credentials unsafeCredentials) {
- this.secretNamespace = unsafeCredentials.secretNamespace
- this.secretName = unsafeCredentials.secretName
- this.usernameKey = unsafeCredentials.usernameKey
- this.passwordKey = unsafeCredentials.passwordKey
- }
+ @JsonPropertyDescription(CONTENT_REPO_CREDENTIALS_DESCRIPTION)
+ String username
+ @JsonPropertyDescription(CONTENT_REPO_CREDENTIALS_DESCRIPTION)
+ @JsonIgnore
+ String password
+ @JsonPropertyDescription(CONTENT_REPO_CREDENTIALS_DESCRIPTION)
+ String secretNamespace
+ @JsonPropertyDescription(CONTENT_REPO_CREDENTIALS_DESCRIPTION)
+ String secretName
+ @JsonPropertyDescription(CONTENT_REPO_CREDENTIALS_DESCRIPTION)
+ String usernameKey = 'username'
+ @JsonPropertyDescription(CONTENT_REPO_CREDENTIALS_DESCRIPTION)
+ String passwordKey = 'password'
+
+ Credentials() {}
+
+ Credentials(String username, String password, String secretName = '', String secretNamespace = '', String usernameKey = "username", String passwordKey = 'password') {
+ this.username = username
+ this.password = password
+ this.secretNamespace = secretNamespace
+ this.secretName = secretName
+ this.usernameKey = usernameKey
+ this.passwordKey = passwordKey
+ }
+
+ Credentials(Credentials unsafeCredentials) {
+ this.secretNamespace = unsafeCredentials.secretNamespace
+ this.secretName = unsafeCredentials.secretName
+ this.usernameKey = unsafeCredentials.usernameKey
+ this.passwordKey = unsafeCredentials.passwordKey
+ }
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/config/MultiTenantSchema.groovy b/src/main/groovy/com/cloudogu/gitops/config/MultiTenantSchema.groovy
index d2a7646b3..5de4f3cad 100644
--- a/src/main/groovy/com/cloudogu/gitops/config/MultiTenantSchema.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/config/MultiTenantSchema.groovy
@@ -3,40 +3,39 @@ package com.cloudogu.gitops.config
import com.cloudogu.gitops.features.git.config.ScmCentralSchema.GitlabCentralConfig
import com.cloudogu.gitops.features.git.config.ScmCentralSchema.ScmManagerCentralConfig
import com.cloudogu.gitops.features.git.config.util.ScmProviderType
+
import com.fasterxml.jackson.annotation.JsonPropertyDescription
import picocli.CommandLine.Mixin
import picocli.CommandLine.Option
class MultiTenantSchema {
- static final String SCM_PROVIDER_TYPE_DESCRIPTION = 'The SCM provider type. Possible values: SCM_MANAGER, GITLAB'
- static final String GITLAB_CONFIG_DESCRIPTION = 'Config for GITLAB'
- static final String SCMM_CONFIG_DESCRIPTION = 'Config for GITLAB'
- static final String CENTRAL_ARGOCD_NAMESPACE_DESCRIPTION = 'Namespace for the centralized Argocd'
- static final String CENTRAL_USEDEDICATED_DESCRIPTION = 'Toggles the Dedicated Instances Mode. See docs for more info'
-
- @Option(
- names = ['--central-scm-provider'],
- description = SCM_PROVIDER_TYPE_DESCRIPTION,
- defaultValue = "SCM_MANAGER"
- )
- @JsonPropertyDescription(SCM_PROVIDER_TYPE_DESCRIPTION)
- ScmProviderType scmProviderType = ScmProviderType.SCM_MANAGER
-
- @JsonPropertyDescription(GITLAB_CONFIG_DESCRIPTION)
- @Mixin
- GitlabCentralConfig gitlab
-
- @JsonPropertyDescription(SCMM_CONFIG_DESCRIPTION)
- @Mixin
- ScmManagerCentralConfig scmManager
-
- @Option(names = ['--central-argocd-namespace'], description = CENTRAL_ARGOCD_NAMESPACE_DESCRIPTION)
- @JsonPropertyDescription(CENTRAL_ARGOCD_NAMESPACE_DESCRIPTION)
- String centralArgocdNamespace = 'argocd'
-
- @Option(names = ['--dedicated-instance'], description = CENTRAL_USEDEDICATED_DESCRIPTION)
- @JsonPropertyDescription(CENTRAL_USEDEDICATED_DESCRIPTION)
- Boolean useDedicatedInstance = false
+ static final String SCM_PROVIDER_TYPE_DESCRIPTION = 'The SCM provider type. Possible values: SCM_MANAGER, GITLAB'
+ static final String GITLAB_CONFIG_DESCRIPTION = 'Config for GITLAB'
+ static final String SCMM_CONFIG_DESCRIPTION = 'Config for GITLAB'
+ static final String CENTRAL_ARGOCD_NAMESPACE_DESCRIPTION = 'Namespace for the centralized Argocd'
+ static final String CENTRAL_USEDEDICATED_DESCRIPTION = 'Toggles the Dedicated Instances Mode. See docs for more info'
+
+ @Option(names = ['--central-scm-provider'],
+ description = SCM_PROVIDER_TYPE_DESCRIPTION,
+ defaultValue = "SCM_MANAGER")
+ @JsonPropertyDescription(SCM_PROVIDER_TYPE_DESCRIPTION)
+ ScmProviderType scmProviderType = ScmProviderType.SCM_MANAGER
+
+ @JsonPropertyDescription(GITLAB_CONFIG_DESCRIPTION)
+ @Mixin
+ GitlabCentralConfig gitlab
+
+ @JsonPropertyDescription(SCMM_CONFIG_DESCRIPTION)
+ @Mixin
+ ScmManagerCentralConfig scmManager
+
+ @Option(names = ['--central-argocd-namespace'], description = CENTRAL_ARGOCD_NAMESPACE_DESCRIPTION)
+ @JsonPropertyDescription(CENTRAL_ARGOCD_NAMESPACE_DESCRIPTION)
+ String centralArgocdNamespace = 'argocd'
+
+ @Option(names = ['--dedicated-instance'], description = CENTRAL_USEDEDICATED_DESCRIPTION)
+ @JsonPropertyDescription(CENTRAL_USEDEDICATED_DESCRIPTION)
+ Boolean useDedicatedInstance = false
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/config/schema/JsonSchemaGenerator.groovy b/src/main/groovy/com/cloudogu/gitops/config/schema/JsonSchemaGenerator.groovy
index 6699cfcd0..27014b745 100644
--- a/src/main/groovy/com/cloudogu/gitops/config/schema/JsonSchemaGenerator.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/config/schema/JsonSchemaGenerator.groovy
@@ -1,34 +1,36 @@
package com.cloudogu.gitops.config.schema
import com.cloudogu.gitops.config.Config
+
+import jakarta.inject.Singleton
+
import com.fasterxml.jackson.annotation.JsonPropertyDescription
import com.fasterxml.jackson.databind.node.ObjectNode
import com.github.victools.jsonschema.generator.*
import com.github.victools.jsonschema.module.jackson.JacksonModule
-import jakarta.inject.Singleton
@Singleton
class JsonSchemaGenerator {
- static ObjectNode createSchema() {
- SchemaGeneratorConfigBuilder configBuilder =
- new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2020_12, OptionPreset.PLAIN_JSON)
- // Make the schema strict: Only allow our fields, warn when additional fields are passed
- .with(Option.FORBIDDEN_ADDITIONAL_PROPERTIES_BY_DEFAULT)
- // Exception to the above: For Maps allow additional fields.
- // We use this to allow inline helm values without having to validate them
- .with(Option.MAP_VALUES_AS_ADDITIONAL_PROPERTIES)
- // All fields can be set to null to use the default
- .with(Option.NULLABLE_FIELDS_BY_DEFAULT)
- .with(new JacksonModule( /* no options for now */))
- // Apply the rule to include only fields with @JsonProperty annotation
- configBuilder.forFields()
- .withIgnoreCheck((FieldScope field) -> {
- // Only include fields that are annotated with @JsonProperty
- return field.getAnnotation(JsonPropertyDescription) == null
- })
+ static ObjectNode createSchema() {
+ SchemaGeneratorConfigBuilder configBuilder =
+ new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2020_12, OptionPreset.PLAIN_JSON)
+ // Make the schema strict: Only allow our fields, warn when additional fields are passed
+ .with(Option.FORBIDDEN_ADDITIONAL_PROPERTIES_BY_DEFAULT)
+ // Exception to the above: For Maps allow additional fields.
+ // We use this to allow inline helm values without having to validate them
+ .with(Option.MAP_VALUES_AS_ADDITIONAL_PROPERTIES)
+ // All fields can be set to null to use the default
+ .with(Option.NULLABLE_FIELDS_BY_DEFAULT)
+ .with(new JacksonModule(/* no options for now */))
+ // Apply the rule to include only fields with @JsonProperty annotation
+ configBuilder.forFields()
+ .withIgnoreCheck((FieldScope field) -> {
+ // Only include fields that are annotated with @JsonProperty
+ return field.getAnnotation(JsonPropertyDescription) == null
+ })
- SchemaGenerator generator = new SchemaGenerator(configBuilder.build())
+ SchemaGenerator generator = new SchemaGenerator(configBuilder.build())
- return generator.generateSchema(Config)
- }
-}
+ return generator.generateSchema(Config)
+ }
+}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/config/schema/JsonSchemaValidator.groovy b/src/main/groovy/com/cloudogu/gitops/config/schema/JsonSchemaValidator.groovy
index d04664b9e..c8f505060 100644
--- a/src/main/groovy/com/cloudogu/gitops/config/schema/JsonSchemaValidator.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/config/schema/JsonSchemaValidator.groovy
@@ -1,27 +1,28 @@
package com.cloudogu.gitops.config.schema
+import groovy.util.logging.Slf4j
+
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.networknt.schema.JsonSchemaFactory
import com.networknt.schema.SpecVersionDetector
-import groovy.util.logging.Slf4j
@Slf4j
class JsonSchemaValidator {
- private static ObjectMapper objectMapper = new ObjectMapper()
-
- static void validate(Map yaml) {
- def json = objectMapper.convertValue(yaml, JsonNode)
- def schemaNode = JsonSchemaGenerator.createSchema()
- def schema = JsonSchemaFactory.getInstance(SpecVersionDetector.detect(schemaNode)).getSchema(schemaNode)
+ private static ObjectMapper objectMapper = new ObjectMapper()
+
+ static void validate(Map yaml) {
+ def json = objectMapper.convertValue(yaml, JsonNode)
+ def schemaNode = JsonSchemaGenerator.createSchema()
+ def schema = JsonSchemaFactory.getInstance(SpecVersionDetector.detect(schemaNode)).getSchema(schemaNode)
- log.debug("yaml configuration converted to json for validate {}", json)
+ log.debug("yaml configuration converted to json for validate {}", json)
- def validationMessages = schema.validate(json)
+ def validationMessages = schema.validate(json)
- if (!validationMessages.isEmpty()) {
- throw new RuntimeException("Config file invalid: " + validationMessages.join("\n"))
- }
- }
-}
+ if (!validationMessages.isEmpty()) {
+ throw new RuntimeException("Config file invalid: " + validationMessages.join("\n"))
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/dependencyinjection/HttpClientFactory.groovy b/src/main/groovy/com/cloudogu/gitops/dependencyinjection/HttpClientFactory.groovy
index 97db911da..174e9423e 100644
--- a/src/main/groovy/com/cloudogu/gitops/dependencyinjection/HttpClientFactory.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/dependencyinjection/HttpClientFactory.groovy
@@ -4,15 +4,8 @@ import com.cloudogu.gitops.config.Config
import com.cloudogu.gitops.config.Credentials
import com.cloudogu.gitops.git.providers.scmmanager.api.AuthorizationInterceptor
import com.cloudogu.gitops.okhttp.RetryInterceptor
-import groovy.transform.TupleConstructor
+
import io.micronaut.context.annotation.Factory
-import jakarta.inject.Named
-import jakarta.inject.Singleton
-import okhttp3.JavaNetCookieJar
-import okhttp3.OkHttpClient
-import okhttp3.logging.HttpLoggingInterceptor
-import org.jetbrains.annotations.NotNull
-import org.slf4j.LoggerFactory
import javax.net.ssl.HostnameVerifier
import javax.net.ssl.SSLContext
@@ -21,82 +14,89 @@ import javax.net.ssl.X509TrustManager
import java.security.SecureRandom
import java.security.cert.CertificateException
import java.security.cert.X509Certificate
+import jakarta.inject.Named
+import jakarta.inject.Singleton
+import groovy.transform.TupleConstructor
+
+import okhttp3.JavaNetCookieJar
+import okhttp3.OkHttpClient
+import okhttp3.logging.HttpLoggingInterceptor
+import org.jetbrains.annotations.NotNull
+import org.slf4j.LoggerFactory
@Factory
class HttpClientFactory {
- static OkHttpClient buildOkHttpClient(Credentials credentials, Boolean isInsecure) {
- def builder = new OkHttpClient.Builder()
- .addInterceptor(new AuthorizationInterceptor(credentials.username, credentials.password))
- .addInterceptor(createLoggingInterceptor())
- .addInterceptor(new RetryInterceptor())
-
- if (isInsecure) {
- def context = insecureSslContext()
- builder.sslSocketFactory(context.socketFactory, context.trustManager)
- }
-
- builder.hostnameVerifier({ hostname, session -> true } as HostnameVerifier)
-
- return builder.build()
- }
-
- @Singleton
- @Named("jenkins")
- OkHttpClient okHttpClientJenkins(Config config) {
- def builder = new OkHttpClient.Builder()
- .cookieJar(new JavaNetCookieJar(new CookieManager()))
- .addInterceptor(createLoggingInterceptor())
- .addInterceptor(new RetryInterceptor())
-
- if (config.application.insecure) {
- def context = insecureSslContext()
- builder.sslSocketFactory(context.socketFactory, context.trustManager)
- }
-
- return builder.build()
- }
-
- static HttpLoggingInterceptor createLoggingInterceptor() {
- def logger = LoggerFactory.getLogger("com.cloudogu.gitops.HttpClient")
-
- def ret = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
- @Override
- void log(@NotNull String msg) {
- logger.trace(msg)
- }
- })
-
- ret.setLevel(HttpLoggingInterceptor.Level.HEADERS)
- ret.redactHeader("Authorization")
-
- return ret
- }
-
- static InsecureSslContext insecureSslContext() {
- def noCheckTrustManager = new X509TrustManager() {
- @Override
- void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
- }
-
- @Override
- void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
- }
-
- @Override
- X509Certificate[] getAcceptedIssuers() {
- return new X509Certificate[0]
- }
- }
- def sslCtxt = SSLContext.getInstance('SSL')
- sslCtxt.init(null, [noCheckTrustManager] as X509TrustManager[], new SecureRandom())
-
- return new InsecureSslContext(sslCtxt.socketFactory, noCheckTrustManager)
- }
-
- @TupleConstructor(defaults = false)
- static class InsecureSslContext {
- final SSLSocketFactory socketFactory
- final X509TrustManager trustManager
- }
+ static OkHttpClient buildOkHttpClient(Credentials credentials, Boolean isInsecure) {
+ def builder = new OkHttpClient.Builder()
+ .addInterceptor(new AuthorizationInterceptor(credentials.username, credentials.password))
+ .addInterceptor(createLoggingInterceptor())
+ .addInterceptor(new RetryInterceptor())
+
+ if (isInsecure) {
+ def context = insecureSslContext()
+ builder.sslSocketFactory(context.socketFactory, context.trustManager)
+ }
+
+ builder.hostnameVerifier({ hostname, session -> true } as HostnameVerifier)
+
+ return builder.build()
+ }
+
+ @Singleton
+ @Named("jenkins")
+ OkHttpClient okHttpClientJenkins(Config config) {
+ def builder = new OkHttpClient.Builder()
+ .cookieJar(new JavaNetCookieJar(new CookieManager()))
+ .addInterceptor(createLoggingInterceptor())
+ .addInterceptor(new RetryInterceptor())
+
+ if (config.application.insecure) {
+ def context = insecureSslContext()
+ builder.sslSocketFactory(context.socketFactory, context.trustManager)
+ }
+
+ return builder.build()
+ }
+
+ static HttpLoggingInterceptor createLoggingInterceptor() {
+ def logger = LoggerFactory.getLogger("com.cloudogu.gitops.HttpClient")
+
+ def ret = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
+ @Override
+ void log(@NotNull String msg) {
+ logger.trace(msg)
+ }
+ })
+
+ ret.setLevel(HttpLoggingInterceptor.Level.HEADERS)
+ ret.redactHeader("Authorization")
+
+ return ret
+ }
+
+ static InsecureSslContext insecureSslContext() {
+ def noCheckTrustManager = new X509TrustManager() {
+ @Override
+ void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {}
+
+ @Override
+ void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {}
+
+ @Override
+ X509Certificate[] getAcceptedIssuers() {
+ return new X509Certificate[0]
+ }
+ }
+ def sslCtxt = SSLContext.getInstance('SSL')
+ sslCtxt.init(null, [noCheckTrustManager] as X509TrustManager[], new SecureRandom())
+
+ return new InsecureSslContext(sslCtxt.socketFactory, noCheckTrustManager)
+ }
+
+ @TupleConstructor(defaults = false)
+ static class InsecureSslContext {
+ final SSLSocketFactory socketFactory
+ final X509TrustManager trustManager
+ }
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/destroy/ArgoCDDestructionHandler.groovy b/src/main/groovy/com/cloudogu/gitops/destroy/ArgoCDDestructionHandler.groovy
index a47a2d712..88d69f4b7 100644
--- a/src/main/groovy/com/cloudogu/gitops/destroy/ArgoCDDestructionHandler.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/destroy/ArgoCDDestructionHandler.groovy
@@ -4,101 +4,91 @@ import com.cloudogu.gitops.config.Config
import com.cloudogu.gitops.features.git.GitHandler
import com.cloudogu.gitops.git.GitRepo
import com.cloudogu.gitops.git.GitRepoFactory
-import com.cloudogu.gitops.utils.FileSystemUtils
import com.cloudogu.gitops.kubernetes.api.HelmClient
import com.cloudogu.gitops.kubernetes.api.K8sClient
+import com.cloudogu.gitops.utils.FileSystemUtils
+
import io.micronaut.core.annotation.Order
-import jakarta.inject.Singleton
import java.nio.file.Path
+import jakarta.inject.Singleton
@Singleton
@Order(100)
class ArgoCDDestructionHandler implements DestructionHandler {
- private K8sClient k8sClient
- private GitRepoFactory repoProvider
- private HelmClient helmClient
- private Config config
- private FileSystemUtils fileSystemUtils
- private GitHandler gitHandler
- ArgoCDDestructionHandler(
- Config config,
- K8sClient k8sClient,
- GitRepoFactory repoProvider,
- HelmClient helmClient,
- FileSystemUtils fileSystemUtils,
- GitHandler gitHandler
- ) {
- this.k8sClient = k8sClient
- this.repoProvider = repoProvider
- this.helmClient = helmClient
- this.config = config
- this.fileSystemUtils = fileSystemUtils
- this.gitHandler = gitHandler
- }
-
- @Override
- void destroy() {
-
- def repo = repoProvider.getRepo("argocd/cloud-resources", gitHandler.resourcesScm)
- repo.cloneRepo()
-
- for (def app in k8sClient.getCustomResource("app")) {
- if (app.name == 'bootstrap' || app.name == 'argocd' || app.name == 'projects') {
- // we don't want bootstrap to kill everything
- // argocd and projects are needed for argocd to function and run finalizers
- continue
- }
-
- k8sClient.patch(
- "app",
- app.name,
- app.namespace,
- 'merge',
- [
- metadata: [
- finalizers: [
- "resources-finalizer.argocd.argoproj.io"
- ]
- ]
- ]
- )
- }
-
- List> appsToBeDeleted = [
- new Tuple2("argocd", "bootstrap"), // first to prevent recreation
- new Tuple2("argocd", "cluster-resources"),
- new Tuple2("argocd", "example-apps"),
- ]
-
- for (def app in appsToBeDeleted) {
- k8sClient.delete("app", app.v1, app.v2)
- }
-
- installArgoCDViaHelm(repo)
- helmClient.uninstall('argocd', 'argocd')
- for (def project in k8sClient.getCustomResource('appprojects')) {
- k8sClient.delete("appproject", project.namespace, project.name)
- }
-
- k8sClient.delete("app", 'argocd', "projects")
- k8sClient.delete("app", 'argocd', "argocd")
-
- k8sClient.delete('secret', 'default', 'jenkins-credentials')
- k8sClient.delete('secret', 'default', 'argocd-repo-creds-scm')
- }
-
- void installArgoCDViaHelm(GitRepo repo) {
- // this is a hack to be able to uninstall using helm
- def namePrefix = config.application.namePrefix
-
- // Install umbrella chart from folder
- String umbrellaChartPath = Path.of(repo.getAbsoluteLocalRepoTmpDir(), 'argocd/')
- // Even if the Chart.lock already contains the repo, we need to add it before resolving it
- // See https://github.com/helm/helm/issues/8036#issuecomment-872502901
- List helmDependencies = fileSystemUtils.readYaml(Path.of(umbrellaChartPath, 'Chart.yaml'))['dependencies']
- helmClient.addRepo('argo', helmDependencies[0]['repository'] as String)
- helmClient.dependencyBuild(umbrellaChartPath)
- helmClient.upgrade('argocd', umbrellaChartPath, [namespace: "${namePrefix}argocd"])
- }
+ private K8sClient k8sClient
+ private GitRepoFactory repoProvider
+ private HelmClient helmClient
+ private Config config
+ private FileSystemUtils fileSystemUtils
+ private GitHandler gitHandler
+
+ ArgoCDDestructionHandler(Config config,
+ K8sClient k8sClient,
+ GitRepoFactory repoProvider,
+ HelmClient helmClient,
+ FileSystemUtils fileSystemUtils,
+ GitHandler gitHandler) {
+ this.k8sClient = k8sClient
+ this.repoProvider = repoProvider
+ this.helmClient = helmClient
+ this.config = config
+ this.fileSystemUtils = fileSystemUtils
+ this.gitHandler = gitHandler
+ }
+
+ @Override
+ void destroy() {
+
+ def repo = repoProvider.getRepo("argocd/cloud-resources", gitHandler.resourcesScm)
+ repo.cloneRepo()
+
+ for (def app in k8sClient.getCustomResource("app")) {
+ if (app.name == 'bootstrap' || app.name == 'argocd' || app.name == 'projects') {
+ // we don't want bootstrap to kill everything
+ // argocd and projects are needed for argocd to function and run finalizers
+ continue
+ }
+
+ k8sClient.patch("app",
+ app.name,
+ app.namespace,
+ 'merge',
+ [metadata: [finalizers: ["resources-finalizer.argocd.argoproj.io"]]])
+ }
+
+ List> appsToBeDeleted = [new Tuple2("argocd", "bootstrap"), // first to prevent recreation
+ new Tuple2("argocd", "cluster-resources"),
+ new Tuple2("argocd", "example-apps"),]
+
+ for (def app in appsToBeDeleted) {
+ k8sClient.delete("app", app.v1, app.v2)
+ }
+
+ installArgoCDViaHelm(repo)
+ helmClient.uninstall('argocd', 'argocd')
+ for (def project in k8sClient.getCustomResource('appprojects')) {
+ k8sClient.delete("appproject", project.namespace, project.name)
+ }
+
+ k8sClient.delete("app", 'argocd', "projects")
+ k8sClient.delete("app", 'argocd', "argocd")
+
+ k8sClient.delete('secret', 'default', 'jenkins-credentials')
+ k8sClient.delete('secret', 'default', 'argocd-repo-creds-scm')
+ }
+
+ void installArgoCDViaHelm(GitRepo repo) {
+ // this is a hack to be able to uninstall using helm
+ def namePrefix = config.application.namePrefix
+
+ // Install umbrella chart from folder
+ String umbrellaChartPath = Path.of(repo.getAbsoluteLocalRepoTmpDir(), 'argocd/')
+ // Even if the Chart.lock already contains the repo, we need to add it before resolving it
+ // See https://github.com/helm/helm/issues/8036#issuecomment-872502901
+ List helmDependencies = fileSystemUtils.readYaml(Path.of(umbrellaChartPath, 'Chart.yaml'))['dependencies']
+ helmClient.addRepo('argo', helmDependencies[0]['repository'] as String)
+ helmClient.dependencyBuild(umbrellaChartPath)
+ helmClient.upgrade('argocd', umbrellaChartPath, [namespace: "${namePrefix}argocd"])
+ }
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/destroy/Destroyer.groovy b/src/main/groovy/com/cloudogu/gitops/destroy/Destroyer.groovy
index b44089ca6..edb8f0e96 100644
--- a/src/main/groovy/com/cloudogu/gitops/destroy/Destroyer.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/destroy/Destroyer.groovy
@@ -1,28 +1,28 @@
package com.cloudogu.gitops.destroy
-import groovy.util.logging.Slf4j
import jakarta.inject.Singleton
+import groovy.util.logging.Slf4j
@Singleton
@Slf4j
class Destroyer {
- final List destructionHandlers
+ final List destructionHandlers
- Destroyer(List destructionHandlers) {
- this.destructionHandlers = destructionHandlers
- }
+ Destroyer(List destructionHandlers) {
+ this.destructionHandlers = destructionHandlers
+ }
- void destroy() {
- log.info("Start destroying")
- for (def handler in destructionHandlers) {
- log.info("Running handler $handler.class.simpleName")
- handler.destroy()
- }
- log.info("Finished destroying")
- }
+ void destroy() {
+ log.info("Start destroying")
+ for (def handler in destructionHandlers) {
+ log.info("Running handler $handler.class.simpleName")
+ handler.destroy()
+ }
+ log.info("Finished destroying")
+ }
- List getDestructionHandlers() {
- return destructionHandlers
- }
-}
+ List getDestructionHandlers() {
+ return destructionHandlers
+ }
+}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/destroy/DestructionHandler.groovy b/src/main/groovy/com/cloudogu/gitops/destroy/DestructionHandler.groovy
index 837eeb5ee..d049bc31b 100644
--- a/src/main/groovy/com/cloudogu/gitops/destroy/DestructionHandler.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/destroy/DestructionHandler.groovy
@@ -1,5 +1,5 @@
package com.cloudogu.gitops.destroy
interface DestructionHandler {
- void destroy()
-}
+ void destroy()
+}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/destroy/JenkinsDestructionHandler.groovy b/src/main/groovy/com/cloudogu/gitops/destroy/JenkinsDestructionHandler.groovy
index 6ea7f5202..1e8290b5c 100644
--- a/src/main/groovy/com/cloudogu/gitops/destroy/JenkinsDestructionHandler.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/destroy/JenkinsDestructionHandler.groovy
@@ -3,29 +3,31 @@ package com.cloudogu.gitops.destroy
import com.cloudogu.gitops.config.Config
import com.cloudogu.gitops.jenkins.GlobalPropertyManager
import com.cloudogu.gitops.jenkins.JobManager
+
import io.micronaut.core.annotation.Order
+
import jakarta.inject.Singleton
@Singleton
@Order(300)
class JenkinsDestructionHandler implements DestructionHandler {
- private JobManager jobManager
- private GlobalPropertyManager globalPropertyManager
- private Config configuration
+ private JobManager jobManager
+ private GlobalPropertyManager globalPropertyManager
+ private Config configuration
- JenkinsDestructionHandler(JobManager jobManager, Config configuration, GlobalPropertyManager globalPropertyManager) {
- this.jobManager = jobManager
- this.configuration = configuration
- this.globalPropertyManager = globalPropertyManager
- }
+ JenkinsDestructionHandler(JobManager jobManager, Config configuration, GlobalPropertyManager globalPropertyManager) {
+ this.jobManager = jobManager
+ this.configuration = configuration
+ this.globalPropertyManager = globalPropertyManager
+ }
- @Override
- void destroy() {
- jobManager.deleteJob("${configuration.application.namePrefix}example-apps")
- globalPropertyManager.deleteGlobalProperty("SCMM_URL")
- globalPropertyManager.deleteGlobalProperty("${configuration.application.namePrefixForEnvVars}REGISTRY_URL")
- globalPropertyManager.deleteGlobalProperty("${configuration.application.namePrefixForEnvVars}REGISTRY_PATH")
- globalPropertyManager.deleteGlobalProperty("${configuration.application.namePrefixForEnvVars}REGISTRY_PROXY_URL")
- globalPropertyManager.deleteGlobalProperty("${configuration.application.namePrefixForEnvVars}K8S_VERSION")
- }
-}
+ @Override
+ void destroy() {
+ jobManager.deleteJob("${configuration.application.namePrefix}example-apps")
+ globalPropertyManager.deleteGlobalProperty("SCMM_URL")
+ globalPropertyManager.deleteGlobalProperty("${configuration.application.namePrefixForEnvVars}REGISTRY_URL")
+ globalPropertyManager.deleteGlobalProperty("${configuration.application.namePrefixForEnvVars}REGISTRY_PATH")
+ globalPropertyManager.deleteGlobalProperty("${configuration.application.namePrefixForEnvVars}REGISTRY_PROXY_URL")
+ globalPropertyManager.deleteGlobalProperty("${configuration.application.namePrefixForEnvVars}K8S_VERSION")
+ }
+}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/destroy/ScmmDestructionHandler.groovy b/src/main/groovy/com/cloudogu/gitops/destroy/ScmmDestructionHandler.groovy
index edf91375e..6c0ee2747 100644
--- a/src/main/groovy/com/cloudogu/gitops/destroy/ScmmDestructionHandler.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/destroy/ScmmDestructionHandler.groovy
@@ -2,48 +2,48 @@ package com.cloudogu.gitops.destroy
import com.cloudogu.gitops.config.Config
import com.cloudogu.gitops.git.providers.scmmanager.api.ScmManagerApiClient
+
import io.micronaut.core.annotation.Order
+
import jakarta.inject.Singleton
@Singleton
@Order(200)
class ScmmDestructionHandler implements DestructionHandler {
- private ScmManagerApiClient scmmApiClient
- private Config config
-
- ScmmDestructionHandler(
- Config config
- ) {
- this.config = config
- this.scmmApiClient = scmmApiClient
- }
-
- @Override
- void destroy() {
- deleteUser("gitops")
- deleteRepository("argocd", "argocd")
- deleteRepository("argocd", "cluster-resources")
- deleteRepository("argocd", "example-apps")
- deleteRepository("3rd-party-dependencies", "ces-build-lib", false)
- deleteRepository("3rd-party-dependencies", "gitops-build-lib", false)
- deleteRepository("3rd-party-dependencies", "spring-boot-helm-chart", false)
- deleteRepository("3rd-party-dependencies", "spring-boot-helm-chart-with-dependency", false)
- }
-
- private void deleteRepository(String namespace, String repository, boolean prefixNamespace = true) {
- def namePrefix = prefixNamespace ? config.application.namePrefix : ''
- def response = scmmApiClient.repositoryApi().delete("${namePrefix}$namespace", repository).execute()
-
- if (response.code() != 204) {
- throw new RuntimeException("Could not delete user $namespace/$repository (${response.code()} ${response.message()}): ${response.errorBody().string()}")
- }
- }
-
- private void deleteUser(String name) {
- def response = scmmApiClient.usersApi().delete("${config.application.namePrefix}$name").execute()
-
- if (response.code() != 204) {
- throw new RuntimeException("Could not delete user $name (${response.code()} ${response.message()}): ${response.errorBody().string()}")
- }
- }
+ private ScmManagerApiClient scmmApiClient
+ private Config config
+
+ ScmmDestructionHandler(Config config) {
+ this.config = config
+ this.scmmApiClient = scmmApiClient
+ }
+
+ @Override
+ void destroy() {
+ deleteUser("gitops")
+ deleteRepository("argocd", "argocd")
+ deleteRepository("argocd", "cluster-resources")
+ deleteRepository("argocd", "example-apps")
+ deleteRepository("3rd-party-dependencies", "ces-build-lib", false)
+ deleteRepository("3rd-party-dependencies", "gitops-build-lib", false)
+ deleteRepository("3rd-party-dependencies", "spring-boot-helm-chart", false)
+ deleteRepository("3rd-party-dependencies", "spring-boot-helm-chart-with-dependency", false)
+ }
+
+ private void deleteRepository(String namespace, String repository, boolean prefixNamespace = true) {
+ def namePrefix = prefixNamespace ? config.application.namePrefix : ''
+ def response = scmmApiClient.repositoryApi().delete("${namePrefix}$namespace", repository).execute()
+
+ if (response.code() != 204) {
+ throw new RuntimeException("Could not delete user $namespace/$repository (${response.code()} ${response.message()}): ${response.errorBody().string()}")
+ }
+ }
+
+ private void deleteUser(String name) {
+ def response = scmmApiClient.usersApi().delete("${config.application.namePrefix}$name").execute()
+
+ if (response.code() != 204) {
+ throw new RuntimeException("Could not delete user $name (${response.code()} ${response.message()}): ${response.errorBody().string()}")
+ }
+ }
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/features/CertManager.groovy b/src/main/groovy/com/cloudogu/gitops/features/CertManager.groovy
index 854a7edaa..b43f30afd 100644
--- a/src/main/groovy/com/cloudogu/gitops/features/CertManager.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/features/CertManager.groovy
@@ -5,49 +5,49 @@ import com.cloudogu.gitops.FeatureWithImage
import com.cloudogu.gitops.config.Config
import com.cloudogu.gitops.features.deployment.DeploymentStrategy
import com.cloudogu.gitops.features.git.GitHandler
+import com.cloudogu.gitops.kubernetes.api.K8sClient
import com.cloudogu.gitops.utils.AirGappedUtils
import com.cloudogu.gitops.utils.FileSystemUtils
-import com.cloudogu.gitops.kubernetes.api.K8sClient
-import groovy.util.logging.Slf4j
+
import io.micronaut.core.annotation.Order
+
import jakarta.inject.Singleton
+import groovy.util.logging.Slf4j
@Slf4j
@Singleton
@Order(160)
class CertManager extends Feature implements FeatureWithImage {
- static final String HELM_VALUES_PATH = "argocd/cluster-resources/apps/cert-manager/templates/certManager-helm-values.ftl.yaml"
-
- final K8sClient k8sClient
- final Config config
- final String namespace = "${config.application.namePrefix}cert-manager"
-
- CertManager(
- Config config,
- FileSystemUtils fileSystemUtils,
- DeploymentStrategy deployer,
- K8sClient k8sClient,
- AirGappedUtils airGappedUtils,
- GitHandler gitHandler
- ) {
- this.deployer = deployer
- this.config = config
- this.fileSystemUtils = fileSystemUtils
- this.k8sClient = k8sClient
- this.airGappedUtils = airGappedUtils
- this.gitHandler = gitHandler
- }
-
- @Override
- boolean isEnabled() {
- return config.features.certManager.active
- }
-
- @Override
- void enable() {
- def helmConfig = config.features.certManager.helm
-
- deployHelmChart('cert-manager', 'cert-manager', namespace, helmConfig, HELM_VALUES_PATH, config)
- }
+ static final String HELM_VALUES_PATH = "argocd/cluster-resources/apps/cert-manager/templates/certManager-helm-values.ftl.yaml"
+
+ final K8sClient k8sClient
+ final Config config
+ final String namespace = "${config.application.namePrefix}cert-manager"
+
+ CertManager(Config config,
+ FileSystemUtils fileSystemUtils,
+ DeploymentStrategy deployer,
+ K8sClient k8sClient,
+ AirGappedUtils airGappedUtils,
+ GitHandler gitHandler) {
+ this.deployer = deployer
+ this.config = config
+ this.fileSystemUtils = fileSystemUtils
+ this.k8sClient = k8sClient
+ this.airGappedUtils = airGappedUtils
+ this.gitHandler = gitHandler
+ }
+
+ @Override
+ boolean isEnabled() {
+ return config.features.certManager.active
+ }
+
+ @Override
+ void enable() {
+ def helmConfig = config.features.certManager.helm
+
+ deployHelmChart('cert-manager', 'cert-manager', namespace, helmConfig, HELM_VALUES_PATH, config)
+ }
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/features/ContentLoader.groovy b/src/main/groovy/com/cloudogu/gitops/features/ContentLoader.groovy
index 81fd19f8a..5a222dfac 100644
--- a/src/main/groovy/com/cloudogu/gitops/features/ContentLoader.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/features/ContentLoader.groovy
@@ -1,5 +1,8 @@
package com.cloudogu.gitops.features
+import static com.cloudogu.gitops.config.Config.ContentRepoType
+import static com.cloudogu.gitops.config.Config.ContentSchema.ContentRepositorySchema
+
import com.cloudogu.gitops.Feature
import com.cloudogu.gitops.config.Config
import com.cloudogu.gitops.config.Config.OverwriteMode
@@ -7,16 +10,19 @@ import com.cloudogu.gitops.config.Credentials
import com.cloudogu.gitops.features.git.GitHandler
import com.cloudogu.gitops.git.GitRepo
import com.cloudogu.gitops.git.GitRepoFactory
+import com.cloudogu.gitops.kubernetes.api.K8sClient
import com.cloudogu.gitops.utils.AllowListFreemarkerObjectWrapper
import com.cloudogu.gitops.utils.FileSystemUtils
-import com.cloudogu.gitops.kubernetes.api.K8sClient
import com.cloudogu.gitops.utils.TemplatingEngine
+
+import io.micronaut.core.annotation.Order
+
+import jakarta.inject.Singleton
+import groovy.util.logging.Slf4j
+
import com.fasterxml.jackson.annotation.JsonIgnore
import freemarker.template.Configuration
import freemarker.template.DefaultObjectWrapperBuilder
-import groovy.util.logging.Slf4j
-import io.micronaut.core.annotation.Order
-import jakarta.inject.Singleton
import org.apache.commons.io.FileUtils
import org.eclipse.jgit.api.CloneCommand
import org.eclipse.jgit.api.Git
@@ -24,553 +30,524 @@ import org.eclipse.jgit.lib.Ref
import org.eclipse.jgit.lib.Repository
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider
-import static com.cloudogu.gitops.config.Config.ContentRepoType
-import static com.cloudogu.gitops.config.Config.ContentSchema.ContentRepositorySchema
-
@Slf4j
@Singleton
@Order(999)
// We want to evaluate content last, to allow for changing all other repos
class ContentLoader extends Feature {
- private Config config
- private K8sClient k8sClient
- private GitRepoFactory repoProvider
- private Jenkins jenkins
- // set by lazy initialisation
- private TemplatingEngine templatingEngine
- // used to clone repos in validation phase
- private List cachedRepoCoordinates = new ArrayList<>()
- private GitHandler gitHandler
-
- protected File mergedReposFolder
-
- //For security reasons we safe the credentialsProvider for each repo here and not in config pro each repo
- @JsonIgnore
- UsernamePasswordCredentialsProvider credentialsProvider
-
- ContentLoader(
- Config config, K8sClient k8sClient, GitRepoFactory repoProvider, Jenkins jenkins, GitHandler gitHandler
- ) {
- this.config = config
- this.k8sClient = k8sClient
- this.repoProvider = repoProvider
- this.jenkins = jenkins
- this.gitHandler = gitHandler
- }
-
- @Override
- boolean isEnabled() {
- return true // for now always on. Once we refactor from Argo CD class we add a param to enable
- }
-
- @Override
- void enable() {
- // ensure cache is cleaned
- clearCache()
- // clones repo to check valid configuration and reuse result for further step.
- cachedRepoCoordinates = cloneContentRepos()
-
- createImagePullSecrets()
-
- createContentRepos()
- }
-
- @Override
- void validate() {
-
- }
-
- @Override
- void preConfigInit(Config configToSet) {
- config.content.repos.each { repo ->
-
- if (!repo.url) {
- throw new RuntimeException("content.repos requires a url parameter.")
- }
- if (repo.target) {
- if (repo.target.count('/') == 0) {
- throw new RuntimeException("content.target needs / to separate namespace/group from repo name. Repo: ${repo.url}")
- }
- }
-
- switch (repo.type) {
- case ContentRepoType.COPY:
- if (!repo.target) {
- throw new RuntimeException("content.repos.type ${ContentRepoType.COPY} requires content.repos.target to be set. Repo: ${repo.url}")
- }
- break
- case ContentRepoType.FOLDER_BASED:
- if (repo.target) {
- throw new RuntimeException("content.repos.type ${ContentRepoType.FOLDER_BASED} does not support target parameter. Repo: ${repo.url}")
- }
- if (repo.targetRef) {
- throw new RuntimeException("content.repos.type ${ContentRepoType.FOLDER_BASED} does not support targetRef parameter. Repo: ${repo.url}")
- }
- break
- case ContentRepoType.MIRROR:
- if (!repo.target) {
- throw new RuntimeException("content.repos.type ${ContentRepoType.MIRROR} requires content.repos.target to be set. Repo: ${repo.url}")
- }
- if (repo.path != ContentRepositorySchema.DEFAULT_PATH) {
- throw new RuntimeException("content.repos.type ${ContentRepoType.MIRROR} does not support path. Current path: ${repo.path}. Repo: ${repo.url}")
- }
- if (repo.templating) {
- throw new RuntimeException("content.repos.type ${ContentRepoType.MIRROR} does not support templating. Repo: ${repo.url}")
- }
- break
- }
- }
- }
-
- void createImagePullSecrets() {
- if (config.registry.createImagePullSecrets) {
- String registryUsername = config.registry.readOnlyUsername ?: config.registry.username
- String registryPassword = config.registry.readOnlyPassword ?: config.registry.password
-
- config.content.namespaces.each { String namespace ->
- def registrySecretName = 'registry'
-
- k8sClient.createNamespace(namespace)
-
- k8sClient.createImagePullSecret(registrySecretName, namespace,
- config.registry.url /* Only domain matters, path would be ignored */,
- registryUsername, registryPassword)
-
- k8sClient.patch('serviceaccount', 'default', namespace,
- [imagePullSecrets: [[name: registrySecretName]]])
-
- if (config.registry.twoRegistries) {
- k8sClient.createImagePullSecret('proxy-registry', namespace,
- config.registry.proxyUrl, config.registry.proxyUsername,
- config.registry.proxyPassword)
- }
- }
- }
- }
-
- void createContentRepos() {
- if (cachedRepoCoordinates.empty) {
- cachedRepoCoordinates = cloneContentRepos()
- }
- pushTargetRepos(cachedRepoCoordinates)
- // after all, clean folders and list
- clearCache()
-
- }
-
- protected List cloneContentRepos() {
- mergedReposFolder = File.createTempDir('gitops-playground-based-content-repos-')
- List repoCoordinates = []
-
- log.debug("Aggregating structure for all ${config.content.repos.size()} repos.")
- config.content.repos.each { repoConfig ->
- createRepoCoordinates(repoConfig, mergedReposFolder, repoCoordinates)
- }
- return repoCoordinates
- }
-
-
- private TemplatingEngine getTemplatingEngine() {
- if (templatingEngine == null) {
- templatingEngine = new TemplatingEngine()
- }
- return templatingEngine
- }
-
-
- private void createRepoCoordinates(ContentRepositorySchema repoConfig, File mergedReposFolder, List repoCoordinates) {
- def repoTmpDir = File.createTempDir('gitops-playground-content-repo-')
- log.debug("Cloning content repo, ${repoConfig.url}, revision ${repoConfig.ref}, path ${repoConfig.path}, overwriteMode ${repoConfig.overwriteMode}")
-
-
- if (repoConfig.credentials?.username != null && repoConfig.credentials?.password != null) {
- credentialsProvider = new UsernamePasswordCredentialsProvider(repoConfig.credentials.username, repoConfig.credentials.password)
- } else if (repoConfig.credentials?.secretName && repoConfig.credentials?.secretNamespace) {
- Credentials credentials = this.k8sClient.k8sJavaApiClient.getCredentialsFromSecret(repoConfig.credentials)
- credentialsProvider = new UsernamePasswordCredentialsProvider(credentials.username, credentials.password)
- }
-
- cloneToLocalFolder(repoConfig, repoTmpDir)
-
- def contentRepoDir = new File(repoTmpDir, repoConfig.path)
- applyTemplatingIfApplicable(repoConfig, contentRepoDir)
-
-
- switch (repoConfig.type) {
- case ContentRepoType.FOLDER_BASED:
- createRepoCoordinatesForTypeFolderBased(repoConfig, repoTmpDir, contentRepoDir, mergedReposFolder, repoCoordinates)
- repoTmpDir.deleteDir()
- break
- case ContentRepoType.COPY:
- createRepoCoordinatesForTypeCopy(repoConfig, contentRepoDir, mergedReposFolder, repoTmpDir, repoCoordinates)
- repoTmpDir.deleteDir()
- break
- case ContentRepoType.MIRROR:
- createRepoCoordinateForTypeMirror(repoConfig, repoTmpDir, repoCoordinates)
- // intentionally not deleting repoTmpDir, it is contained in RepoCoordinates for MIRROR usage
- break
- }
- log.debug("Finished cloning content repos. repoCoordinates=${repoCoordinates}")
- }
-
- private static void createRepoCoordinatesForTypeCopy(ContentRepositorySchema repoConfig, File contentRepoDir, File mergedReposFolder, File repoTmpDir, List repoCoordinates) {
- String namespace = repoConfig.target.split('/')[0]
- String repoName = repoConfig.target.split('/')[1]
-
- def repoCoordinate = mergeRepoDirs(contentRepoDir, namespace, repoName, mergedReposFolder, repoConfig)
- repoCoordinate.refIsTag = GitRepo.isTag(repoTmpDir, repoConfig.ref)
- addRepoCoordinates(repoCoordinates, repoCoordinate)
- }
-
- private static void createRepoCoordinatesForTypeFolderBased(ContentRepositorySchema repoConfig, File repoTmpDir, File contentRepoDir, File mergedReposFolder, List repoCoordinates) {
- boolean refIsTag = GitRepo.isTag(repoTmpDir, repoConfig.ref)
- findRepoDirectories(contentRepoDir)
- .each { contentRepoNamespaceDir ->
- findRepoDirectories(contentRepoNamespaceDir)
- .each { contentRepoFolder ->
- String namespace = contentRepoNamespaceDir.name
- String repoName = contentRepoFolder.name
- def repoCoordinate = mergeRepoDirs(contentRepoFolder, namespace, repoName, mergedReposFolder, repoConfig)
- repoCoordinate.refIsTag = refIsTag
- addRepoCoordinates(repoCoordinates, repoCoordinate)
- }
- }
- }
-
- private static void createRepoCoordinateForTypeMirror(ContentRepositorySchema repoConfig, File repoTmpDir, List repoCoordinates) {
- // Don't merge but keep these in separate dirs.
- // This avoids messing up .git folders with possible confusing exceptions for the user
- String namespace = repoConfig.target.split('/')[0]
- String repoName = repoConfig.target.split('/')[1]
- def repoCoordinate = new RepoCoordinate(
- namespace: namespace,
- repoName: repoName,
- clonedContentRepo: repoTmpDir,
- repoConfig: repoConfig,
- refIsTag: GitRepo.isTag(repoTmpDir, repoConfig.ref)
- )
- addRepoCoordinates(repoCoordinates, repoCoordinate)
- }
-
- /**
- * Merges the files of src into the mergeRepoFolder/namespace/name and adds a new object to repoCoordinates.
- *
- * Note that existing repoCoordinate objects with different overwriteMode are overwritten. The last repo to be mentioned within config.content.repos wins!
- */
- private static RepoCoordinate mergeRepoDirs(File src, String namespace, String repoName, File mergedRepoFolder,
- ContentRepositorySchema repoConfig) {
- File target = new File(new File(mergedRepoFolder, namespace), repoName)
- log.debug("Merging content repo, namespace ${namespace}, repoName ${repoName} from ${src} to ${target}")
- FileUtils.copyDirectory(src, target, new FileSystemUtils.IgnoreDotGitFolderFilter())
-
- def repoCoordinate = new RepoCoordinate(
- namespace: namespace,
- repoName: repoName,
- clonedContentRepo: target,
- repoConfig: repoConfig,
- )
- return repoCoordinate
- }
-
- private static List findRepoDirectories(File srcRepo) {
- srcRepo.listFiles().findAll {
- it.isDirectory() &&
- // Exclude .git for example
- !it.name.startsWith('.')
- }
- }
-
-
- private void applyTemplatingIfApplicable(ContentRepositorySchema repoConfig, File srcPath) {
- if (repoConfig.templating) {
- def engine = getTemplatingEngine()
-
- GitRepo repo = this.repoProvider.getRepo(repoConfig.target, this.gitHandler.tenant)
-
- engine.replaceTemplates(srcPath, [
- config : config,
- scm : [
- baseUrl : repo.gitProvider.url,
- host : repo.gitProvider.host,
- protocol: repo.gitProvider.protocol,
- repoUrl : repo.gitProvider.repoPrefix(),
- ],
- // Allow for using static classes inside the templates
- statics: !config.content.useWhitelist ? new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_32).build().getStaticModels() :
- new AllowListFreemarkerObjectWrapper(Configuration.VERSION_2_3_32, config.content.getAllowedStaticsWhitelist()).getStaticModels()
- ])
- }
- }
-
- private void cloneToLocalFolder(ContentRepositorySchema repoConfig, File repoTmpDir) {
-
-
- def cloneCommand = gitClone()
- .setURI(repoConfig.url)
- .setDirectory(repoTmpDir)
- .setNoCheckout(false)// Checkout default branch
-
- if (credentialsProvider) {
- cloneCommand.setCredentialsProvider(credentialsProvider)
- }
-
- def git = cloneCommand.call()
-
- if (ContentRepoType.MIRROR == repoConfig.type) {
- def fetch = git.fetch()
-
- if (credentialsProvider) {
- fetch.setCredentialsProvider(credentialsProvider)
- }
- fetch.setRefSpecs("+refs/*:refs/*").call() // Fetch all branches and tags
- }
-
- if (repoConfig.ref) {
- def actualRef = findRef(repoConfig, git.repository)
- git.checkout().setName(actualRef).call()
- }
- }
-
- private static String findRef(ContentRepositorySchema repoConfig, Repository gitRepo) {
- // Check if ref exists first to avoid InvalidRefNameException
- // Note that this works for commits and shortname tags but not shortname branches 🙄
- if (gitRepo.resolve(repoConfig.ref)) {
- return repoConfig.ref
- }
-
- // Check branches or tags
- def remoteCommand = Git.lsRemoteRepository()
- .setRemote(repoConfig.url)
- .setHeads(true)
- .setTags(true)
-
- Collection[ refs = remoteCommand.call()
- String potentialRef = refs.find { it.name.endsWith(repoConfig.ref) }?.name
-
- if (!potentialRef) {
- // Jgit silently ignores some missing refs and just continues with default branch.
- // This might lead to unexpected surprises for our users, so better fail explicitly
- throw new RuntimeException("Reference '${repoConfig.ref}' not found in content repository '${repoConfig.url}'")
- }
-
- // Jgit only checks out remote branches when they start in origin/ 🙄
- return potentialRef.replace('refs/heads/', 'origin/')
- }
-
-
- private void pushTargetRepos(List] repoCoordinates) {
- repoCoordinates.each { repoCoordinate ->
-
- GitRepo targetRepo = repoProvider.getRepo(repoCoordinate.fullRepoName, this.gitHandler.tenant)
- boolean isNewRepo = targetRepo.createRepositoryAndSetPermission("", false)
-
- if (isValidForPush(isNewRepo, repoCoordinate)) {
- targetRepo.cloneRepo()
-
- switch (repoCoordinate.repoConfig.type) {
- case ContentRepoType.MIRROR:
- handleRepoMirroring(repoCoordinate, targetRepo)
- break
- // COPY and FOLDER_BASED same treatment
- case ContentRepoType.FOLDER_BASED:
- case ContentRepoType.COPY:
- handleRepoCopyingOrFolderBased(repoCoordinate, targetRepo, isNewRepo)
- break
- }
-
- createJenkinsJobIfApplicable(repoCoordinate, targetRepo)
-
- // cleaning tmp folders
- repoCoordinate.clonedContentRepo.deleteDir()
- new File(targetRepo.absoluteLocalRepoTmpDir).deleteDir()
- } // no else needed
- }
-
- }
-
- /**
- * Copies repoCoordinate to targetRepo, commits and pushes
- * Same logic for both FOLDER_BASED and COPY repo types.
- */
- private static void handleRepoCopyingOrFolderBased(RepoCoordinate repoCoordinate, GitRepo targetRepo, boolean isNewRepo) {
- if (!isNewRepo) {
- clearTargetRepoIfApplicable(repoCoordinate, targetRepo)
- }
- // Avoid overwriting .git in target to avoid, because we don't need it for copying and
- // git pack files are typically read-only, leading to IllegalArgumentException:
- // File parameter 'destFile is not writable: .git/objects/pack/pack-123.pack
- targetRepo.copyDirectoryContents(repoCoordinate.clonedContentRepo.absolutePath, new FileSystemUtils.IgnoreDotGitFolderFilter())
-
- String commitMessage = "Initialize content repo ${repoCoordinate.namespace}/${repoCoordinate.repoName}"
- String targetRefShort = repoCoordinate.repoConfig.targetRef.replace('refs/heads/', '').replace('refs/tags/', '')
- if (targetRefShort) {
- String refSpec = setRefSpec(repoCoordinate, targetRefShort)
- targetRepo.commitAndPush(commitMessage, targetRefShort, refSpec)
- } else {
- targetRepo.commitAndPush(commitMessage)
- }
-
- }
-
- private static String setRefSpec(RepoCoordinate repoCoordinate, String targetRefShort) {
- String refSpec
- if ((repoCoordinate.refIsTag && !repoCoordinate.repoConfig.targetRef.startsWith('refs/heads'))
- || repoCoordinate.repoConfig.targetRef.startsWith('refs/tags')) {
- refSpec = "refs/tags/${targetRefShort}:refs/tags/${targetRefShort}"
- } else {
- refSpec = "HEAD:refs/heads/${targetRefShort}"
- }
- refSpec
- }
-
- private static void clearTargetRepoIfApplicable(RepoCoordinate repoCoordinate, GitRepo targetRepo) {
- if (OverwriteMode.INIT != repoCoordinate.repoConfig.overwriteMode) {
- if (OverwriteMode.RESET == repoCoordinate.repoConfig.overwriteMode) {
- log.info("OverwriteMode ${OverwriteMode.RESET} set for repo '${repoCoordinate.fullRepoName}': " +
- "Deleting existing files in repo and replacing them with new content.")
- targetRepo.clearRepo()
- } else {
- log.debug("OverwriteMode ${OverwriteMode.UPGRADE} set for repo '${repoCoordinate.fullRepoName}': " +
- "Merging new content into existing repo. ")
- }
- }
- }
-
- /**
- * Force pushes repoCoordinate.repoConfig.ref or all refs to targetRepo
- */
- private static void handleRepoMirroring(RepoCoordinate repoCoordinate, GitRepo targetRepo) {
- try (def targetGit = Git.open(new File(targetRepo.absoluteLocalRepoTmpDir))) {
- def remoteUrl = targetGit.repository.config.getString('remote', 'origin', 'url')
-
- // In mirror mode, we mainly need the .git folder to push the whole git history, branches and tags.
- // So copying source to target repo, .git folders are merged.
- // git pack files are typically read-only, leading to
- // IllegalArgumentException: File parameter 'destFile is not writable: .git/objects/pack/pack-123.pack
- // Workaround: make .git writable.
- // Note: Setting target remote in source repo and pushing from there causes other problems like
- // IOException: Source ref someBranch doesn't resolve to any object.
- FileSystemUtils.makeWritable(new File(targetRepo.absoluteLocalRepoTmpDir, '.git'))
-
- targetRepo.copyDirectoryContents(repoCoordinate.clonedContentRepo.absolutePath)
-
- // Restore remote, it could have been overwritten due to a copied .git folder in MIRROR mode
- targetGit.repository.config.setString('remote', 'origin', 'url', remoteUrl)
- targetGit.repository.config.save()
- }
-
- if (repoCoordinate.repoConfig.ref) {
- validateCommitReferences(repoCoordinate)
- if (repoCoordinate.repoConfig.targetRef) {
- log.debug("Mirroring repo '${repoCoordinate.repoConfig.url}' ref '${repoCoordinate.repoConfig.ref}' to target repo ${repoCoordinate.fullRepoName}, targetRef: '${repoCoordinate.repoConfig.targetRef}'")
- targetRepo.pushRef(repoCoordinate.repoConfig.ref, repoCoordinate.repoConfig.targetRef, true)
- } else {
- log.debug("Mirroring repo '${repoCoordinate.repoConfig.url}' ref '${repoCoordinate.repoConfig.ref}' to target repo ${repoCoordinate.fullRepoName}")
- targetRepo.pushRef(repoCoordinate.repoConfig.ref, true)
- }
- } else {
- log.debug("Mirroring whole repo '${repoCoordinate.repoConfig.url}' to target repo ${repoCoordinate.fullRepoName}")
- targetRepo.pushAll(true)
- }
- }
-
- private static void validateCommitReferences(RepoCoordinate repoCoordinate) {
- if (GitRepo.isCommit(repoCoordinate.clonedContentRepo, repoCoordinate.repoConfig.ref)) {
- // Mirroring detached commits does not make a lot of sense and is complicated
- // We would have to branch, push, delete remote branch. Considering this an edge case at the moment!
- throw new RuntimeException("Mirroring commit references is not supported for content repos at the moment. content repository '${repoCoordinate.repoConfig.url}', ref: ${repoCoordinate.repoConfig.ref}")
- }
- }
-
- private void createJenkinsJobIfApplicable(RepoCoordinate repoCoordinate, GitRepo repo) {
- if (repoCoordinate.repoConfig.createJenkinsJob && jenkins.isEnabled()) {
- if (GitRepo.existFileInSomeBranch(repo.absoluteLocalRepoTmpDir, 'Jenkinsfile')) {
- jenkins.createJenkinsjob(repoCoordinate.namespace, repoCoordinate.namespace)
- }
- }
- }
-
- /**
- * Overwrite for testing purposes
- */
- protected CloneCommand gitClone() {
- Git.cloneRepository()
- }
-
- /**
- * Add new repoCoordinates to repos and ensure, newest one override last one.
- * Except for MIRROR, which will have to run separately from COPY/FOLDER_BASED in order to allow overriding by COPY/FOLDER_BASED repoCoordinates for the same repo.
- */
- static void addRepoCoordinates(List repoCoordinates, RepoCoordinate newRepoCoordinate) {
- def existingRepoCoordinates = newRepoCoordinate.findSame(repoCoordinates)
-
- if (!existingRepoCoordinates.isEmpty()) {
- log.debug("Found existing repo coordinates for ${newRepoCoordinate}: ${existingRepoCoordinates}")
-
- // Don't replace MIRROR coordinates, they are separate git operations
- def repoCoordinateToOverwrite = newRepoCoordinate.findSameNotMirror(existingRepoCoordinates)
- if (repoCoordinateToOverwrite) {
- repoCoordinates.remove(repoCoordinateToOverwrite)
- log.debug("Replacing existing repo coordinate ${existingRepoCoordinates} with new one: ${newRepoCoordinate}")
- }
- }
- repoCoordinates << newRepoCoordinate
- }
-
- /**
- * Checks whether the repo already exists and overwrite Mode matches.
- */
- static boolean isValidForPush(boolean isNewRepo, RepoCoordinate repoCoordinate) {
-
- if (!isNewRepo && OverwriteMode.INIT == repoCoordinate.repoConfig.overwriteMode) {
- log.warn("OverwriteMode ${OverwriteMode.INIT} set for repo '${repoCoordinate.fullRepoName}' " +
- "and repo already exists in target: Not pushing content!" +
- "If you want to override, set ${OverwriteMode.UPGRADE} or ${OverwriteMode.RESET} .")
- return false
- }
- return true
- }
-
- private void clearCache() {
- if (mergedReposFolder) {
- mergedReposFolder.deleteDir()
- }
- cachedRepoCoordinates.clear()
- mergedReposFolder = null
- }
-
- static class RepoCoordinate {
- String namespace
- String repoName
- File clonedContentRepo
- ContentRepositorySchema repoConfig
- boolean refIsTag
-
- @Override
- String toString() {
- return "RepoCoordinates{ namespace='$namespace', repoName='$repoName', repoConfig.type='${repoConfig.type}', repoConfig.overwriteMode='${repoConfig.overwriteMode}', clonedContentRepo=$clonedContentRepo', refIsTag='${refIsTag}' }"
- }
-
- String getFullRepoName() {
- return "${namespace}/${repoName}"
- }
-
- /**
- * @return all epoCoordinate with the same fullRepoName. There can be one with either COPY/FOLDER_BASED and many MIRRORs.
- */
- List findSame(List repoCoordinates) {
- repoCoordinates.findAll() { it.fullRepoName == fullRepoName }
- }
-
- /**
- * @return RepoCoordinate with the same fullRepoName and repoConfig.type not MIRROR. There can only ever be one!
- */
- RepoCoordinate findSameNotMirror(List repoCoordinates) {
- repoCoordinates.find() {
- it.fullRepoName == fullRepoName
- && ContentRepoType.MIRROR != it.repoConfig.type
- }
- }
- }
+ private Config config
+ private K8sClient k8sClient
+ private GitRepoFactory repoProvider
+ private Jenkins jenkins
+ // set by lazy initialisation
+ private TemplatingEngine templatingEngine
+ // used to clone repos in validation phase
+ private List cachedRepoCoordinates = new ArrayList<>()
+ private GitHandler gitHandler
+
+ protected File mergedReposFolder
+
+ //For security reasons we safe the credentialsProvider for each repo here and not in config pro each repo
+ @JsonIgnore
+ UsernamePasswordCredentialsProvider credentialsProvider
+
+ ContentLoader(Config config, K8sClient k8sClient, GitRepoFactory repoProvider, Jenkins jenkins, GitHandler gitHandler) {
+ this.config = config
+ this.k8sClient = k8sClient
+ this.repoProvider = repoProvider
+ this.jenkins = jenkins
+ this.gitHandler = gitHandler
+ }
+
+ @Override
+ boolean isEnabled() {
+ return true // for now always on. Once we refactor from Argo CD class we add a param to enable
+ }
+
+ @Override
+ void enable() {
+ // ensure cache is cleaned
+ clearCache()
+ // clones repo to check valid configuration and reuse result for further step.
+ cachedRepoCoordinates = cloneContentRepos()
+
+ createImagePullSecrets()
+
+ createContentRepos()
+ }
+
+ @Override
+ void validate() {
+
+ }
+
+ @Override
+ void preConfigInit(Config configToSet) {
+ config.content.repos.each { repo ->
+
+ if (!repo.url) {
+ throw new RuntimeException("content.repos requires a url parameter.")
+ }
+ if (repo.target) {
+ if (repo.target.count('/') == 0) {
+ throw new RuntimeException("content.target needs / to separate namespace/group from repo name. Repo: ${repo.url}")
+ }
+ }
+
+ switch (repo.type) {
+ case ContentRepoType.COPY:
+ if (!repo.target) {
+ throw new RuntimeException("content.repos.type ${ContentRepoType.COPY} requires content.repos.target to be set. Repo: ${repo.url}")
+ }
+ break
+ case ContentRepoType.FOLDER_BASED:
+ if (repo.target) {
+ throw new RuntimeException("content.repos.type ${ContentRepoType.FOLDER_BASED} does not support target parameter. Repo: ${repo.url}")
+ }
+ if (repo.targetRef) {
+ throw new RuntimeException("content.repos.type ${ContentRepoType.FOLDER_BASED} does not support targetRef parameter. Repo: ${repo.url}")
+ }
+ break
+ case ContentRepoType.MIRROR:
+ if (!repo.target) {
+ throw new RuntimeException("content.repos.type ${ContentRepoType.MIRROR} requires content.repos.target to be set. Repo: ${repo.url}")
+ }
+ if (repo.path != ContentRepositorySchema.DEFAULT_PATH) {
+ throw new RuntimeException("content.repos.type ${ContentRepoType.MIRROR} does not support path. Current path: ${repo.path}. Repo: ${repo.url}")
+ }
+ if (repo.templating) {
+ throw new RuntimeException("content.repos.type ${ContentRepoType.MIRROR} does not support templating. Repo: ${repo.url}")
+ }
+ break
+ }
+ }
+ }
+
+ void createImagePullSecrets() {
+ if (config.registry.createImagePullSecrets) {
+ String registryUsername = config.registry.readOnlyUsername ?: config.registry.username
+ String registryPassword = config.registry.readOnlyPassword ?: config.registry.password
+
+ config.content.namespaces.each { String namespace ->
+ def registrySecretName = 'registry'
+
+ k8sClient.createNamespace(namespace)
+
+ k8sClient.createImagePullSecret(registrySecretName, namespace,
+ config.registry.url /* Only domain matters, path would be ignored */,
+ registryUsername, registryPassword)
+
+ k8sClient.patch('serviceaccount', 'default', namespace,
+ [imagePullSecrets: [[name: registrySecretName]]])
+
+ if (config.registry.twoRegistries) {
+ k8sClient.createImagePullSecret('proxy-registry', namespace,
+ config.registry.proxyUrl, config.registry.proxyUsername,
+ config.registry.proxyPassword)
+ }
+ }
+ }
+ }
+
+ void createContentRepos() {
+ if (cachedRepoCoordinates.empty) {
+ cachedRepoCoordinates = cloneContentRepos()
+ }
+ pushTargetRepos(cachedRepoCoordinates)
+ // after all, clean folders and list
+ clearCache()
+
+ }
+
+ protected List cloneContentRepos() {
+ mergedReposFolder = File.createTempDir('gitops-playground-based-content-repos-')
+ List repoCoordinates = []
+
+ log.debug("Aggregating structure for all ${config.content.repos.size()} repos.")
+ config.content.repos.each { repoConfig -> createRepoCoordinates(repoConfig, mergedReposFolder, repoCoordinates)
+ }
+ return repoCoordinates
+ }
+
+ private TemplatingEngine getTemplatingEngine() {
+ if (templatingEngine == null) {
+ templatingEngine = new TemplatingEngine()
+ }
+ return templatingEngine
+ }
+
+ private void createRepoCoordinates(ContentRepositorySchema repoConfig, File mergedReposFolder, List repoCoordinates) {
+ def repoTmpDir = File.createTempDir('gitops-playground-content-repo-')
+ log.debug("Cloning content repo, ${repoConfig.url}, revision ${repoConfig.ref}, path ${repoConfig.path}, overwriteMode ${repoConfig.overwriteMode}")
+
+ if (repoConfig.credentials?.username != null && repoConfig.credentials?.password != null) {
+ credentialsProvider = new UsernamePasswordCredentialsProvider(repoConfig.credentials.username, repoConfig.credentials.password)
+ } else if (repoConfig.credentials?.secretName && repoConfig.credentials?.secretNamespace) {
+ Credentials credentials = this.k8sClient.k8sJavaApiClient.getCredentialsFromSecret(repoConfig.credentials)
+ credentialsProvider = new UsernamePasswordCredentialsProvider(credentials.username, credentials.password)
+ }
+
+ cloneToLocalFolder(repoConfig, repoTmpDir)
+
+ def contentRepoDir = new File(repoTmpDir, repoConfig.path)
+ applyTemplatingIfApplicable(repoConfig, contentRepoDir)
+
+ switch (repoConfig.type) {
+ case ContentRepoType.FOLDER_BASED:
+ createRepoCoordinatesForTypeFolderBased(repoConfig, repoTmpDir, contentRepoDir, mergedReposFolder, repoCoordinates)
+ repoTmpDir.deleteDir()
+ break
+ case ContentRepoType.COPY:
+ createRepoCoordinatesForTypeCopy(repoConfig, contentRepoDir, mergedReposFolder, repoTmpDir, repoCoordinates)
+ repoTmpDir.deleteDir()
+ break
+ case ContentRepoType.MIRROR:
+ createRepoCoordinateForTypeMirror(repoConfig, repoTmpDir, repoCoordinates)
+ // intentionally not deleting repoTmpDir, it is contained in RepoCoordinates for MIRROR usage
+ break
+ }
+ log.debug("Finished cloning content repos. repoCoordinates=${repoCoordinates}")
+ }
+
+ private static void createRepoCoordinatesForTypeCopy(ContentRepositorySchema repoConfig, File contentRepoDir, File mergedReposFolder, File repoTmpDir,
+ List repoCoordinates) {
+ String namespace = repoConfig.target.split('/')[0]
+ String repoName = repoConfig.target.split('/')[1]
+
+ def repoCoordinate = mergeRepoDirs(contentRepoDir, namespace, repoName, mergedReposFolder, repoConfig)
+ repoCoordinate.refIsTag = GitRepo.isTag(repoTmpDir, repoConfig.ref)
+ addRepoCoordinates(repoCoordinates, repoCoordinate)
+ }
+
+ private static void createRepoCoordinatesForTypeFolderBased(ContentRepositorySchema repoConfig, File repoTmpDir, File contentRepoDir, File mergedReposFolder,
+ List repoCoordinates) {
+ boolean refIsTag = GitRepo.isTag(repoTmpDir, repoConfig.ref)
+ findRepoDirectories(contentRepoDir)
+ .each { contentRepoNamespaceDir ->
+ findRepoDirectories(contentRepoNamespaceDir)
+ .each { contentRepoFolder ->
+ String namespace = contentRepoNamespaceDir.name
+ String repoName = contentRepoFolder.name
+ def repoCoordinate = mergeRepoDirs(contentRepoFolder, namespace, repoName, mergedReposFolder, repoConfig)
+ repoCoordinate.refIsTag = refIsTag
+ addRepoCoordinates(repoCoordinates, repoCoordinate)
+ }
+ }
+ }
+
+ private static void createRepoCoordinateForTypeMirror(ContentRepositorySchema repoConfig, File repoTmpDir, List repoCoordinates) {
+ // Don't merge but keep these in separate dirs.
+ // This avoids messing up .git folders with possible confusing exceptions for the user
+ String namespace = repoConfig.target.split('/')[0]
+ String repoName = repoConfig.target.split('/')[1]
+ def repoCoordinate = new RepoCoordinate(namespace: namespace,
+ repoName: repoName,
+ clonedContentRepo: repoTmpDir,
+ repoConfig: repoConfig,
+ refIsTag: GitRepo.isTag(repoTmpDir, repoConfig.ref))
+ addRepoCoordinates(repoCoordinates, repoCoordinate)
+ }
+
+ /**
+ * Merges the files of src into the mergeRepoFolder/namespace/name and adds a new object to repoCoordinates.
+ *
+ * Note that existing repoCoordinate objects with different overwriteMode are overwritten. The last repo to be mentioned within config.content.repos wins!*/
+ private static RepoCoordinate mergeRepoDirs(File src, String namespace, String repoName, File mergedRepoFolder,
+ ContentRepositorySchema repoConfig) {
+ File target = new File(new File(mergedRepoFolder, namespace), repoName)
+ log.debug("Merging content repo, namespace ${namespace}, repoName ${repoName} from ${src} to ${target}")
+ FileUtils.copyDirectory(src, target, new FileSystemUtils.IgnoreDotGitFolderFilter())
+
+ def repoCoordinate = new RepoCoordinate(namespace: namespace,
+ repoName: repoName,
+ clonedContentRepo: target,
+ repoConfig: repoConfig,)
+ return repoCoordinate
+ }
+
+ private static List findRepoDirectories(File srcRepo) {
+ srcRepo.listFiles().findAll {
+ it.isDirectory() && // Exclude .git for example
+ !it.name.startsWith('.')
+ }
+ }
+
+ private void applyTemplatingIfApplicable(ContentRepositorySchema repoConfig, File srcPath) {
+ if (repoConfig.templating) {
+ def engine = getTemplatingEngine()
+
+ GitRepo repo = this.repoProvider.getRepo(repoConfig.target, this.gitHandler.tenant)
+
+ engine.replaceTemplates(srcPath, [config : config,
+ scm : [baseUrl : repo.gitProvider.url,
+ host : repo.gitProvider.host,
+ protocol: repo.gitProvider.protocol,
+ repoUrl : repo.gitProvider.repoPrefix(),],
+ // Allow for using static classes inside the templates
+ statics: !config.content.useWhitelist ? new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_32).build().getStaticModels() :
+ new AllowListFreemarkerObjectWrapper(Configuration.VERSION_2_3_32, config.content.getAllowedStaticsWhitelist()).getStaticModels()])
+ }
+ }
+
+ private void cloneToLocalFolder(ContentRepositorySchema repoConfig, File repoTmpDir) {
+
+ def cloneCommand = gitClone()
+ .setURI(repoConfig.url)
+ .setDirectory(repoTmpDir)
+ .setNoCheckout(false)
+ // Checkout default branch
+
+ if (credentialsProvider) {
+ cloneCommand.setCredentialsProvider(credentialsProvider)
+ }
+
+ def git = cloneCommand.call()
+
+ if (ContentRepoType.MIRROR == repoConfig.type) {
+ def fetch = git.fetch()
+
+ if (credentialsProvider) {
+ fetch.setCredentialsProvider(credentialsProvider)
+ }
+ fetch.setRefSpecs("+refs/*:refs/*").call() // Fetch all branches and tags
+ }
+
+ if (repoConfig.ref) {
+ def actualRef = findRef(repoConfig, git.repository)
+ git.checkout().setName(actualRef).call()
+ }
+ }
+
+ private static String findRef(ContentRepositorySchema repoConfig, Repository gitRepo) {
+ // Check if ref exists first to avoid InvalidRefNameException
+ // Note that this works for commits and shortname tags but not shortname branches 🙄
+ if (gitRepo.resolve(repoConfig.ref)) {
+ return repoConfig.ref
+ }
+
+ // Check branches or tags
+ def remoteCommand = Git.lsRemoteRepository()
+ .setRemote(repoConfig.url)
+ .setHeads(true)
+ .setTags(true)
+
+ Collection[ refs = remoteCommand.call()
+ String potentialRef = refs.find { it.name.endsWith(repoConfig.ref) }?.name
+
+ if (!potentialRef) {
+ // Jgit silently ignores some missing refs and just continues with default branch.
+ // This might lead to unexpected surprises for our users, so better fail explicitly
+ throw new RuntimeException("Reference '${repoConfig.ref}' not found in content repository '${repoConfig.url}'")
+ }
+
+ // Jgit only checks out remote branches when they start in origin/ 🙄
+ return potentialRef.replace('refs/heads/', 'origin/')
+ }
+
+ private void pushTargetRepos(List] repoCoordinates) {
+ repoCoordinates.each { repoCoordinate ->
+
+ GitRepo targetRepo = repoProvider.getRepo(repoCoordinate.fullRepoName, this.gitHandler.tenant)
+ boolean isNewRepo = targetRepo.createRepositoryAndSetPermission("", false)
+
+ if (isValidForPush(isNewRepo, repoCoordinate)) {
+ targetRepo.cloneRepo()
+
+ switch (repoCoordinate.repoConfig.type) {
+ case ContentRepoType.MIRROR:
+ handleRepoMirroring(repoCoordinate, targetRepo)
+ break
+ // COPY and FOLDER_BASED same treatment
+ case ContentRepoType.FOLDER_BASED:
+ case ContentRepoType.COPY:
+ handleRepoCopyingOrFolderBased(repoCoordinate, targetRepo, isNewRepo)
+ break
+ }
+
+ createJenkinsJobIfApplicable(repoCoordinate, targetRepo)
+
+ // cleaning tmp folders
+ repoCoordinate.clonedContentRepo.deleteDir()
+ new File(targetRepo.absoluteLocalRepoTmpDir).deleteDir()
+ } // no else needed
+ }
+
+ }
+
+ /**
+ * Copies repoCoordinate to targetRepo, commits and pushes
+ * Same logic for both FOLDER_BASED and COPY repo types.*/
+ private static void handleRepoCopyingOrFolderBased(RepoCoordinate repoCoordinate, GitRepo targetRepo, boolean isNewRepo) {
+ if (!isNewRepo) {
+ clearTargetRepoIfApplicable(repoCoordinate, targetRepo)
+ }
+ // Avoid overwriting .git in target to avoid, because we don't need it for copying and
+ // git pack files are typically read-only, leading to IllegalArgumentException:
+ // File parameter 'destFile is not writable: .git/objects/pack/pack-123.pack
+ targetRepo.copyDirectoryContents(repoCoordinate.clonedContentRepo.absolutePath, new FileSystemUtils.IgnoreDotGitFolderFilter())
+
+ String commitMessage = "Initialize content repo ${repoCoordinate.namespace}/${repoCoordinate.repoName}"
+ String targetRefShort = repoCoordinate.repoConfig.targetRef.replace('refs/heads/', '').replace('refs/tags/', '')
+ if (targetRefShort) {
+ String refSpec = setRefSpec(repoCoordinate, targetRefShort)
+ targetRepo.commitAndPush(commitMessage, targetRefShort, refSpec)
+ } else {
+ targetRepo.commitAndPush(commitMessage)
+ }
+
+ }
+
+ private static String setRefSpec(RepoCoordinate repoCoordinate, String targetRefShort) {
+ String refSpec
+ if ((repoCoordinate.refIsTag && !repoCoordinate.repoConfig.targetRef.startsWith('refs/heads')) || repoCoordinate.repoConfig.targetRef.startsWith('refs/tags')) {
+ refSpec = "refs/tags/${targetRefShort}:refs/tags/${targetRefShort}"
+ } else {
+ refSpec = "HEAD:refs/heads/${targetRefShort}"
+ }
+ refSpec
+ }
+
+ private static void clearTargetRepoIfApplicable(RepoCoordinate repoCoordinate, GitRepo targetRepo) {
+ if (OverwriteMode.INIT != repoCoordinate.repoConfig.overwriteMode) {
+ if (OverwriteMode.RESET == repoCoordinate.repoConfig.overwriteMode) {
+ log.info("OverwriteMode ${OverwriteMode.RESET} set for repo '${repoCoordinate.fullRepoName}': " +
+ "Deleting existing files in repo and replacing them with new content.")
+ targetRepo.clearRepo()
+ } else {
+ log.debug("OverwriteMode ${OverwriteMode.UPGRADE} set for repo '${repoCoordinate.fullRepoName}': " + "Merging new content into existing repo. ")
+ }
+ }
+ }
+
+ /**
+ * Force pushes repoCoordinate.repoConfig.ref or all refs to targetRepo*/
+ private static void handleRepoMirroring(RepoCoordinate repoCoordinate, GitRepo targetRepo) {
+ try (def targetGit = Git.open(new File(targetRepo.absoluteLocalRepoTmpDir))) {
+ def remoteUrl = targetGit.repository.config.getString('remote', 'origin', 'url')
+
+ // In mirror mode, we mainly need the .git folder to push the whole git history, branches and tags.
+ // So copying source to target repo, .git folders are merged.
+ // git pack files are typically read-only, leading to
+ // IllegalArgumentException: File parameter 'destFile is not writable: .git/objects/pack/pack-123.pack
+ // Workaround: make .git writable.
+ // Note: Setting target remote in source repo and pushing from there causes other problems like
+ // IOException: Source ref someBranch doesn't resolve to any object.
+ FileSystemUtils.makeWritable(new File(targetRepo.absoluteLocalRepoTmpDir, '.git'))
+
+ targetRepo.copyDirectoryContents(repoCoordinate.clonedContentRepo.absolutePath)
+
+ // Restore remote, it could have been overwritten due to a copied .git folder in MIRROR mode
+ targetGit.repository.config.setString('remote', 'origin', 'url', remoteUrl)
+ targetGit.repository.config.save()
+ }
+
+ if (repoCoordinate.repoConfig.ref) {
+ validateCommitReferences(repoCoordinate)
+ if (repoCoordinate.repoConfig.targetRef) {
+ log.debug("Mirroring repo '${repoCoordinate.repoConfig.url}' ref '${repoCoordinate.repoConfig.ref}' to target repo ${repoCoordinate.fullRepoName}, targetRef: '${repoCoordinate.repoConfig.targetRef}'")
+ targetRepo.pushRef(repoCoordinate.repoConfig.ref, repoCoordinate.repoConfig.targetRef, true)
+ } else {
+ log.debug("Mirroring repo '${repoCoordinate.repoConfig.url}' ref '${repoCoordinate.repoConfig.ref}' to target repo ${repoCoordinate.fullRepoName}")
+ targetRepo.pushRef(repoCoordinate.repoConfig.ref, true)
+ }
+ } else {
+ log.debug("Mirroring whole repo '${repoCoordinate.repoConfig.url}' to target repo ${repoCoordinate.fullRepoName}")
+ targetRepo.pushAll(true)
+ }
+ }
+
+ private static void validateCommitReferences(RepoCoordinate repoCoordinate) {
+ if (GitRepo.isCommit(repoCoordinate.clonedContentRepo, repoCoordinate.repoConfig.ref)) {
+ // Mirroring detached commits does not make a lot of sense and is complicated
+ // We would have to branch, push, delete remote branch. Considering this an edge case at the moment!
+ throw new RuntimeException("Mirroring commit references is not supported for content repos at the moment. content repository '${repoCoordinate.repoConfig.url}', ref: ${repoCoordinate.repoConfig.ref}")
+ }
+ }
+
+ private void createJenkinsJobIfApplicable(RepoCoordinate repoCoordinate, GitRepo repo) {
+ if (repoCoordinate.repoConfig.createJenkinsJob && jenkins.isEnabled()) {
+ if (GitRepo.existFileInSomeBranch(repo.absoluteLocalRepoTmpDir, 'Jenkinsfile')) {
+ jenkins.createJenkinsjob(repoCoordinate.namespace, repoCoordinate.namespace)
+ }
+ }
+ }
+
+ /**
+ * Overwrite for testing purposes*/
+ protected CloneCommand gitClone() {
+ Git.cloneRepository()
+ }
+
+ /**
+ * Add new repoCoordinates to repos and ensure, newest one override last one.
+ * Except for MIRROR, which will have to run separately from COPY/FOLDER_BASED in order to allow overriding by COPY/FOLDER_BASED repoCoordinates for the same repo.*/
+ static void addRepoCoordinates(List repoCoordinates, RepoCoordinate newRepoCoordinate) {
+ def existingRepoCoordinates = newRepoCoordinate.findSame(repoCoordinates)
+
+ if (!existingRepoCoordinates.isEmpty()) {
+ log.debug("Found existing repo coordinates for ${newRepoCoordinate}: ${existingRepoCoordinates}")
+
+ // Don't replace MIRROR coordinates, they are separate git operations
+ def repoCoordinateToOverwrite = newRepoCoordinate.findSameNotMirror(existingRepoCoordinates)
+ if (repoCoordinateToOverwrite) {
+ repoCoordinates.remove(repoCoordinateToOverwrite)
+ log.debug("Replacing existing repo coordinate ${existingRepoCoordinates} with new one: ${newRepoCoordinate}")
+ }
+ }
+ repoCoordinates << newRepoCoordinate
+ }
+
+ /**
+ * Checks whether the repo already exists and overwrite Mode matches.*/
+ static boolean isValidForPush(boolean isNewRepo, RepoCoordinate repoCoordinate) {
+
+ if (!isNewRepo && OverwriteMode.INIT == repoCoordinate.repoConfig.overwriteMode) {
+ log.warn("OverwriteMode ${OverwriteMode.INIT} set for repo '${repoCoordinate.fullRepoName}' " + "and repo already exists in target: Not pushing content!" +
+ "If you want to override, set ${OverwriteMode.UPGRADE} or ${OverwriteMode.RESET} .")
+ return false
+ }
+ return true
+ }
+
+ private void clearCache() {
+ if (mergedReposFolder) {
+ mergedReposFolder.deleteDir()
+ }
+ cachedRepoCoordinates.clear()
+ mergedReposFolder = null
+ }
+
+ static class RepoCoordinate {
+ String namespace
+ String repoName
+ File clonedContentRepo
+ ContentRepositorySchema repoConfig
+ boolean refIsTag
+
+ @Override
+ String toString() {
+ return "RepoCoordinates{ namespace='$namespace', repoName='$repoName', repoConfig.type='${repoConfig.type}', repoConfig.overwriteMode='${repoConfig.overwriteMode}', clonedContentRepo=$clonedContentRepo', refIsTag='${refIsTag}' }"
+ }
+
+ String getFullRepoName() {
+ return "${namespace}/${repoName}"
+ }
+
+ /**
+ * @return all epoCoordinate with the same fullRepoName. There can be one with either COPY/FOLDER_BASED and many MIRRORs.
+ */
+ List findSame(List repoCoordinates) {
+ repoCoordinates.findAll() { it.fullRepoName == fullRepoName }
+ }
+
+ /**
+ * @return RepoCoordinate with the same fullRepoName and repoConfig.type not MIRROR. There can only ever be one!
+ */
+ RepoCoordinate findSameNotMirror(List repoCoordinates) {
+ repoCoordinates.find() {
+ it.fullRepoName == fullRepoName && ContentRepoType.MIRROR != it.repoConfig.type
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/features/ExternalSecretsOperator.groovy b/src/main/groovy/com/cloudogu/gitops/features/ExternalSecretsOperator.groovy
index 2aedc1eac..5c856718d 100644
--- a/src/main/groovy/com/cloudogu/gitops/features/ExternalSecretsOperator.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/features/ExternalSecretsOperator.groovy
@@ -5,49 +5,49 @@ import com.cloudogu.gitops.FeatureWithImage
import com.cloudogu.gitops.config.Config
import com.cloudogu.gitops.features.deployment.DeploymentStrategy
import com.cloudogu.gitops.features.git.GitHandler
+import com.cloudogu.gitops.kubernetes.api.K8sClient
import com.cloudogu.gitops.utils.AirGappedUtils
import com.cloudogu.gitops.utils.FileSystemUtils
-import com.cloudogu.gitops.kubernetes.api.K8sClient
-import groovy.util.logging.Slf4j
+
import io.micronaut.core.annotation.Order
-import jakarta.inject.Singleton
+import jakarta.inject.Singleton
+import groovy.util.logging.Slf4j
@Slf4j
@Singleton
@Order(400)
class ExternalSecretsOperator extends Feature implements FeatureWithImage {
- static final String HELM_VALUES_PATH = "argocd/cluster-resources/apps/external-secrets/templates/values.ftl.yaml"
-
- String namespace = "${config.application.namePrefix}secrets"
- Config config
- K8sClient k8sClient
-
- ExternalSecretsOperator(
- Config config,
- FileSystemUtils fileSystemUtils,
- DeploymentStrategy deployer,
- K8sClient k8sClient,
- AirGappedUtils airGappedUtils,
- GitHandler gitHandler
- ) {
- this.deployer = deployer
- this.config = config
- this.fileSystemUtils = fileSystemUtils
- this.k8sClient = k8sClient
- this.airGappedUtils = airGappedUtils
- this.gitHandler=gitHandler
- }
-
- @Override
- boolean isEnabled() {
- return config.features.secrets.active
- }
-
- @Override
- void enable() {
- def helmConfig = config.features.secrets.externalSecrets.helm
- deployHelmChart('external-secrets-operator', 'external-secrets', namespace, helmConfig, HELM_VALUES_PATH, config)
- }
+ static final String HELM_VALUES_PATH = "argocd/cluster-resources/apps/external-secrets/templates/values.ftl.yaml"
+
+ String namespace = "${config.application.namePrefix}secrets"
+ Config config
+ K8sClient k8sClient
+
+ ExternalSecretsOperator(Config config,
+ FileSystemUtils fileSystemUtils,
+ DeploymentStrategy deployer,
+ K8sClient k8sClient,
+ AirGappedUtils airGappedUtils,
+ GitHandler gitHandler) {
+
+ this.deployer = deployer
+ this.config = config
+ this.fileSystemUtils = fileSystemUtils
+ this.k8sClient = k8sClient
+ this.airGappedUtils = airGappedUtils
+ this.gitHandler = gitHandler
+ }
+
+ @Override
+ boolean isEnabled() {
+ return config.features.secrets.active
+ }
+
+ @Override
+ void enable() {
+ def helmConfig = config.features.secrets.externalSecrets.helm
+ deployHelmChart('external-secrets-operator', 'external-secrets', namespace, helmConfig, HELM_VALUES_PATH, config)
+ }
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/features/Ingress.groovy b/src/main/groovy/com/cloudogu/gitops/features/Ingress.groovy
index 18967db5f..8ecaa2361 100644
--- a/src/main/groovy/com/cloudogu/gitops/features/Ingress.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/features/Ingress.groovy
@@ -5,48 +5,48 @@ import com.cloudogu.gitops.FeatureWithImage
import com.cloudogu.gitops.config.Config
import com.cloudogu.gitops.features.deployment.DeploymentStrategy
import com.cloudogu.gitops.features.git.GitHandler
+import com.cloudogu.gitops.kubernetes.api.K8sClient
import com.cloudogu.gitops.utils.AirGappedUtils
import com.cloudogu.gitops.utils.FileSystemUtils
-import com.cloudogu.gitops.kubernetes.api.K8sClient
-import groovy.util.logging.Slf4j
+
import io.micronaut.core.annotation.Order
+
import jakarta.inject.Singleton
+import groovy.util.logging.Slf4j
@Slf4j
@Singleton
@Order(150)
class Ingress extends Feature implements FeatureWithImage {
- static final String HELM_VALUES_PATH = "argocd/cluster-resources/apps/ingress/templates/ingress-helm-values.ftl.yaml"
-
- String namespace = "${config.application.namePrefix}" + config.features.ingress.ingressNamespace
- Config config
- K8sClient k8sClient
-
- Ingress(
- Config config,
- FileSystemUtils fileSystemUtils,
- DeploymentStrategy deployer,
- K8sClient k8sClient,
- AirGappedUtils airGappedUtils,
- GitHandler gitHandler
- ) {
- this.deployer = deployer
- this.config = config
- this.fileSystemUtils = fileSystemUtils
- this.k8sClient = k8sClient
- this.airGappedUtils = airGappedUtils
- this.gitHandler = gitHandler
- }
-
- @Override
- boolean isEnabled() {
- return config.features.ingress.active
- }
-
- @Override
- void enable() {
- def helmConfig = config.features.ingress.helm
- deployHelmChart('traefik', 'traefik', namespace, helmConfig, HELM_VALUES_PATH, config)
- }
+ static final String HELM_VALUES_PATH = "argocd/cluster-resources/apps/ingress/templates/ingress-helm-values.ftl.yaml"
+
+ String namespace = "${config.application.namePrefix}" + config.features.ingress.ingressNamespace
+ Config config
+ K8sClient k8sClient
+
+ Ingress(Config config,
+ FileSystemUtils fileSystemUtils,
+ DeploymentStrategy deployer,
+ K8sClient k8sClient,
+ AirGappedUtils airGappedUtils,
+ GitHandler gitHandler) {
+ this.deployer = deployer
+ this.config = config
+ this.fileSystemUtils = fileSystemUtils
+ this.k8sClient = k8sClient
+ this.airGappedUtils = airGappedUtils
+ this.gitHandler = gitHandler
+ }
+
+ @Override
+ boolean isEnabled() {
+ return config.features.ingress.active
+ }
+
+ @Override
+ void enable() {
+ def helmConfig = config.features.ingress.helm
+ deployHelmChart('traefik', 'traefik', namespace, helmConfig, HELM_VALUES_PATH, config)
+ }
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/features/Jenkins.groovy b/src/main/groovy/com/cloudogu/gitops/features/Jenkins.groovy
index ffc6c1daf..3637ded73 100644
--- a/src/main/groovy/com/cloudogu/gitops/features/Jenkins.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/features/Jenkins.groovy
@@ -10,269 +10,243 @@ import com.cloudogu.gitops.jenkins.JobManager
import com.cloudogu.gitops.jenkins.PrometheusConfigurator
import com.cloudogu.gitops.jenkins.UserManager
import com.cloudogu.gitops.kubernetes.api.K8sClient
-import com.cloudogu.gitops.utils.*
-import groovy.util.logging.Slf4j
+import com.cloudogu.gitops.utils.CommandExecutor
+import com.cloudogu.gitops.utils.FileSystemUtils
+import com.cloudogu.gitops.utils.NetworkingUtils
+
import io.micronaut.core.annotation.Order
+
import jakarta.inject.Singleton
+import groovy.util.logging.Slf4j
@Slf4j
@Singleton
@Order(70)
class Jenkins extends Feature {
- static final String HELM_VALUES_PATH = "argocd/cluster-resources/apps/jenkins/values.ftl.yaml"
-
- String namespace
- private Config config
- private CommandExecutor commandExecutor
- private GlobalPropertyManager globalPropertyManager
- private JobManager jobManager
- private UserManager userManager
- private PrometheusConfigurator prometheusConfigurator
- private K8sClient k8sClient
- private NetworkingUtils networkingUtils
-
- Jenkins(
- Config config,
- CommandExecutor commandExecutor,
- FileSystemUtils fileSystemUtils,
- GlobalPropertyManager globalPropertyManager,
- JobManager jobManager,
- UserManager userManager,
- PrometheusConfigurator prometheusConfigurator,
- HelmStrategy deployer,
- K8sClient k8sClient,
- NetworkingUtils networkingUtils,
- GitHandler gitHandler
- ) {
- this.config = config
- this.commandExecutor = commandExecutor
- this.fileSystemUtils = fileSystemUtils
- this.globalPropertyManager = globalPropertyManager
- this.jobManager = jobManager
- this.userManager = userManager
- this.prometheusConfigurator = prometheusConfigurator
- this.deployer = deployer
- this.k8sClient = k8sClient
- this.networkingUtils = networkingUtils
- this.gitHandler = gitHandler
-
- if (config.jenkins.internal) {
- this.namespace = "${config.application.namePrefix}jenkins"
- }
- }
-
- @Override
- boolean isEnabled() {
- return config.jenkins.active
- }
-
-
- @Override
- void enable() {
-
- if (config.jenkins.internal) {
-
- k8sClient.createNamespace(namespace)
-
- // Mark the first node for Jenkins and agents. See jenkins/values.ftl.yaml "agent.workingDir" for details.
- // Remove first (in case new nodes were added)
- k8sClient.labelRemove('node', '--all', '', 'node')
- def nodeName = k8sClient.waitForNode().replace('node/', '')
- k8sClient.label('node', nodeName, new Tuple2('node', 'jenkins'))
-
- k8sClient.createSecret('generic', 'jenkins-credentials', namespace,
- new Tuple2('jenkins-admin-user', config.jenkins.username),
- new Tuple2('jenkins-admin-password', config.jenkins.password))
-
- def helmConfig = config.jenkins.helm
- String releaseName = "jenkins"
- addHelmValuesData("dockerGid", findDockerGid())
-
- deployHelmChart('jenkins', releaseName, namespace, helmConfig, HELM_VALUES_PATH, config)
-
- // Defined here: https://github.com/jenkinsci/helm-charts/blob/jenkins-5.8.1/charts/jenkins/templates/_helpers.tpl#L46-L57
- String serviceName = releaseName
- // Update jenkins.url after it is deployed (and ports are known)
- if (config.application.runningInsideK8s) {
- log.debug("Setting jenkins url to k8s service, since installation is running inside k8s")
- config.jenkins.url = networkingUtils.createUrl("${serviceName}.${namespace}.svc.cluster.local", "80")
- } else {
- log.debug("Setting jenkins configs for local single node cluster with internal jenkins. Waiting for NodePort...")
- def port = k8sClient.waitForNodePort(serviceName, namespace)
- String clusterBindAddress = networkingUtils.findClusterBindAddress()
- config.jenkins.url = networkingUtils.createUrl(clusterBindAddress, port)
- }
- }
-
- commandExecutor.execute("${fileSystemUtils.rootDir}/scripts/jenkins/init-jenkins.sh", [
- TRACE : config.application.trace,
- INTERNAL_JENKINS : config.jenkins.internal,
- JENKINS_HELM_CHART_VERSION: config.jenkins.helm.version,
- JENKINS_URL : config.jenkins.url,
- JENKINS_USERNAME : config.jenkins.username,
- JENKINS_PASSWORD : config.jenkins.password,
- SCM_URL : this.gitHandler.tenant.url,
- PREFIXED_SCM_URL : this.gitHandler.tenant.repoPrefix(),
- SCM_PASSWORD : this.gitHandler.tenant.credentials.password,
- SCM_PROVIDER : config.scm.scmProviderType,
- INSTALL_ARGOCD : config.features.argocd.active,
- NAME_PREFIX : config.application.namePrefix,
- INSECURE : config.application.insecure,
- SKIP_RESTART : config.jenkins.skipRestart,
- SKIP_PLUGINS : config.jenkins.skipPlugins
- ])
-
- globalPropertyManager.setGlobalProperty("${config.application.namePrefixForEnvVars}SCM_URL", this.gitHandler.tenant.url)
- globalPropertyManager.setGlobalProperty("${config.application.namePrefixForEnvVars}PREFIXED_SCM_URL", this.gitHandler.tenant.repoPrefix())
-
- if (config.jenkins.additionalEnvs) {
- for (entry in (config.jenkins.additionalEnvs as Map).entrySet()) {
- globalPropertyManager.setGlobalProperty(entry.key.toString(), entry.value.toString())
- }
- }
-
- if (config.registry.url) {
- globalPropertyManager.setGlobalProperty("${config.application.namePrefixForEnvVars}REGISTRY_URL", config.registry.url)
- }
-
- if (config.registry.path) {
- globalPropertyManager.setGlobalProperty("${config.application.namePrefixForEnvVars}REGISTRY_PATH", config.registry.path)
- }
-
- if (config.registry.twoRegistries) {
- globalPropertyManager.setGlobalProperty("${config.application.namePrefixForEnvVars}REGISTRY_PROXY_URL", config.registry.proxyUrl)
- }
-
- if (config.jenkins.mavenCentralMirror) {
- globalPropertyManager.setGlobalProperty("${config.application.namePrefixForEnvVars}MAVEN_CENTRAL_MIRROR", config.jenkins.mavenCentralMirror)
- }
-
- globalPropertyManager.setGlobalProperty("${config.application.namePrefixForEnvVars}K8S_VERSION", Config.K8S_VERSION)
-
- if (userManager.isUsingCasSecurityRealm()) {
- log.trace("Using CAS Security Realm. Must not create user.")
- } else {
- userManager.createUser(config.jenkins.metricsUsername, config.jenkins.metricsPassword)
- }
-
- userManager.grantPermission(config.jenkins.metricsUsername, UserManager.Permissions.METRICS_VIEW)
-
- if (config.features.monitoring.active && config.jenkins.internal) {
- // And external Jenkins can likely not be monitored
- prometheusConfigurator.enableAuthentication()
- }
-
- }
-
- void createJenkinsjob(String namespace, String repoName) {
- def credentialId = "scm-user"
- String prefixedNamespace = "${config.application.namePrefix}${namespace}"
- String jobName = "${config.application.namePrefix}${repoName}"
-
- jobManager.createJob(jobName,
- this.gitHandler.tenant.url,
- prefixedNamespace,
- credentialId)
-
-
- if (config.scm.scmProviderType == ScmProviderType.SCM_MANAGER) {
- jobManager.createCredential(
- jobName,
- credentialId,
- "${config.application.namePrefix}gitops",
- "${config.scm.getScmManager().password}",
- 'credentials for accessing scm-manager')
- }
-
- if (config.scm.scmProviderType == ScmProviderType.GITLAB) {
- jobManager.createCredential(
- jobName,
- credentialId,
- "${config.scm.getGitlab().username}",
- "${config.scm.getGitlab().password}",
- 'credentials for accessing gitlab')
- }
-
- jobManager.createCredential(
- jobName,
- "registry-user",
- "${config.registry.username}",
- "${config.registry.password}",
- 'credentials for accessing the docker-registry for writing images built on jenkins')
-
- if (config.registry.twoRegistries) {
- jobManager.createCredential(
- jobName,
- "registry-proxy-user",
- "${config.registry.proxyUsername}",
- "${config.registry.proxyPassword}",
- 'credentials for accessing the docker-registry that contains 3rd party or base images')
- }
-
- jobManager.startJob(jobName)
- }
-
- protected String findDockerGid() {
- String gid = ''
- def etcGroup = k8sClient.run("tmp-docker-gid-grepper-${new Random().nextInt(10000)}",
- 'irrelevant' /* Redundant, but mandatory param */, namespace, createGidGrepperOverrides(),
- '--restart=Never', '-ti', '--rm', '--quiet')
- // --quiet is necessary to avoid 'pod deleted' output
-
- def lines = etcGroup?.split('\n')
- for (String it : lines) {
- def parts = it.split(":")
- if (parts[0] == 'docker') {
- gid = parts[2]
- break
- }
- }
-
- if (!gid) {
- log.warn('Unable to determine Docker Group ID (GID). Jenkins Agent pods will run as root user (UID 0)!\n' +
- "Group docker not found in /etc/group:\n${etcGroup}")
- return ''
- } else {
- log.debug("Using Docker Group ID (GID) ${gid} for Jenkins Agent pods")
- return gid
- }
- }
-
- Map createGidGrepperOverrides() {
- [
- 'spec': [
- 'containers' : [
- [
- 'name' : 'tmp-docker-gid-grepper',
- // We use the same image for several tasks for performance and maintenance reasons
- 'image' : "${config.jenkins.internalBashImage}",
- 'args' : ['cat', '/etc/group'],
- 'volumeMounts': [
- [
- 'name' : 'group',
- 'mountPath': '/etc/group',
- 'readOnly' : true
- ]
- ]
- ]
- ],
- 'nodeSelector': [
- 'node': 'jenkins'
- ],
- 'volumes' : [
- [
- 'name' : 'group',
- 'hostPath': [
- 'path': '/etc/group'
- ]
- ]
- ]
- ]
- ]
- }
- @Override
- String getActiveNamespaceFromFeature() {
- return isEnabled() && config?.jenkins?.internal ? getNamespace() : null
- }
+ static final String HELM_VALUES_PATH = "argocd/cluster-resources/apps/jenkins/values.ftl.yaml"
+
+ String namespace
+ private Config config
+ private CommandExecutor commandExecutor
+ private GlobalPropertyManager globalPropertyManager
+ private JobManager jobManager
+ private UserManager userManager
+ private PrometheusConfigurator prometheusConfigurator
+ private K8sClient k8sClient
+ private NetworkingUtils networkingUtils
+
+ Jenkins(Config config,
+ CommandExecutor commandExecutor,
+ FileSystemUtils fileSystemUtils,
+ GlobalPropertyManager globalPropertyManager,
+ JobManager jobManager,
+ UserManager userManager,
+ PrometheusConfigurator prometheusConfigurator,
+ HelmStrategy deployer,
+ K8sClient k8sClient,
+ NetworkingUtils networkingUtils,
+ GitHandler gitHandler) {
+ this.config = config
+ this.commandExecutor = commandExecutor
+ this.fileSystemUtils = fileSystemUtils
+ this.globalPropertyManager = globalPropertyManager
+ this.jobManager = jobManager
+ this.userManager = userManager
+ this.prometheusConfigurator = prometheusConfigurator
+ this.deployer = deployer
+ this.k8sClient = k8sClient
+ this.networkingUtils = networkingUtils
+ this.gitHandler = gitHandler
+
+ if (config.jenkins.internal) {
+ this.namespace = "${config.application.namePrefix}jenkins"
+ }
+ }
+
+ @Override
+ boolean isEnabled() {
+ return config.jenkins.active
+ }
+
+ @Override
+ void enable() {
+
+ if (config.jenkins.internal) {
+
+ k8sClient.createNamespace(namespace)
+
+ // Mark the first node for Jenkins and agents. See jenkins/values.ftl.yaml "agent.workingDir" for details.
+ // Remove first (in case new nodes were added)
+ k8sClient.labelRemove('node', '--all', '', 'node')
+ def nodeName = k8sClient.waitForNode().replace('node/', '')
+ k8sClient.label('node', nodeName, new Tuple2('node', 'jenkins'))
+
+ k8sClient.createSecret('generic', 'jenkins-credentials', namespace,
+ new Tuple2('jenkins-admin-user', config.jenkins.username),
+ new Tuple2('jenkins-admin-password', config.jenkins.password))
+
+ def helmConfig = config.jenkins.helm
+ String releaseName = "jenkins"
+ addHelmValuesData("dockerGid", findDockerGid())
+
+ deployHelmChart('jenkins', releaseName, namespace, helmConfig, HELM_VALUES_PATH, config)
+
+ // Defined here: https://github.com/jenkinsci/helm-charts/blob/jenkins-5.8.1/charts/jenkins/templates/_helpers.tpl#L46-L57
+ String serviceName = releaseName
+ // Update jenkins.url after it is deployed (and ports are known)
+ if (config.application.runningInsideK8s) {
+ log.debug("Setting jenkins url to k8s service, since installation is running inside k8s")
+ config.jenkins.url = networkingUtils.createUrl("${serviceName}.${namespace}.svc.cluster.local", "80")
+ } else {
+ log.debug("Setting jenkins configs for local single node cluster with internal jenkins. Waiting for NodePort...")
+ def port = k8sClient.waitForNodePort(serviceName, namespace)
+ String clusterBindAddress = networkingUtils.findClusterBindAddress()
+ config.jenkins.url = networkingUtils.createUrl(clusterBindAddress, port)
+ }
+ }
+
+ commandExecutor.execute("${fileSystemUtils.rootDir}/scripts/jenkins/init-jenkins.sh", [TRACE : config.application.trace,
+ INTERNAL_JENKINS : config.jenkins.internal,
+ JENKINS_HELM_CHART_VERSION: config.jenkins.helm.version,
+ JENKINS_URL : config.jenkins.url,
+ JENKINS_USERNAME : config.jenkins.username,
+ JENKINS_PASSWORD : config.jenkins.password,
+ SCM_URL : this.gitHandler.tenant.url,
+ PREFIXED_SCM_URL : this.gitHandler.tenant.repoPrefix(),
+ SCM_PASSWORD : this.gitHandler.tenant.credentials.password,
+ SCM_PROVIDER : config.scm.scmProviderType,
+ INSTALL_ARGOCD : config.features.argocd.active,
+ NAME_PREFIX : config.application.namePrefix,
+ INSECURE : config.application.insecure,
+ SKIP_RESTART : config.jenkins.skipRestart,
+ SKIP_PLUGINS : config.jenkins.skipPlugins])
+
+ globalPropertyManager.setGlobalProperty("${config.application.namePrefixForEnvVars}SCM_URL", this.gitHandler.tenant.url)
+ globalPropertyManager.setGlobalProperty("${config.application.namePrefixForEnvVars}PREFIXED_SCM_URL", this.gitHandler.tenant.repoPrefix())
+
+ if (config.jenkins.additionalEnvs) {
+ for (entry in (config.jenkins.additionalEnvs as Map).entrySet()) {
+ globalPropertyManager.setGlobalProperty(entry.key.toString(), entry.value.toString())
+ }
+ }
+
+ if (config.registry.url) {
+ globalPropertyManager.setGlobalProperty("${config.application.namePrefixForEnvVars}REGISTRY_URL", config.registry.url)
+ }
+
+ if (config.registry.path) {
+ globalPropertyManager.setGlobalProperty("${config.application.namePrefixForEnvVars}REGISTRY_PATH", config.registry.path)
+ }
+
+ if (config.registry.twoRegistries) {
+ globalPropertyManager.setGlobalProperty("${config.application.namePrefixForEnvVars}REGISTRY_PROXY_URL", config.registry.proxyUrl)
+ }
+
+ if (config.jenkins.mavenCentralMirror) {
+ globalPropertyManager.setGlobalProperty("${config.application.namePrefixForEnvVars}MAVEN_CENTRAL_MIRROR", config.jenkins.mavenCentralMirror)
+ }
+
+ globalPropertyManager.setGlobalProperty("${config.application.namePrefixForEnvVars}K8S_VERSION", Config.K8S_VERSION)
+
+ if (userManager.isUsingCasSecurityRealm()) {
+ log.trace("Using CAS Security Realm. Must not create user.")
+ } else {
+ userManager.createUser(config.jenkins.metricsUsername, config.jenkins.metricsPassword)
+ }
+
+ userManager.grantPermission(config.jenkins.metricsUsername, UserManager.Permissions.METRICS_VIEW)
+
+ if (config.features.monitoring.active && config.jenkins.internal) {
+ // And external Jenkins can likely not be monitored
+ prometheusConfigurator.enableAuthentication()
+ }
+
+ }
+
+ void createJenkinsjob(String namespace, String repoName) {
+ def credentialId = "scm-user"
+ String prefixedNamespace = "${config.application.namePrefix}${namespace}"
+ String jobName = "${config.application.namePrefix}${repoName}"
+
+ jobManager.createJob(jobName,
+ this.gitHandler.tenant.url,
+ prefixedNamespace,
+ credentialId)
+
+ if (config.scm.scmProviderType == ScmProviderType.SCM_MANAGER) {
+ jobManager.createCredential(jobName,
+ credentialId,
+ "${config.application.namePrefix}gitops",
+ "${config.scm.getScmManager().password}",
+ 'credentials for accessing scm-manager')
+ }
+
+ if (config.scm.scmProviderType == ScmProviderType.GITLAB) {
+ jobManager.createCredential(jobName,
+ credentialId,
+ "${config.scm.getGitlab().username}",
+ "${config.scm.getGitlab().password}",
+ 'credentials for accessing gitlab')
+ }
+
+ jobManager.createCredential(jobName,
+ "registry-user",
+ "${config.registry.username}",
+ "${config.registry.password}",
+ 'credentials for accessing the docker-registry for writing images built on jenkins')
+
+ if (config.registry.twoRegistries) {
+ jobManager.createCredential(jobName,
+ "registry-proxy-user",
+ "${config.registry.proxyUsername}",
+ "${config.registry.proxyPassword}",
+ 'credentials for accessing the docker-registry that contains 3rd party or base images')
+ }
+
+ jobManager.startJob(jobName)
+ }
+
+ protected String findDockerGid() {
+ String gid = ''
+ def etcGroup = k8sClient.run("tmp-docker-gid-grepper-${new Random().nextInt(10000)}",
+ 'irrelevant' /* Redundant, but mandatory param */, namespace, createGidGrepperOverrides(),
+ '--restart=Never', '-ti', '--rm', '--quiet')
+ // --quiet is necessary to avoid 'pod deleted' output
+
+ def lines = etcGroup?.split('\n')
+ for (String it : lines) {
+ def parts = it.split(":")
+ if (parts[0] == 'docker') {
+ gid = parts[2]
+ break
+ }
+ }
+
+ if (!gid) {
+ log.warn('Unable to determine Docker Group ID (GID). Jenkins Agent pods will run as root user (UID 0)!\n' + "Group docker not found in /etc/group:\n${etcGroup}")
+ return ''
+ } else {
+ log.debug("Using Docker Group ID (GID) ${gid} for Jenkins Agent pods")
+ return gid
+ }
+ }
+
+ Map createGidGrepperOverrides() {
+ ['spec': ['containers' : [['name' : 'tmp-docker-gid-grepper',
+ // We use the same image for several tasks for performance and maintenance reasons
+ 'image' : "${config.jenkins.internalBashImage}",
+ 'args' : ['cat', '/etc/group'],
+ 'volumeMounts': [['name' : 'group',
+ 'mountPath': '/etc/group',
+ 'readOnly' : true]]]],
+ 'nodeSelector': ['node': 'jenkins'],
+ 'volumes' : [['name' : 'group',
+ 'hostPath': ['path': '/etc/group']]]]]
+ }
+
+ @Override
+ String getActiveNamespaceFromFeature() {
+ return isEnabled() && config?.jenkins?.internal ? getNamespace() : null
+ }
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/features/Mail.groovy b/src/main/groovy/com/cloudogu/gitops/features/Mail.groovy
index 859e050c1..cb15a9370 100644
--- a/src/main/groovy/com/cloudogu/gitops/features/Mail.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/features/Mail.groovy
@@ -5,13 +5,16 @@ import com.cloudogu.gitops.FeatureWithImage
import com.cloudogu.gitops.config.Config
import com.cloudogu.gitops.features.deployment.DeploymentStrategy
import com.cloudogu.gitops.features.git.GitHandler
+import com.cloudogu.gitops.kubernetes.api.K8sClient
import com.cloudogu.gitops.utils.AirGappedUtils
import com.cloudogu.gitops.utils.FileSystemUtils
-import com.cloudogu.gitops.kubernetes.api.K8sClient
-import groovy.transform.CompileStatic
-import groovy.util.logging.Slf4j
+
import io.micronaut.core.annotation.Order
+
import jakarta.inject.Singleton
+import groovy.transform.CompileStatic
+import groovy.util.logging.Slf4j
+
import org.springframework.security.crypto.bcrypt.BCrypt
@Slf4j
@@ -20,45 +23,41 @@ import org.springframework.security.crypto.bcrypt.BCrypt
@CompileStatic
class Mail extends Feature implements FeatureWithImage {
- final static private String HELM_VALUES_PATH = 'argocd/cluster-resources/apps/mail/templates/mail-helm-values.ftl.yaml'
- final private String password
-
- String namespace = "${config.application.namePrefix}monitoring"
- Config config
- K8sClient k8sClient
-
- Mail(
- Config config,
- FileSystemUtils fileSystemUtils,
- DeploymentStrategy deployer,
- K8sClient k8sClient,
- AirGappedUtils airGappedUtils,
- GitHandler gitHandler
- ) {
- this.deployer = deployer
- this.config = config
- this.password = this.config.application.password
- this.k8sClient = k8sClient
- this.fileSystemUtils = fileSystemUtils
- this.airGappedUtils = airGappedUtils
- this.gitHandler = gitHandler
- }
-
- @Override
- boolean isEnabled() {
- return config.features.mail.mailServer
- }
-
- @Override
- void enable() {
- String bcryptMailhogPassword = BCrypt.hashpw(password, BCrypt.gensalt(4))
-
- addHelmValuesData('passwordCrypt', bcryptMailhogPassword)
- addHelmValuesData('mail', [
- // Note that passing the URL object here leads to problems in Graal Native image, see Git history
- host: config.features.mail.mailUrl ? new URL(config.features.mail.mailUrl).host : '',
- ])
-
- deployHelmChart('mailhog', 'mailhog', namespace, config.features.mail.helm, HELM_VALUES_PATH, config)
- }
-}
+ final static private String HELM_VALUES_PATH = 'argocd/cluster-resources/apps/mail/templates/mail-helm-values.ftl.yaml'
+ final private String password
+
+ String namespace = "${config.application.namePrefix}monitoring"
+ Config config
+ K8sClient k8sClient
+
+ Mail(Config config,
+ FileSystemUtils fileSystemUtils,
+ DeploymentStrategy deployer,
+ K8sClient k8sClient,
+ AirGappedUtils airGappedUtils,
+ GitHandler gitHandler) {
+ this.deployer = deployer
+ this.config = config
+ this.password = this.config.application.password
+ this.k8sClient = k8sClient
+ this.fileSystemUtils = fileSystemUtils
+ this.airGappedUtils = airGappedUtils
+ this.gitHandler = gitHandler
+ }
+
+ @Override
+ boolean isEnabled() {
+ return config.features.mail.mailServer
+ }
+
+ @Override
+ void enable() {
+ String bcryptMailhogPassword = BCrypt.hashpw(password, BCrypt.gensalt(4))
+
+ addHelmValuesData('passwordCrypt', bcryptMailhogPassword)
+ addHelmValuesData('mail', [// Note that passing the URL object here leads to problems in Graal Native image, see Git history
+ host: config.features.mail.mailUrl ? new URL(config.features.mail.mailUrl).host : '',])
+
+ deployHelmChart('mailhog', 'mailhog', namespace, config.features.mail.helm, HELM_VALUES_PATH, config)
+ }
+}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/features/Monitoring.groovy b/src/main/groovy/com/cloudogu/gitops/features/Monitoring.groovy
index ab93d5e6c..e83b5087a 100644
--- a/src/main/groovy/com/cloudogu/gitops/features/Monitoring.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/features/Monitoring.groovy
@@ -8,12 +8,16 @@ import com.cloudogu.gitops.features.git.GitHandler
import com.cloudogu.gitops.git.GitRepo
import com.cloudogu.gitops.git.GitRepoFactory
import com.cloudogu.gitops.kubernetes.api.K8sClient
-import com.cloudogu.gitops.utils.*
-import groovy.transform.CompileStatic
-import groovy.util.logging.Slf4j
+import com.cloudogu.gitops.utils.AirGappedUtils
+import com.cloudogu.gitops.utils.FileSystemUtils
+import com.cloudogu.gitops.utils.TemplatingEngine
+
import io.micronaut.core.annotation.Order
-import jakarta.inject.Singleton
+
import java.nio.file.Path
+import jakarta.inject.Singleton
+import groovy.transform.CompileStatic
+import groovy.util.logging.Slf4j
@Slf4j
@Singleton
@@ -21,216 +25,195 @@ import java.nio.file.Path
@CompileStatic
class Monitoring extends Feature implements FeatureWithImage {
- static final String HELM_VALUES_PATH = 'argocd/cluster-resources/apps/monitoring/templates/prometheus-stack-helm-values.ftl.yaml'
- static final String RBAC_NAMESPACE_ISOLATION_TEMPLATE = 'argocd/cluster-resources/apps/monitoring/templates/rbac/namespace-isolation-rbac.ftl.yaml'
- static final String NETWORK_POLICIES_PROMETHEUS_ALLOW_TEMPLATE = 'argocd/cluster-resources/apps/monitoring/templates/netpols/prometheus-allow-scraping.ftl.yaml'
-
- String namespace = "${config.application.namePrefix}monitoring"
- Config config
- K8sClient k8sClient
-
- private GitRepoFactory scmRepoProvider
-
- Monitoring(
- Config config,
- FileSystemUtils fileSystemUtils,
- DeploymentStrategy deployer,
- K8sClient k8sClient,
- AirGappedUtils airGappedUtils,
- GitRepoFactory scmRepoProvider,
- GitHandler gitHandler
- ) {
- this.config = config
- this.fileSystemUtils = fileSystemUtils
- this.deployer = deployer
- this.k8sClient = k8sClient
- this.airGappedUtils = airGappedUtils
- this.scmRepoProvider = scmRepoProvider
- this.gitHandler = gitHandler
- }
-
- @Override
- boolean isEnabled() {
- return config.features.monitoring.active
- }
-
- @Override
- void enable() {
- String uid = ''
- if (config.application.openshift) {
- uid = findValidOpenShiftUid()
- }
-
- addHelmValuesData('monitoring', [grafana: [host: config.features.monitoring.grafanaUrl ? new URL(config.features.monitoring.grafanaUrl).host : '']])
- addHelmValuesData('namespaces', (config.application.namespaces.activeNamespaces ?: []) as LinkedHashSet)
- addHelmValuesData('scm', scmConfigurationMetrics())
- addHelmValuesData('jenkins', jenkinsConfigurationMetrics())
- addHelmValuesData('uid', uid)
-
- // Create secrets imperatively here instead of values.yaml, because we don't want credentials to be visible in the Git repo
- setupMonitoringSecrets()
- createMonitoringCrd()
-
- GitRepo clusterResourcesRepo = scmRepoProvider.getRepo('argocd/cluster-resources', this.gitHandler.resourcesScm)
- clusterResourcesRepo.cloneRepo()
-
- if (config.application.namespaceIsolation || config.application.netpols) {
- if (config.application.namespaceIsolation) { generateNamespaceIsolationRBAC(clusterResourcesRepo) }
- if (config.application.netpols) { generateNetpols(clusterResourcesRepo) }
- }
-
- // Remove dashboards for features that are not enabled
- cleanupUnusedDashboards(clusterResourcesRepo)
-
- clusterResourcesRepo.commitAndPush('Update Prometheus dashboards, RBAC and network policies.')
- deployHelmChart('monitoring', 'kube-prometheus-stack', namespace, config.features.monitoring.helm, HELM_VALUES_PATH, config)
- }
-
- private void setupMonitoringSecrets() {
- k8sClient.createSecret(
- 'generic',
- 'prometheus-metrics-creds-scmm',
- namespace,
- new Tuple2('password', config.application.password)
- )
-
- k8sClient.createSecret(
- 'generic',
- 'prometheus-metrics-creds-jenkins',
- namespace,
- new Tuple2('password', config.jenkins.metricsPassword),
- )
-
- if (config.features.mail.smtpUser || config.features.mail.smtpPassword) {
- k8sClient.createSecret(
- 'generic',
- 'grafana-email-secret',
- namespace,
- new Tuple2('user', config.features.mail.smtpUser),
- new Tuple2('password', config.features.mail.smtpPassword)
- )
- }
- }
-
- private void generateNamespaceIsolationRBAC(GitRepo repo) {
- for (String currentNamespace : config.application.namespaces.activeNamespaces) {
- String rbacYaml = new TemplatingEngine().template(new File(RBAC_NAMESPACE_ISOLATION_TEMPLATE),
- [namespace : currentNamespace,
- namePrefix: config.application.namePrefix,
- config : config,])
- repo.writeFile(
- "apps/monitoring/misc/rbac/${currentNamespace}.yaml",
- rbacYaml
- )
- }
- }
-
- private void generateNetpols(GitRepo repo) {
- for (String currentNamespace : config.application.namespaces.activeNamespaces) {
- String netpolsYaml = new TemplatingEngine().template(new File(NETWORK_POLICIES_PROMETHEUS_ALLOW_TEMPLATE),
- [namespace : currentNamespace,
- namePrefix: config.application.namePrefix,])
-
- repo.writeFile(
- "apps/monitoring/misc/netpols/${currentNamespace}.yaml",
- netpolsYaml
- )
- }
- }
-
- private Map scmConfigurationMetrics() {
- URI uri = this.gitHandler.resourcesScm.prometheusMetricsEndpoint()
- return [
- protocol: uri?.scheme ?: '',
- host : uri?.authority ?: '',
- path : uri?.path ?: '',
- ]
- }
-
- protected void createMonitoringCrd() {
- if (!config.application.skipCrds) {
- def serviceMonitorCrdYaml
- if (config.application.mirrorRepos) {
- serviceMonitorCrdYaml = Path.of(
- "${config.application.localHelmChartFolder}/${config.features.monitoring.helm.chart}/charts/crds/crds/crd-servicemonitors.yaml"
- ).toString()
- } else {
- serviceMonitorCrdYaml =
- "https://raw.githubusercontent.com/prometheus-community/helm-charts/" +
- "kube-prometheus-stack-${config.features.monitoring.helm.version}/" +
- "charts/kube-prometheus-stack/charts/crds/crds/crd-servicemonitors.yaml"
- }
-
- log.debug("Applying ServiceMonitor CRD; Argo CD fails if it is not there. Chicken-egg-problem.\n" +
- "Applying from path ${serviceMonitorCrdYaml}")
- k8sClient.applyYaml(serviceMonitorCrdYaml)
- }
- }
-
- private Map jenkinsConfigurationMetrics() {
- URI uri = baseUriJenkins(config).resolve('prometheus')
- return [
- metricsUsername: config.jenkins.metricsUsername ?: '',
- protocol : uri.scheme ?: '',
- host : uri.authority ?: '',
- path : uri.path ?: '',
- ]
- }
-
- private static URI baseUriJenkins(Config config) {
- if (config.jenkins.internal) {
- return new URI("http://jenkins.${config.application.namePrefix}jenkins.svc.cluster.local/")
- }
- def urlString = config.jenkins?.url?.strip() ?: ""
- if (!urlString) {
- throw new IllegalArgumentException("config.jenkins.url must be set when config.jenkins.internal = false")
- }
- def url = URI.create(urlString)
- return url.toString().endsWith("/") ? url : URI.create(url.toString() + "/")
- }
-
- private String findValidOpenShiftUid() {
- String uidRange = k8sClient.getAnnotation('namespace', namespace, 'openshift.io/sa.scc.uid-range')
-
- if (uidRange) {
- log.debug("found UID=${uidRange}")
- String uid = uidRange.split('/')[0]
- return uid
- } else {
- throw new RuntimeException("Could not find a valid UID! Really running on OpenShift?")
- }
- }
-
- protected void cleanupUnusedDashboards(GitRepo clusterResourcesRepo) {
- String repoRoot = clusterResourcesRepo.getAbsoluteLocalRepoTmpDir()
- String dashboardRoot = "${repoRoot}/apps/prometheusstack/misc/dashboard"
-
- if (!config.features.ingress.active) {
- fileSystemUtils.deleteFile("${dashboardRoot}/traefik-dashboard.yaml")
- fileSystemUtils.deleteFile("${dashboardRoot}/traefik-dashboard-requests-handling.yaml")
- }
-
- if (!config.jenkins.active) {
- fileSystemUtils.deleteFile("${dashboardRoot}/jenkins-dashboard.yaml")
- }
-
- if (!config.scm.scmManager?.url) {
- fileSystemUtils.deleteFile("${dashboardRoot}/scmm-dashboard.yaml")
- }
- }
-
- @Override
- String getNamespace() {
- return namespace
- }
-
- @Override
- K8sClient getK8sClient() {
- return k8sClient
- }
-
- @Override
- Config getConfig() {
- return config
- }
-}
+ static final String HELM_VALUES_PATH = 'argocd/cluster-resources/apps/monitoring/templates/prometheus-stack-helm-values.ftl.yaml'
+ static final String RBAC_NAMESPACE_ISOLATION_TEMPLATE = 'argocd/cluster-resources/apps/monitoring/templates/rbac/namespace-isolation-rbac.ftl.yaml'
+ static final String NETWORK_POLICIES_PROMETHEUS_ALLOW_TEMPLATE = 'argocd/cluster-resources/apps/monitoring/templates/netpols/prometheus-allow-scraping.ftl.yaml'
+
+ String namespace = "${config.application.namePrefix}monitoring"
+ Config config
+ K8sClient k8sClient
+
+ private GitRepoFactory scmRepoProvider
+
+ Monitoring(Config config,
+ FileSystemUtils fileSystemUtils,
+ DeploymentStrategy deployer,
+ K8sClient k8sClient,
+ AirGappedUtils airGappedUtils,
+ GitRepoFactory scmRepoProvider,
+ GitHandler gitHandler) {
+ this.config = config
+ this.fileSystemUtils = fileSystemUtils
+ this.deployer = deployer
+ this.k8sClient = k8sClient
+ this.airGappedUtils = airGappedUtils
+ this.scmRepoProvider = scmRepoProvider
+ this.gitHandler = gitHandler
+ }
+
+ @Override
+ boolean isEnabled() {
+ return config.features.monitoring.active
+ }
+
+ @Override
+ void enable() {
+ String uid = ''
+ if (config.application.openshift) {
+ uid = findValidOpenShiftUid()
+ }
+
+ addHelmValuesData('monitoring', [grafana: [host: config.features.monitoring.grafanaUrl ? new URL(config.features.monitoring.grafanaUrl).host : '']])
+ addHelmValuesData('namespaces', (config.application.namespaces.activeNamespaces ?: []) as LinkedHashSet)
+ addHelmValuesData('scm', scmConfigurationMetrics())
+ addHelmValuesData('jenkins', jenkinsConfigurationMetrics())
+ addHelmValuesData('uid', uid)
+
+ // Create secrets imperatively here instead of values.yaml, because we don't want credentials to be visible in the Git repo
+ setupMonitoringSecrets()
+ createMonitoringCrd()
+
+ GitRepo clusterResourcesRepo = scmRepoProvider.getRepo('argocd/cluster-resources', this.gitHandler.resourcesScm)
+ clusterResourcesRepo.cloneRepo()
+
+ if (config.application.namespaceIsolation || config.application.netpols) {
+ if (config.application.namespaceIsolation) { generateNamespaceIsolationRBAC(clusterResourcesRepo) }
+ if (config.application.netpols) { generateNetpols(clusterResourcesRepo) }
+ }
+
+ // Remove dashboards for features that are not enabled
+ cleanupUnusedDashboards(clusterResourcesRepo)
+
+ clusterResourcesRepo.commitAndPush('Update Prometheus dashboards, RBAC and network policies.')
+ deployHelmChart('monitoring', 'kube-prometheus-stack', namespace, config.features.monitoring.helm, HELM_VALUES_PATH, config)
+ }
+
+ private void setupMonitoringSecrets() {
+ k8sClient.createSecret('generic',
+ 'prometheus-metrics-creds-scmm',
+ namespace,
+ new Tuple2('password', config.application.password))
+
+ k8sClient.createSecret('generic',
+ 'prometheus-metrics-creds-jenkins',
+ namespace,
+ new Tuple2('password', config.jenkins.metricsPassword),)
+
+ if (config.features.mail.smtpUser || config.features.mail.smtpPassword) {
+ k8sClient.createSecret('generic',
+ 'grafana-email-secret',
+ namespace,
+ new Tuple2('user', config.features.mail.smtpUser),
+ new Tuple2('password', config.features.mail.smtpPassword))
+ }
+ }
+
+ private void generateNamespaceIsolationRBAC(GitRepo repo) {
+ for (String currentNamespace : config.application.namespaces.activeNamespaces) {
+ String rbacYaml = new TemplatingEngine().template(new File(RBAC_NAMESPACE_ISOLATION_TEMPLATE),
+ [namespace : currentNamespace,
+ namePrefix: config.application.namePrefix,
+ config : config,])
+ repo.writeFile("apps/monitoring/misc/rbac/${currentNamespace}.yaml",
+ rbacYaml)
+ }
+ }
+
+ private void generateNetpols(GitRepo repo) {
+ for (String currentNamespace : config.application.namespaces.activeNamespaces) {
+ String netpolsYaml = new TemplatingEngine().template(new File(NETWORK_POLICIES_PROMETHEUS_ALLOW_TEMPLATE),
+ [namespace : currentNamespace,
+ namePrefix: config.application.namePrefix,])
+
+ repo.writeFile("apps/monitoring/misc/netpols/${currentNamespace}.yaml",
+ netpolsYaml)
+ }
+ }
+
+ private Map scmConfigurationMetrics() {
+ URI uri = this.gitHandler.resourcesScm.prometheusMetricsEndpoint()
+ return [protocol: uri?.scheme ?: '',
+ host : uri?.authority ?: '',
+ path : uri?.path ?: '',]
+ }
+
+ protected void createMonitoringCrd() {
+ if (!config.application.skipCrds) {
+ def serviceMonitorCrdYaml
+ if (config.application.mirrorRepos) {
+ serviceMonitorCrdYaml = Path.of("${config.application.localHelmChartFolder}/${config.features.monitoring.helm.chart}/charts/crds/crds/crd-servicemonitors.yaml").toString()
+ } else {
+ serviceMonitorCrdYaml = "https://raw.githubusercontent.com/prometheus-community/helm-charts/" + "kube-prometheus-stack-${config.features.monitoring.helm.version}/" +
+ "charts/kube-prometheus-stack/charts/crds/crds/crd-servicemonitors.yaml"
+ }
+
+ log.debug("Applying ServiceMonitor CRD; Argo CD fails if it is not there. Chicken-egg-problem.\n" + "Applying from path ${serviceMonitorCrdYaml}")
+ k8sClient.applyYaml(serviceMonitorCrdYaml)
+ }
+ }
+
+ private Map jenkinsConfigurationMetrics() {
+ URI uri = baseUriJenkins(config).resolve('prometheus')
+ return [metricsUsername: config.jenkins.metricsUsername ?: '',
+ protocol : uri.scheme ?: '',
+ host : uri.authority ?: '',
+ path : uri.path ?: '',]
+ }
+
+ private static URI baseUriJenkins(Config config) {
+ if (config.jenkins.internal) {
+ return new URI("http://jenkins.${config.application.namePrefix}jenkins.svc.cluster.local/")
+ }
+ def urlString = config.jenkins?.url?.strip() ?: ""
+ if (!urlString) {
+ throw new IllegalArgumentException("config.jenkins.url must be set when config.jenkins.internal = false")
+ }
+ def url = URI.create(urlString)
+ return url.toString().endsWith("/") ? url : URI.create(url.toString() + "/")
+ }
+
+ private String findValidOpenShiftUid() {
+ String uidRange = k8sClient.getAnnotation('namespace', namespace, 'openshift.io/sa.scc.uid-range')
+
+ if (uidRange) {
+ log.debug("found UID=${uidRange}")
+ String uid = uidRange.split('/')[0]
+ return uid
+ } else {
+ throw new RuntimeException("Could not find a valid UID! Really running on OpenShift?")
+ }
+ }
+
+ protected void cleanupUnusedDashboards(GitRepo clusterResourcesRepo) {
+ String repoRoot = clusterResourcesRepo.getAbsoluteLocalRepoTmpDir()
+ String dashboardRoot = "${repoRoot}/apps/prometheusstack/misc/dashboard"
+
+ if (!config.features.ingress.active) {
+ fileSystemUtils.deleteFile("${dashboardRoot}/traefik-dashboard.yaml")
+ fileSystemUtils.deleteFile("${dashboardRoot}/traefik-dashboard-requests-handling.yaml")
+ }
+
+ if (!config.jenkins.active) {
+ fileSystemUtils.deleteFile("${dashboardRoot}/jenkins-dashboard.yaml")
+ }
+
+ if (!config.scm.scmManager?.url) {
+ fileSystemUtils.deleteFile("${dashboardRoot}/scmm-dashboard.yaml")
+ }
+ }
+
+ @Override
+ String getNamespace() {
+ return namespace
+ }
+
+ @Override
+ K8sClient getK8sClient() {
+ return k8sClient
+ }
+
+ @Override
+ Config getConfig() {
+ return config
+ }
+}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/features/Registry.groovy b/src/main/groovy/com/cloudogu/gitops/features/Registry.groovy
index 61e66ed96..9610e58ed 100644
--- a/src/main/groovy/com/cloudogu/gitops/features/Registry.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/features/Registry.groovy
@@ -3,80 +3,77 @@ package com.cloudogu.gitops.features
import com.cloudogu.gitops.Feature
import com.cloudogu.gitops.config.Config
import com.cloudogu.gitops.features.deployment.HelmStrategy
-import com.cloudogu.gitops.utils.FileSystemUtils
import com.cloudogu.gitops.kubernetes.api.K8sClient
-import groovy.util.logging.Slf4j
+import com.cloudogu.gitops.utils.FileSystemUtils
+
import io.micronaut.core.annotation.Order
+
import jakarta.inject.Singleton
+import groovy.util.logging.Slf4j
@Slf4j
@Singleton
@Order(40)
class Registry extends Feature {
- /**
- * Local container port of the registry within the pod
- */
- public static final String CONTAINER_PORT = '5000'
+ /**
+ * Local container port of the registry within the pod*/
+ public static final String CONTAINER_PORT = '5000'
- String namespace
- private Config config
- private K8sClient k8sClient
+ String namespace
+ private Config config
+ private K8sClient k8sClient
- Registry(
- Config config,
- FileSystemUtils fileSystemUtils,
- K8sClient k8sClient,
- // For now we deploy imperatively using helm to avoid order problems. In future we could deploy via argocd.
- HelmStrategy deployer
- ) {
- this.deployer = deployer
- this.config = config
- this.fileSystemUtils = fileSystemUtils
- this.k8sClient = k8sClient
+ Registry(Config config,
+ FileSystemUtils fileSystemUtils,
+ K8sClient k8sClient,
+ // For now we deploy imperatively using helm to avoid order problems. In future we could deploy via argocd.
+ HelmStrategy deployer) {
+ this.deployer = deployer
+ this.config = config
+ this.fileSystemUtils = fileSystemUtils
+ this.k8sClient = k8sClient
- if(config.registry.internal) {
- this.namespace = "${config.application.namePrefix}registry"
- }
- }
+ if (config.registry.internal) {
+ this.namespace = "${config.application.namePrefix}registry"
+ }
+ }
- @Override
- boolean isEnabled() {
- return config.registry.active
- }
+ @Override
+ boolean isEnabled() {
+ return config.registry.active
+ }
- @Override
- void enable() {
+ @Override
+ void enable() {
- if (config.registry.internal) {
- addHelmValuesData("service", [
- nodePort: Config.DEFAULT_REGISTRY_PORT,
- type : 'NodePort'
- ])
+ if (config.registry.internal) {
+ addHelmValuesData("service", [nodePort: Config.DEFAULT_REGISTRY_PORT,
+ type : 'NodePort'])
- def helmConfig = config.registry.helm
- deployHelmChart('registry', 'docker-registry', namespace, helmConfig, "", config)
+ def helmConfig = config.registry.helm
+ deployHelmChart('registry', 'docker-registry', namespace, helmConfig, "", config)
- if (config.registry.internalPort != Config.DEFAULT_REGISTRY_PORT) {
- /* Add additional node port
- 30000 is needed as a static by docker via port mapping of k3d, e.g. 32769 -> 30000 on server-0 container
- See "-p 30000" in init-cluster.sh
- e.g 32769 is needed so the kubelet can access the image inside the server-0 container
- */
+ if (config.registry.internalPort != Config.DEFAULT_REGISTRY_PORT) {
+ /* Add additional node port
+ 30000 is needed as a static by docker via port mapping of k3d, e.g. 32769 -> 30000 on server-0 container
+ See "-p 30000" in init-cluster.sh
+ e.g 32769 is needed so the kubelet can access the image inside the server-0 container
+ */
- /* k8sClient.createServiceNodePort('docker-registry-internal-port',
- CONTAINER_PORT, config.registry.internalPort.toString(),
- namespace) */
+ /* k8sClient.createServiceNodePort('docker-registry-internal-port',
+ CONTAINER_PORT, config.registry.internalPort.toString(),
+ namespace) */
- Map selector = new HashMap<>()
- selector.put("app", "docker-registry")
- k8sClient.k8sJavaApiClient.createNodePortService(namespace,
- 'docker-registry-internal-port',
- selector,
- CONTAINER_PORT.toInteger(),
- config.registry.internalPort,
- 'docker-registry-internal-port')
- }
- }
- }
+ Map selector = new HashMap<>()
+ selector.put("app", "docker-registry")
+ k8sClient.k8sJavaApiClient.createNodePortService(namespace,
+ 'docker-registry-internal-port',
+ selector,
+ CONTAINER_PORT.toInteger(),
+ config.registry.internalPort,
+ 'docker-registry-internal-port')
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/features/Vault.groovy b/src/main/groovy/com/cloudogu/gitops/features/Vault.groovy
index 4616144ed..7baf3dd3c 100644
--- a/src/main/groovy/com/cloudogu/gitops/features/Vault.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/features/Vault.groovy
@@ -6,75 +6,74 @@ import com.cloudogu.gitops.config.Config
import com.cloudogu.gitops.features.deployment.DeploymentStrategy
import com.cloudogu.gitops.features.git.GitHandler
import com.cloudogu.gitops.kubernetes.api.K8sClient
-import com.cloudogu.gitops.utils.*
-import groovy.util.logging.Slf4j
+import com.cloudogu.gitops.utils.AirGappedUtils
+import com.cloudogu.gitops.utils.FileSystemUtils
+import com.cloudogu.gitops.utils.TemplatingEngine
+
import io.micronaut.core.annotation.Order
+
import jakarta.inject.Singleton
+import groovy.util.logging.Slf4j
@Slf4j
@Singleton
@Order(500)
class Vault extends Feature implements FeatureWithImage {
- static final String VAULT_START_SCRIPT_PATH = "argocd/cluster-resources/apps/vault/templates/dev-post-start.ftl.sh"
- static final String HELM_VALUES_PATH = "argocd/cluster-resources/apps/vault/templates/values.ftl.yaml"
+ static final String VAULT_START_SCRIPT_PATH = "argocd/cluster-resources/apps/vault/templates/dev-post-start.ftl.sh"
+ static final String HELM_VALUES_PATH = "argocd/cluster-resources/apps/vault/templates/values.ftl.yaml"
- String namespace = "${config.application.namePrefix}secrets"
- Config config
- K8sClient k8sClient
+ String namespace = "${config.application.namePrefix}secrets"
+ Config config
+ K8sClient k8sClient
- Vault(
- Config config,
- FileSystemUtils fileSystemUtils,
- K8sClient k8sClient,
- DeploymentStrategy deployer,
- AirGappedUtils airGappedUtils,
- GitHandler gitHandler
- ) {
- this.deployer = deployer
- this.config = config
- this.fileSystemUtils = fileSystemUtils
- this.k8sClient = k8sClient
- this.airGappedUtils = airGappedUtils
- this.gitHandler = gitHandler
- }
+ Vault(Config config,
+ FileSystemUtils fileSystemUtils,
+ K8sClient k8sClient,
+ DeploymentStrategy deployer,
+ AirGappedUtils airGappedUtils,
+ GitHandler gitHandler) {
+ this.deployer = deployer
+ this.config = config
+ this.fileSystemUtils = fileSystemUtils
+ this.k8sClient = k8sClient
+ this.airGappedUtils = airGappedUtils
+ this.gitHandler = gitHandler
+ }
- @Override
- boolean isEnabled() {
- return config.features.secrets.active
- }
+ @Override
+ boolean isEnabled() {
+ return config.features.secrets.active
+ }
- @Override
- void enable() {
- // Note that some specific configuration steps are implemented in ArgoCD
- def helmConfig = config.features.secrets.vault.helm
+ @Override
+ void enable() {
+ // Note that some specific configuration steps are implemented in ArgoCD
+ def helmConfig = config.features.secrets.vault.helm
- addHelmValuesData("host", config.features.secrets.vault.url ? new URL(config.features.secrets.vault.url as String).host : '')
+ addHelmValuesData("host", config.features.secrets.vault.url ? new URL(config.features.secrets.vault.url as String).host : '')
- String vaultMode = config.features.secrets.vault.mode
- if (vaultMode == 'dev') {
- log.debug('WARNING! Vault dev mode is enabled! In this mode, Vault runs entirely in-memory\n' +
- 'and starts unsealed with a single unseal key. ')
+ String vaultMode = config.features.secrets.vault.mode
+ if (vaultMode == 'dev') {
+ log.debug('WARNING! Vault dev mode is enabled! In this mode, Vault runs entirely in-memory\n' + 'and starts unsealed with a single unseal key. ')
- // Create config map from init script
- // Init script creates/authorizes secrets, users, service accounts, etc.
- def vaultPostStartConfigMap = 'vault-dev-post-start'
- def vaultPostStartVolume = 'dev-post-start'
+ // Create config map from init script
+ // Init script creates/authorizes secrets, users, service accounts, etc.
+ def vaultPostStartConfigMap = 'vault-dev-post-start'
+ def vaultPostStartVolume = 'dev-post-start'
- def templatedFile = fileSystemUtils.copyToTempDir(fileSystemUtils.getRootDir() + "/"+VAULT_START_SCRIPT_PATH)
- def postStartScript = new TemplatingEngine().replaceTemplate(templatedFile.toFile(), [namePrefix: config.application.namePrefix])
+ def templatedFile = fileSystemUtils.copyToTempDir(fileSystemUtils.getRootDir() + "/" + VAULT_START_SCRIPT_PATH)
+ def postStartScript = new TemplatingEngine().replaceTemplate(templatedFile.toFile(), [namePrefix: config.application.namePrefix])
- log.debug('Creating namespace for vault, so it can add its secrets there')
- k8sClient.createNamespace(namespace)
- k8sClient.createConfigMapFromFile(vaultPostStartConfigMap, namespace, postStartScript.absolutePath)
+ log.debug('Creating namespace for vault, so it can add its secrets there')
+ k8sClient.createNamespace(namespace)
+ k8sClient.createConfigMapFromFile(vaultPostStartConfigMap, namespace, postStartScript.absolutePath)
- addHelmValuesData("dev", [
- rootToken: UUID.randomUUID(),
- vaultPostStartConfigMap: vaultPostStartConfigMap,
- vaultPostStartVolume: vaultPostStartVolume,
- postStartScriptName: postStartScript.name
- ])
- }
+ addHelmValuesData("dev", [rootToken : UUID.randomUUID(),
+ vaultPostStartConfigMap: vaultPostStartConfigMap,
+ vaultPostStartVolume : vaultPostStartVolume,
+ postStartScriptName : postStartScript.name])
+ }
- deployHelmChart('vault', 'vault', namespace, helmConfig, HELM_VALUES_PATH, config)
- }
+ deployHelmChart('vault', 'vault', namespace, helmConfig, HELM_VALUES_PATH, config)
+ }
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/features/argocd/ArgoCD.groovy b/src/main/groovy/com/cloudogu/gitops/features/argocd/ArgoCD.groovy
index f42edfc02..0369a4a31 100644
--- a/src/main/groovy/com/cloudogu/gitops/features/argocd/ArgoCD.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/features/argocd/ArgoCD.groovy
@@ -4,346 +4,322 @@ import com.cloudogu.gitops.Feature
import com.cloudogu.gitops.config.Config
import com.cloudogu.gitops.features.git.GitHandler
import com.cloudogu.gitops.git.GitRepoFactory
+import com.cloudogu.gitops.kubernetes.api.HelmClient
+import com.cloudogu.gitops.kubernetes.api.K8sClient
import com.cloudogu.gitops.kubernetes.rbac.RbacDefinition
import com.cloudogu.gitops.kubernetes.rbac.Role
import com.cloudogu.gitops.utils.FileSystemUtils
-import com.cloudogu.gitops.kubernetes.api.HelmClient
-import com.cloudogu.gitops.kubernetes.api.K8sClient
import com.cloudogu.gitops.utils.MapUtils
-import groovy.util.logging.Slf4j
+
import io.micronaut.core.annotation.Order
+
+import java.nio.file.Path
import jakarta.inject.Singleton
+import groovy.util.logging.Slf4j
import org.springframework.security.crypto.bcrypt.BCrypt
-import java.nio.file.Path
-
@Slf4j
@Singleton
@Order(100)
class ArgoCD extends Feature {
- private final String namespace
- private final Config config
- private final K8sClient k8sClient
- private final HelmClient helmClient
- private final FileSystemUtils fileSystemUtils
- private final GitRepoFactory repoProvider
- private final GitHandler gitHandler
- private final String password
-
- private ArgoCDRepoSetup repoSetup
- private RepoLayout clusterResourcesRepo
-
-
- ArgoCD(
- Config config,
- K8sClient k8sClient,
- HelmClient helmClient,
- FileSystemUtils fileSystemUtils,
- GitRepoFactory repoProvider,
- GitHandler gitHandler
- ) {
- this.repoProvider = repoProvider
- this.config = config
- this.k8sClient = k8sClient
- this.helmClient = helmClient
- this.fileSystemUtils = fileSystemUtils
- this.gitHandler = gitHandler
- this.password = config.application.password
- this.namespace = "${config.application.namePrefix}${config.features.argocd.namespace}"
- }
-
- @Override
- boolean isEnabled() {
- config.features.argocd.active
- }
-
- @Override
- void postConfigInit(Config configToSet) {
- // Exit early if not in operator mode or if env list is empty
- if (!configToSet.features.argocd.operator || !configToSet.features.argocd.env) {
- log.debug("Skipping features.argocd.env validation: operator mode is disabled or env list is empty.")
- return
- }
-
- List env = configToSet.features.argocd.env as List>
-
- log.info("Validating env list in features.argocd.env with {} entries.", env.size())
-
- env.each { map ->
- if (!(map instanceof Map) || !map.containsKey('name') || !map.containsKey('value')) {
- throw new IllegalArgumentException("Each env variable in features.argocd.env must be a map with 'name' and 'value'. Invalid entry found: $map")
- }
- }
-
- log.info("Env list validation for features.argocd.env completed successfully.")
- }
-
- @Override
- void enable() {
- this.repoSetup = ArgoCDRepoSetup.create(config, fileSystemUtils, repoProvider, gitHandler)
- this.clusterResourcesRepo = repoSetup.clusterRepoLayout()
-
- log.debug('Cloning Repositories')
- repoSetup.initLocalRepos()
- repoSetup.prepareClusterResourcesRepo()
- repoSetup.commitAndPushAll('Initial Commit')
-
- log.debug('Installing Argo CD')
- installArgoCd()
- }
-
-
- private void installArgoCd() {
-
- log.debug("Creating namespaces")
- k8sClient.createNamespaces(config.application.namespaces.activeNamespaces.toList())
-
- createSCMCredentialsSecret()
-
- if (config.features.mail.smtpUser || config.features.mail.smtpPassword) {
- k8sClient.createSecret(
- 'generic',
- 'argocd-notifications-secret',
- namespace,
- new Tuple2('email-username', config.features.mail.smtpUser),
- new Tuple2('email-password', config.features.mail.smtpPassword)
- )
- }
-
- if (config.features.argocd.operator) {
- generateRBAC()
- deployWithOperator()
- } else {
- if (this.config.features.argocd?.values) {
- String argocdConfigPath = clusterResourcesRepo.helmValuesFile()
- log.debug("extend Argocd values.yaml with ${this.config.features.argocd.values}")
- def argocdYaml = fileSystemUtils.readYaml(
- Path.of(argocdConfigPath))
-
- def result = MapUtils.deepMerge(this.config.features.argocd.values, argocdYaml)
- fileSystemUtils.writeYaml(result, new File (argocdConfigPath))
- log.debug("Argocd values.yaml contains ${result}")
- }
- deployWithHelm()
- }
-
- if (config.multiTenant.useDedicatedInstance) {
- //Bootstrapping dedicated instance
- k8sClient.applyYaml(Path.of(clusterResourcesRepo.projectsDir(), "tenant.yaml").toString())
- k8sClient.applyYaml(Path.of(clusterResourcesRepo.applicationsDir(), "bootstrap.yaml").toString())
-
- //Bootstrapping tenant Argocd projects
- RepoLayout tenantRepoLayout = repoSetup.tenantRepoLayout()
- k8sClient.applyYaml(Path.of(tenantRepoLayout.projectsDir(), "argocd.yaml").toString())
- k8sClient.applyYaml(Path.of(tenantRepoLayout.applicationsDir(), "bootstrap.yaml").toString())
- } else {
- // Bootstrap root application
- k8sClient.applyYaml(Path.of(clusterResourcesRepo.projectsDir(), "argocd.yaml").toString())
- k8sClient.applyYaml(Path.of(clusterResourcesRepo.applicationsDir(), "bootstrap.yaml").toString())
- }
-
- // Delete helm-argo secrets to decouple from helm.
- // This does not delete Argo from the cluster, but you can no longer modify argo directly with helm
- // For development keeping it in helm makes it easier (e.g. for helm uninstall).
- k8sClient.delete('secret', namespace,
- new Tuple2('owner', 'helm'), new Tuple2('name', 'argocd'))
- }
-
- private void deployWithOperator() {
- // Apply argocd yaml from operator folder
- String argocdConfigPath = clusterResourcesRepo.operatorConfigFile()
- if (this.config.features.argocd?.values) {
- log.debug("extend Argocd.yaml with ${this.config.features.argocd.values}")
- def argocdYaml = fileSystemUtils.readYaml(
- Path.of(clusterResourcesRepo.operatorConfigFile()))
-
- def result = MapUtils.deepMerge(this.config.features.argocd.values, argocdYaml)
- fileSystemUtils.writeYaml(result, new File (argocdConfigPath))
- log.debug("Argocd.yaml for operator contains ${result}")
- // reload file
- argocdConfigPath = clusterResourcesRepo.operatorConfigFile()
- }
- k8sClient.applyYaml(argocdConfigPath)
-
- // ArgoCD is not installed until the ArgoCD-Operator did his job.
- // This can take some time, so we wait for the status of the custom resource to become "Available"
- k8sClient.waitForResourcePhase("argocd", "argocd", namespace, "Available")
-
- log.debug("Setting new argocd admin password")
- // Set admin password imperatively here instead of operator/argocd.yaml, because we don't want it to show in git repo
- // The Operator uses an extra secret to store the admin Password, which is not bcrypted
- k8sClient.patch('secret', 'argocd-cluster', namespace,
- [stringData: ['admin.password': password]])
-
- // In newer Versions ArgoCD Operator uses the password in argocd-cluster secret only as generated initial password
- // but we want to set our own admin password so we set the password in both Secrets for consistency
- String bcryptArgoCDPassword = BCrypt.hashpw(password, BCrypt.gensalt(4))
- k8sClient.patch('secret', 'argocd-secret', namespace,
- [stringData: ['admin.password': bcryptArgoCDPassword]])
-
- updatingArgoCDManagedNamespaces()
-
- log.debug("Apply RBAC permissions for ArgoCD in all managed namespaces imperatively")
- // Apply rbac yamls from operator/rbac folder
- String argocdRbacPath = clusterResourcesRepo.operatorRbacDir()
- k8sClient.applyYaml("${argocdRbacPath} --recursive")
- }
-
-
- private void deployWithHelm() {
-
- // Install umbrella chart from argocd/argocd
- String umbrellaChartPath = clusterResourcesRepo.helmDir()
- // Even if the Chart.lock already contains the repo, we need to add it before resolving it
- // See https://github.com/helm/helm/issues/8036#issuecomment-872502901
- List helmDependencies = fileSystemUtils.readYaml(
- Path.of(clusterResourcesRepo.chartYaml()))['dependencies']
- helmClient.addRepo('argo', helmDependencies[0]['repository'] as String)
- helmClient.dependencyBuild(umbrellaChartPath)
- helmClient.upgrade('argocd', umbrellaChartPath, [namespace: namespace])
-
- log.debug("Setting new argocd admin password")
- // Set admin password imperatively here instead of values.yaml, because we don't want it to show in git repo
- String bcryptArgoCDPassword = BCrypt.hashpw(password, BCrypt.gensalt(4))
- k8sClient.patch('secret', 'argocd-secret', namespace,
- [stringData: ['admin.password': bcryptArgoCDPassword]])
-
- }
-
- // The ArgoCD instance installed via an operator only manages its deployment namespace.
- // To manage additional namespaces, we need to update the 'argocd-default-cluster-config' secret with all managed namespaces.
- void updatingArgoCDManagedNamespaces() {
-
- log.debug("Updating managed namespaces in ArgoCD configuration secret.")
- def namespaceList = !config.multiTenant.useDedicatedInstance ?
- config.application.namespaces.activeNamespaces :
- config.application.namespaces.tenantNamespaces
-
- k8sClient.patch('secret', 'argocd-default-cluster-config', namespace,
- [stringData: ['namespaces': namespaceList.join(',')]])
-
- if (config.multiTenant.useDedicatedInstance) {
- // Append new namespaces to existing ones from the secret.
- // `kubectl patch` can't merge list subfields, so we read, decode, merge, and update the secret.
- // This ensures all centrally managed namespaces are preserved.
- String base64Namespaces = k8sClient.getArgoCDNamespacesSecret('argocd-default-cluster-config', config.multiTenant.centralArgocdNamespace)
- byte[] decodedBytes = Base64.decoder.decode(base64Namespaces)
- String decoded = new String(decodedBytes, "UTF-8")
- def decodedList = decoded?.split(',') as List ?: []
- def activeList = config.application.namespaces.activeNamespaces?.flatten() as List ?: []
- def merged = (decodedList + activeList).unique().join(',')
- log.debug("Updating Central Argocd 'argocd-default-cluster-config' secret")
- k8sClient.patch('secret', 'argocd-default-cluster-config', config.multiTenant.centralArgocdNamespace,
- [stringData: ['namespaces': merged]])
- }
- }
-
- private void generateRBAC() {
-
- log.debug("Generate RBAC permissions for ArgoCD in all managed namespaces")
-
- if (config.multiTenant.useDedicatedInstance) {
- //Generating Tenant Namespace RBACs for Tenant Argocd
- for (String ns : config.application.namespaces.tenantNamespaces) {
- new RbacDefinition(Role.Variant.ARGOCD)
- .withName("argocd")
- .withNamespace(ns)
- .withServiceAccountsFrom(
- namespace,
- ["argocd-argocd-server", "argocd-argocd-application-controller", "argocd-applicationset-controller"]
- )
- .withConfig(config)
- .withRepo(repoSetup.clusterResources.repo)
- .withSubfolder(clusterResourcesRepo.operatorRbacTenantSubfolder())
- .generate()
- }
-
- //Generating Central ArgoCD RBACs for managed namespaces
- for (String ns : config.application.namespaces.activeNamespaces) {
- log.debug("Generate RBAC permissions for centralized ArgoCD to access tenant ArgoCDs")
- new RbacDefinition(Role.Variant.ARGOCD)
- .withName('argocd-central')
- .withNamespace(ns)
- .withServiceAccountsFrom(
- config.multiTenant.centralArgocdNamespace,
- ["argocd-argocd-server", "argocd-argocd-application-controller", "argocd-applicationset-controller"]
- )
- .withConfig(config)
- .withRepo(repoSetup.clusterResources.repo)
- .withSubfolder(clusterResourcesRepo.operatorRbacSubfolder())
- .generate()
- }
- } else {
- for (String ns : config.application.namespaces.activeNamespaces) {
- new RbacDefinition(Role.Variant.ARGOCD)
- .withName("argocd")
- .withNamespace(ns)
- .withServiceAccountsFrom(
- namespace,
- ["argocd-argocd-server", "argocd-argocd-application-controller", "argocd-applicationset-controller"]
- )
- .withConfig(config)
- .withRepo(repoSetup.clusterResources.repo)
- .withSubfolder(clusterResourcesRepo.operatorRbacSubfolder())
- .generate()
- }
-
- if (config.application.clusterAdmin) {
- new RbacDefinition(Role.Variant.CLUSTER_ADMIN)
- .withName("argocd-cluster-admin")
- .withNamespace(namespace)
- .withServiceAccountsFrom(
- namespace,
- ["argocd-argocd-server", "argocd-argocd-application-controller", "argocd-applicationset-controller"]
- )
- .withConfig(config)
- .withRepo(repoSetup.clusterResources.repo)
- .withSubfolder(clusterResourcesRepo.operatorRbacSubfolder())
- .generate()
- }
- }
- }
-
- protected void createSCMCredentialsSecret() {
- log.debug("Creating repo credential secret that is used by argocd to access repos in ${config.scm.scmProviderType.toString()}")
-
- // Create secret imperatively here instead of values.yaml, because we don't want it to show in git repo
- createRepoCredentialsSecret(
- 'argocd-repo-creds-scm',
- namespace,
- gitHandler.tenant.url,
- gitHandler.tenant.credentials.username,
- gitHandler.tenant.credentials.password
- )
-
- if (config.multiTenant.useDedicatedInstance) {
- log.debug("Creating central repo credential secret that is used by argocd to access repos in ${config.scm.scmProviderType.toString()}")
-
- // Create secret imperatively here instead of values.yaml, because we don't want it to show in git repo
- createRepoCredentialsSecret(
- 'argocd-repo-creds-central-scm',
- config.multiTenant.centralArgocdNamespace,
- gitHandler.central.url,
- gitHandler.central.credentials.username,
- gitHandler.central.credentials.password
- )
- }
- }
-
- private void createRepoCredentialsSecret(String secretName, String ns, String url, String username, String password) {
- k8sClient.createSecret('generic', secretName, ns,
- new Tuple2('url', url),
- new Tuple2('username', username),
- new Tuple2('password', password)
- )
- k8sClient.label('secret', secretName, ns,
- new Tuple2('argocd.argoproj.io/secret-type', 'repo-creds'))
- }
-
- protected ArgoCDRepoSetup getRepoSetup() {
- return this.repoSetup
- }
+ private final String namespace
+ private final Config config
+ private final K8sClient k8sClient
+ private final HelmClient helmClient
+ private final FileSystemUtils fileSystemUtils
+ private final GitRepoFactory repoProvider
+ private final GitHandler gitHandler
+ private final String password
+
+ private ArgoCDRepoSetup repoSetup
+ private RepoLayout clusterResourcesRepo
+
+ ArgoCD(Config config,
+ K8sClient k8sClient,
+ HelmClient helmClient,
+ FileSystemUtils fileSystemUtils,
+ GitRepoFactory repoProvider,
+ GitHandler gitHandler) {
+ this.repoProvider = repoProvider
+ this.config = config
+ this.k8sClient = k8sClient
+ this.helmClient = helmClient
+ this.fileSystemUtils = fileSystemUtils
+ this.gitHandler = gitHandler
+ this.password = config.application.password
+ this.namespace = "${config.application.namePrefix}${config.features.argocd.namespace}"
+ }
+
+ @Override
+ boolean isEnabled() {
+ config.features.argocd.active
+ }
+
+ @Override
+ void postConfigInit(Config configToSet) {
+ // Exit early if not in operator mode or if env list is empty
+ if (!configToSet.features.argocd.operator || !configToSet.features.argocd.env) {
+ log.debug("Skipping features.argocd.env validation: operator mode is disabled or env list is empty.")
+ return
+ }
+
+ List env = configToSet.features.argocd.env as List>
+
+ log.info("Validating env list in features.argocd.env with {} entries.", env.size())
+
+ env.each { map ->
+ if (!(map instanceof Map) || !map.containsKey('name') || !map.containsKey('value')) {
+ throw new IllegalArgumentException("Each env variable in features.argocd.env must be a map with 'name' and 'value'. Invalid entry found: $map")
+ }
+ }
+
+ log.info("Env list validation for features.argocd.env completed successfully.")
+ }
+
+ @Override
+ void enable() {
+ this.repoSetup = ArgoCDRepoSetup.create(config, fileSystemUtils, repoProvider, gitHandler)
+ this.clusterResourcesRepo = repoSetup.clusterRepoLayout()
+
+ log.debug('Cloning Repositories')
+ repoSetup.initLocalRepos()
+ repoSetup.prepareClusterResourcesRepo()
+ repoSetup.commitAndPushAll('Initial Commit')
+
+ log.debug('Installing Argo CD')
+ installArgoCd()
+ }
+
+ private void installArgoCd() {
+
+ log.debug("Creating namespaces")
+ k8sClient.createNamespaces(config.application.namespaces.activeNamespaces.toList())
+
+ createSCMCredentialsSecret()
+
+ if (config.features.mail.smtpUser || config.features.mail.smtpPassword) {
+ k8sClient.createSecret('generic',
+ 'argocd-notifications-secret',
+ namespace,
+ new Tuple2('email-username', config.features.mail.smtpUser),
+ new Tuple2('email-password', config.features.mail.smtpPassword))
+ }
+
+ if (config.features.argocd.operator) {
+ generateRBAC()
+ deployWithOperator()
+ } else {
+ if (this.config.features.argocd?.values) {
+ String argocdConfigPath = clusterResourcesRepo.helmValuesFile()
+ log.debug("extend Argocd values.yaml with ${this.config.features.argocd.values}")
+ def argocdYaml = fileSystemUtils.readYaml(Path.of(argocdConfigPath))
+
+ def result = MapUtils.deepMerge(this.config.features.argocd.values, argocdYaml)
+ fileSystemUtils.writeYaml(result, new File(argocdConfigPath))
+ log.debug("Argocd values.yaml contains ${result}")
+ }
+ deployWithHelm()
+ }
+
+ if (config.multiTenant.useDedicatedInstance) {
+ //Bootstrapping dedicated instance
+ k8sClient.applyYaml(Path.of(clusterResourcesRepo.projectsDir(), "tenant.yaml").toString())
+ k8sClient.applyYaml(Path.of(clusterResourcesRepo.applicationsDir(), "bootstrap.yaml").toString())
+
+ //Bootstrapping tenant Argocd projects
+ RepoLayout tenantRepoLayout = repoSetup.tenantRepoLayout()
+ k8sClient.applyYaml(Path.of(tenantRepoLayout.projectsDir(), "argocd.yaml").toString())
+ k8sClient.applyYaml(Path.of(tenantRepoLayout.applicationsDir(), "bootstrap.yaml").toString())
+ } else {
+ // Bootstrap root application
+ k8sClient.applyYaml(Path.of(clusterResourcesRepo.projectsDir(), "argocd.yaml").toString())
+ k8sClient.applyYaml(Path.of(clusterResourcesRepo.applicationsDir(), "bootstrap.yaml").toString())
+ }
+
+ // Delete helm-argo secrets to decouple from helm.
+ // This does not delete Argo from the cluster, but you can no longer modify argo directly with helm
+ // For development keeping it in helm makes it easier (e.g. for helm uninstall).
+ k8sClient.delete('secret', namespace,
+ new Tuple2('owner', 'helm'), new Tuple2('name', 'argocd'))
+ }
+
+ private void deployWithOperator() {
+ // Apply argocd yaml from operator folder
+ String argocdConfigPath = clusterResourcesRepo.operatorConfigFile()
+ if (this.config.features.argocd?.values) {
+ log.debug("extend Argocd.yaml with ${this.config.features.argocd.values}")
+ def argocdYaml = fileSystemUtils.readYaml(Path.of(clusterResourcesRepo.operatorConfigFile()))
+
+ def result = MapUtils.deepMerge(this.config.features.argocd.values, argocdYaml)
+ fileSystemUtils.writeYaml(result, new File(argocdConfigPath))
+ log.debug("Argocd.yaml for operator contains ${result}")
+ // reload file
+ argocdConfigPath = clusterResourcesRepo.operatorConfigFile()
+ }
+ k8sClient.applyYaml(argocdConfigPath)
+
+ // ArgoCD is not installed until the ArgoCD-Operator did his job.
+ // This can take some time, so we wait for the status of the custom resource to become "Available"
+ k8sClient.waitForResourcePhase("argocd", "argocd", namespace, "Available")
+
+ log.debug("Setting new argocd admin password")
+ // Set admin password imperatively here instead of operator/argocd.yaml, because we don't want it to show in git repo
+ // The Operator uses an extra secret to store the admin Password, which is not bcrypted
+ k8sClient.patch('secret', 'argocd-cluster', namespace,
+ [stringData: ['admin.password': password]])
+
+ // In newer Versions ArgoCD Operator uses the password in argocd-cluster secret only as generated initial password
+ // but we want to set our own admin password so we set the password in both Secrets for consistency
+ String bcryptArgoCDPassword = BCrypt.hashpw(password, BCrypt.gensalt(4))
+ k8sClient.patch('secret', 'argocd-secret', namespace,
+ [stringData: ['admin.password': bcryptArgoCDPassword]])
+
+ updatingArgoCDManagedNamespaces()
+
+ log.debug("Apply RBAC permissions for ArgoCD in all managed namespaces imperatively")
+ // Apply rbac yamls from operator/rbac folder
+ String argocdRbacPath = clusterResourcesRepo.operatorRbacDir()
+ k8sClient.applyYaml("${argocdRbacPath} --recursive")
+ }
+
+ private void deployWithHelm() {
+
+ // Install umbrella chart from argocd/argocd
+ String umbrellaChartPath = clusterResourcesRepo.helmDir()
+ // Even if the Chart.lock already contains the repo, we need to add it before resolving it
+ // See https://github.com/helm/helm/issues/8036#issuecomment-872502901
+ List helmDependencies = fileSystemUtils.readYaml(Path.of(clusterResourcesRepo.chartYaml()))['dependencies']
+ helmClient.addRepo('argo', helmDependencies[0]['repository'] as String)
+ helmClient.dependencyBuild(umbrellaChartPath)
+ helmClient.upgrade('argocd', umbrellaChartPath, [namespace: namespace])
+
+ log.debug("Setting new argocd admin password")
+ // Set admin password imperatively here instead of values.yaml, because we don't want it to show in git repo
+ String bcryptArgoCDPassword = BCrypt.hashpw(password, BCrypt.gensalt(4))
+ k8sClient.patch('secret', 'argocd-secret', namespace,
+ [stringData: ['admin.password': bcryptArgoCDPassword]])
+
+ }
+
+ // The ArgoCD instance installed via an operator only manages its deployment namespace.
+ // To manage additional namespaces, we need to update the 'argocd-default-cluster-config' secret with all managed namespaces.
+ void updatingArgoCDManagedNamespaces() {
+
+ log.debug("Updating managed namespaces in ArgoCD configuration secret.")
+ def namespaceList = !config.multiTenant.useDedicatedInstance ? config.application.namespaces.activeNamespaces : config.application.namespaces.tenantNamespaces
+
+ k8sClient.patch('secret', 'argocd-default-cluster-config', namespace,
+ [stringData: ['namespaces': namespaceList.join(',')]])
+
+ if (config.multiTenant.useDedicatedInstance) {
+ // Append new namespaces to existing ones from the secret.
+ // `kubectl patch` can't merge list subfields, so we read, decode, merge, and update the secret.
+ // This ensures all centrally managed namespaces are preserved.
+ String base64Namespaces = k8sClient.getArgoCDNamespacesSecret('argocd-default-cluster-config', config.multiTenant.centralArgocdNamespace)
+ byte[] decodedBytes = Base64.decoder.decode(base64Namespaces)
+ String decoded = new String(decodedBytes, "UTF-8")
+ def decodedList = decoded?.split(',') as List ?: []
+ def activeList = config.application.namespaces.activeNamespaces?.flatten() as List ?: []
+ def merged = (decodedList + activeList).unique().join(',')
+ log.debug("Updating Central Argocd 'argocd-default-cluster-config' secret")
+ k8sClient.patch('secret', 'argocd-default-cluster-config', config.multiTenant.centralArgocdNamespace,
+ [stringData: ['namespaces': merged]])
+ }
+ }
+
+ private void generateRBAC() {
+
+ log.debug("Generate RBAC permissions for ArgoCD in all managed namespaces")
+
+ if (config.multiTenant.useDedicatedInstance) {
+ //Generating Tenant Namespace RBACs for Tenant Argocd
+ for (String ns : config.application.namespaces.tenantNamespaces) {
+ new RbacDefinition(Role.Variant.ARGOCD)
+ .withName("argocd")
+ .withNamespace(ns)
+ .withServiceAccountsFrom(namespace,
+ ["argocd-argocd-server", "argocd-argocd-application-controller", "argocd-applicationset-controller"])
+ .withConfig(config)
+ .withRepo(repoSetup.clusterResources.repo)
+ .withSubfolder(clusterResourcesRepo.operatorRbacTenantSubfolder())
+ .generate()
+ }
+
+ //Generating Central ArgoCD RBACs for managed namespaces
+ for (String ns : config.application.namespaces.activeNamespaces) {
+ log.debug("Generate RBAC permissions for centralized ArgoCD to access tenant ArgoCDs")
+ new RbacDefinition(Role.Variant.ARGOCD)
+ .withName('argocd-central')
+ .withNamespace(ns)
+ .withServiceAccountsFrom(config.multiTenant.centralArgocdNamespace,
+ ["argocd-argocd-server", "argocd-argocd-application-controller", "argocd-applicationset-controller"])
+ .withConfig(config)
+ .withRepo(repoSetup.clusterResources.repo)
+ .withSubfolder(clusterResourcesRepo.operatorRbacSubfolder())
+ .generate()
+ }
+ } else {
+ for (String ns : config.application.namespaces.activeNamespaces) {
+ new RbacDefinition(Role.Variant.ARGOCD)
+ .withName("argocd")
+ .withNamespace(ns)
+ .withServiceAccountsFrom(namespace,
+ ["argocd-argocd-server", "argocd-argocd-application-controller", "argocd-applicationset-controller"])
+ .withConfig(config)
+ .withRepo(repoSetup.clusterResources.repo)
+ .withSubfolder(clusterResourcesRepo.operatorRbacSubfolder())
+ .generate()
+ }
+
+ if (config.application.clusterAdmin) {
+ new RbacDefinition(Role.Variant.CLUSTER_ADMIN)
+ .withName("argocd-cluster-admin")
+ .withNamespace(namespace)
+ .withServiceAccountsFrom(namespace,
+ ["argocd-argocd-server", "argocd-argocd-application-controller", "argocd-applicationset-controller"])
+ .withConfig(config)
+ .withRepo(repoSetup.clusterResources.repo)
+ .withSubfolder(clusterResourcesRepo.operatorRbacSubfolder())
+ .generate()
+ }
+ }
+ }
+
+ protected void createSCMCredentialsSecret() {
+ log.debug("Creating repo credential secret that is used by argocd to access repos in ${config.scm.scmProviderType.toString()}")
+
+ // Create secret imperatively here instead of values.yaml, because we don't want it to show in git repo
+ createRepoCredentialsSecret('argocd-repo-creds-scm',
+ namespace,
+ gitHandler.tenant.url,
+ gitHandler.tenant.credentials.username,
+ gitHandler.tenant.credentials.password)
+
+ if (config.multiTenant.useDedicatedInstance) {
+ log.debug("Creating central repo credential secret that is used by argocd to access repos in ${config.scm.scmProviderType.toString()}")
+
+ // Create secret imperatively here instead of values.yaml, because we don't want it to show in git repo
+ createRepoCredentialsSecret('argocd-repo-creds-central-scm',
+ config.multiTenant.centralArgocdNamespace,
+ gitHandler.central.url,
+ gitHandler.central.credentials.username,
+ gitHandler.central.credentials.password)
+ }
+ }
+
+ private void createRepoCredentialsSecret(String secretName, String ns, String url, String username, String password) {
+ k8sClient.createSecret('generic', secretName, ns,
+ new Tuple2('url', url),
+ new Tuple2('username', username),
+ new Tuple2('password', password))
+ k8sClient.label('secret', secretName, ns,
+ new Tuple2('argocd.argoproj.io/secret-type', 'repo-creds'))
+ }
+
+ protected ArgoCDRepoSetup getRepoSetup() {
+ return this.repoSetup
+ }
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/features/argocd/ArgoCDRepoSetup.groovy b/src/main/groovy/com/cloudogu/gitops/features/argocd/ArgoCDRepoSetup.groovy
index 5d013a467..d4f5a92fb 100644
--- a/src/main/groovy/com/cloudogu/gitops/features/argocd/ArgoCDRepoSetup.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/features/argocd/ArgoCDRepoSetup.groovy
@@ -5,160 +5,153 @@ import com.cloudogu.gitops.features.git.GitHandler
import com.cloudogu.gitops.git.GitRepoFactory
import com.cloudogu.gitops.git.providers.GitProvider
import com.cloudogu.gitops.utils.FileSystemUtils
-import groovy.util.logging.Slf4j
import java.nio.file.Path
+import groovy.util.logging.Slf4j
/**
* Holds ArgoCD-related repo initialization actions (cluster-resources + optional tenant bootstrap)
- * and encapsulates the initialization logic (single-instance vs. dedicated instance).
- */
+ * and encapsulates the initialization logic (single-instance vs. dedicated instance).*/
@Slf4j
class ArgoCDRepoSetup {
- final RepoInitializationAction clusterResources
- final RepoInitializationAction tenantBootstrap // may be null
- final List allRepos
-
- private final Config config
- private final FileSystemUtils fileSystemUtils
-
- private ArgoCDRepoSetup(Config config,
- FileSystemUtils fileSystemUtils,
- RepoInitializationAction clusterResources,
- RepoInitializationAction tenantBootstrap,
- List allRepos) {
- this.config = config
- this.fileSystemUtils = fileSystemUtils
- this.clusterResources = clusterResources
- this.tenantBootstrap = tenantBootstrap
- this.allRepos = allRepos
- }
-
- static ArgoCDRepoSetup create(Config config, FileSystemUtils fileSystemUtils, GitRepoFactory repoFactory, GitHandler gitHandler) {
- RepoInitializationAction cluster
- RepoInitializationAction tenant
- List all = []
-
- if (config.multiTenant.useDedicatedInstance) {
- // Dedicated instance: tenant bootstrap (tenant provider) + cluster-resources (central provider)
- tenant = createRepoInitializationAction(config, repoFactory, gitHandler,
- 'argocd/cluster-resources/apps/argocd/multiTenant/tenant',
- 'argocd/cluster-resources',
- gitHandler.tenant
- )
- all.add(tenant)
-
- cluster = createRepoInitializationAction(config, repoFactory, gitHandler,
- 'argocd/cluster-resources',
- 'argocd/cluster-resources',
- gitHandler.central
- )
- all.add(cluster)
-
- } else {
- // Single instance: only cluster-resources (tenant provider)
- cluster = createRepoInitializationAction(config, repoFactory, gitHandler,
- 'argocd/cluster-resources',
- 'argocd/cluster-resources',
- gitHandler.tenant
- )
- all.add(cluster)
- }
-
- // Configure which subdirectories should be copied into the cluster-resources repo
- cluster.subDirsToCopy = determineClusterResourceSubDirs(config)
-
- return new ArgoCDRepoSetup(config, fileSystemUtils, cluster, tenant, all)
- }
-
- RepoLayout clusterRepoLayout() {
- new RepoLayout(clusterResources.repo.getAbsoluteLocalRepoTmpDir())
- }
-
- RepoLayout tenantRepoLayout() {
- if (tenantBootstrap == null) {
- throw new IllegalStateException("tenantBootstrap repo is not initialized (single-instance mode).")
- }
- new RepoLayout(tenantBootstrap.repo.getAbsoluteLocalRepoTmpDir())
- }
-
- void initLocalRepos() {
- allRepos.each { it.initLocalRepo() }
- }
-
- void prepareClusterResourcesRepo() {
- RepoLayout layout = clusterRepoLayout()
-
- if (config.features.argocd.operator) {
- fileSystemUtils.deleteDir(layout.helmDir())
- } else {
- fileSystemUtils.deleteDir(layout.operatorDir())
- }
-
- if (config.multiTenant.useDedicatedInstance) {
- log.debug("Deleting unnecessary non dedicated instances folders from argocd repo: applications=${clusterRepoLayout().applicationsDir()}, projects=${clusterRepoLayout().projectsDir()}, tenant=${clusterRepoLayout().multiTenantDir()}/tenant")
- FileSystemUtils.deleteDir clusterRepoLayout().applicationsDir()
- FileSystemUtils.deleteDir clusterRepoLayout().projectsDir()
- fileSystemUtils.moveDirectoryMergeOverwrite(Path.of(clusterRepoLayout().multiTenantDir() + "/central"), Path.of(clusterRepoLayout().argocdRoot()))
- FileSystemUtils.deleteDir clusterRepoLayout().multiTenantDir()
- } else {
- fileSystemUtils.deleteDir(layout.multiTenantDir())
- }
-
- if (!config.application.netpols) {
- fileSystemUtils.deleteFile(layout.netpolFile())
- }
- }
-
- void commitAndPushAll(String message) {
- allRepos.each { it.repo.commitAndPush(message) }
- }
-
- private static Set determineClusterResourceSubDirs(Config config) {
- Set clusterResourceSubDirs = new LinkedHashSet<>()
-
- clusterResourceSubDirs.add(RepoLayout.argocdSubdirRel())
-
- if (config.features.certManager.active) {
- clusterResourceSubDirs.add(RepoLayout.certManagerSubdirRel())
- }
- if (config.features.ingress.active) {
- clusterResourceSubDirs.add(RepoLayout.ingressSubdirRel())
- }
- if (config.jenkins.active) {
- clusterResourceSubDirs.add(RepoLayout.jenkinsSubdirRel())
- }
- if (config.features.mail.active) {
- clusterResourceSubDirs.add(RepoLayout.mailhogSubdirRel())
- }
- if (config.features.monitoring.active) {
- clusterResourceSubDirs.add(RepoLayout.monitoringSubdirRel())
- }
- if (config.scm.scmManager?.url) {
- clusterResourceSubDirs.add(RepoLayout.scmManagerSubdirRel())
- }
- if (config.features.secrets.active) {
- clusterResourceSubDirs.add(RepoLayout.secretsSubdirRel())
- clusterResourceSubDirs.add(RepoLayout.vaultSubdirRel())
- }
-
- return clusterResourceSubDirs
- }
-
- private static RepoInitializationAction createRepoInitializationAction(
- Config config,
- GitRepoFactory repoFactory,
- GitHandler gitHandler,
- String localSrcDir,
- String scmRepoTarget,
- GitProvider gitProvider
- ) {
- new RepoInitializationAction(
- config,
- repoFactory.getRepo(scmRepoTarget, gitProvider),
- gitHandler,
- localSrcDir
- )
- }
-}
+ final RepoInitializationAction clusterResources
+ final RepoInitializationAction tenantBootstrap
+ // may be null
+ final List allRepos
+
+ private final Config config
+ private final FileSystemUtils fileSystemUtils
+
+ private ArgoCDRepoSetup(Config config,
+ FileSystemUtils fileSystemUtils,
+ RepoInitializationAction clusterResources,
+ RepoInitializationAction tenantBootstrap,
+ List allRepos) {
+ this.config = config
+ this.fileSystemUtils = fileSystemUtils
+ this.clusterResources = clusterResources
+ this.tenantBootstrap = tenantBootstrap
+ this.allRepos = allRepos
+ }
+
+ static ArgoCDRepoSetup create(Config config, FileSystemUtils fileSystemUtils, GitRepoFactory repoFactory, GitHandler gitHandler) {
+ RepoInitializationAction cluster
+ RepoInitializationAction tenant
+ List all = []
+
+ if (config.multiTenant.useDedicatedInstance) {
+ // Dedicated instance: tenant bootstrap (tenant provider) + cluster-resources (central provider)
+ tenant = createRepoInitializationAction(config, repoFactory, gitHandler,
+ 'argocd/cluster-resources/apps/argocd/multiTenant/tenant',
+ 'argocd/cluster-resources',
+ gitHandler.tenant)
+ all.add(tenant)
+
+ cluster = createRepoInitializationAction(config, repoFactory, gitHandler,
+ 'argocd/cluster-resources',
+ 'argocd/cluster-resources',
+ gitHandler.central)
+ all.add(cluster)
+
+ } else {
+ // Single instance: only cluster-resources (tenant provider)
+ cluster = createRepoInitializationAction(config, repoFactory, gitHandler,
+ 'argocd/cluster-resources',
+ 'argocd/cluster-resources',
+ gitHandler.tenant)
+ all.add(cluster)
+ }
+
+ // Configure which subdirectories should be copied into the cluster-resources repo
+ cluster.subDirsToCopy = determineClusterResourceSubDirs(config)
+
+ return new ArgoCDRepoSetup(config, fileSystemUtils, cluster, tenant, all)
+ }
+
+ RepoLayout clusterRepoLayout() {
+ new RepoLayout(clusterResources.repo.getAbsoluteLocalRepoTmpDir())
+ }
+
+ RepoLayout tenantRepoLayout() {
+ if (tenantBootstrap == null) {
+ throw new IllegalStateException("tenantBootstrap repo is not initialized (single-instance mode).")
+ }
+ new RepoLayout(tenantBootstrap.repo.getAbsoluteLocalRepoTmpDir())
+ }
+
+ void initLocalRepos() {
+ allRepos.each { it.initLocalRepo() }
+ }
+
+ void prepareClusterResourcesRepo() {
+ RepoLayout layout = clusterRepoLayout()
+
+ if (config.features.argocd.operator) {
+ fileSystemUtils.deleteDir(layout.helmDir())
+ } else {
+ fileSystemUtils.deleteDir(layout.operatorDir())
+ }
+
+ if (config.multiTenant.useDedicatedInstance) {
+ log.debug("Deleting unnecessary non dedicated instances folders from argocd repo: applications=${clusterRepoLayout().applicationsDir()}, projects=${clusterRepoLayout().projectsDir()}, tenant=${clusterRepoLayout().multiTenantDir()}/tenant")
+ FileSystemUtils.deleteDir clusterRepoLayout().applicationsDir()
+ FileSystemUtils.deleteDir clusterRepoLayout().projectsDir()
+ fileSystemUtils.moveDirectoryMergeOverwrite(Path.of(clusterRepoLayout().multiTenantDir() + "/central"), Path.of(clusterRepoLayout().argocdRoot()))
+ FileSystemUtils.deleteDir clusterRepoLayout().multiTenantDir()
+ } else {
+ fileSystemUtils.deleteDir(layout.multiTenantDir())
+ }
+
+ if (!config.application.netpols) {
+ fileSystemUtils.deleteFile(layout.netpolFile())
+ }
+ }
+
+ void commitAndPushAll(String message) {
+ allRepos.each { it.repo.commitAndPush(message) }
+ }
+
+ private static Set determineClusterResourceSubDirs(Config config) {
+ Set clusterResourceSubDirs = new LinkedHashSet<>()
+
+ clusterResourceSubDirs.add(RepoLayout.argocdSubdirRel())
+
+ if (config.features.certManager.active) {
+ clusterResourceSubDirs.add(RepoLayout.certManagerSubdirRel())
+ }
+ if (config.features.ingress.active) {
+ clusterResourceSubDirs.add(RepoLayout.ingressSubdirRel())
+ }
+ if (config.jenkins.active) {
+ clusterResourceSubDirs.add(RepoLayout.jenkinsSubdirRel())
+ }
+ if (config.features.mail.active) {
+ clusterResourceSubDirs.add(RepoLayout.mailhogSubdirRel())
+ }
+ if (config.features.monitoring.active) {
+ clusterResourceSubDirs.add(RepoLayout.monitoringSubdirRel())
+ }
+ if (config.scm.scmManager?.url) {
+ clusterResourceSubDirs.add(RepoLayout.scmManagerSubdirRel())
+ }
+ if (config.features.secrets.active) {
+ clusterResourceSubDirs.add(RepoLayout.secretsSubdirRel())
+ clusterResourceSubDirs.add(RepoLayout.vaultSubdirRel())
+ }
+
+ return clusterResourceSubDirs
+ }
+
+ private static RepoInitializationAction createRepoInitializationAction(Config config,
+ GitRepoFactory repoFactory,
+ GitHandler gitHandler,
+ String localSrcDir,
+ String scmRepoTarget,
+ GitProvider gitProvider) {
+ new RepoInitializationAction(config,
+ repoFactory.getRepo(scmRepoTarget, gitProvider),
+ gitHandler,
+ localSrcDir)
+ }
+}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/features/argocd/RepoInitializationAction.groovy b/src/main/groovy/com/cloudogu/gitops/features/argocd/RepoInitializationAction.groovy
index 39c4cf4ef..a61971f67 100644
--- a/src/main/groovy/com/cloudogu/gitops/features/argocd/RepoInitializationAction.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/features/argocd/RepoInitializationAction.groovy
@@ -3,135 +3,125 @@ package com.cloudogu.gitops.features.argocd
import com.cloudogu.gitops.config.Config
import com.cloudogu.gitops.features.git.GitHandler
import com.cloudogu.gitops.git.GitRepo
-import freemarker.template.DefaultObjectWrapperBuilder
+
import groovy.util.logging.Slf4j
+import freemarker.template.DefaultObjectWrapperBuilder
+
@Slf4j
class RepoInitializationAction {
- private GitRepo repo
- private String copyFromDirectory
- Set subDirsToCopy = [] as Set
- private Config config
- private GitHandler gitHandler
-
- RepoInitializationAction(Config config, GitRepo repo,GitHandler gitHandler, String copyFromDirectory) {
- this.config = config
- this.repo = repo
- this.copyFromDirectory = copyFromDirectory
- this.gitHandler = gitHandler
- }
-
- /**
- * Clone repo from SCM and initialize it by copying only the configured subdirectories.
- * Afterwards we can edit these files.
- */
- void initLocalRepo() {
- repo.cloneRepo()
-
- log.debug("Initializing repo ${repo.repoTarget} from ${copyFromDirectory} with subdirs: ${subDirsToCopy}")
- repo.copyDirectoryContents(copyFromDirectory, createSubdirFilter())
- replaceTemplates()
- }
-
- void replaceTemplates() {
- Map templateModel = buildTemplateValues(config)
- repo.replaceTemplates(templateModel)
- }
-
- GitRepo getRepo() {
- return repo
- }
-
- private Map buildTemplateValues(Config config) {
- def model = [
- tenantName: config.application.tenantName,
- argocd : [host: config.features.argocd.url ? new URL(config.features.argocd.url).host : ""],
- scm : [
- baseUrl : this.repo.gitProvider.url,
- host : this.repo.gitProvider.host,
- protocol: this.repo.gitProvider.protocol,
- repoUrl : this.repo.gitProvider.repoPrefix(),
- centralScmUrl: this.gitHandler.central?.repoPrefix() ?: ''
- ],
- config : config,
- // Allow for using static classes inside the templates
- statics : new DefaultObjectWrapperBuilder(freemarker.template.Configuration.VERSION_2_3_32).build().getStaticModels()
- ] as Map
-
- return model
- }
-
- private FileFilter createSubdirFilter() {
- if (!subDirsToCopy || subDirsToCopy.isEmpty()) {
- return { File f -> true } as FileFilter
- }
-
- File srcRoot = new File(copyFromDirectory).canonicalFile
-
- // Normalize entries like "argocd", "apps/monitoring" to "argocd/" or "apps/monitoring/"
- Set prefixes = subDirsToCopy.collect { String s ->
- def norm = s.replace('\\', '/')
- norm = norm.replaceAll('^/+', '').replaceAll('/+$', '')
- return norm + '/'
- } as Set
-
- boolean hasPrefixes = !prefixes.isEmpty()
-
- // Templates that MUST be copied (chart templates), even though they match the global templates-exclude
- Set templateIncludePrefixes = [
- 'apps/argocd/argocd/templates/'
- ] as Set
-
- return { File f ->
- File canon = f.canonicalFile
- String rel = srcRoot.toURI().relativize(canon.toURI()).toString()
- rel = rel.replace('\\', '/')
-
- // Always copy the root (copyFromDirectory itself), otherwise we can't build up the directory structure
- if (rel == '' || rel == '.') {
- return true
- }
-
- boolean isDir = f.isDirectory()
- // For directories, always compare using a trailing slash
- String relDir = rel.endsWith('/') ? rel : rel + '/'
-
- // --- Exception: keep required chart templates (e.g., ArgoCD chart templates) ---
- // If the current path is inside an explicitly allowed templates subtree, always allow it.
- if (templateIncludePrefixes.any { String p ->
- (isDir ? relDir : rel).startsWith(p)
- }) {
- return true
- }
-
- // --- Global excludes for feature templates ---
- // do NOT copy anything under apps/**/templates/** into the SCM repo
- if (rel.startsWith('apps/') && relDir.contains('/templates/')) {
- return false
- }
-
- // If no prefixes are configured, copy everything (except templates)
- if (!hasPrefixes) {
- return true
- }
-
- if (isDir) {
- // Allow a directory if it is:
- // - exactly one of the requested subdirs, or
- // - inside one of them, or
- // - a parent of one of them (needed to keep the tree structure).
- return prefixes.any { String p ->
- relDir == p || relDir.startsWith(p) || p.startsWith(relDir)
- }
- } else {
- // Only copy files that are directly under one of the allowed subtrees
- return prefixes.any { String p ->
- rel.startsWith(p)
- }
- }
- } as FileFilter
- }
-
-
+ private GitRepo repo
+ private String copyFromDirectory
+ Set subDirsToCopy = [] as Set
+ private Config config
+ private GitHandler gitHandler
+
+ RepoInitializationAction(Config config, GitRepo repo, GitHandler gitHandler, String copyFromDirectory) {
+ this.config = config
+ this.repo = repo
+ this.copyFromDirectory = copyFromDirectory
+ this.gitHandler = gitHandler
+ }
+
+ /**
+ * Clone repo from SCM and initialize it by copying only the configured subdirectories.
+ * Afterwards we can edit these files.*/
+ void initLocalRepo() {
+ repo.cloneRepo()
+
+ log.debug("Initializing repo ${repo.repoTarget} from ${copyFromDirectory} with subdirs: ${subDirsToCopy}")
+ repo.copyDirectoryContents(copyFromDirectory, createSubdirFilter())
+ replaceTemplates()
+ }
+
+ void replaceTemplates() {
+ Map templateModel = buildTemplateValues(config)
+ repo.replaceTemplates(templateModel)
+ }
+
+ GitRepo getRepo() {
+ return repo
+ }
+
+ private Map buildTemplateValues(Config config) {
+ def model = [tenantName: config.application.tenantName,
+ argocd : [host: config.features.argocd.url ? new URL(config.features.argocd.url).host : ""],
+ scm : [baseUrl : this.repo.gitProvider.url,
+ host : this.repo.gitProvider.host,
+ protocol : this.repo.gitProvider.protocol,
+ repoUrl : this.repo.gitProvider.repoPrefix(),
+ centralScmUrl: this.gitHandler.central?.repoPrefix() ?: ''],
+ config : config,
+ // Allow for using static classes inside the templates
+ statics : new DefaultObjectWrapperBuilder(freemarker.template.Configuration.VERSION_2_3_32).build().getStaticModels()] as Map
+
+ return model
+ }
+
+ private FileFilter createSubdirFilter() {
+ if (!subDirsToCopy || subDirsToCopy.isEmpty()) {
+ return { File f -> true } as FileFilter
+ }
+
+ File srcRoot = new File(copyFromDirectory).canonicalFile
+
+ // Normalize entries like "argocd", "apps/monitoring" to "argocd/" or "apps/monitoring/"
+ Set prefixes = subDirsToCopy.collect { String s ->
+ def norm = s.replace('\\', '/')
+ norm = norm.replaceAll('^/+', '').replaceAll('/+$', '')
+ return norm + '/'
+ } as Set
+
+ boolean hasPrefixes = !prefixes.isEmpty()
+
+ // Templates that MUST be copied (chart templates), even though they match the global templates-exclude
+ Set templateIncludePrefixes = ['apps/argocd/argocd/templates/'] as Set
+
+ return { File f ->
+ File canon = f.canonicalFile
+ String rel = srcRoot.toURI().relativize(canon.toURI()).toString()
+ rel = rel.replace('\\', '/')
+
+ // Always copy the root (copyFromDirectory itself), otherwise we can't build up the directory structure
+ if (rel == '' || rel == '.') {
+ return true
+ }
+
+ boolean isDir = f.isDirectory()
+ // For directories, always compare using a trailing slash
+ String relDir = rel.endsWith('/') ? rel : rel + '/'
+
+ // --- Exception: keep required chart templates (e.g., ArgoCD chart templates) ---
+ // If the current path is inside an explicitly allowed templates subtree, always allow it.
+ if (templateIncludePrefixes.any { String p -> (isDir ? relDir : rel).startsWith(p)
+ }) {
+ return true
+ }
+
+ // --- Global excludes for feature templates ---
+ // do NOT copy anything under apps/**/templates/** into the SCM repo
+ if (rel.startsWith('apps/') && relDir.contains('/templates/')) {
+ return false
+ }
+
+ // If no prefixes are configured, copy everything (except templates)
+ if (!hasPrefixes) {
+ return true
+ }
+
+ if (isDir) {
+ // Allow a directory if it is:
+ // - exactly one of the requested subdirs, or
+ // - inside one of them, or
+ // - a parent of one of them (needed to keep the tree structure).
+ return prefixes.any { String p -> relDir == p || relDir.startsWith(p) || p.startsWith(relDir)
+ }
+ } else {
+ // Only copy files that are directly under one of the allowed subtrees
+ return prefixes.any { String p -> rel.startsWith(p)
+ }
+ }
+ } as FileFilter
+ }
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/features/argocd/RepoLayout.groovy b/src/main/groovy/com/cloudogu/gitops/features/argocd/RepoLayout.groovy
index f258c89d0..edf1253d6 100644
--- a/src/main/groovy/com/cloudogu/gitops/features/argocd/RepoLayout.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/features/argocd/RepoLayout.groovy
@@ -3,130 +3,135 @@ package com.cloudogu.gitops.features.argocd
import java.nio.file.Path
class RepoLayout {
- private static final String APPS_MONITORING_DIR = 'apps/monitoring'
- private static final String APPS_SECRETS_DIR = 'apps/external-secrets'
- private static final String APPS_VAULT_DIR = 'apps/vault'
- private static final String APPS_CERTMANAGER_DIR = 'apps/cert-manager'
- private static final String APPS_JENKINS_DIR = 'apps/jenkins'
- private static final String APPS_INGRESS_DIR = 'apps/ingress'
- private static final String APPS_MAILHOG_DIR = 'apps/mail'
- private static final String APPS_SCMMANAGER_DIR = 'apps/scm-manager'
- private static final String APPS_ARGOCD_DIR = 'apps/argocd'
-
- private static final String OPERATOR_DIR = 'operator'
- private static final String MULTITENANT_DIR = 'multiTenant'
- private static final String APPLICATIONS_DIR = 'applications'
- private static final String PROJECTS_DIR = 'projects'
- private static final String HELM_DIR = 'argocd' // argocd/argocd
- private static final String NETPOL_YAML = 'templates/allow-namespaces.yaml'
-
- private final String repoRootDir
-
- RepoLayout(String repoRootDir) {
- this.repoRootDir = repoRootDir
- }
-
- String rootDir() {
- repoRootDir
- }
-
- String argocdRoot() {
- Path.of(repoRootDir, APPS_ARGOCD_DIR).toString()
- }
-
- // --- folder ---
-
- String operatorDir() {
- Path.of(argocdRoot(), OPERATOR_DIR).toString()
- }
-
- String operatorRbacDir() {
- // "cluster-resources/apps/argocd/operator/rbac"
- Path.of(operatorDir(), "rbac").toString()
- }
-
- String operatorConfigFile() {
- // "cluster-resources/apps/argocd/operator/argocd.yaml"
- Path.of(operatorDir(), "argocd.yaml").toString()
- }
-
- String multiTenantDir() {
- Path.of(argocdRoot(), MULTITENANT_DIR).toString()
- }
-
- String applicationsDir() {
- Path.of(argocdRoot(), APPLICATIONS_DIR).toString()
- }
-
- String projectsDir() {
- Path.of(argocdRoot(), PROJECTS_DIR).toString()
- }
-
- String helmDir() {
- Path.of(argocdRoot(), HELM_DIR).toString()
- }
-
- String helmValuesFile() {
- // "cluster-resources/apps/argocd/argocd/values.yaml"
- Path.of(helmDir(), "values.yaml").toString()
- }
-
- String chartYaml() {
- Path.of(helmDir(), "Chart.yaml").toString()
- }
-
- String netpolFile() {
- Path.of(helmDir(), NETPOL_YAML).toString()
- }
-
- String monitoringDir() {
- Path.of(repoRootDir, APPS_MONITORING_DIR).toString()
- }
-
- String vaultDir() {
- Path.of(repoRootDir, APPS_VAULT_DIR).toString()
- }
-
- static String monitoringSubdirRel() {
- APPS_MONITORING_DIR
- }
-
- static String secretsSubdirRel() {
- APPS_SECRETS_DIR
- }
- static String vaultSubdirRel() {
- APPS_VAULT_DIR
- }
-
- static String certManagerSubdirRel() {
- APPS_CERTMANAGER_DIR
- }
-
- static String jenkinsSubdirRel() {
- APPS_JENKINS_DIR
- }
-
- static String ingressSubdirRel() {
- APPS_INGRESS_DIR
- }
- static String mailhogSubdirRel() {
- APPS_MAILHOG_DIR
- }
- static String scmManagerSubdirRel() {
- APPS_SCMMANAGER_DIR
- }
- static String argocdSubdirRel() {
- APPS_ARGOCD_DIR
- }
-
- // --- relative subfolders for RBAC (passed to RbacDefinition.withSubfolder) ---
- static String operatorRbacSubfolder() {
- // "argocd/operator/rbac"
- "${APPS_ARGOCD_DIR}/${OPERATOR_DIR}/rbac"
- }
-
- static String operatorRbacTenantSubfolder() {
- // "argocd/operator/rbac/tenant"
- "${operatorRbacSubfolder()}/tenant"
- }
-}
+ private static final String APPS_MONITORING_DIR = 'apps/monitoring'
+ private static final String APPS_SECRETS_DIR = 'apps/external-secrets'
+ private static final String APPS_VAULT_DIR = 'apps/vault'
+ private static final String APPS_CERTMANAGER_DIR = 'apps/cert-manager'
+ private static final String APPS_JENKINS_DIR = 'apps/jenkins'
+ private static final String APPS_INGRESS_DIR = 'apps/ingress'
+ private static final String APPS_MAILHOG_DIR = 'apps/mail'
+ private static final String APPS_SCMMANAGER_DIR = 'apps/scm-manager'
+ private static final String APPS_ARGOCD_DIR = 'apps/argocd'
+
+ private static final String OPERATOR_DIR = 'operator'
+ private static final String MULTITENANT_DIR = 'multiTenant'
+ private static final String APPLICATIONS_DIR = 'applications'
+ private static final String PROJECTS_DIR = 'projects'
+ private static final String HELM_DIR = 'argocd'
+ // argocd/argocd
+ private static final String NETPOL_YAML = 'templates/allow-namespaces.yaml'
+
+ private final String repoRootDir
+
+ RepoLayout(String repoRootDir) {
+ this.repoRootDir = repoRootDir
+ }
+
+ String rootDir() {
+ repoRootDir
+ }
+
+ String argocdRoot() {
+ Path.of(repoRootDir, APPS_ARGOCD_DIR).toString()
+ }
+
+ // --- folder ---
+
+ String operatorDir() {
+ Path.of(argocdRoot(), OPERATOR_DIR).toString()
+ }
+
+ String operatorRbacDir() {
+ // "cluster-resources/apps/argocd/operator/rbac"
+ Path.of(operatorDir(), "rbac").toString()
+ }
+
+ String operatorConfigFile() {
+ // "cluster-resources/apps/argocd/operator/argocd.yaml"
+ Path.of(operatorDir(), "argocd.yaml").toString()
+ }
+
+ String multiTenantDir() {
+ Path.of(argocdRoot(), MULTITENANT_DIR).toString()
+ }
+
+ String applicationsDir() {
+ Path.of(argocdRoot(), APPLICATIONS_DIR).toString()
+ }
+
+ String projectsDir() {
+ Path.of(argocdRoot(), PROJECTS_DIR).toString()
+ }
+
+ String helmDir() {
+ Path.of(argocdRoot(), HELM_DIR).toString()
+ }
+
+ String helmValuesFile() {
+ // "cluster-resources/apps/argocd/argocd/values.yaml"
+ Path.of(helmDir(), "values.yaml").toString()
+ }
+
+ String chartYaml() {
+ Path.of(helmDir(), "Chart.yaml").toString()
+ }
+
+ String netpolFile() {
+ Path.of(helmDir(), NETPOL_YAML).toString()
+ }
+
+ String monitoringDir() {
+ Path.of(repoRootDir, APPS_MONITORING_DIR).toString()
+ }
+
+ String vaultDir() {
+ Path.of(repoRootDir, APPS_VAULT_DIR).toString()
+ }
+
+ static String monitoringSubdirRel() {
+ APPS_MONITORING_DIR
+ }
+
+ static String secretsSubdirRel() {
+ APPS_SECRETS_DIR
+ }
+
+ static String vaultSubdirRel() {
+ APPS_VAULT_DIR
+ }
+
+ static String certManagerSubdirRel() {
+ APPS_CERTMANAGER_DIR
+ }
+
+ static String jenkinsSubdirRel() {
+ APPS_JENKINS_DIR
+ }
+
+ static String ingressSubdirRel() {
+ APPS_INGRESS_DIR
+ }
+
+ static String mailhogSubdirRel() {
+ APPS_MAILHOG_DIR
+ }
+
+ static String scmManagerSubdirRel() {
+ APPS_SCMMANAGER_DIR
+ }
+
+ static String argocdSubdirRel() {
+ APPS_ARGOCD_DIR
+ }
+
+ // --- relative subfolders for RBAC (passed to RbacDefinition.withSubfolder) ---
+ static String operatorRbacSubfolder() {
+ // "argocd/operator/rbac"
+ "${APPS_ARGOCD_DIR}/${OPERATOR_DIR}/rbac"
+ }
+
+ static String operatorRbacTenantSubfolder() {
+ // "argocd/operator/rbac/tenant"
+ "${operatorRbacSubfolder()}/tenant"
+ }
+}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/features/deployment/ArgoCdApplicationStrategy.groovy b/src/main/groovy/com/cloudogu/gitops/features/deployment/ArgoCdApplicationStrategy.groovy
index a86d2ab1e..06c354256 100644
--- a/src/main/groovy/com/cloudogu/gitops/features/deployment/ArgoCdApplicationStrategy.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/features/deployment/ArgoCdApplicationStrategy.groovy
@@ -5,149 +5,125 @@ import com.cloudogu.gitops.features.git.GitHandler
import com.cloudogu.gitops.git.GitRepo
import com.cloudogu.gitops.git.GitRepoFactory
import com.cloudogu.gitops.utils.FileSystemUtils
-import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator
-import com.fasterxml.jackson.dataformat.yaml.YAMLMapper
-import groovy.util.logging.Slf4j
-import jakarta.inject.Singleton
import java.nio.file.Path
+import jakarta.inject.Singleton
+import groovy.util.logging.Slf4j
+
+import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator
+import com.fasterxml.jackson.dataformat.yaml.YAMLMapper
@Singleton
@Slf4j
class ArgoCdApplicationStrategy implements DeploymentStrategy {
- private FileSystemUtils fileSystemUtils
- private Config config
- private final GitRepoFactory gitRepoProvider
-
- private GitHandler gitHandler
-
- ArgoCdApplicationStrategy(
- Config config,
- FileSystemUtils fileSystemUtils,
- GitRepoFactory gitRepoProvider,
- GitHandler gitHandler
- ) {
- this.gitRepoProvider = gitRepoProvider
- this.fileSystemUtils = fileSystemUtils
- this.config = config
- this.gitHandler = gitHandler
- }
-
- @Override
- @SuppressWarnings('GroovyGStringKey')
- // Using dynamic strings as keys seems an easy to read way to avoid more ifs
- void deployFeature(String repoURL, String repoName, String chartOrPath, String version, String namespace,
- String releaseName, Path helmValuesPath, RepoType repoType) {
- log.trace("Deploying helm chart via ArgoCD: ${releaseName}. Reading values from ${helmValuesPath}")
- def namePrefix = config.application.namePrefix
- def shallCreateNamespace = config.features['argocd']['operator'] ? "CreateNamespace=false" : "CreateNamespace=true"
-
- GitRepo clusterResourcesRepo = gitRepoProvider.getRepo('argocd/cluster-resources', this.gitHandler.resourcesScm)
- clusterResourcesRepo.cloneRepo()
-
- String project = "cluster-resources"
- String namespaceName = "${namePrefix}argocd"
- String featureName = repoName
- //DedicatedInstances
- if (config.multiTenant.useDedicatedInstance) {
- repoName = "${config.application.namePrefix}${repoName}"
- namespaceName = "${config.multiTenant.centralArgocdNamespace}"
- project = config.application.namePrefix.replaceFirst(/-$/, "")
- }
-
- // Feature-Name -> Ordner under apps/
- String featurePath = "apps/${featureName}"
-
-
- String valuesRelPath = "${featurePath}/${featureName}-gop-helm.yaml" // relative to repo-root
- // inline values from tmpHelmValues file into ArgoCD Application YAML
- def inlineValues = helmValuesPath.toFile().text
- clusterResourcesRepo.writeFile(valuesRelPath, inlineValues)
-
- //GOP should not overwrite this file
- String userValuesRelPath = "${featurePath}/${featureName}-user-values.yaml"
- clusterResourcesRepo.writeFile(userValuesRelPath, "")
-
- // 1) helm source (external chart source)
- def helmSource = [
- repoURL : repoURL,
- (chooseKeyChartOrPath(repoType)) : chartOrPath,
- targetRevision : version,
- helm : [
- releaseName: releaseName,
- valueFiles : [
- "\$values/${valuesRelPath}".toString(),
- "\$values/${userValuesRelPath}".toString()
- ],
- ignoreMissingValueFiles: true
- ]
- ]
-
- // 2) Git source for values
- // - repoURL: cluster-resources repo
- // - ref: values → used in valueFiles as $values
- // - path: apps/ → additional manifests
- def featureRepoUrl = "${clusterResourcesRepo.gitProvider.repoPrefix()}argocd/cluster-resources.git".toString()
- def gitSource = [
- repoURL : featureRepoUrl,
- targetRevision: "main",
- ref : "values",
- path : featurePath,
- directory : [recurse: true]
- ]
-
- def sources = [helmSource, gitSource]
-
- // Prepare ArgoCD Application YAML
- def yamlMapper = YAMLMapper.builder()
- .enable(YAMLGenerator.Feature.LITERAL_BLOCK_STYLE)
- .build()
-
- def yamlResult = yamlMapper.writeValueAsString([
- apiVersion: "argoproj.io/v1alpha1",
- kind : "Application",
- metadata : [
- name : repoName,
- namespace: namespaceName
- ],
- spec : [
- destination: [
- server : "https://kubernetes.default.svc",
- namespace: namespace
- ],
- project : project,
- sources : sources,
- syncPolicy : [
- automated : [
- prune : true,
- selfHeal: true
- ],
- syncOptions: [
- // So that we can apply very large resources (e.g. prometheus CRD)
- "ServerSideApply=true",
- // Create namespaces for helm charts (while not using the argocd-operater mode)
- shallCreateNamespace
- ]
- ]
- ]
- ])
-
- String appManifestPath="apps/argocd/applications/${releaseName}.yaml"
- clusterResourcesRepo.writeFile(appManifestPath, yamlResult)
-
- log.debug("Deploying helm release ${releaseName} basing on chart ${chartOrPath} from ${repoURL}, version " +
- "${version}, into namespace ${namespace}. Using Argo CD application:\n${yamlResult}")
-
- clusterResourcesRepo.commitAndPush("Added $repoName/$chartOrPath to ArgoCD")
- }
-
- String chooseKeyChartOrPath(RepoType repoType) {
- switch (repoType) {
- case RepoType.HELM: 'chart'
- break
- case RepoType.GIT: 'path'
- break
- default: throw new RuntimeException("Repo type ${repoType} not implemented for ${this.class.simpleName}")
- }
- }
+ private FileSystemUtils fileSystemUtils
+ private Config config
+ private final GitRepoFactory gitRepoProvider
+
+ private GitHandler gitHandler
+
+ ArgoCdApplicationStrategy(Config config,
+ FileSystemUtils fileSystemUtils,
+ GitRepoFactory gitRepoProvider,
+ GitHandler gitHandler) {
+ this.gitRepoProvider = gitRepoProvider
+ this.fileSystemUtils = fileSystemUtils
+ this.config = config
+ this.gitHandler = gitHandler
+ }
+
+ @Override
+ @SuppressWarnings('GroovyGStringKey')
+ // Using dynamic strings as keys seems an easy to read way to avoid more ifs
+ void deployFeature(String repoURL, String repoName, String chartOrPath, String version, String namespace,
+ String releaseName, Path helmValuesPath, RepoType repoType) {
+ log.trace("Deploying helm chart via ArgoCD: ${releaseName}. Reading values from ${helmValuesPath}")
+ def namePrefix = config.application.namePrefix
+ def shallCreateNamespace = config.features['argocd']['operator'] ? "CreateNamespace=false" : "CreateNamespace=true"
+
+ GitRepo clusterResourcesRepo = gitRepoProvider.getRepo('argocd/cluster-resources', this.gitHandler.resourcesScm)
+ clusterResourcesRepo.cloneRepo()
+
+ String project = "cluster-resources"
+ String namespaceName = "${namePrefix}argocd"
+ String featureName = repoName
+ //DedicatedInstances
+ if (config.multiTenant.useDedicatedInstance) {
+ repoName = "${config.application.namePrefix}${repoName}"
+ namespaceName = "${config.multiTenant.centralArgocdNamespace}"
+ project = config.application.namePrefix.replaceFirst(/-$/, "")
+ }
+
+ // Feature-Name -> Ordner under apps/
+ String featurePath = "apps/${featureName}"
+
+ String valuesRelPath = "${featurePath}/${featureName}-gop-helm.yaml"
+ // relative to repo-root
+ // inline values from tmpHelmValues file into ArgoCD Application YAML
+ def inlineValues = helmValuesPath.toFile().text
+ clusterResourcesRepo.writeFile(valuesRelPath, inlineValues)
+
+ //GOP should not overwrite this file
+ String userValuesRelPath = "${featurePath}/${featureName}-user-values.yaml"
+ clusterResourcesRepo.writeFile(userValuesRelPath, "")
+
+ // 1) helm source (external chart source)
+ def helmSource = [repoURL : repoURL,
+ (chooseKeyChartOrPath(repoType)): chartOrPath,
+ targetRevision : version,
+ helm : [releaseName : releaseName,
+ valueFiles : ["\$values/${valuesRelPath}".toString(),
+ "\$values/${userValuesRelPath}".toString()],
+ ignoreMissingValueFiles: true]]
+
+ // 2) Git source for values
+ // - repoURL: cluster-resources repo
+ // - ref: values → used in valueFiles as $values
+ // - path: apps/ → additional manifests
+ def featureRepoUrl = "${clusterResourcesRepo.gitProvider.repoPrefix()}argocd/cluster-resources.git".toString()
+ def gitSource = [repoURL : featureRepoUrl,
+ targetRevision: "main",
+ ref : "values",
+ path : featurePath,
+ directory : [recurse: true]]
+
+ def sources = [helmSource, gitSource]
+
+ // Prepare ArgoCD Application YAML
+ def yamlMapper = YAMLMapper.builder()
+ .enable(YAMLGenerator.Feature.LITERAL_BLOCK_STYLE)
+ .build()
+
+ def yamlResult = yamlMapper.writeValueAsString([apiVersion: "argoproj.io/v1alpha1",
+ kind : "Application",
+ metadata : [name : repoName,
+ namespace: namespaceName],
+ spec : [destination: [server : "https://kubernetes.default.svc",
+ namespace: namespace],
+ project : project,
+ sources : sources,
+ syncPolicy : [automated : [prune : true,
+ selfHeal: true],
+ syncOptions: [// So that we can apply very large resources (e.g. prometheus CRD)
+ "ServerSideApply=true",
+ // Create namespaces for helm charts (while not using the argocd-operater mode)
+ shallCreateNamespace]]]])
+
+ String appManifestPath = "apps/argocd/applications/${releaseName}.yaml"
+ clusterResourcesRepo.writeFile(appManifestPath, yamlResult)
+
+ log.debug("Deploying helm release ${releaseName} basing on chart ${chartOrPath} from ${repoURL}, version " + "${version}, into namespace ${namespace}. Using Argo CD application:\n${yamlResult}")
+
+ clusterResourcesRepo.commitAndPush("Added $repoName/$chartOrPath to ArgoCD")
+ }
+
+ String chooseKeyChartOrPath(RepoType repoType) {
+ switch (repoType) {
+ case RepoType.HELM: 'chart'
+ break
+ case RepoType.GIT: 'path'
+ break
+ default: throw new RuntimeException("Repo type ${repoType} not implemented for ${this.class.simpleName}")
+ }
+ }
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/features/deployment/Deployer.groovy b/src/main/groovy/com/cloudogu/gitops/features/deployment/Deployer.groovy
index 7d7578aeb..e5ccba8c5 100644
--- a/src/main/groovy/com/cloudogu/gitops/features/deployment/Deployer.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/features/deployment/Deployer.groovy
@@ -3,30 +3,30 @@ package com.cloudogu.gitops.features.deployment
import com.cloudogu.gitops.config.Config
import io.micronaut.context.annotation.Primary
-import jakarta.inject.Singleton
import java.nio.file.Path
+import jakarta.inject.Singleton
@Singleton
@Primary
class Deployer implements DeploymentStrategy {
- private Config config
- private ArgoCdApplicationStrategy argoCdStrategy
- private HelmStrategy helmStrategy
+ private Config config
+ private ArgoCdApplicationStrategy argoCdStrategy
+ private HelmStrategy helmStrategy
- Deployer(Config config, ArgoCdApplicationStrategy argoCdStrategy, HelmStrategy helmStrategy) {
- this.helmStrategy = helmStrategy
- this.argoCdStrategy = argoCdStrategy
- this.config = config
- }
+ Deployer(Config config, ArgoCdApplicationStrategy argoCdStrategy, HelmStrategy helmStrategy) {
+ this.helmStrategy = helmStrategy
+ this.argoCdStrategy = argoCdStrategy
+ this.config = config
+ }
- @Override
- void deployFeature(String repoURL, String repoName, String chartOrPath, String version, String namespace,
- String releaseName, Path helmValuesPath, RepoType repoType) {
- if (config.features['argocd']['active']) {
- argoCdStrategy.deployFeature(repoURL, repoName, chartOrPath, version, namespace, releaseName, helmValuesPath, repoType)
- } else {
- helmStrategy.deployFeature(repoURL, repoName, chartOrPath, version, namespace, releaseName, helmValuesPath, repoType)
- }
- }
-}
+ @Override
+ void deployFeature(String repoURL, String repoName, String chartOrPath, String version, String namespace,
+ String releaseName, Path helmValuesPath, RepoType repoType) {
+ if (config.features['argocd']['active']) {
+ argoCdStrategy.deployFeature(repoURL, repoName, chartOrPath, version, namespace, releaseName, helmValuesPath, repoType)
+ } else {
+ helmStrategy.deployFeature(repoURL, repoName, chartOrPath, version, namespace, releaseName, helmValuesPath, repoType)
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/features/deployment/DeploymentStrategy.groovy b/src/main/groovy/com/cloudogu/gitops/features/deployment/DeploymentStrategy.groovy
index d2cf3a6e4..15348b424 100644
--- a/src/main/groovy/com/cloudogu/gitops/features/deployment/DeploymentStrategy.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/features/deployment/DeploymentStrategy.groovy
@@ -3,13 +3,15 @@ package com.cloudogu.gitops.features.deployment
import java.nio.file.Path
interface DeploymentStrategy {
- void deployFeature(String repoURL, String repoName, String chartOrPath, String version, String namespace,
- String releaseName, Path helmValuesPath, RepoType repoType)
-
- default void deployFeature(String repoURL, String repoName, String chart, String version, String namespace,
- String releaseName, Path helmValuesPath) {
- deployFeature(repoURL, repoName, chart, version, namespace, releaseName, helmValuesPath, RepoType.HELM)
- }
-
- enum RepoType { HELM, GIT }
-}
+ void deployFeature(String repoURL, String repoName, String chartOrPath, String version, String namespace,
+ String releaseName, Path helmValuesPath, RepoType repoType)
+
+ default void deployFeature(String repoURL, String repoName, String chart, String version, String namespace,
+ String releaseName, Path helmValuesPath) {
+ deployFeature(repoURL, repoName, chart, version, namespace, releaseName, helmValuesPath, RepoType.HELM)
+ }
+
+ enum RepoType {
+ HELM, GIT
+ }
+}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/features/deployment/HelmStrategy.groovy b/src/main/groovy/com/cloudogu/gitops/features/deployment/HelmStrategy.groovy
index 384be0052..18ec6270f 100644
--- a/src/main/groovy/com/cloudogu/gitops/features/deployment/HelmStrategy.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/features/deployment/HelmStrategy.groovy
@@ -1,41 +1,38 @@
package com.cloudogu.gitops.features.deployment
import com.cloudogu.gitops.config.Config
-
import com.cloudogu.gitops.kubernetes.api.HelmClient
-import groovy.util.logging.Slf4j
-import jakarta.inject.Singleton
import java.nio.file.Path
+import jakarta.inject.Singleton
+import groovy.util.logging.Slf4j
@Slf4j
@Singleton
class HelmStrategy implements DeploymentStrategy {
- private HelmClient helmClient
- private Config config
-
- HelmStrategy(Config config, HelmClient helmClient) {
- this.config = config
- this.helmClient = helmClient
- }
-
- @Override
- void deployFeature(String repoURL, String repoName, String chartOrPath, String version, String namespace,
- String releaseName, Path helmValuesPath, RepoType repoType) {
-
- if (repoType == RepoType.GIT) {
- // This would be possible with plugins or by pulling the repo first, but for now, we don't need it
- throw new RuntimeException("Unable to deploy helm chart via Helm CLI from Git URL, because helm does not support this out of the box.\n" +
- "Repo URL: ${repoURL}")
- }
-
- log.debug("Imperatively deploying helm release ${releaseName} basing on chart ${chartOrPath} from ${repoURL}, " +
- "version ${version}, into namespace ${namespace}. Using values:\n${helmValuesPath.toFile().text}")
-
- helmClient.addRepo(repoName, repoURL)
- helmClient.upgrade(releaseName, "$repoName/$chartOrPath",
- [namespace: namespace,
- version : version,
- values : helmValuesPath.toString()])
- }
+ private HelmClient helmClient
+ private Config config
+
+ HelmStrategy(Config config, HelmClient helmClient) {
+ this.config = config
+ this.helmClient = helmClient
+ }
+
+ @Override
+ void deployFeature(String repoURL, String repoName, String chartOrPath, String version, String namespace,
+ String releaseName, Path helmValuesPath, RepoType repoType) {
+
+ if (repoType == RepoType.GIT) {
+ // This would be possible with plugins or by pulling the repo first, but for now, we don't need it
+ throw new RuntimeException("Unable to deploy helm chart via Helm CLI from Git URL, because helm does not support this out of the box.\n" + "Repo URL: ${repoURL}")
+ }
+
+ log.debug("Imperatively deploying helm release ${releaseName} basing on chart ${chartOrPath} from ${repoURL}, " + "version ${version}, into namespace ${namespace}. Using values:\n${helmValuesPath.toFile().text}")
+
+ helmClient.addRepo(repoName, repoURL)
+ helmClient.upgrade(releaseName, "$repoName/$chartOrPath",
+ [namespace: namespace,
+ version : version,
+ values : helmValuesPath.toString()])
+ }
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/features/git/GitHandler.groovy b/src/main/groovy/com/cloudogu/gitops/features/git/GitHandler.groovy
index 9df50be7c..d55e42818 100644
--- a/src/main/groovy/com/cloudogu/gitops/features/git/GitHandler.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/features/git/GitHandler.groovy
@@ -7,134 +7,129 @@ import com.cloudogu.gitops.features.git.config.util.ScmProviderType
import com.cloudogu.gitops.git.providers.GitProvider
import com.cloudogu.gitops.git.providers.gitlab.Gitlab
import com.cloudogu.gitops.git.providers.scmmanager.ScmManager
-import com.cloudogu.gitops.utils.FileSystemUtils
import com.cloudogu.gitops.kubernetes.api.K8sClient
+import com.cloudogu.gitops.utils.FileSystemUtils
import com.cloudogu.gitops.utils.NetworkingUtils
-import groovy.util.logging.Slf4j
+
import io.micronaut.core.annotation.Order
+
import jakarta.inject.Singleton
+import groovy.util.logging.Slf4j
@Slf4j
@Singleton
@Order(60)
class GitHandler extends Feature {
- Config config
-
- NetworkingUtils networkingUtils
- HelmStrategy helmStrategy
- FileSystemUtils fileSystemUtils
- K8sClient k8sClient
-
- GitProvider tenant
- GitProvider central
-
-
- GitHandler(Config config, HelmStrategy helmStrategy, FileSystemUtils fileSystemUtils, K8sClient k8sClient, NetworkingUtils networkingUtils) {
- this.config = config
- this.helmStrategy = helmStrategy
- this.fileSystemUtils = fileSystemUtils
- this.k8sClient = k8sClient
- this.networkingUtils = networkingUtils
- }
-
- @Override
- boolean isEnabled() {
- return true
- }
-
- void validate() {
- if (config.scm.scmManager.url) {
- config.scm.scmManager.internal = false
- config.scm.scmManager.urlForJenkins = config.scm.scmManager.url
- } else {
- log.debug("Setting configs for internal SCM-Manager")
- // We use the K8s service as default name here, because it is the only option:
- // "scmm.localhost" will not work inside the Pods and k3d-container IP + Port (e.g. 172.x.y.z:9091)
- // will not work on Windows and MacOS.
- config.scm.scmManager.urlForJenkins =
- "http://scmm.${config.application.namePrefix}scm-manager.svc.cluster.local/scm"
-
- // More internal fields are set lazily in ScmManger.groovy (after SCMM is deployed and ports are known)
- }
- config.scm.scmManager.gitOpsUsername="${config.application.namePrefix}gitops"
-
- if (config.scm.gitlab.url) {
- config.scm.scmProviderType = ScmProviderType.GITLAB
- config.scm.scmManager = null
- if (!config.scm.gitlab.password || !config.scm.gitlab.parentGroupId) {
- throw new RuntimeException('GitLab configuration incomplete: please provide both password (PAT) and parentGroupId')
- }
- }
-
-
-
- }
-
- //Retrieves the appropriate SCM for cluster resources depending on whether the environment is multi-tenant or not.
- GitProvider getResourcesScm() {
- if (central) {
- return central
- } else if (tenant) {
- return tenant
- } else {
- throw new IllegalStateException("No SCM provider found.")
- }
- }
-
- @Override
- void enable() {
- //TenantSCM
- switch (config.scm.scmProviderType) {
- case ScmProviderType.GITLAB:
- this.tenant = new Gitlab(this.config, this.config.scm.gitlab)
- break
- case ScmProviderType.SCM_MANAGER:
- def prefixedNamespace = "${config.application.namePrefix}scm-manager".toString()
- config.scm.scmManager.namespace = prefixedNamespace
- this.tenant = new ScmManager(this.config, config.scm.scmManager, helmStrategy,k8sClient, networkingUtils, true)
- // this.tenant.setup() setup will be here in future
- break
- default:
- throw new IllegalArgumentException("Unsupported SCM provider found in TenantSCM")
- }
-
- if (config.multiTenant.useDedicatedInstance) {
- switch (config.multiTenant.scmProviderType) {
- case ScmProviderType.GITLAB:
- this.central = new Gitlab(this.config, this.config.multiTenant.gitlab)
- break
- case ScmProviderType.SCM_MANAGER:
- this.central = new ScmManager(this.config, config.multiTenant.scmManager, helmStrategy,k8sClient, networkingUtils)
- break
- default:
- throw new IllegalArgumentException("Unsupported SCM-Central provider: ${config.scm.scmProviderType}")
- }
- }
-
- //can be removed if we combine argocd and cluster-resources
- final String namePrefix = (config?.application?.namePrefix ?: "").trim()
- if (this.central) {
- setupRepos(this.central, namePrefix)
- setupRepos(this.tenant, namePrefix)
- } else {
- setupRepos(this.tenant, namePrefix)
- }
- }
-
- static void setupRepos(GitProvider gitProvider, String namePrefix = "") {
- gitProvider.createRepository(
- withOrgPrefix(namePrefix, "argocd/cluster-resources"),
- "GitOps repo for basic cluster-resources"
- )
- }
-
- /**
- * Adds a prefix to the group/namespace part (before the first '/'):
- * Example: "argocd/argocd" + "foo-" => "foo-argocd/argocd"
- */
- static String withOrgPrefix(String prefix, String repoPath) {
- if (!prefix) return repoPath
- return prefix + repoPath
- }
+ Config config
+
+ NetworkingUtils networkingUtils
+ HelmStrategy helmStrategy
+ FileSystemUtils fileSystemUtils
+ K8sClient k8sClient
+
+ GitProvider tenant
+ GitProvider central
+
+ GitHandler(Config config, HelmStrategy helmStrategy, FileSystemUtils fileSystemUtils, K8sClient k8sClient, NetworkingUtils networkingUtils) {
+ this.config = config
+ this.helmStrategy = helmStrategy
+ this.fileSystemUtils = fileSystemUtils
+ this.k8sClient = k8sClient
+ this.networkingUtils = networkingUtils
+ }
+
+ @Override
+ boolean isEnabled() {
+ return true
+ }
+
+ void validate() {
+ if (config.scm.scmManager.url) {
+ config.scm.scmManager.internal = false
+ config.scm.scmManager.urlForJenkins = config.scm.scmManager.url
+ } else {
+ log.debug("Setting configs for internal SCM-Manager")
+ // We use the K8s service as default name here, because it is the only option:
+ // "scmm.localhost" will not work inside the Pods and k3d-container IP + Port (e.g. 172.x.y.z:9091)
+ // will not work on Windows and MacOS.
+ config.scm.scmManager.urlForJenkins = "http://scmm.${config.application.namePrefix}scm-manager.svc.cluster.local/scm"
+
+ // More internal fields are set lazily in ScmManger.groovy (after SCMM is deployed and ports are known)
+ }
+ config.scm.scmManager.gitOpsUsername = "${config.application.namePrefix}gitops"
+
+ if (config.scm.gitlab.url) {
+ config.scm.scmProviderType = ScmProviderType.GITLAB
+ config.scm.scmManager = null
+ if (!config.scm.gitlab.password || !config.scm.gitlab.parentGroupId) {
+ throw new RuntimeException('GitLab configuration incomplete: please provide both password (PAT) and parentGroupId')
+ }
+ }
+
+ }
+
+ //Retrieves the appropriate SCM for cluster resources depending on whether the environment is multi-tenant or not.
+ GitProvider getResourcesScm() {
+ if (central) {
+ return central
+ } else if (tenant) {
+ return tenant
+ } else {
+ throw new IllegalStateException("No SCM provider found.")
+ }
+ }
+
+ @Override
+ void enable() {
+ //TenantSCM
+ switch (config.scm.scmProviderType) {
+ case ScmProviderType.GITLAB:
+ this.tenant = new Gitlab(this.config, this.config.scm.gitlab)
+ break
+ case ScmProviderType.SCM_MANAGER:
+ def prefixedNamespace = "${config.application.namePrefix}scm-manager".toString()
+ config.scm.scmManager.namespace = prefixedNamespace
+ this.tenant = new ScmManager(this.config, config.scm.scmManager, helmStrategy, k8sClient, networkingUtils, true)
+ // this.tenant.setup() setup will be here in future
+ break
+ default:
+ throw new IllegalArgumentException("Unsupported SCM provider found in TenantSCM")
+ }
+
+ if (config.multiTenant.useDedicatedInstance) {
+ switch (config.multiTenant.scmProviderType) {
+ case ScmProviderType.GITLAB:
+ this.central = new Gitlab(this.config, this.config.multiTenant.gitlab)
+ break
+ case ScmProviderType.SCM_MANAGER:
+ this.central = new ScmManager(this.config, config.multiTenant.scmManager, helmStrategy, k8sClient, networkingUtils)
+ break
+ default:
+ throw new IllegalArgumentException("Unsupported SCM-Central provider: ${config.scm.scmProviderType}")
+ }
+ }
+
+ //can be removed if we combine argocd and cluster-resources
+ final String namePrefix = (config?.application?.namePrefix ?: "").trim()
+ if (this.central) {
+ setupRepos(this.central, namePrefix)
+ setupRepos(this.tenant, namePrefix)
+ } else {
+ setupRepos(this.tenant, namePrefix)
+ }
+ }
+
+ static void setupRepos(GitProvider gitProvider, String namePrefix = "") {
+ gitProvider.createRepository(withOrgPrefix(namePrefix, "argocd/cluster-resources"),
+ "GitOps repo for basic cluster-resources")
+ }
+
+ /**
+ * Adds a prefix to the group/namespace part (before the first '/'):
+ * Example: "argocd/argocd" + "foo-" => "foo-argocd/argocd"*/
+ static String withOrgPrefix(String prefix, String repoPath) {
+ if (!prefix) return repoPath
+ return prefix + repoPath
+ }
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/features/git/config/ScmCentralSchema.groovy b/src/main/groovy/com/cloudogu/gitops/features/git/config/ScmCentralSchema.groovy
index b8cb97f56..c2aa66ba8 100644
--- a/src/main/groovy/com/cloudogu/gitops/features/git/config/ScmCentralSchema.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/features/git/config/ScmCentralSchema.groovy
@@ -4,91 +4,92 @@ import com.cloudogu.gitops.config.Config
import com.cloudogu.gitops.config.Credentials
import com.cloudogu.gitops.features.git.config.util.GitlabConfig
import com.cloudogu.gitops.features.git.config.util.ScmManagerConfig
+
import com.fasterxml.jackson.annotation.JsonPropertyDescription
import picocli.CommandLine.Option
class ScmCentralSchema {
- static class GitlabCentralConfig implements GitlabConfig {
+ static class GitlabCentralConfig implements GitlabConfig {
- public static final String CENTRAL_GITLAB_URL_DESCRIPTION = "URL for external Gitlab"
- public static final String CENTRAL_GITLAB_USERNAME_DESCRIPTION = "GitLab username for API access. Must be 'oauth2' when using Personal Access Token (PAT) authentication"
- public static final String CENTRAL_GITLAB_PASSWORD_DESCRIPTION = "Password for SCM Manager authentication"
- public static final String CENTRAL_GITLAB_PARENTGROUP_ID_DESCRIPTION = "Main Group for Gitlab where the GOP creates it's groups/repos"
+ public static final String CENTRAL_GITLAB_URL_DESCRIPTION = "URL for external Gitlab"
+ public static final String CENTRAL_GITLAB_USERNAME_DESCRIPTION = "GitLab username for API access. Must be 'oauth2' when using Personal Access Token (PAT) authentication"
+ public static final String CENTRAL_GITLAB_PASSWORD_DESCRIPTION = "Password for SCM Manager authentication"
+ public static final String CENTRAL_GITLAB_PARENTGROUP_ID_DESCRIPTION = "Main Group for Gitlab where the GOP creates it's groups/repos"
- // Only supports external Gitlab for now
- @Option(names = ['--central-gitlab-url'], description = CENTRAL_GITLAB_URL_DESCRIPTION)
- @JsonPropertyDescription(CENTRAL_GITLAB_URL_DESCRIPTION)
- String url = 'https://gitlab.com/'
+ // Only supports external Gitlab for now
+ @Option(names = ['--central-gitlab-url'], description = CENTRAL_GITLAB_URL_DESCRIPTION)
+ @JsonPropertyDescription(CENTRAL_GITLAB_URL_DESCRIPTION)
+ String url = 'https://gitlab.com/'
- @Option(names = ['--central-gitlab-username'], description = CENTRAL_GITLAB_USERNAME_DESCRIPTION)
- @JsonPropertyDescription(CENTRAL_GITLAB_USERNAME_DESCRIPTION)
- String username = 'oauth2.0'
+ @Option(names = ['--central-gitlab-username'], description = CENTRAL_GITLAB_USERNAME_DESCRIPTION)
+ @JsonPropertyDescription(CENTRAL_GITLAB_USERNAME_DESCRIPTION)
+ String username = 'oauth2.0'
- @Option(names = ['--central-gitlab-token'], description = CENTRAL_GITLAB_PASSWORD_DESCRIPTION)
- @JsonPropertyDescription(CENTRAL_GITLAB_PASSWORD_DESCRIPTION)
- String password = ''
+ @Option(names = ['--central-gitlab-token'], description = CENTRAL_GITLAB_PASSWORD_DESCRIPTION)
+ @JsonPropertyDescription(CENTRAL_GITLAB_PASSWORD_DESCRIPTION)
+ String password = ''
- @Option(names = ['--central-gitlab-group-id'], description = CENTRAL_GITLAB_PARENTGROUP_ID_DESCRIPTION)
- @JsonPropertyDescription(CENTRAL_GITLAB_PARENTGROUP_ID_DESCRIPTION)
- String parentGroupId = ''
+ @Option(names = ['--central-gitlab-group-id'], description = CENTRAL_GITLAB_PARENTGROUP_ID_DESCRIPTION)
+ @JsonPropertyDescription(CENTRAL_GITLAB_PARENTGROUP_ID_DESCRIPTION)
+ String parentGroupId = ''
- Credentials getCredentials() {
- return new Credentials(username, password)
- }
+ Credentials getCredentials() {
+ return new Credentials(username, password)
+ }
- String gitOpsUsername = ''
- String defaultVisibility = ''
- }
+ String gitOpsUsername = ''
+ String defaultVisibility = ''
+ }
- static class ScmManagerCentralConfig implements ScmManagerConfig {
+ static class ScmManagerCentralConfig implements ScmManagerConfig {
- public static final String CENTRAL_SCMM_INTERNAL_DESCRIPTION = 'SCM for Central Management is running on the same cluster, so k8s internal URLs can be used for access'
- public static final String CENTRAL_SCMM_URL_DESCRIPTION = 'URL for the centralized Management Repo'
- public static final String CENTRAL_SCMM_USERNAME_DESCRIPTION = 'CENTRAL SCMM username'
- public static final String CENTRAL_SCMM_PASSWORD_DESCRIPTION = 'CENTRAL SCMM password'
- public static final String CENTRAL_SCMM_PATH_DESCRIPTION = 'Root path for SCM Manager. In SCM-Manager it is always "repo"'
- public static final String CENTRAL_SCMM_NAMESPACE_DESCRIPTION = 'Namespace where to find the Central SCMM'
+ public static final String CENTRAL_SCMM_INTERNAL_DESCRIPTION = 'SCM for Central Management is running on the same cluster, so k8s internal URLs can be used for access'
+ public static final String CENTRAL_SCMM_URL_DESCRIPTION = 'URL for the centralized Management Repo'
+ public static final String CENTRAL_SCMM_USERNAME_DESCRIPTION = 'CENTRAL SCMM username'
+ public static final String CENTRAL_SCMM_PASSWORD_DESCRIPTION = 'CENTRAL SCMM password'
+ public static final String CENTRAL_SCMM_PATH_DESCRIPTION = 'Root path for SCM Manager. In SCM-Manager it is always "repo"'
+ public static final String CENTRAL_SCMM_NAMESPACE_DESCRIPTION = 'Namespace where to find the Central SCMM'
- @Option(names = ['--central-scmm-internal'], description = CENTRAL_SCMM_INTERNAL_DESCRIPTION)
- @JsonPropertyDescription(CENTRAL_SCMM_INTERNAL_DESCRIPTION)
- Boolean internal = false
+ @Option(names = ['--central-scmm-internal'], description = CENTRAL_SCMM_INTERNAL_DESCRIPTION)
+ @JsonPropertyDescription(CENTRAL_SCMM_INTERNAL_DESCRIPTION)
+ Boolean internal = false
- @Option(names = ['--central-scmm-url'], description = CENTRAL_SCMM_URL_DESCRIPTION)
- @JsonPropertyDescription(CENTRAL_SCMM_URL_DESCRIPTION)
- String url = ''
+ @Option(names = ['--central-scmm-url'], description = CENTRAL_SCMM_URL_DESCRIPTION)
+ @JsonPropertyDescription(CENTRAL_SCMM_URL_DESCRIPTION)
+ String url = ''
- @Option(names = ['--central-scmm-username'], description = CENTRAL_SCMM_USERNAME_DESCRIPTION)
- @JsonPropertyDescription(CENTRAL_SCMM_USERNAME_DESCRIPTION)
- String username = ''
+ @Option(names = ['--central-scmm-username'], description = CENTRAL_SCMM_USERNAME_DESCRIPTION)
+ @JsonPropertyDescription(CENTRAL_SCMM_USERNAME_DESCRIPTION)
+ String username = ''
- @Option(names = ['--central-scmm-password'], description = CENTRAL_SCMM_PASSWORD_DESCRIPTION)
- @JsonPropertyDescription(CENTRAL_SCMM_PASSWORD_DESCRIPTION)
- String password = ''
+ @Option(names = ['--central-scmm-password'], description = CENTRAL_SCMM_PASSWORD_DESCRIPTION)
+ @JsonPropertyDescription(CENTRAL_SCMM_PASSWORD_DESCRIPTION)
+ String password = ''
- @Option(names = ['--central-scmm-root-path'], description = CENTRAL_SCMM_PATH_DESCRIPTION)
- @JsonPropertyDescription(CENTRAL_SCMM_PATH_DESCRIPTION)
- String rootPath = 'repo'
+ @Option(names = ['--central-scmm-root-path'], description = CENTRAL_SCMM_PATH_DESCRIPTION)
+ @JsonPropertyDescription(CENTRAL_SCMM_PATH_DESCRIPTION)
+ String rootPath = 'repo'
- @Option(names = ['--central-scmm-namespace'], description = CENTRAL_SCMM_NAMESPACE_DESCRIPTION)
- @JsonPropertyDescription(CENTRAL_SCMM_NAMESPACE_DESCRIPTION)
- String namespace = 'scm-manager'
+ @Option(names = ['--central-scmm-namespace'], description = CENTRAL_SCMM_NAMESPACE_DESCRIPTION)
+ @JsonPropertyDescription(CENTRAL_SCMM_NAMESPACE_DESCRIPTION)
+ String namespace = 'scm-manager'
- @Override
- String getIngress() {
- return null //Needed for setup
- }
+ @Override
+ String getIngress() {
+ return null //Needed for setup
+ }
- @Override
- Config.HelmConfigWithValues getHelm() {
- return null //Needed for setup
- }
+ @Override
+ Config.HelmConfigWithValues getHelm() {
+ return null //Needed for setup
+ }
- Credentials getCredentials() {
- return new Credentials(username, password)
- }
+ Credentials getCredentials() {
+ return new Credentials(username, password)
+ }
- String gitOpsUsername = ''
+ String gitOpsUsername = ''
- }
+ }
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/features/git/config/ScmTenantSchema.groovy b/src/main/groovy/com/cloudogu/gitops/features/git/config/ScmTenantSchema.groovy
index 33b7f72fb..3d04ef1b9 100644
--- a/src/main/groovy/com/cloudogu/gitops/features/git/config/ScmTenantSchema.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/features/git/config/ScmTenantSchema.groovy
@@ -1,162 +1,157 @@
package com.cloudogu.gitops.features.git.config
+import static com.cloudogu.gitops.config.ConfigConstants.HELM_CONFIG_DESCRIPTION
+
import com.cloudogu.gitops.config.Config
import com.cloudogu.gitops.config.Credentials
import com.cloudogu.gitops.features.git.config.util.GitlabConfig
import com.cloudogu.gitops.features.git.config.util.ScmManagerConfig
import com.cloudogu.gitops.features.git.config.util.ScmProviderType
import com.cloudogu.gitops.utils.NetworkingUtils
+
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonMerge
import com.fasterxml.jackson.annotation.JsonPropertyDescription
import picocli.CommandLine.Mixin
import picocli.CommandLine.Option
-import static com.cloudogu.gitops.config.ConfigConstants.HELM_CONFIG_DESCRIPTION
-
class ScmTenantSchema {
- static final String GITLAB_CONFIG_DESCRIPTION = 'Config for GITLAB'
- static final String SCMM_CONFIG_DESCRIPTION = 'Config for GITLAB'
- static final String SCM_PROVIDER_TYPE_DESCRIPTION = 'The SCM provider type. Possible values: SCM_MANAGER, GITLAB'
- static final String GITOPSUSERNAME_DESCRIPTION = 'Username for the Gitops User'
-
- @Option(
- names = ['--scm-provider'],
- description = SCM_PROVIDER_TYPE_DESCRIPTION,
- defaultValue = "SCM_MANAGER"
- )
- @JsonPropertyDescription(SCM_PROVIDER_TYPE_DESCRIPTION)
- ScmProviderType scmProviderType = ScmProviderType.SCM_MANAGER
-
- @JsonPropertyDescription(GITLAB_CONFIG_DESCRIPTION)
- @Mixin
- GitlabTenantConfig gitlab
+ static final String GITLAB_CONFIG_DESCRIPTION = 'Config for GITLAB'
+ static final String SCMM_CONFIG_DESCRIPTION = 'Config for GITLAB'
+ static final String SCM_PROVIDER_TYPE_DESCRIPTION = 'The SCM provider type. Possible values: SCM_MANAGER, GITLAB'
+ static final String GITOPSUSERNAME_DESCRIPTION = 'Username for the Gitops User'
+
+ @Option(names = ['--scm-provider'],
+ description = SCM_PROVIDER_TYPE_DESCRIPTION,
+ defaultValue = "SCM_MANAGER")
+ @JsonPropertyDescription(SCM_PROVIDER_TYPE_DESCRIPTION)
+ ScmProviderType scmProviderType = ScmProviderType.SCM_MANAGER
- @JsonPropertyDescription(SCMM_CONFIG_DESCRIPTION)
- @Mixin
- ScmManagerTenantConfig scmManager
+ @JsonPropertyDescription(GITLAB_CONFIG_DESCRIPTION)
+ @Mixin
+ GitlabTenantConfig gitlab
- @JsonIgnore
- Boolean internal = { ->
- return (gitlab.internal || scmManager.internal)
- }
+ @JsonPropertyDescription(SCMM_CONFIG_DESCRIPTION)
+ @Mixin
+ ScmManagerTenantConfig scmManager
+ @JsonIgnore
+ Boolean internal = { -> return (gitlab.internal || scmManager.internal)
+ }
- static class GitlabTenantConfig implements GitlabConfig {
+ static class GitlabTenantConfig implements GitlabConfig {
- static final String GITLAB_INTERNAL_DESCRIPTION = 'True if Gitlab is running in the same K8s cluster. For now we only support access by external URL'
- static final String GITLAB_URL_DESCRIPTION = "Base URL for the Gitlab instance"
- static final String GITLAB_USERNAME_DESCRIPTION = 'Defaults to: oauth2.0 when PAT token is given.'
- static final String GITLAB_TOKEN_DESCRIPTION = 'PAT Token for the account. Needs read/write repo permissions. See docs for mor information'
- static final String GITLAB_PARENT_GROUP_ID = 'Number for the Gitlab Group where the repos and subgroups should be created'
+ static final String GITLAB_INTERNAL_DESCRIPTION = 'True if Gitlab is running in the same K8s cluster. For now we only support access by external URL'
+ static final String GITLAB_URL_DESCRIPTION = "Base URL for the Gitlab instance"
+ static final String GITLAB_USERNAME_DESCRIPTION = 'Defaults to: oauth2.0 when PAT token is given.'
+ static final String GITLAB_TOKEN_DESCRIPTION = 'PAT Token for the account. Needs read/write repo permissions. See docs for mor information'
+ static final String GITLAB_PARENT_GROUP_ID = 'Number for the Gitlab Group where the repos and subgroups should be created'
- @JsonPropertyDescription(GITLAB_INTERNAL_DESCRIPTION)
- Boolean internal = false
+ @JsonPropertyDescription(GITLAB_INTERNAL_DESCRIPTION)
+ Boolean internal = false
- @Option(names = ['--gitlab-url'], description = GITLAB_URL_DESCRIPTION)
- @JsonPropertyDescription(GITLAB_URL_DESCRIPTION)
- String url
+ @Option(names = ['--gitlab-url'], description = GITLAB_URL_DESCRIPTION)
+ @JsonPropertyDescription(GITLAB_URL_DESCRIPTION)
+ String url
- @Option(names = ['--gitlab-username'], description = GITLAB_USERNAME_DESCRIPTION)
- @JsonPropertyDescription(GITLAB_USERNAME_DESCRIPTION)
- String username = 'oauth2.0'
+ @Option(names = ['--gitlab-username'], description = GITLAB_USERNAME_DESCRIPTION)
+ @JsonPropertyDescription(GITLAB_USERNAME_DESCRIPTION)
+ String username = 'oauth2.0'
- @Option(names = ['--gitlab-token'], description = GITLAB_TOKEN_DESCRIPTION)
- @JsonPropertyDescription(GITLAB_TOKEN_DESCRIPTION)
- String password
+ @Option(names = ['--gitlab-token'], description = GITLAB_TOKEN_DESCRIPTION)
+ @JsonPropertyDescription(GITLAB_TOKEN_DESCRIPTION)
+ String password
- @Option(names = ['--gitlab-group-id'], description = GITLAB_PARENT_GROUP_ID)
- @JsonPropertyDescription(GITLAB_PARENT_GROUP_ID)
- String parentGroupId = ''
+ @Option(names = ['--gitlab-group-id'], description = GITLAB_PARENT_GROUP_ID)
+ @JsonPropertyDescription(GITLAB_PARENT_GROUP_ID)
+ String parentGroupId = ''
- @JsonIgnore
- Credentials getCredentials() {
- return new Credentials(username, password)
- }
+ @JsonIgnore
+ Credentials getCredentials() {
+ return new Credentials(username, password)
+ }
- @JsonPropertyDescription(GITOPSUSERNAME_DESCRIPTION)
- String gitOpsUsername = ''
- String defaultVisibility = ''
+ @JsonPropertyDescription(GITOPSUSERNAME_DESCRIPTION)
+ String gitOpsUsername = ''
+ String defaultVisibility = ''
- }
+ }
- static class ScmManagerTenantConfig implements ScmManagerConfig {
+ static class ScmManagerTenantConfig implements ScmManagerConfig {
- static final String SCMM_SKIP_RESTART_DESCRIPTION = 'Skips restarting SCM-Manager after plugin installation. Use with caution! If the plugins are not installed up front, the installation will likely fail. The intended use case for this is after the first installation, for config changes only. Do not use on first installation or upgrades.\''
- static final String SCMM_SKIP_PLUGINS_DESCRIPTION = 'Skips plugin installation. Use with caution! If the plugins are not installed up front, the installation will likely fail. The intended use case for this is after the first installation, for config changes only. Do not use on first installation or upgrades.'
- static final String SCMM_URL_DESCRIPTION = 'The host of your external scm-manager'
- static final String SCMM_USERNAME_DESCRIPTION = 'Mandatory when scmm-url is set'
- static final String SCMM_PASSWORD_DESCRIPTION = 'Mandatory when scmm-url is set'
- static final String SCMM_ROOT_PATH_DESCRIPTION = 'Sets the root path for the Git Repositories. In SCM-Manager it is always "repo"'
- static final String SCMM_NAMESPACE_DESCRIPTION = 'Namespace where SCM-Manager should run'
+ static final String SCMM_SKIP_RESTART_DESCRIPTION = 'Skips restarting SCM-Manager after plugin installation. Use with caution! If the plugins are not installed up front, the installation will likely fail. The intended use case for this is after the first installation, for config changes only. Do not use on first installation or upgrades.\''
+ static final String SCMM_SKIP_PLUGINS_DESCRIPTION = 'Skips plugin installation. Use with caution! If the plugins are not installed up front, the installation will likely fail. The intended use case for this is after the first installation, for config changes only. Do not use on first installation or upgrades.'
+ static final String SCMM_URL_DESCRIPTION = 'The host of your external scm-manager'
+ static final String SCMM_USERNAME_DESCRIPTION = 'Mandatory when scmm-url is set'
+ static final String SCMM_PASSWORD_DESCRIPTION = 'Mandatory when scmm-url is set'
+ static final String SCMM_ROOT_PATH_DESCRIPTION = 'Sets the root path for the Git Repositories. In SCM-Manager it is always "repo"'
+ static final String SCMM_NAMESPACE_DESCRIPTION = 'Namespace where SCM-Manager should run'
- Boolean internal = true
+ Boolean internal = true
- @Option(names = ['--scmm-url'], description = SCMM_URL_DESCRIPTION)
- @JsonPropertyDescription(SCMM_URL_DESCRIPTION)
- String url = ''
+ @Option(names = ['--scmm-url'], description = SCMM_URL_DESCRIPTION)
+ @JsonPropertyDescription(SCMM_URL_DESCRIPTION)
+ String url = ''
- @Option(names = ['--scmm-namespace'], description = SCMM_NAMESPACE_DESCRIPTION)
- @JsonPropertyDescription(SCMM_NAMESPACE_DESCRIPTION)
- String namespace = 'scm-manager'
+ @Option(names = ['--scmm-namespace'], description = SCMM_NAMESPACE_DESCRIPTION)
+ @JsonPropertyDescription(SCMM_NAMESPACE_DESCRIPTION)
+ String namespace = 'scm-manager'
- @Option(names = ['--scmm-username'], description = SCMM_USERNAME_DESCRIPTION)
- @JsonPropertyDescription(SCMM_USERNAME_DESCRIPTION)
- String username = Config.DEFAULT_ADMIN_USER
+ @Option(names = ['--scmm-username'], description = SCMM_USERNAME_DESCRIPTION)
+ @JsonPropertyDescription(SCMM_USERNAME_DESCRIPTION)
+ String username = Config.DEFAULT_ADMIN_USER
- @Option(names = ['--scmm-password'], description = SCMM_PASSWORD_DESCRIPTION)
- @JsonPropertyDescription(SCMM_PASSWORD_DESCRIPTION)
- String password = Config.DEFAULT_ADMIN_PW
+ @Option(names = ['--scmm-password'], description = SCMM_PASSWORD_DESCRIPTION)
+ @JsonPropertyDescription(SCMM_PASSWORD_DESCRIPTION)
+ String password = Config.DEFAULT_ADMIN_PW
- @JsonPropertyDescription(HELM_CONFIG_DESCRIPTION)
- @JsonMerge
- Config.HelmConfigWithValues helm = new Config.HelmConfigWithValues(
- chart: 'scm-manager',
- repoURL: 'https://packages.scm-manager.org/repository/helm-v2-releases/',
- version: '3.11.4',
- values: [:]
- )
+ @JsonPropertyDescription(HELM_CONFIG_DESCRIPTION)
+ @JsonMerge
+ Config.HelmConfigWithValues helm = new Config.HelmConfigWithValues(chart: 'scm-manager',
+ repoURL: 'https://packages.scm-manager.org/repository/helm-v2-releases/',
+ version: '3.11.4',
+ values: [:])
- @Option(names = ['--scmm-root-path'], description = SCMM_ROOT_PATH_DESCRIPTION)
- @JsonPropertyDescription(SCMM_ROOT_PATH_DESCRIPTION)
- String rootPath = 'repo'
+ @Option(names = ['--scmm-root-path'], description = SCMM_ROOT_PATH_DESCRIPTION)
+ @JsonPropertyDescription(SCMM_ROOT_PATH_DESCRIPTION)
+ String rootPath = 'repo'
- /* When installing from via Docker we have to distinguish scmm.url (which is a local IP address) from
- the SCMM URL used by jenkins.
+ /* When installing from via Docker we have to distinguish scmm.url (which is a local IP address) from
+ the SCMM URL used by jenkins.
- This is necessary to make the build on push feature (webhooks from SCMM to Jenkins that trigger builds) work
- in k3d.
- The webhook contains repository URLs that start with the "Base URL" Setting of SCMM.
- Jenkins checks these repo URLs and triggers all builds that match repo URLs.
+ This is necessary to make the build on push feature (webhooks from SCMM to Jenkins that trigger builds) work
+ in k3d.
+ The webhook contains repository URLs that start with the "Base URL" Setting of SCMM.
+ Jenkins checks these repo URLs and triggers all builds that match repo URLs.
- This value is set as "Base URL" in SCMM Settings and in Jenkins Job.
+ This value is set as "Base URL" in SCMM Settings and in Jenkins Job.
- See ApplicationConfigurator.addScmmConfig() and the comment at jenkins.urlForScmm */
+ See ApplicationConfigurator.addScmmConfig() and the comment at jenkins.urlForScmm */
- String urlForJenkins = ''
+ String urlForJenkins = ''
- @JsonIgnore
- String getHost() { return NetworkingUtils.getHost(url) }
+ @JsonIgnore
+ String getHost() { return NetworkingUtils.getHost(url) }
- @JsonIgnore
- String getProtocol() { return NetworkingUtils.getProtocol(url) }
- String ingress = ''
+ @JsonIgnore
+ String getProtocol() { return NetworkingUtils.getProtocol(url) }
+ String ingress = ''
- @Option(names = ['--scmm-skip-restart'], description = SCMM_SKIP_RESTART_DESCRIPTION)
- @JsonPropertyDescription(SCMM_SKIP_RESTART_DESCRIPTION)
- Boolean skipRestart = false
+ @Option(names = ['--scmm-skip-restart'], description = SCMM_SKIP_RESTART_DESCRIPTION)
+ @JsonPropertyDescription(SCMM_SKIP_RESTART_DESCRIPTION)
+ Boolean skipRestart = false
- @Option(names = ['--scmm-skip-plugins'], description = SCMM_SKIP_PLUGINS_DESCRIPTION)
- @JsonPropertyDescription(SCMM_SKIP_PLUGINS_DESCRIPTION)
- Boolean skipPlugins = false
+ @Option(names = ['--scmm-skip-plugins'], description = SCMM_SKIP_PLUGINS_DESCRIPTION)
+ @JsonPropertyDescription(SCMM_SKIP_PLUGINS_DESCRIPTION)
+ Boolean skipPlugins = false
- @JsonPropertyDescription(GITOPSUSERNAME_DESCRIPTION)
- String gitOpsUsername = ''
+ @JsonPropertyDescription(GITOPSUSERNAME_DESCRIPTION)
+ String gitOpsUsername = ''
- @JsonIgnore
- Credentials getCredentials() {
- return new Credentials(username, password)
- }
- }
+ @JsonIgnore
+ Credentials getCredentials() {
+ return new Credentials(username, password)
+ }
+ }
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/features/git/config/util/GitlabConfig.groovy b/src/main/groovy/com/cloudogu/gitops/features/git/config/util/GitlabConfig.groovy
index 304ef0264..70da8ef7d 100644
--- a/src/main/groovy/com/cloudogu/gitops/features/git/config/util/GitlabConfig.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/features/git/config/util/GitlabConfig.groovy
@@ -3,9 +3,13 @@ package com.cloudogu.gitops.features.git.config.util
import com.cloudogu.gitops.config.Credentials
interface GitlabConfig {
- String getUrl()
- String getParentGroupId()
- String getDefaultVisibility()
- String getGitOpsUsername()
- Credentials getCredentials()
+ String getUrl()
+
+ String getParentGroupId()
+
+ String getDefaultVisibility()
+
+ String getGitOpsUsername()
+
+ Credentials getCredentials()
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/features/git/config/util/ScmManagerConfig.groovy b/src/main/groovy/com/cloudogu/gitops/features/git/config/util/ScmManagerConfig.groovy
index 5e92bc6c7..e99d9c7d4 100644
--- a/src/main/groovy/com/cloudogu/gitops/features/git/config/util/ScmManagerConfig.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/features/git/config/util/ScmManagerConfig.groovy
@@ -3,17 +3,24 @@ package com.cloudogu.gitops.features.git.config.util
import com.cloudogu.gitops.config.Config
import com.cloudogu.gitops.config.Credentials
-
interface ScmManagerConfig {
- Boolean getInternal()
- String getUrl()
- String getUsername()
- String getPassword()
- String getNamespace()
- String getIngress()
- Config.HelmConfigWithValues getHelm()
- String getRootPath()
- String getGitOpsUsername()
-
- Credentials getCredentials()
+ Boolean getInternal()
+
+ String getUrl()
+
+ String getUsername()
+
+ String getPassword()
+
+ String getNamespace()
+
+ String getIngress()
+
+ Config.HelmConfigWithValues getHelm()
+
+ String getRootPath()
+
+ String getGitOpsUsername()
+
+ Credentials getCredentials()
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/features/git/config/util/ScmProviderType.groovy b/src/main/groovy/com/cloudogu/gitops/features/git/config/util/ScmProviderType.groovy
index 5685e2e71..062f5fd8e 100644
--- a/src/main/groovy/com/cloudogu/gitops/features/git/config/util/ScmProviderType.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/features/git/config/util/ScmProviderType.groovy
@@ -1,6 +1,6 @@
package com.cloudogu.gitops.features.git.config.util
enum ScmProviderType {
- GITLAB,
- SCM_MANAGER
+ GITLAB,
+ SCM_MANAGER
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/git/GitRepo.groovy b/src/main/groovy/com/cloudogu/gitops/git/GitRepo.groovy
index fd0001fc6..81137b880 100644
--- a/src/main/groovy/com/cloudogu/gitops/git/GitRepo.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/git/GitRepo.groovy
@@ -9,7 +9,9 @@ import com.cloudogu.gitops.git.providers.RepoUrlScope
import com.cloudogu.gitops.git.providers.Scope
import com.cloudogu.gitops.utils.FileSystemUtils
import com.cloudogu.gitops.utils.TemplatingEngine
+
import groovy.util.logging.Slf4j
+
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.ListBranchCommand
import org.eclipse.jgit.api.PushCommand
@@ -27,275 +29,260 @@ import org.eclipse.jgit.treewalk.filter.PathFilter
@Slf4j
class GitRepo {
- static final String NAMESPACE_3RD_PARTY_DEPENDENCIES = '3rd-party-dependencies'
-
- private final Config config
- public GitProvider gitProvider
- private final FileSystemUtils fileSystemUtils
-
- private final String repoTarget
- private final boolean insecure
- private final String gitName
- private final String gitEmail
-
- private Git gitMemoization
- private final String absoluteLocalRepoTmpDir
-
- GitRepo(Config config,
- GitProvider gitProvider,
- String repoTarget,
- FileSystemUtils fileSystemUtils) {
- def tmpDir = File.createTempDir()
- tmpDir.deleteOnExit()
- this.absoluteLocalRepoTmpDir = tmpDir.absolutePath
- this.config = config
- this.gitProvider = gitProvider
- this.fileSystemUtils = fileSystemUtils
-
- this.repoTarget = "${config.application.namePrefix}${repoTarget}"
-
- this.insecure = config.application.insecure
- this.gitName = config.application.gitName
- this.gitEmail = config.application.gitEmail
- }
-
- String getRepoTarget() {
- return repoTarget
- }
-
- boolean createRepositoryAndSetPermission(String description, boolean initialize = true) {
- def isNewRepo = this.gitProvider.createRepository(repoTarget, description, initialize)
- if (gitProvider.getGitOpsUsername()) {
- gitProvider.setRepositoryPermission(
- repoTarget,
- gitProvider.getGitOpsUsername(),
- AccessRole.WRITE,
- Scope.USER
- )
- }
- return isNewRepo
-
- }
-
- String getAbsoluteLocalRepoTmpDir() {
- return absoluteLocalRepoTmpDir
- }
-
- void cloneRepo() {
- def cloneUrl = getGitRepositoryUrl()
- log.debug("Cloning ${repoTarget}, Origin: ${cloneUrl}")
- Git.cloneRepository()
- .setURI(cloneUrl)
- .setDirectory(new File(absoluteLocalRepoTmpDir))
- .setCredentialsProvider(getCredentialProvider())
- .call()
- }
-
- void commitAndPush(String message, String tag) {
- commitAndPush(message, tag, 'HEAD:refs/heads/main')
- }
-
-
- void commitAndPush(String commitMessage, String tag, String refSpec) {
- log.debug("Adding files to ${repoTarget}")
- def git = getGit()
- git.add().addFilepattern(".").call()
-
- if (git.status().call().hasUncommittedChanges()) {
- log.debug("Commiting ${repoTarget}")
- git.commit()
- .setSign(false)
- .setMessage(commitMessage)
- .setAuthor(gitName, gitEmail)
- .setCommitter("${gitName} - GOP v${Version.NAME.split(',')[0].replace('(','')}", gitEmail) //parsing the Versions from the full text in Version.Name. In local Dev there is no Tag->Version is empty
- .call()
-
- def pushCommand = createPushCommand(refSpec)
-
- if (tag) {
- log.debug("Setting tag '${tag}' on repo: ${repoTarget}")
- // Delete existing tags first to get idempotence
- git.tagDelete().setTags(tag).call()
- git.tag()
- .setName(tag)
- .call()
- pushCommand.setPushTags()
- }
-
- log.debug("Pushing repo: ${repoTarget}, refSpec: ${refSpec}")
- pushCommand.call()
- } else {
- log.debug("No changes after add, nothing to commit or push on repo: ${repoTarget}")
- }
- }
-
-
- void commitAndPush(String commitMessage) {
- commitAndPush(commitMessage, null, 'HEAD:refs/heads/main')
- }
- /**
- * Push all refs, i.e. all tags and branches
- */
-
- void pushAll(boolean force) {
- createPushCommand('refs/*:refs/*').setForce(force).call()
- }
-
-
- void pushRef(String ref, boolean force) {
- pushRef(ref, ref, force)
- }
-
-
- void pushRef(String ref, String targetRef, boolean force) {
- createPushCommand("${ref}:${targetRef}").setForce(force).call()
- }
-
-
- /**
- * Delete all files in this repository
- */
- void clearRepo() {
- fileSystemUtils.deleteFilesExcept(new File(absoluteLocalRepoTmpDir), ".git")
- }
-
-
- void copyDirectoryContents(String srcDir) {
- copyDirectoryContents(srcDir, (FileFilter) null)
- }
-
-
- void copyDirectoryContents(String srcDir, FileFilter fileFilter) {
- if (!srcDir) {
- log.warn("Source directory is not defined. Nothing to copy?")
- return
- }
-
- log.debug("Initializing repo $repoTarget from $srcDir")
- String absoluteSrcDirLocation = new File(srcDir).isAbsolute()
- ? srcDir
- : "${fileSystemUtils.getRootDir()}/${srcDir}"
- fileSystemUtils.copyDirectory(absoluteSrcDirLocation, absoluteLocalRepoTmpDir, fileFilter)
- }
-
-
- void writeFile(String path, String content) {
- def file = new File("$absoluteLocalRepoTmpDir/$path")
- fileSystemUtils.createDirectory(file.parent)
- file.createNewFile()
- file.text = content
- }
-
- void replaceTemplates(Map parameters) {
- new TemplatingEngine().replaceTemplates(new File(absoluteLocalRepoTmpDir), parameters)
- }
-
- String getGitRepositoryUrl() {
- return this.gitProvider.repoUrl(repoTarget, RepoUrlScope.CLIENT)
- }
-
- static boolean isCommit(File repoPath, String ref) {
- if (!ref) {
- return false
- }
-
- try (Git git = Git.open(repoPath)) {
- // Get all branch and tag names
- def allRefs = []
-
- // Add all branch names (without refs/heads/ prefix)
- git.branchList().call().each { branch ->
- allRefs.add(branch.name.replaceFirst('refs/heads/', ''))
- }
-
- // Add all tag names (without refs/tags/ prefix)
- git.tagList().call().each { tag ->
- allRefs.add(tag.name.replaceFirst('refs/tags/', ''))
- }
-
- // If the ref matches any branch or tag name, it's not a commit hash
- if (allRefs.contains(ref)) {
- return false
- }
-
- // If it's not a branch or tag, try to resolve it as a commit
- def objectId = git.repository.resolve(ref)
- return objectId != null
-
- }
- }
-
- /**
- * checks, if file exists in repo in some branch.
- * @param pathToRepo
- * @param filename
- */
- static boolean existFileInSomeBranch(String repo, String filename) {
- String filenameToSearch = filename
- File repoPath = new File(repo + '/.git')
-
- try (def git = Git.open(repoPath)) {
- List[ branches = git
- .branchList()
- .setListMode(ListBranchCommand.ListMode.ALL)
- .call()
-
- for (Ref branch : branches) {
- String branchName = branch.getName()
-
- ObjectId commitId = git.repository.resolve(branchName)
- if (commitId == null) {
- continue
- }
- try (RevWalk revWalk = new RevWalk(git.repository)) {
- RevCommit commit = revWalk.parseCommit(commitId)
- try (TreeWalk treeWalk = new TreeWalk(git.repository)) {
-
- treeWalk.addTree(commit.getTree())
- treeWalk.setFilter(PathFilter.create(filenameToSearch))
-
- if (treeWalk.next()) {
- log.debug("File ${filename} found in branch ${branchName}")
-
- return true
- }
- }
- }
- }
- }
- log.debug("File ${filename} not found in repository ${repoPath}")
- return false
- }
-
- static boolean isTag(File repo, String ref) {
- if (!ref) {
- return false
- }
- try (def git = Git.open(repo)) {
- git.tagList().call().any { it.name.endsWith("/" + ref) || it.name == ref }
- }
- }
-
- private PushCommand createPushCommand(String refSpec) {
- getGit()
- .push()
- .setRemote(getGitRepositoryUrl())
- .setRefSpecs(new RefSpec(refSpec))
- .setCredentialsProvider(getCredentialProvider())
- }
-
- private Git getGit() {
- if (gitMemoization != null) {
- return gitMemoization
- }
-
- return gitMemoization = Git.open(new File(absoluteLocalRepoTmpDir))
- }
-
- private CredentialsProvider getCredentialProvider() {
- def auth = this.gitProvider.getCredentials()
- def passwordAuthentication = new UsernamePasswordCredentialsProvider(auth.username, auth.password)
- return insecure ? new ChainingCredentialsProvider(new InsecureCredentialProvider(), passwordAuthentication) : passwordAuthentication
- }
+ static final String NAMESPACE_3RD_PARTY_DEPENDENCIES = '3rd-party-dependencies'
+
+ private final Config config
+ public GitProvider gitProvider
+ private final FileSystemUtils fileSystemUtils
+
+ private final String repoTarget
+ private final boolean insecure
+ private final String gitName
+ private final String gitEmail
+
+ private Git gitMemoization
+ private final String absoluteLocalRepoTmpDir
+
+ GitRepo(Config config,
+ GitProvider gitProvider,
+ String repoTarget,
+ FileSystemUtils fileSystemUtils) {
+ def tmpDir = File.createTempDir()
+ tmpDir.deleteOnExit()
+ this.absoluteLocalRepoTmpDir = tmpDir.absolutePath
+ this.config = config
+ this.gitProvider = gitProvider
+ this.fileSystemUtils = fileSystemUtils
+
+ this.repoTarget = "${config.application.namePrefix}${repoTarget}"
+
+ this.insecure = config.application.insecure
+ this.gitName = config.application.gitName
+ this.gitEmail = config.application.gitEmail
+ }
+
+ String getRepoTarget() {
+ return repoTarget
+ }
+
+ boolean createRepositoryAndSetPermission(String description, boolean initialize = true) {
+ def isNewRepo = this.gitProvider.createRepository(repoTarget, description, initialize)
+ if (gitProvider.getGitOpsUsername()) {
+ gitProvider.setRepositoryPermission(repoTarget,
+ gitProvider.getGitOpsUsername(),
+ AccessRole.WRITE,
+ Scope.USER)
+ }
+ return isNewRepo
+
+ }
+
+ String getAbsoluteLocalRepoTmpDir() {
+ return absoluteLocalRepoTmpDir
+ }
+
+ void cloneRepo() {
+ def cloneUrl = getGitRepositoryUrl()
+ log.debug("Cloning ${repoTarget}, Origin: ${cloneUrl}")
+ Git.cloneRepository()
+ .setURI(cloneUrl)
+ .setDirectory(new File(absoluteLocalRepoTmpDir))
+ .setCredentialsProvider(getCredentialProvider())
+ .call()
+ }
+
+ void commitAndPush(String message, String tag) {
+ commitAndPush(message, tag, 'HEAD:refs/heads/main')
+ }
+
+ void commitAndPush(String commitMessage, String tag, String refSpec) {
+ log.debug("Adding files to ${repoTarget}")
+ def git = getGit()
+ git.add().addFilepattern(".").call()
+
+ if (git.status().call().hasUncommittedChanges()) {
+ log.debug("Commiting ${repoTarget}")
+ git.commit()
+ .setSign(false)
+ .setMessage(commitMessage)
+ .setAuthor(gitName, gitEmail)
+ .setCommitter("${gitName} - GOP v${Version.NAME.split(',')[0].replace('(', '')}", gitEmail) //parsing the Versions from the full text in Version.Name. In local Dev there is no Tag->Version is empty
+ .call()
+
+ def pushCommand = createPushCommand(refSpec)
+
+ if (tag) {
+ log.debug("Setting tag '${tag}' on repo: ${repoTarget}")
+ // Delete existing tags first to get idempotence
+ git.tagDelete().setTags(tag).call()
+ git.tag()
+ .setName(tag)
+ .call()
+ pushCommand.setPushTags()
+ }
+
+ log.debug("Pushing repo: ${repoTarget}, refSpec: ${refSpec}")
+ pushCommand.call()
+ } else {
+ log.debug("No changes after add, nothing to commit or push on repo: ${repoTarget}")
+ }
+ }
+
+ void commitAndPush(String commitMessage) {
+ commitAndPush(commitMessage, null, 'HEAD:refs/heads/main')
+ }
+
+ /**
+ * Push all refs, i.e. all tags and branches*/
+
+ void pushAll(boolean force) {
+ createPushCommand('refs/*:refs/*').setForce(force).call()
+ }
+
+ void pushRef(String ref, boolean force) {
+ pushRef(ref, ref, force)
+ }
+
+ void pushRef(String ref, String targetRef, boolean force) {
+ createPushCommand("${ref}:${targetRef}").setForce(force).call()
+ }
+
+ /**
+ * Delete all files in this repository*/
+ void clearRepo() {
+ fileSystemUtils.deleteFilesExcept(new File(absoluteLocalRepoTmpDir), ".git")
+ }
+
+ void copyDirectoryContents(String srcDir) {
+ copyDirectoryContents(srcDir, (FileFilter) null)
+ }
+
+ void copyDirectoryContents(String srcDir, FileFilter fileFilter) {
+ if (!srcDir) {
+ log.warn("Source directory is not defined. Nothing to copy?")
+ return
+ }
+
+ log.debug("Initializing repo $repoTarget from $srcDir")
+ String absoluteSrcDirLocation = new File(srcDir).isAbsolute() ? srcDir : "${fileSystemUtils.getRootDir()}/${srcDir}"
+ fileSystemUtils.copyDirectory(absoluteSrcDirLocation, absoluteLocalRepoTmpDir, fileFilter)
+ }
+
+ void writeFile(String path, String content) {
+ def file = new File("$absoluteLocalRepoTmpDir/$path")
+ fileSystemUtils.createDirectory(file.parent)
+ file.createNewFile()
+ file.text = content
+ }
+
+ void replaceTemplates(Map parameters) {
+ new TemplatingEngine().replaceTemplates(new File(absoluteLocalRepoTmpDir), parameters)
+ }
+
+ String getGitRepositoryUrl() {
+ return this.gitProvider.repoUrl(repoTarget, RepoUrlScope.CLIENT)
+ }
+
+ static boolean isCommit(File repoPath, String ref) {
+ if (!ref) {
+ return false
+ }
+
+ try (Git git = Git.open(repoPath)) {
+ // Get all branch and tag names
+ def allRefs = []
+
+ // Add all branch names (without refs/heads/ prefix)
+ git.branchList().call().each { branch -> allRefs.add(branch.name.replaceFirst('refs/heads/', ''))
+ }
+
+ // Add all tag names (without refs/tags/ prefix)
+ git.tagList().call().each { tag -> allRefs.add(tag.name.replaceFirst('refs/tags/', ''))
+ }
+
+ // If the ref matches any branch or tag name, it's not a commit hash
+ if (allRefs.contains(ref)) {
+ return false
+ }
+
+ // If it's not a branch or tag, try to resolve it as a commit
+ def objectId = git.repository.resolve(ref)
+ return objectId != null
+
+ }
+ }
+
+ /**
+ * checks, if file exists in repo in some branch.
+ * @param pathToRepo
+ * @param filename
+ */
+ static boolean existFileInSomeBranch(String repo, String filename) {
+ String filenameToSearch = filename
+ File repoPath = new File(repo + '/.git')
+
+ try (def git = Git.open(repoPath)) {
+ List][ branches = git
+ .branchList()
+ .setListMode(ListBranchCommand.ListMode.ALL)
+ .call()
+
+ for (Ref branch : branches) {
+ String branchName = branch.getName()
+
+ ObjectId commitId = git.repository.resolve(branchName)
+ if (commitId == null) {
+ continue
+ }
+ try (RevWalk revWalk = new RevWalk(git.repository)) {
+ RevCommit commit = revWalk.parseCommit(commitId)
+ try (TreeWalk treeWalk = new TreeWalk(git.repository)) {
+
+ treeWalk.addTree(commit.getTree())
+ treeWalk.setFilter(PathFilter.create(filenameToSearch))
+
+ if (treeWalk.next()) {
+ log.debug("File ${filename} found in branch ${branchName}")
+
+ return true
+ }
+ }
+ }
+ }
+ }
+ log.debug("File ${filename} not found in repository ${repoPath}")
+ return false
+ }
+
+ static boolean isTag(File repo, String ref) {
+ if (!ref) {
+ return false
+ }
+ try (def git = Git.open(repo)) {
+ git.tagList().call().any { it.name.endsWith("/" + ref) || it.name == ref }
+ }
+ }
+
+ private PushCommand createPushCommand(String refSpec) {
+ getGit()
+ .push()
+ .setRemote(getGitRepositoryUrl())
+ .setRefSpecs(new RefSpec(refSpec))
+ .setCredentialsProvider(getCredentialProvider())
+ }
+
+ private Git getGit() {
+ if (gitMemoization != null) {
+ return gitMemoization
+ }
+
+ return gitMemoization = Git.open(new File(absoluteLocalRepoTmpDir))
+ }
+
+ private CredentialsProvider getCredentialProvider() {
+ def auth = this.gitProvider.getCredentials()
+ def passwordAuthentication = new UsernamePasswordCredentialsProvider(auth.username, auth.password)
+ return insecure ? new ChainingCredentialsProvider(new InsecureCredentialProvider(), passwordAuthentication) : passwordAuthentication
+ }
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/git/GitRepoFactory.groovy b/src/main/groovy/com/cloudogu/gitops/git/GitRepoFactory.groovy
index 31e0b3221..9cbc63d04 100644
--- a/src/main/groovy/com/cloudogu/gitops/git/GitRepoFactory.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/git/GitRepoFactory.groovy
@@ -3,20 +3,21 @@ package com.cloudogu.gitops.git
import com.cloudogu.gitops.config.Config
import com.cloudogu.gitops.git.providers.GitProvider
import com.cloudogu.gitops.utils.FileSystemUtils
+
import jakarta.inject.Singleton
@Singleton
class GitRepoFactory {
- protected final Config config
- protected final FileSystemUtils fileSystemUtils
+ protected final Config config
+ protected final FileSystemUtils fileSystemUtils
- GitRepoFactory(Config config, FileSystemUtils fileSystemUtils) {
- this.fileSystemUtils = fileSystemUtils
- this.config = config
- }
+ GitRepoFactory(Config config, FileSystemUtils fileSystemUtils) {
+ this.fileSystemUtils = fileSystemUtils
+ this.config = config
+ }
- GitRepo getRepo(String repoTarget, GitProvider gitProvider) {
- return new GitRepo(config, gitProvider, repoTarget, fileSystemUtils)
- }
+ GitRepo getRepo(String repoTarget, GitProvider gitProvider) {
+ return new GitRepo(config, gitProvider, repoTarget, fileSystemUtils)
+ }
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/git/jgit/helpers/InsecureCredentialProvider.groovy b/src/main/groovy/com/cloudogu/gitops/git/jgit/helpers/InsecureCredentialProvider.groovy
index 6dfc9c866..346b42c44 100644
--- a/src/main/groovy/com/cloudogu/gitops/git/jgit/helpers/InsecureCredentialProvider.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/git/jgit/helpers/InsecureCredentialProvider.groovy
@@ -18,33 +18,32 @@ import org.eclipse.jgit.transport.URIish
* @link https://archive.eclipse.org/jgit/site/4.10.0.201712302008-r/apidocs/org/eclipse/jgit/transport/CredentialsProvider.html
*/
class InsecureCredentialProvider extends CredentialsProvider {
- @Override
- boolean isInteractive() {
- return false
- }
+ @Override
+ boolean isInteractive() {
+ return false
+ }
- @Override
- boolean supports(CredentialItem... items) {
- def message = items.find { it instanceof CredentialItem.InformationalMessage }
- if (message == null) {
- return false
- }
+ @Override
+ boolean supports(CredentialItem... items) {
+ def message = items.find { it instanceof CredentialItem.InformationalMessage }
+ if (message == null) {
+ return false
+ }
- return message.promptText =~ /^A secure connection to .* could not be established/
- }
+ return message.promptText =~ /^A secure connection to .* could not be established/
+ }
- @Override
- boolean get(URIish uri, CredentialItem... items) throws UnsupportedCredentialItem {
- items.findAll { it instanceof CredentialItem.YesNoType }.each {
- if (it.promptText == "Skip SSL verification for this single git operation" ||
- it.promptText =~ /^Skip SSL verification for git operations for repository/) {
- (it as CredentialItem.YesNoType).setValue(true)
- } else if (it.promptText == "Always skip SSL verification for this server from now on") {
- // otherwise we would persistently overwrite our $HOME/.gitconfig
- (it as CredentialItem.YesNoType).setValue(false)
- }
- }
+ @Override
+ boolean get(URIish uri, CredentialItem... items) throws UnsupportedCredentialItem {
+ items.findAll { it instanceof CredentialItem.YesNoType }.each {
+ if (it.promptText == "Skip SSL verification for this single git operation" || it.promptText =~ /^Skip SSL verification for git operations for repository/) {
+ (it as CredentialItem.YesNoType).setValue(true)
+ } else if (it.promptText == "Always skip SSL verification for this server from now on") {
+ // otherwise we would persistently overwrite our $HOME/.gitconfig
+ (it as CredentialItem.YesNoType).setValue(false)
+ }
+ }
- return true
- }
+ return true
+ }
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/git/providers/GitProvider.groovy b/src/main/groovy/com/cloudogu/gitops/git/providers/GitProvider.groovy
index a8cdb9702..90800971e 100644
--- a/src/main/groovy/com/cloudogu/gitops/git/providers/GitProvider.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/git/providers/GitProvider.groovy
@@ -4,59 +4,59 @@ import com.cloudogu.gitops.config.Credentials
interface GitProvider {
- default boolean createRepository(String repoTarget, String description) {
- return createRepository(repoTarget, description, true);
- }
+ default boolean createRepository(String repoTarget, String description) {
+ return createRepository(repoTarget, description, true);
+ }
- boolean createRepository(String repoTarget, String description, boolean initialize)
+ boolean createRepository(String repoTarget, String description, boolean initialize)
- void setRepositoryPermission(String repoTarget, String principal, AccessRole role, Scope scope)
+ void setRepositoryPermission(String repoTarget, String principal, AccessRole role, Scope scope)
- default String repoUrl(String repoTarget) {
- return repoUrl(repoTarget, RepoUrlScope.IN_CLUSTER);
- }
+ default String repoUrl(String repoTarget) {
+ return repoUrl(repoTarget, RepoUrlScope.IN_CLUSTER);
+ }
- String repoUrl(String repoTarget, RepoUrlScope scope);
+ String repoUrl(String repoTarget, RepoUrlScope scope);
- String repoPrefix()
+ String repoPrefix()
- Credentials getCredentials()
+ Credentials getCredentials()
- URI prometheusMetricsEndpoint()
+ URI prometheusMetricsEndpoint()
- /**
- * Deletes the given repository on the provider, if supported.
- * Note: This capability is not used by the current destruction flow,
- * which talks directly to provider-specific clients (e.g. ScmManagerApiClient).*/
- void deleteRepository(String namespace, String repository, boolean prefixNamespace)
+ /**
+ * Deletes the given repository on the provider, if supported.
+ * Note: This capability is not used by the current destruction flow,
+ * which talks directly to provider-specific clients (e.g. ScmManagerApiClient).*/
+ void deleteRepository(String namespace, String repository, boolean prefixNamespace)
- /**
- * Deletes a user account on the provider, if supported.
- * Note: Not used by the current destruction flow; kept as an optional capability
- * on the GitProvider abstraction */
- void deleteUser(String name)
+ /**
+ * Deletes a user account on the provider, if supported.
+ * Note: Not used by the current destruction flow; kept as an optional capability
+ * on the GitProvider abstraction */
+ void deleteUser(String name)
- /**
- * Sets the default branch of a repository, if supported by the provider;
- * kept as an optional capability on the GitProvider abstraction */
- void setDefaultBranch(String repoTarget, String branch)
+ /**
+ * Sets the default branch of a repository, if supported by the provider;
+ * kept as an optional capability on the GitProvider abstraction */
+ void setDefaultBranch(String repoTarget, String branch)
- String getUrl()
+ String getUrl()
- String getProtocol()
+ String getProtocol()
- String getHost()
+ String getHost()
- String getGitOpsUsername()
+ String getGitOpsUsername()
}
enum AccessRole {
- READ, WRITE, MAINTAIN, ADMIN, OWNER
+ READ, WRITE, MAINTAIN, ADMIN, OWNER
}
enum Scope {
- USER, GROUP
+ USER, GROUP
}
/**
@@ -67,9 +67,8 @@ enum Scope {
* regardless of their location.
* If the application itself runs inside Kubernetes, the Service DNS is used;
* otherwise, NodePort (for internal installations) or externalBase (for external ones)
- * is selected automatically.
- */
+ * is selected automatically.*/
enum RepoUrlScope {
- IN_CLUSTER,
- CLIENT
+ IN_CLUSTER,
+ CLIENT
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/git/providers/gitlab/Gitlab.groovy b/src/main/groovy/com/cloudogu/gitops/git/providers/gitlab/Gitlab.groovy
index b73f34354..37c8dbceb 100644
--- a/src/main/groovy/com/cloudogu/gitops/git/providers/gitlab/Gitlab.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/git/providers/gitlab/Gitlab.groovy
@@ -7,7 +7,10 @@ import com.cloudogu.gitops.git.providers.AccessRole
import com.cloudogu.gitops.git.providers.GitProvider
import com.cloudogu.gitops.git.providers.RepoUrlScope
import com.cloudogu.gitops.git.providers.Scope
+
+import java.util.logging.Level
import groovy.util.logging.Slf4j
+
import org.gitlab4j.api.GitLabApi
import org.gitlab4j.api.GitLabApiException
import org.gitlab4j.api.models.AccessLevel
@@ -15,395 +18,377 @@ import org.gitlab4j.api.models.Group
import org.gitlab4j.api.models.Project
import org.gitlab4j.api.models.Visibility
-import java.util.logging.Level
-
@Slf4j
class Gitlab implements GitProvider {
- private final Config config
- private final GitLabApi api
- private GitlabConfig gitlabConfig
-
- Gitlab(Config config, GitlabConfig gitlabConfig) {
- this.config = config
- this.gitlabConfig = gitlabConfig
-
- String url = Objects.requireNonNull(gitlabConfig.getUrl(), "Missing gitlab url in config.scm.gitlab.url").trim()
- String pat = Objects.requireNonNull(gitlabConfig.getCredentials()?.password, "Missing gitlab token").trim()
- this.api = new GitLabApi(url, pat)
- this.api.enableRequestResponseLogging(Level.ALL)
- }
-
- @Override
- boolean createRepository(String repoTarget, String description, boolean initialize) {
- def repoNamespace = repoTarget.split('/', 2)[0]
- def repoName = repoTarget.split('/', 2)[1]
-
-// def repoNamespacePrefixed = config.application.namePrefix + repoNamespace
- // 1) Resolve parent by numeric ID (do NOT treat the ID as a path!)
- Group parent = parentGroup()
- String repoNamespacePath = repoNamespace.toLowerCase()
- String projectPath = repoName.toLowerCase()
-
- long subgroupId = ensureSubgroupUnderParentId(parent, repoNamespacePath)
- String fullProjectPath = "${parentFullPath()}/${repoNamespacePath}/${projectPath}"
-
-
- if (findProject(fullProjectPath).present) {
- log.info("GitLab project already exists: ${fullProjectPath}")
- return false
- }
-
- def project = new Project()
- .withName(repoName)
- .withPath(projectPath)
- .withDescription(description ?: "")
- .withIssuesEnabled(false)
- .withMergeRequestsEnabled(false)
- .withWikiEnabled(false)
- .withSnippetsEnabled(false)
- .withNamespaceId(subgroupId)
- .withInitializeWithReadme(initialize)
- project.visibility = toVisibility(gitlabConfig.defaultVisibility)
-
- def created = api.projectApi.createProject(project)
- log.info("Created GitLab project ${created.getPathWithNamespace()} (id=${created.id})")
- return true
- }
-
- @Override
- void setRepositoryPermission(String repoTarget, String principal, AccessRole role, Scope scope) {
- String fullPath = resolveFullPath(repoTarget)
- Project project = findProjectOrThrow(fullPath)
- AccessLevel level = toAccessLevel(role, scope)
- if (scope == Scope.GROUP) {
- def group = api.groupApi.getGroups(principal)
- .find { it.fullPath == principal || it.path == principal || it.name == principal }
- if (!group) throw new IllegalArgumentException("Group '${principal}' not found")
- api.projectApi.shareProject(project.id, group.id, level, null)
- } else {
- def user = api.userApi.findUsers(principal)
- .find { it.username == principal || it.email == principal }
- if (!user) throw new IllegalArgumentException("User '${principal}' not found")
- api.projectApi.addMember(project.id, user.id, level)
- }
- }
-
- @Override
- String repoUrl(String repoTarget, RepoUrlScope scope) {
- String base = gitlabConfig.url.strip()
- return "${base}/${parentFullPath()}/${repoTarget}.git"
- }
-
- @Override
- String repoPrefix() {
- String base = gitlabConfig.url.strip()
- def prefix = (config.application.namePrefix ?: "").strip()
- return "${base}/${parentFullPath()}/${prefix}"
-
- }
-
-
- @Override
- Credentials getCredentials() {
- return this.gitlabConfig.credentials
- }
-
- @Override
- String getProtocol() {
- return gitlabConfig.url
- }
-
- String getHost() {
- return gitlabConfig.url
- }
-
- @Override
- String getGitOpsUsername() {
- return gitlabConfig.gitOpsUsername
- }
-
- @Override
- String getUrl() {
- return this.gitlabConfig.url
- }
-
-
- /**
- * Prometheus integration is only required for SCM-Manager.
- * GitLab provides its own built-in Prometheus metrics, so we don't expose an endpoint here.
- */
- @Override
- URI prometheusMetricsEndpoint() {
- return null
- }
-
- /**
- * No-op by design. GitLab repository deletion is not managed through this abstraction.
- * Kept for interface compatibility only.
- */
- @Override
- void deleteRepository(String namespace, String repository, boolean prefixNamespace) {
- // intentionally left blank
- }
-
- /**
- * No-op by design. User deletion is not supported or handled through this provider.
- * Kept for interface compatibility only.
- */
- @Override
- void deleteUser(String name) {
- // intentionally left blank
- }
-
- /**
- * No-op by design. Default branch management is not implemented via this abstraction.
- * Kept for interface compatibility only.
- */
- @Override
- void setDefaultBranch(String repoTarget, String branch) {
- // intentionally left blank
- }
-
- private Group parentGroup() {
- String raw = gitlabConfig?.parentGroupId?.trim()
- if (!raw) throw new IllegalArgumentException("--gitlab-group-id is required")
-
- boolean isNumeric = raw ==~ /\d+/
-
- def groupApi = api.getGroupApi()
- if (isNumeric) {
- return groupApi.getGroup(Long.parseLong(raw))
- } else {
- return groupApi.getGroup(raw.replaceAll('^/+', ''))
- }
- }
-
- private String parentFullPath() {
- parentGroup().fullPath
- }
-
- /** Ensure a single-level subgroup exists under 'parent'; return its namespace (group) ID. */
- private long ensureSubgroupUnderParentId(Group parent, String segPath) {
- // 1) Already there?
- Group existing = findDirectSubgroupByPath(parent.id as Long, segPath)
- if (existing != null) return existing.id as Long
-
-
- // 2) Guard against project/subgroup name collision in the same parent
- Project collision = findDirectProjectByPath(parent.id as Long, segPath)
- if (collision != null) {
- throw new IllegalStateException(
- "Cannot create subgroup '${segPath}' under '${parent.fullPath}': " +
- "a project with that path already exists at '${parent.fullPath}/${segPath}'. " +
- "Rename/transfer the project first or choose a different subgroup name."
- )
- }
-
- // 3) Create subgroup
- Group toCreate = new Group()
- .withName(segPath) // display name
- .withPath(segPath) // (lowercase etc.)
- .withParentId(parent.id)
-
-
- try {
- Group created = api.groupApi.addGroup(toCreate)
- log.info("Created group {}", created.fullPath)
- return created.id as Long
- } catch (GitLabApiException e) {
- // If someone created it in parallel, treat 400/409 as "exists" and re-fetch
- if (e.httpStatus in [400, 409]) {
- Group retry = findDirectSubgroupByPath(parent.id as Long, segPath)
- if (retry != null) return retry.id as Long
- }
- def ve = e.hasValidationErrors() ? e.getValidationErrors() : null
- log.error("addGroup failed (parent={}, segPath={}, status={}, message={}, validationErrors={})",
- parent.fullPath, segPath, e.httpStatus, e.getMessage(), ve)
- throw e
- }
- }
-
-
- /** Find a direct subgroup of 'parentId' with the exact path . */
- private Group findDirectSubgroupByPath(Long parentId, String segPath) {
- // uses the overload: getSubGroups(Object idOrPath)
- List] subGroups = api.groupApi.getSubGroups(parentId)
- return subGroups?.find { Group subGroup -> subGroup.path == segPath }
- }
-
-
- /** Find a direct project of 'parentId' with the exact path . */
- private Project findDirectProjectByPath(Long parentId, String path) {
- // uses the overload: getProjects(Object idOrPath)
- List projects = api.groupApi.getProjects(parentId)
- return projects?.find { Project project -> project.path == path }
- }
-
-
- // ---- Helpers ----
- private Optional findProject(String fullPath) {
- try {
- return Optional.ofNullable(api.projectApi.getProject(fullPath))
- } catch (Exception ignore) {
- return Optional.empty()
- }
- }
-
- private Project findProjectOrThrow(String fullPath) {
- return findProject(fullPath).orElseThrow {
- new IllegalStateException("GitLab project '${fullPath}' not found")
- }
- }
-
- private String resolveFullPath(String repoTarget) {
- if (!gitlabConfig.parentGroupId) {
- throw new IllegalStateException("gitlab.parentGroup is not set")
- }
- return "${gitlabConfig.parentGroupId}/${repoTarget}"
- }
-
-
- private static Visibility toVisibility(String s) {
- switch ((s ?: "private").toLowerCase()) {
- case "public": return Visibility.PUBLIC
- case "internal": return Visibility.INTERNAL
- default: return Visibility.PRIVATE
- }
- }
-
-// provider-agnostic AccessRole → GitLab AccessLevel
- private static AccessLevel toAccessLevel(AccessRole role, Scope scope) {
- switch (role) {
- case AccessRole.READ:
- // GitLab: Guests usually can't read private repo code; Reporter can.
- return AccessLevel.REPORTER
- case AccessRole.WRITE:
- // Typical push/merge permissions
- return AccessLevel.DEVELOPER
- case AccessRole.MAINTAIN:
- return AccessLevel.MAINTAINER
- case AccessRole.ADMIN:
- // No separate project-level "admin" → cap at Maintainer
- return AccessLevel.MAINTAINER
- case AccessRole.OWNER:
- // OWNER is meaningful for groups/namespaces; for users on a project we cap to MAINTAINER
- return (scope == Scope.GROUP) ? AccessLevel.OWNER : AccessLevel.MAINTAINER
- default:
- throw new IllegalArgumentException("Unknown role: ${role}")
- }
- }
-
-
- //TODO when git abctraction feature is ready, we will create before merge to main a branch, that
- // contain this code as preservation for oop
- /* ================================= SETUP CODE ====================================
- void setup() {
- log.info("Creating Gitlab Groups")
- def mainGroupName = "${config.application.namePrefix}scm".toString()
- Group mainSCMGroup = this.gitlabApi.groupApi.getGroup(mainGroupName)
- if (!mainSCMGroup) {
- def tempGroup = new Group()
- .withName(mainGroupName)
- .withPath(mainGroupName.toLowerCase())
- .withParentId(null)
-
- mainSCMGroup = this.gitlabApi.groupApi.addGroup(tempGroup)
- }
-
- String argoCDGroupName = 'argocd'
- Optional argoCDGroup = getGroup("${mainGroupName}/${argoCDGroupName}")
- if (argoCDGroup.isEmpty()) {
- def tempGroup = new Group()
- .withName(argoCDGroupName)
- .withPath(argoCDGroupName.toLowerCase())
- .withParentId(mainSCMGroup.id)
-
- argoCDGroup = addGroup(tempGroup)
- }
-
- argoCDGroup.ifPresent(this.&createArgoCDRepos)
-
- String dependencysGroupName = '3rd-party-dependencies'
- Optional dependencysGroup = getGroup("${mainGroupName}/${dependencysGroupName}")
- if (dependencysGroup.isEmpty()) {
- def tempGroup = new Group()
- .withName(dependencysGroupName)
- .withPath(dependencysGroupName.toLowerCase())
- .withParentId(mainSCMGroup.id)
-
- addGroup(tempGroup)
- }
-
- String exercisesGroupName = 'exercises'
- Optional exercisesGroup = getGroup("${mainGroupName}/${exercisesGroupName}")
- if (exercisesGroup.isEmpty()) {
- def tempGroup = new Group()
- .withName(exercisesGroupName)
- .withPath(exercisesGroupName.toLowerCase())
- .withParentId(mainSCMGroup.id)
-
- exercisesGroup = addGroup(tempGroup)
- }
-
- exercisesGroup.ifPresent(this.&createExercisesRepos)
- }
-
- void createRepo(String name, String description) {
- Optional project = getProject("${parentGroup.getFullPath()}/${name}".toString())
- if (project.isEmpty()) {
- Project projectSpec = new Project()
- .withName(name)
- .withDescription(description)
- .withIssuesEnabled(true)
- .withMergeRequestsEnabled(true)
- .withWikiEnabled(true)
- .withSnippetsEnabled(true)
- .withPublic(false)
- .withNamespaceId(this.gitlabConfig.parentGroup.toLong())
- .withInitializeWithReadme(true)
-
- project = Optional.ofNullable(this.gitlabApi.projectApi.createProject(projectSpec))
- log.info("Project ${projectSpec} created in Gitlab!")
- }
- removeBranchProtection(project.get())
- }
-
- void removeBranchProtection(Project project) {
- try {
- this.gitlabApi.getProtectedBranchesApi().unprotectBranch(project.getId(), project.getDefaultBranch())
- log.debug("Unprotected default branch: " + project.getDefaultBranch())
- } catch (Exception ex) {
- log.error("Failed to unprotect default branch '${project.getDefaultBranch()}' for project '${project.getName()}' (ID: ${project.getId()})", ex)
- }
- }
-
-
- private Optional getGroup(String groupName) {
- try {
- return Optional.ofNullable(this.gitlabApi.groupApi.getGroup(groupName))
- } catch (Exception e) {
- return Optional.empty()
- }
- }
-
- private Optional addGroup(Group group) {
- try {
- return Optional.ofNullable(this.gitlabApi.groupApi.addGroup(group))
- } catch (Exception e) {
- return Optional.empty()
- }
- }
-
- private Optional getProject(String projectPath) {
- try {
- return Optional.ofNullable(this.gitlabApi.projectApi.getProject(projectPath))
- } catch (Exception e) {
- return Optional.empty()
-
-
- }
- }
+ private final Config config
+ private final GitLabApi api
+ private GitlabConfig gitlabConfig
+
+ Gitlab(Config config, GitlabConfig gitlabConfig) {
+ this.config = config
+ this.gitlabConfig = gitlabConfig
+
+ String url = Objects.requireNonNull(gitlabConfig.getUrl(), "Missing gitlab url in config.scm.gitlab.url").trim()
+ String pat = Objects.requireNonNull(gitlabConfig.getCredentials()?.password, "Missing gitlab token").trim()
+ this.api = new GitLabApi(url, pat)
+ this.api.enableRequestResponseLogging(Level.ALL)
+ }
+
+ @Override
+ boolean createRepository(String repoTarget, String description, boolean initialize) {
+ def repoNamespace = repoTarget.split('/', 2)[0]
+ def repoName = repoTarget.split('/', 2)[1]
+
+ // def repoNamespacePrefixed = config.application.namePrefix + repoNamespace
+ // 1) Resolve parent by numeric ID (do NOT treat the ID as a path!)
+ Group parent = parentGroup()
+ String repoNamespacePath = repoNamespace.toLowerCase()
+ String projectPath = repoName.toLowerCase()
+
+ long subgroupId = ensureSubgroupUnderParentId(parent, repoNamespacePath)
+ String fullProjectPath = "${parentFullPath()}/${repoNamespacePath}/${projectPath}"
+
+ if (findProject(fullProjectPath).present) {
+ log.info("GitLab project already exists: ${fullProjectPath}")
+ return false
+ }
+
+ def project = new Project()
+ .withName(repoName)
+ .withPath(projectPath)
+ .withDescription(description ?: "")
+ .withIssuesEnabled(false)
+ .withMergeRequestsEnabled(false)
+ .withWikiEnabled(false)
+ .withSnippetsEnabled(false)
+ .withNamespaceId(subgroupId)
+ .withInitializeWithReadme(initialize)
+ project.visibility = toVisibility(gitlabConfig.defaultVisibility)
+
+ def created = api.projectApi.createProject(project)
+ log.info("Created GitLab project ${created.getPathWithNamespace()} (id=${created.id})")
+ return true
+ }
+
+ @Override
+ void setRepositoryPermission(String repoTarget, String principal, AccessRole role, Scope scope) {
+ String fullPath = resolveFullPath(repoTarget)
+ Project project = findProjectOrThrow(fullPath)
+ AccessLevel level = toAccessLevel(role, scope)
+ if (scope == Scope.GROUP) {
+ def group = api.groupApi.getGroups(principal)
+ .find { it.fullPath == principal || it.path == principal || it.name == principal }
+ if (!group) throw new IllegalArgumentException("Group '${principal}' not found")
+ api.projectApi.shareProject(project.id, group.id, level, null)
+ } else {
+ def user = api.userApi.findUsers(principal)
+ .find { it.username == principal || it.email == principal }
+ if (!user) throw new IllegalArgumentException("User '${principal}' not found")
+ api.projectApi.addMember(project.id, user.id, level)
+ }
+ }
+
+ @Override
+ String repoUrl(String repoTarget, RepoUrlScope scope) {
+ String base = gitlabConfig.url.strip()
+ return "${base}/${parentFullPath()}/${repoTarget}.git"
+ }
+
+ @Override
+ String repoPrefix() {
+ String base = gitlabConfig.url.strip()
+ def prefix = (config.application.namePrefix ?: "").strip()
+ return "${base}/${parentFullPath()}/${prefix}"
+
+ }
+
+ @Override
+ Credentials getCredentials() {
+ return this.gitlabConfig.credentials
+ }
+
+ @Override
+ String getProtocol() {
+ return gitlabConfig.url
+ }
+
+ String getHost() {
+ return gitlabConfig.url
+ }
+
+ @Override
+ String getGitOpsUsername() {
+ return gitlabConfig.gitOpsUsername
+ }
+
+ @Override
+ String getUrl() {
+ return this.gitlabConfig.url
+ }
+
+ /**
+ * Prometheus integration is only required for SCM-Manager.
+ * GitLab provides its own built-in Prometheus metrics, so we don't expose an endpoint here.*/
+ @Override
+ URI prometheusMetricsEndpoint() {
+ return null
+ }
+
+ /**
+ * No-op by design. GitLab repository deletion is not managed through this abstraction.
+ * Kept for interface compatibility only.*/
+ @Override
+ void deleteRepository(String namespace, String repository, boolean prefixNamespace) {
+ // intentionally left blank
+ }
+
+ /**
+ * No-op by design. User deletion is not supported or handled through this provider.
+ * Kept for interface compatibility only.*/
+ @Override
+ void deleteUser(String name) {
+ // intentionally left blank
+ }
+
+ /**
+ * No-op by design. Default branch management is not implemented via this abstraction.
+ * Kept for interface compatibility only.*/
+ @Override
+ void setDefaultBranch(String repoTarget, String branch) {
+ // intentionally left blank
+ }
+
+ private Group parentGroup() {
+ String raw = gitlabConfig?.parentGroupId?.trim()
+ if (!raw) throw new IllegalArgumentException("--gitlab-group-id is required")
+
+ boolean isNumeric = raw ==~ /\d+/
+
+ def groupApi = api.getGroupApi()
+ if (isNumeric) {
+ return groupApi.getGroup(Long.parseLong(raw))
+ } else {
+ return groupApi.getGroup(raw.replaceAll('^/+', ''))
+ }
+ }
+
+ private String parentFullPath() {
+ parentGroup().fullPath
+ }
+
+ /** Ensure a single-level subgroup exists under 'parent'; return its namespace (group) ID. */
+ private long ensureSubgroupUnderParentId(Group parent, String segPath) {
+ // 1) Already there?
+ Group existing = findDirectSubgroupByPath(parent.id as Long, segPath)
+ if (existing != null) return existing.id as Long
+
+
+ // 2) Guard against project/subgroup name collision in the same parent
+ Project collision = findDirectProjectByPath(parent.id as Long, segPath)
+ if (collision != null) {
+ throw new IllegalStateException("Cannot create subgroup '${segPath}' under '${parent.fullPath}': " + "a project with that path already exists at '${parent.fullPath}/${segPath}'. " +
+ "Rename/transfer the project first or choose a different subgroup name.")
+ }
+
+ // 3) Create subgroup
+ Group toCreate = new Group()
+ .withName(segPath) // display name
+ .withPath(segPath) // (lowercase etc.)
+ .withParentId(parent.id)
+
+ try {
+ Group created = api.groupApi.addGroup(toCreate)
+ log.info("Created group {}", created.fullPath)
+ return created.id as Long
+ } catch (GitLabApiException e) {
+ // If someone created it in parallel, treat 400/409 as "exists" and re-fetch
+ if (e.httpStatus in [400, 409]) {
+ Group retry = findDirectSubgroupByPath(parent.id as Long, segPath)
+ if (retry != null) return retry.id as Long
+ }
+ def ve = e.hasValidationErrors() ? e.getValidationErrors() : null
+ log.error("addGroup failed (parent={}, segPath={}, status={}, message={}, validationErrors={})",
+ parent.fullPath, segPath, e.httpStatus, e.getMessage(), ve)
+ throw e
+ }
+ }
+
+ /** Find a direct subgroup of 'parentId' with the exact path . */
+ private Group findDirectSubgroupByPath(Long parentId, String segPath) {
+ // uses the overload: getSubGroups(Object idOrPath)
+ List subGroups = api.groupApi.getSubGroups(parentId)
+ return subGroups?.find { Group subGroup -> subGroup.path == segPath }
+ }
+
+ /** Find a direct project of 'parentId' with the exact path . */
+ private Project findDirectProjectByPath(Long parentId, String path) {
+ // uses the overload: getProjects(Object idOrPath)
+ List projects = api.groupApi.getProjects(parentId)
+ return projects?.find { Project project -> project.path == path }
+ }
+
+ // ---- Helpers ----
+ private Optional findProject(String fullPath) {
+ try {
+ return Optional.ofNullable(api.projectApi.getProject(fullPath))
+ } catch (Exception ignore) {
+ return Optional.empty()
+ }
+ }
+
+ private Project findProjectOrThrow(String fullPath) {
+ return findProject(fullPath).orElseThrow {
+ new IllegalStateException("GitLab project '${fullPath}' not found")
+ }
+ }
+
+ private String resolveFullPath(String repoTarget) {
+ if (!gitlabConfig.parentGroupId) {
+ throw new IllegalStateException("gitlab.parentGroup is not set")
+ }
+ return "${gitlabConfig.parentGroupId}/${repoTarget}"
+ }
+
+ private static Visibility toVisibility(String s) {
+ switch ((s ?: "private").toLowerCase()) {
+ case "public": return Visibility.PUBLIC
+ case "internal": return Visibility.INTERNAL
+ default: return Visibility.PRIVATE
+ }
+ }
+
+ // provider-agnostic AccessRole → GitLab AccessLevel
+ private static AccessLevel toAccessLevel(AccessRole role, Scope scope) {
+ switch (role) {
+ case AccessRole.READ:
+ // GitLab: Guests usually can't read private repo code; Reporter can.
+ return AccessLevel.REPORTER
+ case AccessRole.WRITE:
+ // Typical push/merge permissions
+ return AccessLevel.DEVELOPER
+ case AccessRole.MAINTAIN:
+ return AccessLevel.MAINTAINER
+ case AccessRole.ADMIN:
+ // No separate project-level "admin" → cap at Maintainer
+ return AccessLevel.MAINTAINER
+ case AccessRole.OWNER:
+ // OWNER is meaningful for groups/namespaces; for users on a project we cap to MAINTAINER
+ return (scope == Scope.GROUP) ? AccessLevel.OWNER : AccessLevel.MAINTAINER
+ default:
+ throw new IllegalArgumentException("Unknown role: ${role}")
+ }
+ }
+
+ //TODO when git abctraction feature is ready, we will create before merge to main a branch, that
+ // contain this code as preservation for oop
+ /* ================================= SETUP CODE ====================================
+ void setup() {
+ log.info("Creating Gitlab Groups")
+ def mainGroupName = "${config.application.namePrefix}scm".toString()
+ Group mainSCMGroup = this.gitlabApi.groupApi.getGroup(mainGroupName)
+ if (!mainSCMGroup) {
+ def tempGroup = new Group()
+ .withName(mainGroupName)
+ .withPath(mainGroupName.toLowerCase())
+ .withParentId(null)
+
+ mainSCMGroup = this.gitlabApi.groupApi.addGroup(tempGroup)
+ }
+
+ String argoCDGroupName = 'argocd'
+ Optional argoCDGroup = getGroup("${mainGroupName}/${argoCDGroupName}")
+ if (argoCDGroup.isEmpty()) {
+ def tempGroup = new Group()
+ .withName(argoCDGroupName)
+ .withPath(argoCDGroupName.toLowerCase())
+ .withParentId(mainSCMGroup.id)
+
+ argoCDGroup = addGroup(tempGroup)
+ }
+
+ argoCDGroup.ifPresent(this.&createArgoCDRepos)
+
+ String dependencysGroupName = '3rd-party-dependencies'
+ Optional dependencysGroup = getGroup("${mainGroupName}/${dependencysGroupName}")
+ if (dependencysGroup.isEmpty()) {
+ def tempGroup = new Group()
+ .withName(dependencysGroupName)
+ .withPath(dependencysGroupName.toLowerCase())
+ .withParentId(mainSCMGroup.id)
+
+ addGroup(tempGroup)
+ }
+
+ String exercisesGroupName = 'exercises'
+ Optional exercisesGroup = getGroup("${mainGroupName}/${exercisesGroupName}")
+ if (exercisesGroup.isEmpty()) {
+ def tempGroup = new Group()
+ .withName(exercisesGroupName)
+ .withPath(exercisesGroupName.toLowerCase())
+ .withParentId(mainSCMGroup.id)
+
+ exercisesGroup = addGroup(tempGroup)
+ }
+
+ exercisesGroup.ifPresent(this.&createExercisesRepos)
+ }
+
+ void createRepo(String name, String description) {
+ Optional project = getProject("${parentGroup.getFullPath()}/${name}".toString())
+ if (project.isEmpty()) {
+ Project projectSpec = new Project()
+ .withName(name)
+ .withDescription(description)
+ .withIssuesEnabled(true)
+ .withMergeRequestsEnabled(true)
+ .withWikiEnabled(true)
+ .withSnippetsEnabled(true)
+ .withPublic(false)
+ .withNamespaceId(this.gitlabConfig.parentGroup.toLong())
+ .withInitializeWithReadme(true)
+
+ project = Optional.ofNullable(this.gitlabApi.projectApi.createProject(projectSpec))
+ log.info("Project ${projectSpec} created in Gitlab!")
+ }
+ removeBranchProtection(project.get())
+ }
+
+ void removeBranchProtection(Project project) {
+ try {
+ this.gitlabApi.getProtectedBranchesApi().unprotectBranch(project.getId(), project.getDefaultBranch())
+ log.debug("Unprotected default branch: " + project.getDefaultBranch())
+ } catch (Exception ex) {
+ log.error("Failed to unprotect default branch '${project.getDefaultBranch()}' for project '${project.getName()}' (ID: ${project.getId()})", ex)
+ }
+ }
+
+
+ private Optional getGroup(String groupName) {
+ try {
+ return Optional.ofNullable(this.gitlabApi.groupApi.getGroup(groupName))
+ } catch (Exception e) {
+ return Optional.empty()
+ }
+ }
+
+ private Optional addGroup(Group group) {
+ try {
+ return Optional.ofNullable(this.gitlabApi.groupApi.addGroup(group))
+ } catch (Exception e) {
+ return Optional.empty()
+ }
+ }
+
+ private Optional getProject(String projectPath) {
+ try {
+ return Optional.ofNullable(this.gitlabApi.projectApi.getProject(projectPath))
+ } catch (Exception e) {
+ return Optional.empty()
+
+
+ }
+ }
*/
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/Permission.groovy b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/Permission.groovy
index ffe623bcd..78178ed70 100644
--- a/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/Permission.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/Permission.groovy
@@ -1,24 +1,24 @@
package com.cloudogu.gitops.git.providers.scmmanager
class Permission {
- final String name
- final Role role
- final List verbs
- final boolean groupPermission
+ final String name
+ final Role role
+ final List verbs
+ final boolean groupPermission
- Permission(String name, Role role, boolean groupPermission = false, List verbs = []) {
- this.name = name
- this.role = role
- this.verbs = verbs
- this.groupPermission = groupPermission
- }
+ Permission(String name, Role role, boolean groupPermission = false, List verbs = []) {
+ this.name = name
+ this.role = role
+ this.verbs = verbs
+ this.groupPermission = groupPermission
+ }
- @Override
- String toString() {
- "Permission{name='$name', role=$role, verbs=$verbs, groupPermission=$groupPermission}"
- }
+ @Override
+ String toString() {
+ "Permission{name='$name', role=$role, verbs=$verbs, groupPermission=$groupPermission}"
+ }
- enum Role {
- READ, WRITE,OWNER
- }
+ enum Role {
+ READ, WRITE, OWNER
+ }
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/ScmManager.groovy b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/ScmManager.groovy
index 473cf2dca..3ef2b6a58 100644
--- a/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/ScmManager.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/ScmManager.groovy
@@ -12,184 +12,180 @@ import com.cloudogu.gitops.git.providers.scmmanager.api.Repository
import com.cloudogu.gitops.git.providers.scmmanager.api.ScmManagerApiClient
import com.cloudogu.gitops.kubernetes.api.K8sClient
import com.cloudogu.gitops.utils.NetworkingUtils
+
import groovy.util.logging.Slf4j
+
import retrofit2.Response
@Slf4j
class ScmManager implements GitProvider {
- ScmManagerUrlResolver urls
- ScmManagerApiClient apiClient
- ScmManagerConfig scmmConfig
-
- NetworkingUtils networkingUtils
- HelmStrategy helmStrategy
- K8sClient k8sClient
- Config config
- ScmManagerSetup scmManagerSetup
-
- ScmManager(Config config, ScmManagerConfig scmmConfig, HelmStrategy helmStrategy, K8sClient k8sClient, NetworkingUtils networkingUtils, Boolean installNeeded = false) {
- this.scmmConfig = scmmConfig
- this.config = config
- this.helmStrategy = helmStrategy
- this.k8sClient = k8sClient
- this.networkingUtils = networkingUtils
- init(installNeeded)
- }
-
- void init(installNeeded) {
- // --- Init Setup ---
- if (this.scmmConfig.internal && installNeeded) {
- this.scmManagerSetup = new ScmManagerSetup(this)
- this.scmManagerSetup.setupHelm()
- this.urls = new ScmManagerUrlResolver(this.config, this.scmmConfig, this.k8sClient, this.networkingUtils)
- this.apiClient = new ScmManagerApiClient(this.urls.clientApiBase().toString(), this.scmmConfig.credentials, this.config.application.insecure)
- this.scmManagerSetup.waitForScmmAvailable()
- this.scmManagerSetup.configure()
- } else {
- this.urls = new ScmManagerUrlResolver(this.config, this.scmmConfig, this.k8sClient, this.networkingUtils)
- this.apiClient = new ScmManagerApiClient(this.urls.clientApiBase().toString(), this.scmmConfig.credentials, this.config.application.insecure)
- }
- }
-
- // --- Git operations ---
- @Override
- boolean createRepository(String repoTarget, String description, boolean initialize) {
- def repoNamespace = repoTarget.split('/', 2)[0]
- def repoName = repoTarget.split('/', 2)[1]
- def repo = new Repository(repoNamespace, repoName, description ?: "")
- Response response = apiClient.repositoryApi().create(repo, initialize).execute()
- return handle201or409(response, "Repository ${repoNamespace}/${repoName}")
- }
-
- @Override
- void setRepositoryPermission(String repoTarget, String principal, AccessRole role, Scope scope) {
- def repoNamespace = repoTarget.split('/', 2)[0]
- def repoName = repoTarget.split('/', 2)[1]
-
- boolean isGroup = (scope == Scope.GROUP)
- Permission.Role scmManagerRole = mapToScmManager(role)
- def permission = new Permission(principal, scmManagerRole, isGroup)
-
- Response response = apiClient.repositoryApi().createPermission(repoNamespace, repoName, permission).execute()
- handle201or409(response, "Permission on ${repoNamespace}/${repoName}")
- }
-
- @Override
- Credentials getCredentials() {
- return this.scmmConfig.credentials
- }
-
-
- @Override
- String getGitOpsUsername() {
- return scmmConfig.gitOpsUsername
- }
-
- // --- In-cluster / Endpoints ---
- /** In-cluster base …/scm (without trailing slash) */
- @Override
- String getUrl() {
- return urls.inClusterBase().toString()
- }
-
- /** In-cluster repo prefix: …/scm//[] */
- @Override
- String repoPrefix() {
- return urls.inClusterRepoPrefix()
- }
-
-
- /** …/scm/// */
- @Override
- String repoUrl(String repoTarget, RepoUrlScope scope) {
- switch (scope) {
- case RepoUrlScope.CLIENT:
- return urls.clientRepoUrl(repoTarget)
- case RepoUrlScope.IN_CLUSTER:
- return urls.inClusterRepoUrl(repoTarget)
- default:
- return urls.inClusterRepoUrl(repoTarget)
- }
- }
-
- @Override
- String getProtocol() {
- return urls.inClusterBase().scheme // e.g. "http"
- }
-
- @Override
- String getHost() {
- return urls.inClusterBase().host // e.g. "scmm.ns.svc.cluster.local"
- }
-
- /** …/scm/api/v2/metrics/prometheus — client-side, typically scraped externally */
- @Override
- URI prometheusMetricsEndpoint() {
- return urls.prometheusEndpoint()
- }
-
- /**
- * No-op by design. Not used: ScmmDestructionHandler deletes repositories via ScmManagerApiClient.
- * Kept for interface compatibility only. */
- @Override
- void deleteRepository(String namespace, String repository, boolean prefixNamespace) {
- // intentionally left blank
- }
-
- /**
- * No-op by design. Not used: ScmmDestructionHandler deletes users via ScmManagerApiClient.
- * Kept for interface compatibility only. */
- @Override
- void deleteUser(String name) {
- // intentionally left blank
- }
-
- /**
- * No-op by design. Default branch management is not implemented via this abstraction.
- * Kept for interface compatibility only.
- */
- @Override
- void setDefaultBranch(String repoTarget, String branch) {
- // intentionally left blank
- }
-
- // --- helpers ---
- private static Permission.Role mapToScmManager(AccessRole role) {
- switch (role) {
- case AccessRole.READ: return Permission.Role.READ
- case AccessRole.WRITE: return Permission.Role.WRITE
- case AccessRole.MAINTAIN:
- // SCM-manager doesn't know MAINTAIN -> downgrade to WRITE
- log.warn("SCM-Manager: Mapping MAINTAIN → WRITE")
- return Permission.Role.WRITE
- case AccessRole.ADMIN: return Permission.Role.OWNER
- case AccessRole.OWNER: return Permission.Role.OWNER
- }
- }
-
- private static boolean handle201or409(Response> response, String what) {
- int code = response.code()
- if (code == 409) {
- log.debug("${what} already exists — ignoring (HTTP 409)")
- return false
- } else if (code != 201) {
- throw new RuntimeException("Could not create ${what}" +
- "HTTP Details: ${response.code()} ${response.message()}: ${response.errorBody().string()}")
- }
- return true// because its created
- }
-
- /** Test-only constructor (package-private on purpose). */
- ScmManager(Config config, ScmManagerConfig scmmConfig,
- ScmManagerUrlResolver urls,
- ScmManagerApiClient apiClient) {
- this.scmmConfig = Objects.requireNonNull(scmmConfig, "scmmConfig must not be null")
- this.urls = Objects.requireNonNull(urls, "urls must not be null")
- this.apiClient = apiClient ?: new ScmManagerApiClient(
- urls.clientApiBase().toString(),
- scmmConfig.credentials,
- Objects.requireNonNull(config, "config must not be null").application.insecure
- )
- }
+ ScmManagerUrlResolver urls
+ ScmManagerApiClient apiClient
+ ScmManagerConfig scmmConfig
+
+ NetworkingUtils networkingUtils
+ HelmStrategy helmStrategy
+ K8sClient k8sClient
+ Config config
+ ScmManagerSetup scmManagerSetup
+
+ ScmManager(Config config, ScmManagerConfig scmmConfig, HelmStrategy helmStrategy, K8sClient k8sClient, NetworkingUtils networkingUtils, Boolean installNeeded = false) {
+ this.scmmConfig = scmmConfig
+ this.config = config
+ this.helmStrategy = helmStrategy
+ this.k8sClient = k8sClient
+ this.networkingUtils = networkingUtils
+ init(installNeeded)
+ }
+
+ void init(installNeeded) {
+ // --- Init Setup ---
+ if (this.scmmConfig.internal && installNeeded) {
+ this.scmManagerSetup = new ScmManagerSetup(this)
+ this.scmManagerSetup.setupHelm()
+ this.urls = new ScmManagerUrlResolver(this.config, this.scmmConfig, this.k8sClient, this.networkingUtils)
+ this.apiClient = new ScmManagerApiClient(this.urls.clientApiBase().toString(), this.scmmConfig.credentials, this.config.application.insecure)
+ this.scmManagerSetup.waitForScmmAvailable()
+ this.scmManagerSetup.configure()
+ } else {
+ this.urls = new ScmManagerUrlResolver(this.config, this.scmmConfig, this.k8sClient, this.networkingUtils)
+ this.apiClient = new ScmManagerApiClient(this.urls.clientApiBase().toString(), this.scmmConfig.credentials, this.config.application.insecure)
+ }
+ }
+
+ // --- Git operations ---
+ @Override
+ boolean createRepository(String repoTarget, String description, boolean initialize) {
+ def repoNamespace = repoTarget.split('/', 2)[0]
+ def repoName = repoTarget.split('/', 2)[1]
+ def repo = new Repository(repoNamespace, repoName, description ?: "")
+ Response response = apiClient.repositoryApi().create(repo, initialize).execute()
+ return handle201or409(response, "Repository ${repoNamespace}/${repoName}")
+ }
+
+ @Override
+ void setRepositoryPermission(String repoTarget, String principal, AccessRole role, Scope scope) {
+ def repoNamespace = repoTarget.split('/', 2)[0]
+ def repoName = repoTarget.split('/', 2)[1]
+
+ boolean isGroup = (scope == Scope.GROUP)
+ Permission.Role scmManagerRole = mapToScmManager(role)
+ def permission = new Permission(principal, scmManagerRole, isGroup)
+
+ Response response = apiClient.repositoryApi().createPermission(repoNamespace, repoName, permission).execute()
+ handle201or409(response, "Permission on ${repoNamespace}/${repoName}")
+ }
+
+ @Override
+ Credentials getCredentials() {
+ return this.scmmConfig.credentials
+ }
+
+ @Override
+ String getGitOpsUsername() {
+ return scmmConfig.gitOpsUsername
+ }
+
+ // --- In-cluster / Endpoints ---
+ /** In-cluster base …/scm (without trailing slash) */
+ @Override
+ String getUrl() {
+ return urls.inClusterBase().toString()
+ }
+
+ /** In-cluster repo prefix: …/scm//[] */
+ @Override
+ String repoPrefix() {
+ return urls.inClusterRepoPrefix()
+ }
+
+ /** …/scm/// */
+ @Override
+ String repoUrl(String repoTarget, RepoUrlScope scope) {
+ switch (scope) {
+ case RepoUrlScope.CLIENT:
+ return urls.clientRepoUrl(repoTarget)
+ case RepoUrlScope.IN_CLUSTER:
+ return urls.inClusterRepoUrl(repoTarget)
+ default:
+ return urls.inClusterRepoUrl(repoTarget)
+ }
+ }
+
+ @Override
+ String getProtocol() {
+ return urls.inClusterBase().scheme // e.g. "http"
+ }
+
+ @Override
+ String getHost() {
+ return urls.inClusterBase().host // e.g. "scmm.ns.svc.cluster.local"
+ }
+
+ /** …/scm/api/v2/metrics/prometheus — client-side, typically scraped externally */
+ @Override
+ URI prometheusMetricsEndpoint() {
+ return urls.prometheusEndpoint()
+ }
+
+ /**
+ * No-op by design. Not used: ScmmDestructionHandler deletes repositories via ScmManagerApiClient.
+ * Kept for interface compatibility only. */
+ @Override
+ void deleteRepository(String namespace, String repository, boolean prefixNamespace) {
+ // intentionally left blank
+ }
+
+ /**
+ * No-op by design. Not used: ScmmDestructionHandler deletes users via ScmManagerApiClient.
+ * Kept for interface compatibility only. */
+ @Override
+ void deleteUser(String name) {
+ // intentionally left blank
+ }
+
+ /**
+ * No-op by design. Default branch management is not implemented via this abstraction.
+ * Kept for interface compatibility only.*/
+ @Override
+ void setDefaultBranch(String repoTarget, String branch) {
+ // intentionally left blank
+ }
+
+ // --- helpers ---
+ private static Permission.Role mapToScmManager(AccessRole role) {
+ switch (role) {
+ case AccessRole.READ: return Permission.Role.READ
+ case AccessRole.WRITE: return Permission.Role.WRITE
+ case AccessRole.MAINTAIN:
+ // SCM-manager doesn't know MAINTAIN -> downgrade to WRITE
+ log.warn("SCM-Manager: Mapping MAINTAIN → WRITE")
+ return Permission.Role.WRITE
+ case AccessRole.ADMIN: return Permission.Role.OWNER
+ case AccessRole.OWNER: return Permission.Role.OWNER
+ }
+ }
+
+ private static boolean handle201or409(Response> response, String what) {
+ int code = response.code()
+ if (code == 409) {
+ log.debug("${what} already exists — ignoring (HTTP 409)")
+ return false
+ } else if (code != 201) {
+ throw new RuntimeException("Could not create ${what}" + "HTTP Details: ${response.code()} ${response.message()}: ${response.errorBody().string()}")
+ }
+ return true // because its created
+ }
+
+ /** Test-only constructor (package-private on purpose). */
+ ScmManager(Config config, ScmManagerConfig scmmConfig,
+ ScmManagerUrlResolver urls,
+ ScmManagerApiClient apiClient) {
+ this.scmmConfig = Objects.requireNonNull(scmmConfig, "scmmConfig must not be null")
+ this.urls = Objects.requireNonNull(urls, "urls must not be null")
+ this.apiClient = apiClient ?: new ScmManagerApiClient(urls.clientApiBase().toString(),
+ scmmConfig.credentials,
+ Objects.requireNonNull(config, "config must not be null").application.insecure)
+ }
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/ScmManagerSetup.groovy b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/ScmManagerSetup.groovy
index 33f0008d0..9511a3b9b 100644
--- a/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/ScmManagerSetup.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/ScmManagerSetup.groovy
@@ -5,184 +5,170 @@ import com.cloudogu.gitops.git.providers.scmmanager.api.ScmManagerUser
import com.cloudogu.gitops.utils.FileSystemUtils
import com.cloudogu.gitops.utils.MapUtils
import com.cloudogu.gitops.utils.TemplatingEngine
+
import groovy.util.logging.Slf4j
@Slf4j
class ScmManagerSetup {
- private ScmManager scmManager
-
- static final String HELM_VALUES_PATH = "argocd/cluster-resources/apps/scm-manager/templates/values.ftl.yaml"
-
- ScmManagerSetup(ScmManager scmManager) {
- this.scmManager = scmManager
- }
-
- void waitForScmmAvailable(int timeoutSeconds = 180, int intervalMillis = 5000, int startDelay = 0) {
- long startTime = System.currentTimeMillis()
- long timeoutMillis = timeoutSeconds * 1000L
- sleep(startDelay)
- while (System.currentTimeMillis() - startTime < timeoutMillis) {
- try {
- def call = scmManager.apiClient.generalApi().checkScmmAvailable()
- def response = call.execute()
-
- if (response.successful) {
- return
- }
- } catch (Exception e) {
- log.debug("Waiting for SCM-Manager... Error: ${e.message}")
- }
-
-
- sleep(intervalMillis)
- }
- throw new RuntimeException("Timeout: SCM-Manager did not respond with 200 OK within ${timeoutSeconds} seconds")
- }
-
- void configure() {
- installScmmPlugins()
- setSetupConfigs()
- if (this.scmManager.config.jenkins.active) {
- configureJenkinsPlugin()
- }
- addDefaultUsers()
- log.info("ScmManager Setup finished!")
- }
-
- void setupHelm() {
- def releaseName = 'scmm'
-
- def templatedMap = TemplatingEngine.templateToMap(HELM_VALUES_PATH, [
- host : this.scmManager.scmmConfig.ingress,
- username : this.scmManager.scmmConfig.credentials.username,
- password : this.scmManager.scmmConfig.credentials.password,
- helm : this.scmManager.scmmConfig.helm,
- releaseName: releaseName
- ])
-
- def helmConfig = this.scmManager.scmmConfig.helm
- def mergedMap = MapUtils.deepMerge(helmConfig.values, templatedMap)
- def tempValuesPath = new FileSystemUtils().writeTempFile(mergedMap)
- this.scmManager.helmStrategy.deployFeature(
- helmConfig.repoURL,
- 'scm-manager',
- helmConfig.chart,
- helmConfig.version,
- this.scmManager.scmmConfig.namespace,
- releaseName,
- tempValuesPath
- )
- }
-
- def installScmmPlugins() {
-
- if (this.scmManager.config.scm.scmManager.skipPlugins) {
- log.debug("Skipping SCM plugin installation")
- return
- }
-
- def pluginNames = [
- "scm-mail-plugin",
- "scm-review-plugin",
- "scm-code-editor-plugin",
- "scm-editor-plugin",
- "scm-landingpage-plugin",
- "scm-el-plugin",
- "scm-readme-plugin",
- "scm-webhook-plugin",
- "scm-ci-plugin",
- "scm-metrics-prometheus-plugin"
- ]
-
- if (this.scmManager.config.jenkins.active) {
- pluginNames.add("scm-jenkins-plugin")
- }
- Boolean restartForThisPlugin = false
- pluginNames.each { String pluginName ->
- log.debug("Installing Plugin ${pluginName} ...")
- restartForThisPlugin = !this.scmManager.config.scm.scmManager.skipRestart && pluginName == pluginNames.last()
- ScmManagerApiClient.handleApiResponse(scmManager.apiClient.pluginApi().install(pluginName, restartForThisPlugin))
- }
-
- log.debug("SCM-Manager plugin installation finished successfully!")
- if (restartForThisPlugin) {
- waitForScmmAvailable(180,2000,100)
- }
- }
-
- void setSetupConfigs() {
- def setupConfigs = [
- enableProxy : false,
- proxyPort : 8080,
- proxyServer : "proxy.mydomain.com",
- proxyUser : null,
- proxyPassword : null,
- realmDescription : "SONIA :: SCM Manager",
- disableGroupingGrid : false,
- dateFormat : "YYYY-MM-DD HH:mm:ss",
- anonymousAccessEnabled : false,
- anonymousMode : "OFF",
- baseUrl : this.scmManager.url,
- forceBaseUrl : false,
- loginAttemptLimit : -1,
- proxyExcludes : [],
- skipFailedAuthenticators: false,
- pluginUrl : "https://plugin-center-api.scm-manager.org/api/v1/plugins/{version}?os={os}&arch={arch}",
- loginAttemptLimitTimeout: 300,
- enabledXsrfProtection : true,
- namespaceStrategy : "CustomNamespaceStrategy",
- loginInfoUrl : "https://login-info.scm-manager.org/api/v1/login-info",
- releaseFeedUrl : "https://scm-manager.org/download/rss.xml",
- mailDomainName : "scm-manager.local",
- adminGroups : [],
- adminUsers : []
- ]
-
- ScmManagerApiClient.handleApiResponse(scmManager.apiClient.generalApi().setConfig(setupConfigs))
- log.debug("Successfully added SCMM Setup Configs")
- }
-
- void configureJenkinsPlugin() {
-
- def jenkinsPluginConfig = [
- disableRepositoryConfiguration: false,
- disableMercurialTrigger : false,
- disableGitTrigger : false,
- disableEventTrigger : false,
- url : this.scmManager.config.jenkins.urlForScm
- ] as Map
-
- ScmManagerApiClient.handleApiResponse(this.scmManager.apiClient.pluginApi().configureJenkinsPlugin(jenkinsPluginConfig))
- log.debug("Successfully configured JenkinsPlugin in SCM-Manager.")
- }
-
- void addDefaultUsers() {
- def metricsUsername = "${this.scmManager.config.application.namePrefix}metrics"
- addUser(this.scmManager.scmmConfig.gitOpsUsername, this.scmManager.scmmConfig.password)
- addUser(metricsUsername, this.scmManager.scmmConfig.password)
- grantUserPermissions(metricsUsername, ["metrics:read"])
- }
-
- void addUser(String username, String password, String email = 'changeme@test.local') {
- ScmManagerUser userRequest = [
- name : username,
- displayName: username,
- mail : email,
- external : false,
- password : password,
- active : true,
- _links : [:]
- ]
- ScmManagerApiClient.handleApiResponse(scmManager.apiClient.usersApi().addUser(userRequest))
- log.debug("Successfully created SCM-Manager User.")
- }
-
- void grantUserPermissions(String username, List permissions) {
- def permissionBody = [
- permissions: permissions
- ]
- ScmManagerApiClient.handleApiResponse(scmManager.apiClient.usersApi().setPermissionForUser(username, permissionBody))
- log.debug("Granted permissions ${permissions} to user ${username}.")
- }
+ private ScmManager scmManager
+
+ static final String HELM_VALUES_PATH = "argocd/cluster-resources/apps/scm-manager/templates/values.ftl.yaml"
+
+ ScmManagerSetup(ScmManager scmManager) {
+ this.scmManager = scmManager
+ }
+
+ void waitForScmmAvailable(int timeoutSeconds = 180, int intervalMillis = 5000, int startDelay = 0) {
+ long startTime = System.currentTimeMillis()
+ long timeoutMillis = timeoutSeconds * 1000L
+ sleep(startDelay)
+ while (System.currentTimeMillis() - startTime < timeoutMillis) {
+ try {
+ def call = scmManager.apiClient.generalApi().checkScmmAvailable()
+ def response = call.execute()
+
+ if (response.successful) {
+ return
+ }
+ } catch (Exception e) {
+ log.debug("Waiting for SCM-Manager... Error: ${e.message}")
+ }
+
+ sleep(intervalMillis)
+ }
+ throw new RuntimeException("Timeout: SCM-Manager did not respond with 200 OK within ${timeoutSeconds} seconds")
+ }
+
+ void configure() {
+ installScmmPlugins()
+ setSetupConfigs()
+ if (this.scmManager.config.jenkins.active) {
+ configureJenkinsPlugin()
+ }
+ addDefaultUsers()
+ log.info("ScmManager Setup finished!")
+ }
+
+ void setupHelm() {
+ def releaseName = 'scmm'
+
+ def templatedMap = TemplatingEngine.templateToMap(HELM_VALUES_PATH, [host : this.scmManager.scmmConfig.ingress,
+ username : this.scmManager.scmmConfig.credentials.username,
+ password : this.scmManager.scmmConfig.credentials.password,
+ helm : this.scmManager.scmmConfig.helm,
+ releaseName: releaseName])
+
+ def helmConfig = this.scmManager.scmmConfig.helm
+ def mergedMap = MapUtils.deepMerge(helmConfig.values, templatedMap)
+ def tempValuesPath = new FileSystemUtils().writeTempFile(mergedMap)
+ this.scmManager.helmStrategy.deployFeature(helmConfig.repoURL,
+ 'scm-manager',
+ helmConfig.chart,
+ helmConfig.version,
+ this.scmManager.scmmConfig.namespace,
+ releaseName,
+ tempValuesPath)
+ }
+
+ def installScmmPlugins() {
+
+ if (this.scmManager.config.scm.scmManager.skipPlugins) {
+ log.debug("Skipping SCM plugin installation")
+ return
+ }
+
+ def pluginNames = ["scm-mail-plugin",
+ "scm-review-plugin",
+ "scm-code-editor-plugin",
+ "scm-editor-plugin",
+ "scm-landingpage-plugin",
+ "scm-el-plugin",
+ "scm-readme-plugin",
+ "scm-webhook-plugin",
+ "scm-ci-plugin",
+ "scm-metrics-prometheus-plugin"]
+
+ if (this.scmManager.config.jenkins.active) {
+ pluginNames.add("scm-jenkins-plugin")
+ }
+ Boolean restartForThisPlugin = false
+ pluginNames.each { String pluginName ->
+ log.debug("Installing Plugin ${pluginName} ...")
+ restartForThisPlugin = !this.scmManager.config.scm.scmManager.skipRestart && pluginName == pluginNames.last()
+ ScmManagerApiClient.handleApiResponse(scmManager.apiClient.pluginApi().install(pluginName, restartForThisPlugin))
+ }
+
+ log.debug("SCM-Manager plugin installation finished successfully!")
+ if (restartForThisPlugin) {
+ waitForScmmAvailable(180, 2000, 100)
+ }
+ }
+
+ void setSetupConfigs() {
+ def setupConfigs = [enableProxy : false,
+ proxyPort : 8080,
+ proxyServer : "proxy.mydomain.com",
+ proxyUser : null,
+ proxyPassword : null,
+ realmDescription : "SONIA :: SCM Manager",
+ disableGroupingGrid : false,
+ dateFormat : "YYYY-MM-DD HH:mm:ss",
+ anonymousAccessEnabled : false,
+ anonymousMode : "OFF",
+ baseUrl : this.scmManager.url,
+ forceBaseUrl : false,
+ loginAttemptLimit : -1,
+ proxyExcludes : [],
+ skipFailedAuthenticators: false,
+ pluginUrl : "https://plugin-center-api.scm-manager.org/api/v1/plugins/{version}?os={os}&arch={arch}",
+ loginAttemptLimitTimeout: 300,
+ enabledXsrfProtection : true,
+ namespaceStrategy : "CustomNamespaceStrategy",
+ loginInfoUrl : "https://login-info.scm-manager.org/api/v1/login-info",
+ releaseFeedUrl : "https://scm-manager.org/download/rss.xml",
+ mailDomainName : "scm-manager.local",
+ adminGroups : [],
+ adminUsers : []]
+
+ ScmManagerApiClient.handleApiResponse(scmManager.apiClient.generalApi().setConfig(setupConfigs))
+ log.debug("Successfully added SCMM Setup Configs")
+ }
+
+ void configureJenkinsPlugin() {
+
+ def jenkinsPluginConfig = [disableRepositoryConfiguration: false,
+ disableMercurialTrigger : false,
+ disableGitTrigger : false,
+ disableEventTrigger : false,
+ url : this.scmManager.config.jenkins.urlForScm] as Map
+
+ ScmManagerApiClient.handleApiResponse(this.scmManager.apiClient.pluginApi().configureJenkinsPlugin(jenkinsPluginConfig))
+ log.debug("Successfully configured JenkinsPlugin in SCM-Manager.")
+ }
+
+ void addDefaultUsers() {
+ def metricsUsername = "${this.scmManager.config.application.namePrefix}metrics"
+ addUser(this.scmManager.scmmConfig.gitOpsUsername, this.scmManager.scmmConfig.password)
+ addUser(metricsUsername, this.scmManager.scmmConfig.password)
+ grantUserPermissions(metricsUsername, ["metrics:read"])
+ }
+
+ void addUser(String username, String password, String email = 'changeme@test.local') {
+ ScmManagerUser userRequest = [name : username,
+ displayName: username,
+ mail : email,
+ external : false,
+ password : password,
+ active : true,
+ _links : [:]]
+ ScmManagerApiClient.handleApiResponse(scmManager.apiClient.usersApi().addUser(userRequest))
+ log.debug("Successfully created SCM-Manager User.")
+ }
+
+ void grantUserPermissions(String username, List permissions) {
+ def permissionBody = [permissions: permissions]
+ ScmManagerApiClient.handleApiResponse(scmManager.apiClient.usersApi().setPermissionForUser(username, permissionBody))
+ log.debug("Granted permissions ${permissions} to user ${username}.")
+ }
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/ScmManagerUrlResolver.groovy b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/ScmManagerUrlResolver.groovy
index 5b9cab08a..f0d470873 100644
--- a/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/ScmManagerUrlResolver.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/ScmManagerUrlResolver.groovy
@@ -4,122 +4,121 @@ import com.cloudogu.gitops.config.Config
import com.cloudogu.gitops.features.git.config.util.ScmManagerConfig
import com.cloudogu.gitops.kubernetes.api.K8sClient
import com.cloudogu.gitops.utils.NetworkingUtils
+
import groovy.util.logging.Slf4j
@Slf4j
class ScmManagerUrlResolver {
- private final Config config
- private final ScmManagerConfig scmm
- private final K8sClient k8s
- private final NetworkingUtils net
-
- private URI cachedClusterBind
+ private final Config config
+ private final ScmManagerConfig scmm
+ private final K8sClient k8s
+ private final NetworkingUtils net
- private final String releaseName = 'scmm'
+ private URI cachedClusterBind
- ScmManagerUrlResolver(Config config, ScmManagerConfig scmm, K8sClient k8s, NetworkingUtils net) {
- this.config = config
- this.scmm = scmm
- this.k8s = k8s
- this.net = net
- }
+ private final String releaseName = 'scmm'
- // ---------- Public API used by ScmManager ----------
+ ScmManagerUrlResolver(Config config, ScmManagerConfig scmm, K8sClient k8s, NetworkingUtils net) {
+ this.config = config
+ this.scmm = scmm
+ this.k8s = k8s
+ this.net = net
+ }
- /** Client base …/scm (no trailing slash) */
- URI clientBase() { noTrailSlash(ensureScm(clientBaseRaw())) }
+ // ---------- Public API used by ScmManager ----------
- /** Client API base …/scm/api/ */
- URI clientApiBase() { withSlash(clientBase()).resolve("api/") }
+ /** Client base …/scm (no trailing slash) */
+ URI clientBase() { noTrailSlash(ensureScm(clientBaseRaw())) }
- /** Client repo base …/scm/repo (no trailing slash) */
- URI clientRepoBase() { noTrailSlash(withSlash(clientBase()).resolve("${root()}/")) }
+ /** Client API base …/scm/api/ */
+ URI clientApiBase() { withSlash(clientBase()).resolve("api/") }
+ /** Client repo base …/scm/repo (no trailing slash) */
+ URI clientRepoBase() { noTrailSlash(withSlash(clientBase()).resolve("${root()}/")) }
- /** In-cluster base …/scm (no trailing slash) */
- URI inClusterBase() { noTrailSlash(ensureScm(inClusterBaseRaw())) }
+ /** In-cluster base …/scm (no trailing slash) */
+ URI inClusterBase() { noTrailSlash(ensureScm(inClusterBaseRaw())) }
- /** In-cluster repo prefix …/scm/repo/[] */
- String inClusterRepoPrefix() {
- def prefix = (config.application.namePrefix ?: "").strip()
- def base = withSlash(inClusterBase())
- def url = withSlash(base.resolve(root()))
+ /** In-cluster repo prefix …/scm/repo/[] */
+ String inClusterRepoPrefix() {
+ def prefix = (config.application.namePrefix ?: "").strip()
+ def base = withSlash(inClusterBase())
+ def url = withSlash(base.resolve(root()))
- return URI.create(url.toString() + prefix).toString()
- }
+ return URI.create(url.toString() + prefix).toString()
+ }
- /** In-cluster repo URL …/scm/repo// */
- String inClusterRepoUrl(String repoTarget) {
- def repo = repoTarget.strip()
- noTrailSlash(withSlash(inClusterBase()).resolve("${root()}/${repo}/")).toString()
- }
+ /** In-cluster repo URL …/scm/repo// */
+ String inClusterRepoUrl(String repoTarget) {
+ def repo = repoTarget.strip()
+ noTrailSlash(withSlash(inClusterBase()).resolve("${root()}/${repo}/")).toString()
+ }
- /** Client repo URL …/scm/repo// (no trailing slash) */
- String clientRepoUrl(String repoTarget) {
- def repo = repoTarget.strip()
- noTrailSlash(withSlash(clientRepoBase()).resolve("${repo}/")).toString()
- }
+ /** Client repo URL …/scm/repo// (no trailing slash) */
+ String clientRepoUrl(String repoTarget) {
+ def repo = repoTarget.strip()
+ noTrailSlash(withSlash(clientRepoBase()).resolve("${repo}/")).toString()
+ }
- /** …/scm/api/v2/metrics/prometheus */
- URI prometheusEndpoint() { withSlash(clientBase()).resolve("api/v2/metrics/prometheus") }
+ /** …/scm/api/v2/metrics/prometheus */
+ URI prometheusEndpoint() { withSlash(clientBase()).resolve("api/v2/metrics/prometheus") }
- // ---------- Base resolution ----------
+ // ---------- Base resolution ----------
- private URI clientBaseRaw() {
- if (Boolean.TRUE == scmm.internal)
- return config.application.runningInsideK8s ? serviceDnsBase() : nodePortBase()
- return externalBase()
- }
+ private URI clientBaseRaw() {
+ if (Boolean.TRUE == scmm.internal) return config.application.runningInsideK8s ? serviceDnsBase() : nodePortBase()
+ return externalBase()
+ }
- private URI inClusterBaseRaw() {
- return scmm.internal ? serviceDnsBase() : externalBase()
- }
+ private URI inClusterBaseRaw() {
+ return scmm.internal ? serviceDnsBase() : externalBase()
+ }
- private URI serviceDnsBase() {
- def namespace = (scmm.namespace ?: "scm-manager").strip()
- URI.create("http://scmm.${namespace}.svc.cluster.local")
- }
+ private URI serviceDnsBase() {
+ def namespace = (scmm.namespace ?: "scm-manager").strip()
+ URI.create("http://scmm.${namespace}.svc.cluster.local")
+ }
- private URI externalBase() {
- def url = (scmm.url ?: "").strip()
- if (url) return URI.create(url)
+ private URI externalBase() {
+ def url = (scmm.url ?: "").strip()
+ if (url) return URI.create(url)
- def ingress = (scmm.ingress ?: "").strip()
- if (ingress) return URI.create("http://${ingress}")
- throw new IllegalArgumentException("Either scmm.url or scmm.ingress must be set when internal=false")
- }
+ def ingress = (scmm.ingress ?: "").strip()
+ if (ingress) return URI.create("http://${ingress}")
+ throw new IllegalArgumentException("Either scmm.url or scmm.ingress must be set when internal=false")
+ }
- private URI nodePortBase() {
- if (cachedClusterBind) return cachedClusterBind
+ private URI nodePortBase() {
+ if (cachedClusterBind) return cachedClusterBind
- def namespace = (scmm.namespace ?: "scm-manager").strip()
+ def namespace = (scmm.namespace ?: "scm-manager").strip()
- final def port = k8s.waitForNodePort(releaseName, namespace)
- final def host = net.findClusterBindAddress()
- cachedClusterBind = new URI("http://${host}:${port}")
- return cachedClusterBind
- }
+ final def port = k8s.waitForNodePort(releaseName, namespace)
+ final def host = net.findClusterBindAddress()
+ cachedClusterBind = new URI("http://${host}:${port}")
+ return cachedClusterBind
+ }
- // ---------- Helpers ----------
+ // ---------- Helpers ----------
- private String root() {
- (scmm.rootPath ?: "repo").strip()
- }
+ private String root() {
+ (scmm.rootPath ?: "repo").strip()
+ }
- private static URI ensureScm(URI u) {
- def us = withSlash(u)
- def path = us.path ?: ""
- path.endsWith("/scm/") ? us : us.resolve("scm/")
- }
+ private static URI ensureScm(URI u) {
+ def us = withSlash(u)
+ def path = us.path ?: ""
+ path.endsWith("/scm/") ? us : us.resolve("scm/")
+ }
- private static URI withSlash(URI u) {
- def s = u.toString()
- s.endsWith('/') ? u : URI.create(s + '/')
- }
+ private static URI withSlash(URI u) {
+ def s = u.toString()
+ s.endsWith('/') ? u : URI.create(s + '/')
+ }
- private static URI noTrailSlash(URI u) {
- def s = u.toString()
- s.endsWith('/') ? URI.create(s.substring(0, s.length() - 1)) : u
- }
+ private static URI noTrailSlash(URI u) {
+ def s = u.toString()
+ s.endsWith('/') ? URI.create(s.substring(0, s.length() - 1)) : u
+ }
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/AuthorizationInterceptor.groovy b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/AuthorizationInterceptor.groovy
index 28d799bf3..f4a50a924 100644
--- a/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/AuthorizationInterceptor.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/AuthorizationInterceptor.groovy
@@ -1,26 +1,25 @@
package com.cloudogu.gitops.git.providers.scmmanager.api
-
import okhttp3.Credentials
import okhttp3.Interceptor
import okhttp3.Response
import org.jetbrains.annotations.NotNull
class AuthorizationInterceptor implements Interceptor {
- private String username
- private String password
+ private String username
+ private String password
- AuthorizationInterceptor(String username, String password) {
- this.username = username
- this.password = password
- }
+ AuthorizationInterceptor(String username, String password) {
+ this.username = username
+ this.password = password
+ }
- @Override
- Response intercept(@NotNull Chain chain) throws IOException {
- def newRequest = chain.request().newBuilder()
- .header("Authorization", Credentials.basic(username, password))
- .build()
+ @Override
+ Response intercept(@NotNull Chain chain) throws IOException {
+ def newRequest = chain.request().newBuilder()
+ .header("Authorization", Credentials.basic(username, password))
+ .build()
- return chain.proceed(newRequest)
- }
+ return chain.proceed(newRequest)
+ }
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/PluginApi.groovy b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/PluginApi.groovy
index 5e17d1a12..034cfff75 100644
--- a/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/PluginApi.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/PluginApi.groovy
@@ -1,18 +1,13 @@
package com.cloudogu.gitops.git.providers.scmmanager.api
import retrofit2.Call
-import retrofit2.http.Body
-import retrofit2.http.Headers
-import retrofit2.http.POST
-import retrofit2.http.PUT
-import retrofit2.http.Path
-import retrofit2.http.Query
+import retrofit2.http.*
interface PluginApi {
- @POST("v2/plugins/available/{name}/install")
- Call install(@Path("name") String name, @Query("restart") Boolean restart)
+ @POST("v2/plugins/available/{name}/install")
+ Call install(@Path("name") String name, @Query("restart") Boolean restart)
- @PUT("v2/config/jenkins/")
- @Headers("Content-Type: application/json")
- Call configureJenkinsPlugin(@Body Map config)
+ @PUT("v2/config/jenkins/")
+ @Headers("Content-Type: application/json")
+ Call configureJenkinsPlugin(@Body Map config)
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/Repository.groovy b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/Repository.groovy
index 9dea6dd14..60774b6bf 100644
--- a/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/Repository.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/Repository.groovy
@@ -1,26 +1,26 @@
package com.cloudogu.gitops.git.providers.scmmanager.api
class Repository {
- final String name
- final String namespace
- final String type
- final String contact
- final String description
+ final String name
+ final String namespace
+ final String type
+ final String contact
+ final String description
- Repository(String namespace, String name, String description = null, String contact = null, String type = 'git') {
- this.namespace = namespace
- this.name = name
- this.type = type
- this.contact = contact
- this.description = description
- }
-
- String getFullRepoName() {
- return "${namespace}/${name}"
- }
-
- @Override
- String toString() {
- "Repository{name='$name', namespace='$namespace', type='$type', contact='$contact', description='$description'}"
- }
+ Repository(String namespace, String name, String description = null, String contact = null, String type = 'git') {
+ this.namespace = namespace
+ this.name = name
+ this.type = type
+ this.contact = contact
+ this.description = description
+ }
+
+ String getFullRepoName() {
+ return "${namespace}/${name}"
+ }
+
+ @Override
+ String toString() {
+ "Repository{name='$name', namespace='$namespace', type='$type', contact='$contact', description='$description'}"
+ }
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/RepositoryApi.groovy b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/RepositoryApi.groovy
index 4893da921..896e88211 100644
--- a/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/RepositoryApi.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/RepositoryApi.groovy
@@ -1,19 +1,19 @@
package com.cloudogu.gitops.git.providers.scmmanager.api
import com.cloudogu.gitops.git.providers.scmmanager.Permission
-import okhttp3.ResponseBody
+
import retrofit2.Call
import retrofit2.http.*
interface RepositoryApi {
- @DELETE("v2/repositories/{namespace}/{name}")
- Call delete(@Path("namespace") String namespace, @Path("name") String name)
+ @DELETE("v2/repositories/{namespace}/{name}")
+ Call delete(@Path("namespace") String namespace, @Path("name") String name)
- @POST("v2/repositories/")
- @Headers("Content-Type: application/vnd.scmm-repository+json;v=2")
- Call create(@Body Repository repository, @Query("initialize") boolean initialize)
+ @POST("v2/repositories/")
+ @Headers("Content-Type: application/vnd.scmm-repository+json;v=2")
+ Call create(@Body Repository repository, @Query("initialize") boolean initialize)
- @POST("v2/repositories/{namespace}/{name}/permissions/")
- @Headers("Content-Type: application/vnd.scmm-repositoryPermission+json")
- Call createPermission(@Path("namespace") String namespace, @Path("name") String name, @Body Permission permission)
+ @POST("v2/repositories/{namespace}/{name}/permissions/")
+ @Headers("Content-Type: application/vnd.scmm-repositoryPermission+json")
+ Call createPermission(@Path("namespace") String namespace, @Path("name") String name, @Body Permission permission)
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/ScmManagerApi.groovy b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/ScmManagerApi.groovy
index d06e832be..3ef1f0bf1 100644
--- a/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/ScmManagerApi.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/ScmManagerApi.groovy
@@ -8,10 +8,10 @@ import retrofit2.http.PUT
interface ScmManagerApi {
- @GET("v2")
- Call checkScmmAvailable()
+ @GET("v2")
+ Call checkScmmAvailable()
- @PUT("v2/config")
- @Headers("Content-Type: application/vnd.scmm-config+json;v=2")
- Call setConfig(@Body Map config)
+ @PUT("v2/config")
+ @Headers("Content-Type: application/vnd.scmm-config+json;v=2")
+ Call setConfig(@Body Map config)
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/ScmManagerApiClient.groovy b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/ScmManagerApiClient.groovy
index a9a5f0ce7..3daa7833b 100644
--- a/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/ScmManagerApiClient.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/ScmManagerApiClient.groovy
@@ -1,9 +1,10 @@
package com.cloudogu.gitops.git.providers.scmmanager.api
-
import com.cloudogu.gitops.config.Credentials
import com.cloudogu.gitops.dependencyinjection.HttpClientFactory
+
import groovy.util.logging.Slf4j
+
import okhttp3.OkHttpClient
import retrofit2.Call
import retrofit2.Response
@@ -11,65 +12,62 @@ import retrofit2.Retrofit
import retrofit2.converter.jackson.JacksonConverterFactory
/**
- * Parent class for all SCMM Apis that lazily creates the APIs
- */
+ * Parent class for all SCMM Apis that lazily creates the APIs*/
@Slf4j
class ScmManagerApiClient {
- Credentials credentials
- OkHttpClient okHttpClient
- String url
+ Credentials credentials
+ OkHttpClient okHttpClient
+ String url
- ScmManagerApiClient(String url, Credentials credentials, Boolean isInsecure) {
- this.url = url
- this.credentials = credentials
- this.okHttpClient = HttpClientFactory.buildOkHttpClient(credentials, isInsecure)
- }
+ ScmManagerApiClient(String url, Credentials credentials, Boolean isInsecure) {
+ this.url = url
+ this.credentials = credentials
+ this.okHttpClient = HttpClientFactory.buildOkHttpClient(credentials, isInsecure)
+ }
- UsersApi usersApi() {
- return retrofit().create(UsersApi)
- }
+ UsersApi usersApi() {
+ return retrofit().create(UsersApi)
+ }
- RepositoryApi repositoryApi() {
- return retrofit().create(RepositoryApi)
- }
+ RepositoryApi repositoryApi() {
+ return retrofit().create(RepositoryApi)
+ }
- ScmManagerApi generalApi() {
- return retrofit().create(ScmManagerApi)
- }
+ ScmManagerApi generalApi() {
+ return retrofit().create(ScmManagerApi)
+ }
- PluginApi pluginApi() {
- return retrofit().create(PluginApi)
- }
+ PluginApi pluginApi() {
+ return retrofit().create(PluginApi)
+ }
- static handleApiResponse(Call apiCall, String additionalMessage = "") {
- try {
- Response response = apiCall.execute()
+ static handleApiResponse(Call apiCall, String additionalMessage = "") {
+ try {
+ Response response = apiCall.execute()
- if (!response.isSuccessful() &&
- response.code() != 409 &&
- response.code() != 201) {
- def errorMessage = "API call failed!'. HTTP Status: ${response.code()} - ${response.message()}"
- if (additionalMessage) {
- errorMessage += " Additional Info: ${additionalMessage}"
- }
- log.error(errorMessage)
- throw new RuntimeException(errorMessage)
- } else {
- log.debug("Successfully completed ${apiCall}")
- }
- } catch (Exception e) {
- def errorMessage = "Error executing API: ${e.message}"
- log.error(errorMessage, e)
- throw new RuntimeException(errorMessage, e)
- }
- }
+ if (!response.isSuccessful() && response.code() != 409 && response.code() != 201) {
+ def errorMessage = "API call failed!'. HTTP Status: ${response.code()} - ${response.message()}"
+ if (additionalMessage) {
+ errorMessage += " Additional Info: ${additionalMessage}"
+ }
+ log.error(errorMessage)
+ throw new RuntimeException(errorMessage)
+ } else {
+ log.debug("Successfully completed ${apiCall}")
+ }
+ } catch (Exception e) {
+ def errorMessage = "Error executing API: ${e.message}"
+ log.error(errorMessage, e)
+ throw new RuntimeException(errorMessage, e)
+ }
+ }
- protected Retrofit retrofit() {
- return new Retrofit.Builder()
- .baseUrl(this.url)
- .client(okHttpClient)
- // Converts HTTP body objects from groovy to JSON
- .addConverterFactory(JacksonConverterFactory.create())
- .build()
- }
+ protected Retrofit retrofit() {
+ return new Retrofit.Builder()
+ .baseUrl(this.url)
+ .client(okHttpClient)
+ // Converts HTTP body objects from groovy to JSON
+ .addConverterFactory(JacksonConverterFactory.create())
+ .build()
+ }
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/ScmManagerUser.groovy b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/ScmManagerUser.groovy
index c33af243a..38af2d492 100644
--- a/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/ScmManagerUser.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/ScmManagerUser.groovy
@@ -1,11 +1,11 @@
package com.cloudogu.gitops.git.providers.scmmanager.api
class ScmManagerUser {
- String name
- String displayName
- String mail
- boolean external = false
- String password
- boolean active = true
- Map _links = [:]
+ String name
+ String displayName
+ String mail
+ boolean external = false
+ String password
+ boolean active = true
+ Map _links = [:]
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/UsersApi.groovy b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/UsersApi.groovy
index c59a27f44..a1d8bcaed 100644
--- a/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/UsersApi.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/git/providers/scmmanager/api/UsersApi.groovy
@@ -1,26 +1,18 @@
package com.cloudogu.gitops.git.providers.scmmanager.api
-import okhttp3.ResponseBody
import retrofit2.Call
-import retrofit2.http.Body
-import retrofit2.http.DELETE
-import retrofit2.http.Headers
-import retrofit2.http.POST
-import retrofit2.http.PUT
-import retrofit2.http.Path
+import retrofit2.http.*
interface UsersApi {
- @DELETE("v2/users/{id}")
- Call delete(@Path("id") String id)
+ @DELETE("v2/users/{id}")
+ Call delete(@Path("id") String id)
- @Headers(["Content-Type: application/vnd.scmm-user+json;v=2"])
- @POST("v2/users")
- Call addUser(@Body ScmManagerUser user)
+ @Headers(["Content-Type: application/vnd.scmm-user+json;v=2"])
+ @POST("v2/users")
+ Call addUser(@Body ScmManagerUser user)
- @Headers(["Content-Type: application/vnd.scmm-permissionCollection+json;v=2"])
- @PUT("v2/users/{username}/permissions")
- Call setPermissionForUser(
- @Path("username") String username,
- @Body Map> permissions
- )
+ @Headers(["Content-Type: application/vnd.scmm-permissionCollection+json;v=2"])
+ @PUT("v2/users/{username}/permissions")
+ Call setPermissionForUser(@Path("username") String username,
+ @Body Map> permissions)
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/jenkins/GlobalPropertyManager.groovy b/src/main/groovy/com/cloudogu/gitops/jenkins/GlobalPropertyManager.groovy
index f58d5f338..030f474ca 100644
--- a/src/main/groovy/com/cloudogu/gitops/jenkins/GlobalPropertyManager.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/jenkins/GlobalPropertyManager.groovy
@@ -1,19 +1,20 @@
package com.cloudogu.gitops.jenkins
import jakarta.inject.Singleton
+
import org.intellij.lang.annotations.Language
@Singleton
class GlobalPropertyManager {
- private JenkinsApiClient apiClient
+ private JenkinsApiClient apiClient
- GlobalPropertyManager(JenkinsApiClient apiClient) {
- this.apiClient = apiClient
- }
+ GlobalPropertyManager(JenkinsApiClient apiClient) {
+ this.apiClient = apiClient
+ }
- void setGlobalProperty(String key, String value) {
- @Language("groovy")
- def script = """
+ void setGlobalProperty(String key, String value) {
+ @Language("groovy")
+ def script = """
instance = Jenkins.getInstance()
globalNodeProperties = instance.getGlobalNodeProperties()
envVarsNodePropertyList = globalNodeProperties.getAll(hudson.slaves.EnvironmentVariablesNodeProperty.class)
@@ -36,15 +37,15 @@ class GlobalPropertyManager {
print("Done")
"""
- def result = apiClient.runScript(script)
- if (result != 'Done') {
- throw new RuntimeException("Could not create global property: $result")
- }
- }
+ def result = apiClient.runScript(script)
+ if (result != 'Done') {
+ throw new RuntimeException("Could not create global property: $result")
+ }
+ }
- void deleteGlobalProperty(String key) {
- @Language("groovy")
- def script = """
+ void deleteGlobalProperty(String key) {
+ @Language("groovy")
+ def script = """
def instance = Jenkins.getInstance()
def globalNodeProperties = instance.getGlobalNodeProperties()
def envVarsNodePropertyList = globalNodeProperties.getAll(hudson.slaves.EnvironmentVariablesNodeProperty.class)
@@ -59,9 +60,9 @@ class GlobalPropertyManager {
print("Done")
"""
- def result = apiClient.runScript(script)
- if (result != 'Nothing to do' && result != 'Done') {
- throw new RuntimeException("Could not delete global property: $result")
- }
- }
-}
+ def result = apiClient.runScript(script)
+ if (result != 'Nothing to do' && result != 'Done') {
+ throw new RuntimeException("Could not delete global property: $result")
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/jenkins/JenkinsApiClient.groovy b/src/main/groovy/com/cloudogu/gitops/jenkins/JenkinsApiClient.groovy
index bc5ae8d94..cc41e4344 100644
--- a/src/main/groovy/com/cloudogu/gitops/jenkins/JenkinsApiClient.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/jenkins/JenkinsApiClient.groovy
@@ -1,116 +1,116 @@
package com.cloudogu.gitops.jenkins
import com.cloudogu.gitops.config.Config
-import groovy.json.JsonSlurper
-import groovy.util.logging.Slf4j
+
import jakarta.inject.Named
import jakarta.inject.Singleton
+import groovy.json.JsonSlurper
+import groovy.util.logging.Slf4j
+
import okhttp3.*
@Slf4j
@Singleton
class JenkinsApiClient {
- private Config config
-
- private OkHttpClient client
-
- // Number of retries in uncommonly high, because we might have to outlive a unexpected Jenkins restart
- private int maxRetries = 180
- private int waitPeriodInMs = 2000
-
- JenkinsApiClient(
- Config config,
- @Named("jenkins") OkHttpClient client
- ) {
-
- if (config.application.insecure) {
- this.client = client.newBuilder()
- .hostnameVerifier({ hostname, session -> true })
- .build()
- } else {
- this.client = client
- }
- this.config = config
- }
-
- String runScript(String code) {
- log.trace("Running groovy script in Jenkins: {}", code)
- def response = postRequestWithCrumb("scriptText", new FormBody.Builder().add("script", code).build())
- if (response.code() != 200) {
- throw new RuntimeException("Could not run script. Status code ${response.code()}")
- }
-
- return response.body().string()
- }
-
- Response postRequestWithCrumb(String url, RequestBody postData = null) {
- return sendRequestWithRetries {
- Request.Builder request = buildRequest(url)
- .header("Jenkins-Crumb", getCrumb())
-
- if (postData != null) {
- request.method("POST", postData)
- } else {
- // Explicitly set empty body. Otherwise okhttp sends GET
- RequestBody emptyBody = RequestBody.create("", null)
- request.method("POST", emptyBody)
- }
-
- request.build()
- }
- }
-
- private String getCrumb() {
- log.trace("Getting Crumb for Jenkins")
- def response = sendRequestWithRetries { buildRequest("crumbIssuer/api/json").build() }
-
- if (response.code() != 200) {
- throw new RuntimeException("Could not create crumb. Status code ${response.code()}")
- }
-
- def json = new JsonSlurper().parse(response.body().byteStream())
-
- if (!json instanceof Map || !(json as Map).containsKey('crumb')) {
- throw new RuntimeException("Could not create crumb. Invalid json.")
- }
-
- return json['crumb']
- }
-
- private Request.Builder buildRequest(String url) {
- return new Request.Builder()
- .url("${config.jenkins.url}/$url")
- .header("Authorization", Credentials.basic(config.jenkins.username, config.jenkins.password))
- }
-
- // We pass a closure, so that we actually refetch a new crumb for a failed request
- // The Jenkins ApiClient has it's own retry logic on top of RetryInterceptor, because of crumb lifetime and restarts
- private Response sendRequestWithRetries(Closure request) {
- def retry = 0
- Response response = null
- do {
- response = client.newCall(request()).execute()
- if (!shouldRetryRequest(response)) {
- break
- }
- Thread.sleep(waitPeriodInMs)
- } while (++retry < maxRetries)
-
- return response
- }
-
- private boolean shouldRetryRequest(Response response) {
- // We might run into a 403 due to an invalid crumb from a previous session before jenkins was restarted.
- // Here in the ApiClient, we simply retry all 401 and 403 including fetching a new crumb
- return response.code() in [401, 403]
- }
-
- protected void setMaxRetries(int retries) {
- this.maxRetries = retries
- }
-
- protected setWaitPeriodInMs(int waitPeriodInMs) {
- this.waitPeriodInMs = waitPeriodInMs
- }
-
-}
+ private Config config
+
+ private OkHttpClient client
+
+ // Number of retries in uncommonly high, because we might have to outlive a unexpected Jenkins restart
+ private int maxRetries = 180
+ private int waitPeriodInMs = 2000
+
+ JenkinsApiClient(Config config,
+ @Named("jenkins") OkHttpClient client) {
+
+ if (config.application.insecure) {
+ this.client = client.newBuilder()
+ .hostnameVerifier({ hostname, session -> true })
+ .build()
+ } else {
+ this.client = client
+ }
+ this.config = config
+ }
+
+ String runScript(String code) {
+ log.trace("Running groovy script in Jenkins: {}", code)
+ def response = postRequestWithCrumb("scriptText", new FormBody.Builder().add("script", code).build())
+ if (response.code() != 200) {
+ throw new RuntimeException("Could not run script. Status code ${response.code()}")
+ }
+
+ return response.body().string()
+ }
+
+ Response postRequestWithCrumb(String url, RequestBody postData = null) {
+ return sendRequestWithRetries {
+ Request.Builder request = buildRequest(url)
+ .header("Jenkins-Crumb", getCrumb())
+
+ if (postData != null) {
+ request.method("POST", postData)
+ } else {
+ // Explicitly set empty body. Otherwise okhttp sends GET
+ RequestBody emptyBody = RequestBody.create("", null)
+ request.method("POST", emptyBody)
+ }
+
+ request.build()
+ }
+ }
+
+ private String getCrumb() {
+ log.trace("Getting Crumb for Jenkins")
+ def response = sendRequestWithRetries { buildRequest("crumbIssuer/api/json").build() }
+
+ if (response.code() != 200) {
+ throw new RuntimeException("Could not create crumb. Status code ${response.code()}")
+ }
+
+ def json = new JsonSlurper().parse(response.body().byteStream())
+
+ if (!json instanceof Map || !(json as Map).containsKey('crumb')) {
+ throw new RuntimeException("Could not create crumb. Invalid json.")
+ }
+
+ return json['crumb']
+ }
+
+ private Request.Builder buildRequest(String url) {
+ return new Request.Builder()
+ .url("${config.jenkins.url}/$url")
+ .header("Authorization", Credentials.basic(config.jenkins.username, config.jenkins.password))
+ }
+
+ // We pass a closure, so that we actually refetch a new crumb for a failed request
+ // The Jenkins ApiClient has it's own retry logic on top of RetryInterceptor, because of crumb lifetime and restarts
+ private Response sendRequestWithRetries(Closure request) {
+ def retry = 0
+ Response response = null
+ do {
+ response = client.newCall(request()).execute()
+ if (!shouldRetryRequest(response)) {
+ break
+ }
+ Thread.sleep(waitPeriodInMs)
+ } while (++retry < maxRetries)
+
+ return response
+ }
+
+ private boolean shouldRetryRequest(Response response) {
+ // We might run into a 403 due to an invalid crumb from a previous session before jenkins was restarted.
+ // Here in the ApiClient, we simply retry all 401 and 403 including fetching a new crumb
+ return response.code() in [401, 403]
+ }
+
+ protected void setMaxRetries(int retries) {
+ this.maxRetries = retries
+ }
+
+ protected setWaitPeriodInMs(int waitPeriodInMs) {
+ this.waitPeriodInMs = waitPeriodInMs
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/jenkins/JobManager.groovy b/src/main/groovy/com/cloudogu/gitops/jenkins/JobManager.groovy
index c262211d8..eeab52c77 100644
--- a/src/main/groovy/com/cloudogu/gitops/jenkins/JobManager.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/jenkins/JobManager.groovy
@@ -1,9 +1,11 @@
package com.cloudogu.gitops.jenkins
import com.cloudogu.gitops.utils.TemplatingEngine
+
+import jakarta.inject.Singleton
import groovy.json.JsonOutput
import groovy.util.logging.Slf4j
-import jakarta.inject.Singleton
+
import okhttp3.FormBody
import okhttp3.MediaType
import okhttp3.RequestBody
@@ -12,88 +14,79 @@ import org.intellij.lang.annotations.Language
@Singleton
@Slf4j
class JobManager {
- private JenkinsApiClient apiClient
-
- JobManager(JenkinsApiClient apiClient) {
- this.apiClient = apiClient
- }
-
- void createCredential(String jobName, String id, String username, String password, String description) {
- def response = apiClient.postRequestWithCrumb(
- "job/$jobName/credentials/store/folder/domain/_/createCredentials",
- new FormBody.Builder()
- .add("json", JsonOutput.toJson([
- credentials: [
- scope : "GLOBAL",
- id : id,
- username : username,
- password : password,
- description: description,
- $class : "com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl",
- ]
- ]))
- .build()
- )
-
- if (response.code() != 200) {
- throw new RuntimeException("Could not create credential id=$id,job=$jobName. StatusCode: ${response.code()}")
- }
- }
-
- /**
- * @return true, if created; false if job already exists and nothing was changed.
- */
- boolean createJob(String name, String serverUrl, String jobNamespace, String credentialsId) {
- if (jobExists(name)) {
- log.warn("Job '${name}' already exists, ignoring.")
- return false
- } else {
- // Note for development: the XML representation of an existing job can be exporting by adding /config.xml to the URL
- String payloadXml = new TemplatingEngine().template(new File('argocd/cluster-resources/apps/jenkins/templates/namespaceJobTemplate.xml.ftl'),
- [
- SCMM_NAMESPACE_JOB_SERVER_URL : serverUrl,
- SCMM_NAMESPACE_JOB_NAMESPACE : jobNamespace,
- SCMM_NAMESPACE_JOB_CREDENTIALS_ID: credentialsId
- ])
-
- RequestBody body = RequestBody.create(payloadXml, MediaType.get("text/xml"))
-
- def response = apiClient.postRequestWithCrumb("createItem?name=$name", body)
-
- if (response.code() != 200) {
- throw new RuntimeException("Could not create job '${name}'. StatusCode: ${response.code()}")
- }
- }
- return true
- }
-
- boolean jobExists(String name) {
- def response= apiClient.postRequestWithCrumb("job/$name")
-
- return response.code() == 200
- }
-
- void deleteJob(String name) {
- if (name.contains("'")) {
- throw new RuntimeException('Job name cannot contain quotes.')
- }
-
- @Language("groovy")
- String script = "print(Jenkins.instance.getItem('$name')?.delete())"
- def result = apiClient.runScript(script)
-
- if (result != 'null') {
- throw new RuntimeException("Could not delete job $name")
- }
- }
-
- void startJob(String jobName) {
-
- def response= apiClient.postRequestWithCrumb(
- "job/$jobName/build?delay=0sec")
-
- if (response.code() != 200) {
- throw new RuntimeException("Could not trigger build of Jenkins job: $jobName. StatusCode: ${response.code()}")
- }
- }
-}
+ private JenkinsApiClient apiClient
+
+ JobManager(JenkinsApiClient apiClient) {
+ this.apiClient = apiClient
+ }
+
+ void createCredential(String jobName, String id, String username, String password, String description) {
+ def response = apiClient.postRequestWithCrumb("job/$jobName/credentials/store/folder/domain/_/createCredentials",
+ new FormBody.Builder()
+ .add("json", JsonOutput.toJson([credentials: [scope : "GLOBAL",
+ id : id,
+ username : username,
+ password : password,
+ description: description,
+ $class : "com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl",]]))
+ .build())
+
+ if (response.code() != 200) {
+ throw new RuntimeException("Could not create credential id=$id,job=$jobName. StatusCode: ${response.code()}")
+ }
+ }
+
+ /**
+ * @return true, if created; false if job already exists and nothing was changed.
+ */
+ boolean createJob(String name, String serverUrl, String jobNamespace, String credentialsId) {
+ if (jobExists(name)) {
+ log.warn("Job '${name}' already exists, ignoring.")
+ return false
+ } else {
+ // Note for development: the XML representation of an existing job can be exporting by adding /config.xml to the URL
+ String payloadXml = new TemplatingEngine().template(new File('argocd/cluster-resources/apps/jenkins/templates/namespaceJobTemplate.xml.ftl'),
+ [SCMM_NAMESPACE_JOB_SERVER_URL : serverUrl,
+ SCMM_NAMESPACE_JOB_NAMESPACE : jobNamespace,
+ SCMM_NAMESPACE_JOB_CREDENTIALS_ID: credentialsId])
+
+ RequestBody body = RequestBody.create(payloadXml, MediaType.get("text/xml"))
+
+ def response = apiClient.postRequestWithCrumb("createItem?name=$name", body)
+
+ if (response.code() != 200) {
+ throw new RuntimeException("Could not create job '${name}'. StatusCode: ${response.code()}")
+ }
+ }
+ return true
+ }
+
+ boolean jobExists(String name) {
+ def response = apiClient.postRequestWithCrumb("job/$name")
+
+ return response.code() == 200
+ }
+
+ void deleteJob(String name) {
+ if (name.contains("'")) {
+ throw new RuntimeException('Job name cannot contain quotes.')
+ }
+
+ @Language("groovy")
+ String script = "print(Jenkins.instance.getItem('$name')?.delete())"
+ def result = apiClient.runScript(script)
+
+ if (result != 'null') {
+ throw new RuntimeException("Could not delete job $name")
+ }
+ }
+
+ void startJob(String jobName) {
+
+ def response = apiClient.postRequestWithCrumb("job/$jobName/build?delay=0sec")
+
+ if (response.code() != 200) {
+ throw new RuntimeException("Could not trigger build of Jenkins job: $jobName. StatusCode: ${response.code()}")
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/jenkins/PrometheusConfigurator.groovy b/src/main/groovy/com/cloudogu/gitops/jenkins/PrometheusConfigurator.groovy
index 0a8266837..d5edae78c 100644
--- a/src/main/groovy/com/cloudogu/gitops/jenkins/PrometheusConfigurator.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/jenkins/PrometheusConfigurator.groovy
@@ -4,14 +4,14 @@ import jakarta.inject.Singleton
@Singleton
class PrometheusConfigurator {
- private final JenkinsApiClient apiClient
+ private final JenkinsApiClient apiClient
- PrometheusConfigurator(JenkinsApiClient apiClient) {
- this.apiClient = apiClient
- }
+ PrometheusConfigurator(JenkinsApiClient apiClient) {
+ this.apiClient = apiClient
+ }
- void enableAuthentication() {
- def result = apiClient.runScript("""
+ void enableAuthentication() {
+ def result = apiClient.runScript("""
import org.jenkinsci.plugins.prometheus.config.*
def config = Jenkins.instance.getDescriptor(PrometheusConfiguration)
@@ -20,8 +20,8 @@ class PrometheusConfigurator {
print(config.useAuthenticatedEndpoint)
""")
- if (result != "true") {
- throw new RuntimeException("Cannot enable authentication for prometheus: $result")
- }
- }
-}
+ if (result != "true") {
+ throw new RuntimeException("Cannot enable authentication for prometheus: $result")
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/jenkins/UserManager.groovy b/src/main/groovy/com/cloudogu/gitops/jenkins/UserManager.groovy
index 1eab3ba5c..f42008431 100644
--- a/src/main/groovy/com/cloudogu/gitops/jenkins/UserManager.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/jenkins/UserManager.groovy
@@ -1,46 +1,47 @@
package com.cloudogu.gitops.jenkins
-import groovy.util.logging.Slf4j
import jakarta.inject.Singleton
+import groovy.util.logging.Slf4j
+
import org.intellij.lang.annotations.Language
@Singleton
@Slf4j
class UserManager {
- private JenkinsApiClient apiClient
+ private JenkinsApiClient apiClient
- UserManager(JenkinsApiClient apiClient) {
- this.apiClient = apiClient
- }
+ UserManager(JenkinsApiClient apiClient) {
+ this.apiClient = apiClient
+ }
- void createUser(String username, String password) {
- log.debug("Add user $username to jenkins")
-
- @Language("Groovy")
- def script = """
+ void createUser(String username, String password) {
+ log.debug("Add user $username to jenkins")
+
+ @Language("Groovy")
+ def script = """
def realm = Jenkins.getInstance().getSecurityRealm()
def user = realm.createAccount('${escapeString(username)}', '${escapeString(password)}')
print(user)
"""
-
- def result = apiClient.runScript(script)
- if (result != username) {
- throw new RuntimeException("Error when creating user: $result")
- }
- }
+ def result = apiClient.runScript(script)
+
+ if (result != username) {
+ throw new RuntimeException("Error when creating user: $result")
+ }
+ }
- void grantPermission(String username, Permissions permission) {
- if (!isUsingMatrixBasedPermissions()) {
- log.debug("Is not using matrix based permission. Does not need to add permission.")
- return
- }
+ void grantPermission(String username, Permissions permission) {
+ if (!isUsingMatrixBasedPermissions()) {
+ log.debug("Is not using matrix based permission. Does not need to add permission.")
+ return
+ }
- log.debug("Grant user $username permission $permission")
+ log.debug("Grant user $username permission $permission")
- @Language("Groovy")
- def script = """
+ @Language("Groovy")
+ def script = """
import org.jenkinsci.plugins.matrixauth.PermissionEntry
import org.jenkinsci.plugins.matrixauth.AuthorizationType
@@ -50,53 +51,55 @@ class UserManager {
}
print(permissions[${permission.toJenkinsPermissionEnum()}].add(new PermissionEntry(AuthorizationType.USER, '${escapeString(username)}')))
"""
- def result = apiClient.runScript(script)
+ def result = apiClient.runScript(script)
+
+ if (result !in ["true", "false"]) {
+ // Both are valid return values for Set.add(). true == was already in set, false == was not already in set
+ throw new RuntimeException("Failed to add permission $permission to $username: $result")
+ }
+ }
- if (result !in ["true", "false"]) { // Both are valid return values for Set.add(). true == was already in set, false == was not already in set
- throw new RuntimeException("Failed to add permission $permission to $username: $result")
- }
- }
+ boolean isUsingMatrixBasedPermissions() {
+ def result = apiClient.runScript("print(Jenkins.getInstance().getAuthorizationStrategy().class)")
- boolean isUsingMatrixBasedPermissions() {
- def result = apiClient.runScript("print(Jenkins.getInstance().getAuthorizationStrategy().class)")
+ if (!result.startsWith("class ")) {
+ throw new RuntimeException("Error when trying to determine authorization strategy: $result")
+ }
- if (!result.startsWith("class ")) {
- throw new RuntimeException("Error when trying to determine authorization strategy: $result")
- }
+ return result == "class hudson.security.GlobalMatrixAuthorizationStrategy" || result == "class hudson.security.ProjectMatrixAuthorizationStrategy"
+ }
- return result == "class hudson.security.GlobalMatrixAuthorizationStrategy" || result == "class hudson.security.ProjectMatrixAuthorizationStrategy"
- }
+ boolean isUsingCasSecurityRealm() {
+ def result = apiClient.runScript("print(Jenkins.getInstance().getSecurityRealm().class)")
- boolean isUsingCasSecurityRealm() {
- def result = apiClient.runScript("print(Jenkins.getInstance().getSecurityRealm().class)")
+ if (!result.startsWith("class ")) {
+ throw new RuntimeException("Error when trying to determine security realm: $result")
+ }
- if (!result.startsWith("class ")) {
- throw new RuntimeException("Error when trying to determine security realm: $result")
- }
+ return result == "class org.jenkinsci.plugins.cas.CasSecurityRealm"
+ }
- return result == "class org.jenkinsci.plugins.cas.CasSecurityRealm"
- }
+ private String escapeString(String str) {
+ if (str.contains("\\")) {
+ // We don't want get in trouble with escaping,
+ // e.g. `foo\'foo` => `foo\\'foo`. Now we would have a backslash followed by an unescaped quote.
+ throw new IllegalArgumentException("Backslashes within the escaped variables are forbidden.")
+ }
- private String escapeString(String str) {
- if (str.contains("\\")) {
- // We don't want get in trouble with escaping,
- // e.g. `foo\'foo` => `foo\\'foo`. Now we would have a backslash followed by an unescaped quote.
- throw new IllegalArgumentException("Backslashes within the escaped variables are forbidden.")
- }
+ return str.replace("'", "\\'")
+ }
- return str.replace("'", "\\'")
- }
+ enum Permissions {
+ METRICS_VIEW("jenkins.metrics.api.Metrics.VIEW")
- enum Permissions {
- METRICS_VIEW("jenkins.metrics.api.Metrics.VIEW")
+ private final String value
- private final String value
- Permissions(String value){
- this.value = value
- }
+ Permissions(String value) {
+ this.value = value
+ }
- String toJenkinsPermissionEnum() {
- return value
- }
- }
-}
+ String toJenkinsPermissionEnum() {
+ return value
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/kubernetes/api/HelmClient.groovy b/src/main/groovy/com/cloudogu/gitops/kubernetes/api/HelmClient.groovy
index 5a0d3599c..29b33abc0 100644
--- a/src/main/groovy/com/cloudogu/gitops/kubernetes/api/HelmClient.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/kubernetes/api/HelmClient.groovy
@@ -1,51 +1,52 @@
package com.cloudogu.gitops.kubernetes.api
import com.cloudogu.gitops.utils.CommandExecutor
-import groovy.util.logging.Slf4j
+
import jakarta.inject.Singleton
+import groovy.util.logging.Slf4j
@Slf4j
@Singleton
class HelmClient {
- private CommandExecutor commandExecutor
-
- HelmClient(CommandExecutor commandExecutor) {
- this.commandExecutor = commandExecutor
- }
-
- String addRepo(String repoName, String url) {
- helm(['repo', 'add', repoName, url ])
- }
-
- String dependencyBuild(String path) {
- helm(['dependency', 'build', path ])
- }
-
- String upgrade(String release, String chartOrPath, Map args) {
- helm(['upgrade', '-i', release, chartOrPath, '--create-namespace' ], args)
- }
-
- String template(String release, String chartOrPath, Map args = [:]) {
- helm(['template', release, chartOrPath ], args)
- }
-
- private String helm(List verbAndParams, Map args = [:]) {
- List command = ['helm'] + verbAndParams
-
- for (entry in args) {
- String key = entry.key
- String value = entry.value
- command += "--${key}".toString()
- command += value
- }
-
- commandExecutor.execute(command as String[]).stdOut
- }
-
- String uninstall(String release, String namespace) {
- String[] command = ["helm", "uninstall", release, '--namespace', namespace]
-
- commandExecutor.execute(command).stdOut
- }
+ private CommandExecutor commandExecutor
+
+ HelmClient(CommandExecutor commandExecutor) {
+ this.commandExecutor = commandExecutor
+ }
+
+ String addRepo(String repoName, String url) {
+ helm(['repo', 'add', repoName, url])
+ }
+
+ String dependencyBuild(String path) {
+ helm(['dependency', 'build', path])
+ }
+
+ String upgrade(String release, String chartOrPath, Map args) {
+ helm(['upgrade', '-i', release, chartOrPath, '--create-namespace'], args)
+ }
+
+ String template(String release, String chartOrPath, Map args = [:]) {
+ helm(['template', release, chartOrPath], args)
+ }
+
+ private String helm(List verbAndParams, Map args = [:]) {
+ List command = ['helm'] + verbAndParams
+
+ for (entry in args) {
+ String key = entry.key
+ String value = entry.value
+ command += "--${key}".toString()
+ command += value
+ }
+
+ commandExecutor.execute(command as String[]).stdOut
+ }
+
+ String uninstall(String release, String namespace) {
+ String[] command = ["helm", "uninstall", release, '--namespace', namespace]
+
+ commandExecutor.execute(command).stdOut
+ }
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/kubernetes/api/K8sClient.groovy b/src/main/groovy/com/cloudogu/gitops/kubernetes/api/K8sClient.groovy
index cb2127522..a4ab82d56 100644
--- a/src/main/groovy/com/cloudogu/gitops/kubernetes/api/K8sClient.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/kubernetes/api/K8sClient.groovy
@@ -3,577 +3,550 @@ package com.cloudogu.gitops.kubernetes.api
import com.cloudogu.gitops.config.Config
import com.cloudogu.gitops.utils.CommandExecutor
import com.cloudogu.gitops.utils.FileSystemUtils
-import com.cloudogu.gitops.config.Credentials
+
+import jakarta.inject.Provider
+import jakarta.inject.Singleton
import groovy.json.JsonBuilder
import groovy.json.JsonSlurper
import groovy.transform.Immutable
import groovy.util.logging.Slf4j
-import jakarta.inject.Provider
-import jakarta.inject.Singleton
@Slf4j
@Singleton
class K8sClient {
- private static final String[] APPLY_FROM_STDIN = ['kubectl', 'apply', '-f-']
-
- protected int SLEEPTIME = 1000
- protected int DEFAULT_RETRIES = 120
-
- private CommandExecutor commandExecutor
- private FileSystemUtils fileSystemUtils
- private Provider configProvider
- public K8sJavaApiClient k8sJavaApiClient
-
- K8sClient(
- CommandExecutor commandExecutor,
- FileSystemUtils fileSystemUtils,
- Provider configProvider
- ) {
- this.fileSystemUtils = fileSystemUtils
- this.commandExecutor = commandExecutor
- this.configProvider = configProvider
- this.k8sJavaApiClient = new K8sJavaApiClient()
- }
-
- private String waitForOutput(String[] command, String[] additionalCommand, String logMessage, String failureMessage, int maxTries = DEFAULT_RETRIES) {
- int tryCount = 0
- String output = ""
-
- log.debug(logMessage)
- while (output.isEmpty() && tryCount < maxTries) {
- if (!additionalCommand) {
- output = commandExecutor.execute(command).stdOut
- } else {
- output = commandExecutor.execute(command, additionalCommand).stdOut
- }
-
- if (output.isEmpty()) {
- tryCount++
- log.debug("Still waiting... (try $tryCount/$maxTries)")
- sleep(SLEEPTIME)
- }
- }
-
- if (output.isEmpty()) {
- throw new RuntimeException(failureMessage)
- }
-
- return output
- }
-
- private String waitForOutput(String[] command, String logMessage, String failureMessage, int maxTries = DEFAULT_RETRIES) {
- waitForOutput(command, null, logMessage, failureMessage, maxTries)
- }
-
- String waitForInternalNodeIp() {
- String node = waitForNode()
- // For k3d this is either the host's IP or the IP address of the k3d API server's container IP (when --bind-localhost=false)
- // Note that this might return multiple InternalIP (IPV4 and IPV6) - we assume the first one is IPV4 (break after first)
- String[] command = ["kubectl", "get", "$node",
- "--template='{{range .status.addresses}}{{ if eq .type \"InternalIP\" }}{{.address}}{{break}}{{end}}{{end}}'"]
- String output = waitForOutput(
- command,
- "Waiting for internal IP of node $node",
- "Failed to retrieve internal node IP"
- )
-
- log.debug("Internal IP of node $node: $output")
- return output
- }
-
- String waitForNodePort(String serviceName, String namespace) {
-
- String[] command = new Kubectl("get", "service", serviceName)
- .namespace(namespace)
- .mandatory("-o", "jsonpath={.spec.ports[0].nodePort}")
- .build()
-
- String output = waitForOutput(
- command,
- "Getting node port for service $serviceName, ns=$namespace",
- "Failed to get node port for service $serviceName, ns=$namespace"
- )
-
- log.debug("Node port for service $serviceName, ns=$namespace: $output")
- return output
- }
-
- /**
- * @return A string containing "node/nodeName", e.g. "node/k3d-gitops-playground-server-0"
- */
- String waitForNode() {
- String[] command1 = ['kubectl', 'get', 'node', '-oname']
- String[] command2 = ['head', '-n1']
-
- String output = waitForOutput(
- command1, command2,
- "Waiting for first node of the cluster to become ready",
- "Failed waiting for node of the cluster to become ready"
- )
-
- log.debug("First node of the cluster is ready: $output")
- return output
- }
-
- String applyYaml(String yamlLocation) {
- commandExecutor.execute("kubectl apply -f $yamlLocation").stdOut
- }
-
- /**
- * Creates a namespace with the specified name if it does not already exist.
- *
- * @param name the name of the namespace to create. Must not be {@code null} or empty.
- *
- * @throws IllegalArgumentException if the {@code name} is {@code null} or empty.
- * @throws RuntimeException if an error occurs during the creation of the namespace,
- * such as insufficient permissions.
- */
- void createNamespace(String name) {
- validateNamespace(name)
-
- if (!exists(name)) {
-
- log.debug("Namespace ${name} does not exist, proceeding to create.")
-
- // Create the namespace
- String[] createNamespaceCommand = new Kubectl("create", "namespace", name).build()
- try {
- CommandExecutor.Output createNamespaceOutput = commandExecutor.execute(createNamespaceCommand)
- log.debug("Namespace ${name} created successfully.")
- } catch (Exception e) {
- throw new RuntimeException("Failed to create namespace ${name} (possibly due to insufficient permissions)", e)
- }
- }
-
-
- }
-
- private boolean exists(String namespace) {
-// Check if the namespace already exists based on exitCode
- String[] checkNamespaceCommand = new Kubectl("get", "namespace", namespace).build()
- CommandExecutor.Output checkNamespaceOutput = commandExecutor.execute(checkNamespaceCommand, false)
-
- if (checkNamespaceOutput.exitCode == 0) {
- log.debug("Namespace ${namespace} already exists.")
- return true
- }
- return false
- }
-
- private void validateNamespace(String name) {
- if (name == null || name.trim().isEmpty()) {
- throw new IllegalArgumentException("Namespace name must be provided and cannot be null or empty.")
- }
- }
-
- /**
- * Creates multiple namespaces based on the given list of namespace names.
- *
- * @param names a list of strings representing the names of the namespaces to be created.
- * Must not be {@code null}.
- *
- * @throws IllegalArgumentException if the {@code names} list is {@code null}.
- */
- void createNamespaces(List names) {
- if (names == null) {
- throw new IllegalArgumentException("Namespaces must be provided and cannot be null.")
- }
- names.each { name ->
- createNamespace(name)
- }
- }
-
- /**
- * Idempotent create, i.e. overwrites if exists.
- */
- void createSecret(String type, String name, String namespace = '', Tuple2... literals) {
- def command1 = kubectl('create', 'secret', type, name)
- .namespace(namespace)
- .mandatory('--from-literal', literals)
- .dryRunOutputYaml()
- .build()
-
- commandExecutor.execute(command1, APPLY_FROM_STDIN)
- }
-
- String getArgoCDNamespacesSecret(String name, String namespace = '') {
- String[] command = ["kubectl", "get", 'secret', name, "-n", "${namespace}", '-ojsonpath={.data.namespaces}']
- String output = waitForOutput(
- command,
- "Getting Secret from Cluster",
- "Failed getting Secret from Cluster"
- )
- return output
- }
-
- /**
- * Idempotent create, i.e. overwrites if exists.
- */
- void createImagePullSecret(String name, String namespace = '', String host, String user, String password) {
- def command1 = kubectl('create', 'secret', 'docker-registry', name)
- .namespace(namespace)
- .mandatory('--docker-server', host)
- .mandatory('--docker-username', user)
- .mandatory('--docker-password', password)
- .dryRunOutputYaml()
- .build()
-
- commandExecutor.execute(command1, APPLY_FROM_STDIN)
- }
-
- /**
- * Idempotent create, i.e. overwrites if exists.
- */
- void createConfigMapFromFile(String name, String namespace = '', String filePath) {
- def command1 = kubectl('create', 'configmap', name)
- .namespace(namespace)
- .mandatory('--from-file', filePath)
- .dryRunOutputYaml()
- .build()
-
- commandExecutor.execute(command1, APPLY_FROM_STDIN)
- }
-
- /**
- * Idempotent create, i.e. overwrites if exists.
- *
- * @param tcp Port pairs can be specified as ':'.
- */
- void createServiceNodePort(String name, String tcp, String nodePort = '', String namespace = '') {
- def command1 = kubectl('create', 'service', 'nodeport', name)
- .namespace(namespace)
- .mandatory('--tcp', tcp)
- .optional('--node-port', nodePort)
- .dryRunOutputYaml()
- .build()
-
- commandExecutor.execute(command1, APPLY_FROM_STDIN)
- }
-
- void labelRemove(String resource, String name, String namespace = '', String... keys) {
- Tuple2[] tuples = keys.collect { new Tuple2("${it}-", "") }.toArray(new Tuple2[0])
- label(resource, name, namespace, tuples)
- }
-
- void label(String resource, String name, String namespace = '', Tuple2... keyValues) {
- if (!keyValues) {
- throw new RuntimeException("Missing key-value-pairs")
- }
- String command =
- "kubectl label ${resource} ${name}${namespace ? " -n ${namespace}" : ''} " +
- '--overwrite ' + // Make idempotent
- keyValues.collect { "${it.v1}${it.v2 ? "=${it.v2}" : ''}" }.join(' ')
- commandExecutor.execute(command)
- }
-
- String run(String name, String image, String namespace = '', Map overrides = [:], String... params) {
-
- def command1 = kubectl('run', name)
- .mandatory('--image', image)
- .namespace(namespace)
- .optional(params)
- .optional('--overrides', mapToJson(overrides, 'kubectl run overrides'))
- .build()
-
- commandExecutor.execute(command1).stdOut
- }
-
- void patch(String resource, String name, String namespace = '', String type = '', Map yaml) {
- // We're using a patch file here, instead of a patch JSON (--patch), because of quoting issues
- // ERROR c.c.gitops.utils.CommandExecutor - Stderr: error: unable to parse "'{\"stringData\":": yaml: found unexpected end of stream
- File patchYaml = File.createTempFile('gitops-playground-patch-yaml', '')
- log.trace("Writing patch YAML: ${yaml}")
- fileSystemUtils.writeYaml(yaml, patchYaml)
-
- // kubectl patch secret argocd-secret -p '{"stringData": { "admin.password": "'"${bcryptArgoCDPassword}"'"}}' || true
- String command =
- "kubectl patch ${resource} ${name}${namespace ? " -n ${namespace}" : ''}" +
- (type ? " --type=$type" : '') +
- " --patch-file=${patchYaml.absolutePath}"
- commandExecutor.execute(command)
- }
-
- void delete(String resource, String namespace = '', Tuple2... selectors) {
- if (!selectors) {
- throw new RuntimeException("Missing selectors")
- }
- // kubectl delete secret -n argocd -l owner=helm,name=argocd
- String command =
- "kubectl delete ${resource}${namespace ? " -n ${namespace}" : ''}" +
- ' --ignore-not-found=true ' + // Make idempotent
- selectors.collect { "--selector=${it.v1}=${it.v2}" }.join(' ')
-
- commandExecutor.execute(command)
- }
-
- void delete(String resource, String namespace, String name) {
- String command =
- "kubectl delete ${resource}${namespace ? " -n ${namespace}" : ''}" +
- " $name" +
- ' --ignore-not-found=true ' // Make idempotent
-
- commandExecutor.execute(command)
- }
-
- List getCustomResource(String resource) {
- String[] command = ["kubectl", "get", resource, "-A", "-o", "jsonpath={range .items[*]}{.metadata.namespace}{','}{.metadata.name}{'\\n'}{end}"]
- def result = commandExecutor.execute(command)
-
- if (!result.stdOut) {
- return []
- }
-
- return result.stdOut.split('\n').collect { line ->
- def parts = line.split(',')
- new CustomResource(parts[0].trim(), parts[1].trim())
- }
- }
-
- String getConfigMap(String mapName, String key) {
- String[] command = ["kubectl", "get", "configmap", mapName, "-o", "jsonpath={.data['" + key.replace(".", "\\.") + "']}"]
- def result = commandExecutor.execute(command, false)
- if (result.exitCode != 0) {
- throw new RuntimeException("Could not fetch configmap $mapName: ${result.stdErr}")
- }
-
- if (result.stdOut == "") {
- throw new RuntimeException("Could not fetch $key within config-map $mapName")
- }
-
- return result.stdOut
- }
-
- String getCurrentContext() {
- // When running inside a pod this might fail
- def output = commandExecutor.execute('kubectl config current-context', false)
- if (!output.stdOut) {
- output.stdOut = '(current context not set)'
- }
- return output.stdOut
- }
-
- /**
- * @param resource resource to get the annotation from
- * @param name name of the resource, only one resource allowed!
- * @param key key of the annotation
- * @param namespace namespace of the resource (if not cluster wide)
- *
- * @return the value of the annotation
- */
- String getAnnotation(String resource, String name, String key, String namespace = '') {
- List commandAsList = [
- "kubectl",
- "get",
- resource,
- name,
- "-o",
- // jsonpath expects a single resource object
- // some requests with multiple resources may result in a listed response
- // that does not match the jsonpath
- "jsonpath={.metadata.annotations}"
- ]
- if (namespace) {
- commandAsList.add("-n $namespace" as String)
- }
- String[] command = commandAsList.toArray(new String[0])
- def result = commandExecutor.execute(command, false)
- if (!result.getStdErr().isEmpty()) {
- throw new RuntimeException("Failed to fetch data from resource [$resource/$name] in namespace [$namespace]: ${result.stdErr}")
- }
- log.debug("getAnnotation returns = ${result.stdOut}")
- def value = new JsonSlurper().parseText(result.stdOut) as Map
- String myResult = value[key]
- return myResult
- }
-
-
- private Kubectl kubectl(String... args) {
- new Kubectl(args)
- }
-
- /**
- * Patches the nodePort of a specified port in a service.
- *
- * @param serviceName The name of the service to patch.
- * @param namespace The namespace of the service.
- * @param portName The name of the port to patch.
- * @param newNodePort The new nodePort value to set.
- *
- * @throws IllegalArgumentException if name, namespace, portName, and nodePort are invalid.
- * @throws RuntimeException if an error occurs while patching the service (i.e. portName not found).
- */
- void patchServiceNodePort(String serviceName, String namespace, String portName, int newNodePort) {
- validateInputForPatch(serviceName, namespace, portName, newNodePort)
-
- // Get the current service spec to find the index of the port to patch
- String[] getServiceCommand = new Kubectl("get", "service", serviceName)
- .namespace(namespace)
- .mandatory("-o", "json")
- .build()
- CommandExecutor.Output getServiceOutput = commandExecutor.execute(getServiceCommand)
- def serviceSpec = new JsonSlurper().parseText(getServiceOutput.stdOut)
- def ports = serviceSpec['spec']['ports']
-
- // Find the index of the port to patch
- def portIndex = ports.findIndexOf { it['name'] == portName }
- if (portIndex == -1) {
- throw new RuntimeException("Port with name ${portName} not found in service ${serviceName}.")
- }
-
- // Create the JSON patch for the specific port
- def patch = [
- [
- op : "replace",
- path : "/spec/ports/${portIndex}/nodePort",
- value: newNodePort
- ]
- ]
- String patchJson = new JsonBuilder(patch).toString()
-
- // Apply the patch
- String[] patchCommand = new Kubectl("patch", "service", serviceName)
- .namespace(namespace)
- .mandatory("--type", "json")
- .mandatory("-p", patchJson)
- .build()
- CommandExecutor.Output patchOutput = commandExecutor.execute(patchCommand)
- log.debug("Service ${serviceName} in namespace ${namespace} successfully patched with nodePort ${newNodePort} for port ${portName}.")
- }
-
- private static String mapToJson(Map kubectlJson, String debugPrefix) {
- if (kubectlJson.isEmpty()) {
- return ''
- }
-
- JsonBuilder json = new JsonBuilder(kubectlJson)
- log.debug("${debugPrefix} JSON pretty printed:\n${json.toPrettyString()}")
- // Note that toPrettyString() will lead to empty results in some shell, e.g. plain sh 🧐
- return json.toString()
- }
-
- private void validateInputForPatch(String serviceName, String namespace, String portName, int newNodePort) {
- if (!serviceName || !namespace || !portName || newNodePort <= 0) {
- throw new IllegalArgumentException("Service name, namespace, port name, and valid nodePort must be provided")
- }
- }
-
- /**
- * Waits until the specified resource reaches the desired phase.
- *
- * @param resourceType The type of the Kubernetes resource (e.g., pod, deployment).
- * @param resourceName The name of the specific resource.
- * @param namespace The namespace of the resource.
- * @param desiredPhase The desired phase to wait for (e.g., Running, Succeeded).
- * @param timeoutSeconds The maximum time to wait for the desired phase in seconds.
- * @param checkIntervalSeconds The interval between status checks in seconds.
- *
- * @throws IllegalArgumentException if Resource type, name, namespace, desired phase, Timeout and check interval are invalid.
- * @throws RuntimeException if the desired phase is not reached within the timeout period.
- */
- void waitForResourcePhase(String resourceType, String resourceName, String namespace, String desiredPhase, int timeoutSeconds, int checkIntervalSeconds) {
- validateInputForWaitPhase(resourceType, resourceName, namespace, desiredPhase, timeoutSeconds, checkIntervalSeconds)
-
- long startTime = System.currentTimeMillis()
- long endTime = startTime + (timeoutSeconds * 1000)
-
- while (System.currentTimeMillis() < endTime) {
- String[] command = new Kubectl("get", resourceType, resourceName)
- .namespace(namespace)
- .mandatory("-o", "jsonpath={.status.phase}")
- .build()
-
- def output = commandExecutor.execute(command)
- String phase = output.stdOut.trim()
- if (phase == desiredPhase) {
- log.debug("Resource ${resourceType}/${resourceName} in namespace ${namespace} reached the desired phase: ${desiredPhase}")
- return
- }
-
- log.debug("Current phase: ${phase}. Waiting for phase: ${desiredPhase}...")
- sleep(checkIntervalSeconds * 1000)
- }
-
- // Never reached the desired Phase, so throw a RuntimeException and end the execution
- throw new RuntimeException("Timeout reached. Resource ${resourceType}/${resourceName} in namespace ${namespace} did not reach the desired phase: ${desiredPhase} within ${timeoutSeconds} seconds.")
- }
-
- private void validateInputForWaitPhase(String resourceType, String resourceName, String namespace, String desiredPhase, int timeoutSeconds, int checkIntervalSeconds) {
- if (!resourceType || !resourceName || !namespace || !desiredPhase) {
- throw new IllegalArgumentException("Resource type, name, namespace, and desired phase must be provided")
- }
- if (timeoutSeconds <= 0 || checkIntervalSeconds <= 0) {
- throw new IllegalArgumentException("Timeout and check interval must be greater than zero")
- }
- }
-
- /**
- * Waits for a specific resource to reach the desired phase with default timeout and interval.
- *
- * @param resourceType The type of the Kubernetes resource (e.g., pod, deployment).
- * @param resourceName The name of the specific resource.
- * @param namespace The namespace of the resource.
- * @param desiredPhase The desired phase to wait for (e.g., Running, Succeeded).
- *
- * @see #waitForResourcePhase(String, String, String, String, int, int)
- */
- void waitForResourcePhase(String resourceType, String resourceName, String namespace, String desiredPhase) {
- waitForResourcePhase(resourceType, resourceName, namespace, desiredPhase, 60, 1)
- }
-
- @Immutable
- static class CustomResource {
- String namespace
- String name
- }
-
- private class Kubectl {
- private List command = ['kubectl']
-
- Kubectl(String... args) {
- command.addAll(args)
- }
-
- Kubectl namespace(String namespace) {
- if (namespace) {
- this.command += ['-n', namespace]
- }
- return this
- }
-
- Kubectl mandatory(String paramName, String value) {
- // Here we could assert that value != null. For historical reasons we don't, for now.
- this.command += [paramName, value]
- return this
- }
-
- Kubectl mandatory(String paramName, Tuple2... values) {
- if (!values) {
- throw new RuntimeException("Missing values for parameter '${paramName}' in command '${command.join(' ')}'")
- }
- values.each { command += [paramName, "${it.v1}=${it.v2 ? it.v2 : ''}".toString()] }
- return this
- }
-
- Kubectl optional(String paramName, String value) {
- if (value) {
- this.command += [paramName, value]
- }
- return this
- }
-
- Kubectl optional(String... params) {
- command.addAll(params)
- return this
- }
-
- Kubectl dryRunOutputYaml() {
- this.command += ['--dry-run=client', '-oyaml']
- return this
- }
-
- String[] build() {
- this.command
- }
- }
+ private static final String[] APPLY_FROM_STDIN = ['kubectl', 'apply', '-f-']
+
+ protected int SLEEPTIME = 1000
+ protected int DEFAULT_RETRIES = 120
+
+ private CommandExecutor commandExecutor
+ private FileSystemUtils fileSystemUtils
+ private Provider configProvider
+ public K8sJavaApiClient k8sJavaApiClient
+
+ K8sClient(CommandExecutor commandExecutor,
+ FileSystemUtils fileSystemUtils,
+ Provider configProvider) {
+ this.fileSystemUtils = fileSystemUtils
+ this.commandExecutor = commandExecutor
+ this.configProvider = configProvider
+ this.k8sJavaApiClient = new K8sJavaApiClient()
+ }
+
+ private String waitForOutput(String[] command, String[] additionalCommand, String logMessage, String failureMessage, int maxTries = DEFAULT_RETRIES) {
+ int tryCount = 0
+ String output = ""
+
+ log.debug(logMessage)
+ while (output.isEmpty() && tryCount < maxTries) {
+ if (!additionalCommand) {
+ output = commandExecutor.execute(command).stdOut
+ } else {
+ output = commandExecutor.execute(command, additionalCommand).stdOut
+ }
+
+ if (output.isEmpty()) {
+ tryCount++
+ log.debug("Still waiting... (try $tryCount/$maxTries)")
+ sleep(SLEEPTIME)
+ }
+ }
+
+ if (output.isEmpty()) {
+ throw new RuntimeException(failureMessage)
+ }
+
+ return output
+ }
+
+ private String waitForOutput(String[] command, String logMessage, String failureMessage, int maxTries = DEFAULT_RETRIES) {
+ waitForOutput(command, null, logMessage, failureMessage, maxTries)
+ }
+
+ String waitForInternalNodeIp() {
+ String node = waitForNode()
+ // For k3d this is either the host's IP or the IP address of the k3d API server's container IP (when --bind-localhost=false)
+ // Note that this might return multiple InternalIP (IPV4 and IPV6) - we assume the first one is IPV4 (break after first)
+ String[] command = ["kubectl", "get", "$node",
+ "--template='{{range .status.addresses}}{{ if eq .type \"InternalIP\" }}{{.address}}{{break}}{{end}}{{end}}'"]
+ String output = waitForOutput(command,
+ "Waiting for internal IP of node $node",
+ "Failed to retrieve internal node IP")
+
+ log.debug("Internal IP of node $node: $output")
+ return output
+ }
+
+ String waitForNodePort(String serviceName, String namespace) {
+
+ String[] command = new Kubectl("get", "service", serviceName)
+ .namespace(namespace)
+ .mandatory("-o", "jsonpath={.spec.ports[0].nodePort}")
+ .build()
+
+ String output = waitForOutput(command,
+ "Getting node port for service $serviceName, ns=$namespace",
+ "Failed to get node port for service $serviceName, ns=$namespace")
+
+ log.debug("Node port for service $serviceName, ns=$namespace: $output")
+ return output
+ }
+
+ /**
+ * @return A string containing "node/nodeName", e.g. "node/k3d-gitops-playground-server-0"
+ */
+ String waitForNode() {
+ String[] command1 = ['kubectl', 'get', 'node', '-oname']
+ String[] command2 = ['head', '-n1']
+
+ String output = waitForOutput(command1, command2,
+ "Waiting for first node of the cluster to become ready",
+ "Failed waiting for node of the cluster to become ready")
+
+ log.debug("First node of the cluster is ready: $output")
+ return output
+ }
+
+ String applyYaml(String yamlLocation) {
+ commandExecutor.execute("kubectl apply -f $yamlLocation").stdOut
+ }
+
+ /**
+ * Creates a namespace with the specified name if it does not already exist.
+ *
+ * @param name the name of the namespace to create. Must not be {@code null} or empty.
+ *
+ * @throws IllegalArgumentException if the {@code name} is {@code null} or empty.
+ * @throws RuntimeException if an error occurs during the creation of the namespace,
+ * such as insufficient permissions.
+ */
+ void createNamespace(String name) {
+ validateNamespace(name)
+
+ if (!exists(name)) {
+
+ log.debug("Namespace ${name} does not exist, proceeding to create.")
+
+ // Create the namespace
+ String[] createNamespaceCommand = new Kubectl("create", "namespace", name).build()
+ try {
+ CommandExecutor.Output createNamespaceOutput = commandExecutor.execute(createNamespaceCommand)
+ log.debug("Namespace ${name} created successfully.")
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to create namespace ${name} (possibly due to insufficient permissions)", e)
+ }
+ }
+
+ }
+
+ private boolean exists(String namespace) {
+ // Check if the namespace already exists based on exitCode
+ String[] checkNamespaceCommand = new Kubectl("get", "namespace", namespace).build()
+ CommandExecutor.Output checkNamespaceOutput = commandExecutor.execute(checkNamespaceCommand, false)
+
+ if (checkNamespaceOutput.exitCode == 0) {
+ log.debug("Namespace ${namespace} already exists.")
+ return true
+ }
+ return false
+ }
+
+ private void validateNamespace(String name) {
+ if (name == null || name.trim().isEmpty()) {
+ throw new IllegalArgumentException("Namespace name must be provided and cannot be null or empty.")
+ }
+ }
+
+ /**
+ * Creates multiple namespaces based on the given list of namespace names.
+ *
+ * @param names a list of strings representing the names of the namespaces to be created.
+ * Must not be {@code null}.
+ *
+ * @throws IllegalArgumentException if the {@code names} list is {@code null}.
+ */
+ void createNamespaces(List names) {
+ if (names == null) {
+ throw new IllegalArgumentException("Namespaces must be provided and cannot be null.")
+ }
+ names.each { name -> createNamespace(name)
+ }
+ }
+
+ /**
+ * Idempotent create, i.e. overwrites if exists.*/
+ void createSecret(String type, String name, String namespace = '', Tuple2... literals) {
+ def command1 = kubectl('create', 'secret', type, name)
+ .namespace(namespace)
+ .mandatory('--from-literal', literals)
+ .dryRunOutputYaml()
+ .build()
+
+ commandExecutor.execute(command1, APPLY_FROM_STDIN)
+ }
+
+ String getArgoCDNamespacesSecret(String name, String namespace = '') {
+ String[] command = ["kubectl", "get", 'secret', name, "-n", "${namespace}", '-ojsonpath={.data.namespaces}']
+ String output = waitForOutput(command,
+ "Getting Secret from Cluster",
+ "Failed getting Secret from Cluster")
+ return output
+ }
+
+ /**
+ * Idempotent create, i.e. overwrites if exists.*/
+ void createImagePullSecret(String name, String namespace = '', String host, String user, String password) {
+ def command1 = kubectl('create', 'secret', 'docker-registry', name)
+ .namespace(namespace)
+ .mandatory('--docker-server', host)
+ .mandatory('--docker-username', user)
+ .mandatory('--docker-password', password)
+ .dryRunOutputYaml()
+ .build()
+
+ commandExecutor.execute(command1, APPLY_FROM_STDIN)
+ }
+
+ /**
+ * Idempotent create, i.e. overwrites if exists.*/
+ void createConfigMapFromFile(String name, String namespace = '', String filePath) {
+ def command1 = kubectl('create', 'configmap', name)
+ .namespace(namespace)
+ .mandatory('--from-file', filePath)
+ .dryRunOutputYaml()
+ .build()
+
+ commandExecutor.execute(command1, APPLY_FROM_STDIN)
+ }
+
+ /**
+ * Idempotent create, i.e. overwrites if exists.
+ *
+ * @param tcp Port pairs can be specified as ':'.
+ */
+ void createServiceNodePort(String name, String tcp, String nodePort = '', String namespace = '') {
+ def command1 = kubectl('create', 'service', 'nodeport', name)
+ .namespace(namespace)
+ .mandatory('--tcp', tcp)
+ .optional('--node-port', nodePort)
+ .dryRunOutputYaml()
+ .build()
+
+ commandExecutor.execute(command1, APPLY_FROM_STDIN)
+ }
+
+ void labelRemove(String resource, String name, String namespace = '', String... keys) {
+ Tuple2[] tuples = keys.collect { new Tuple2("${it}-", "") }.toArray(new Tuple2[0])
+ label(resource, name, namespace, tuples)
+ }
+
+ void label(String resource, String name, String namespace = '', Tuple2... keyValues) {
+ if (!keyValues) {
+ throw new RuntimeException("Missing key-value-pairs")
+ }
+ String command =
+ "kubectl label ${resource} ${name}${namespace ? " -n ${namespace}" : ''} " + '--overwrite ' + // Make idempotent
+ keyValues.collect { "${it.v1}${it.v2 ? "=${it.v2}" : ''}" }.join(' ')
+ commandExecutor.execute(command)
+ }
+
+ String run(String name, String image, String namespace = '', Map overrides = [:], String... params) {
+
+ def command1 = kubectl('run', name)
+ .mandatory('--image', image)
+ .namespace(namespace)
+ .optional(params)
+ .optional('--overrides', mapToJson(overrides, 'kubectl run overrides'))
+ .build()
+
+ commandExecutor.execute(command1).stdOut
+ }
+
+ void patch(String resource, String name, String namespace = '', String type = '', Map yaml) {
+ // We're using a patch file here, instead of a patch JSON (--patch), because of quoting issues
+ // ERROR c.c.gitops.utils.CommandExecutor - Stderr: error: unable to parse "'{\"stringData\":": yaml: found unexpected end of stream
+ File patchYaml = File.createTempFile('gitops-playground-patch-yaml', '')
+ log.trace("Writing patch YAML: ${yaml}")
+ fileSystemUtils.writeYaml(yaml, patchYaml)
+
+ // kubectl patch secret argocd-secret -p '{"stringData": { "admin.password": "'"${bcryptArgoCDPassword}"'"}}' || true
+ String command =
+ "kubectl patch ${resource} ${name}${namespace ? " -n ${namespace}" : ''}" + (type ? " --type=$type" : '') + " --patch-file=${patchYaml.absolutePath}"
+ commandExecutor.execute(command)
+ }
+
+ void delete(String resource, String namespace = '', Tuple2... selectors) {
+ if (!selectors) {
+ throw new RuntimeException("Missing selectors")
+ }
+ // kubectl delete secret -n argocd -l owner=helm,name=argocd
+ String command =
+ "kubectl delete ${resource}${namespace ? " -n ${namespace}" : ''}" + ' --ignore-not-found=true ' + // Make idempotent
+ selectors.collect { "--selector=${it.v1}=${it.v2}" }.join(' ')
+
+ commandExecutor.execute(command)
+ }
+
+ void delete(String resource, String namespace, String name) {
+ String command =
+ "kubectl delete ${resource}${namespace ? " -n ${namespace}" : ''}" + " $name" + ' --ignore-not-found=true '
+ // Make idempotent
+
+ commandExecutor.execute(command)
+ }
+
+ List getCustomResource(String resource) {
+ String[] command = ["kubectl", "get", resource, "-A", "-o", "jsonpath={range .items[*]}{.metadata.namespace}{','}{.metadata.name}{'\\n'}{end}"]
+ def result = commandExecutor.execute(command)
+
+ if (!result.stdOut) {
+ return []
+ }
+
+ return result.stdOut.split('\n').collect { line ->
+ def parts = line.split(',')
+ new CustomResource(parts[0].trim(), parts[1].trim())
+ }
+ }
+
+ String getConfigMap(String mapName, String key) {
+ String[] command = ["kubectl", "get", "configmap", mapName, "-o", "jsonpath={.data['" + key.replace(".", "\\.") + "']}"]
+ def result = commandExecutor.execute(command, false)
+ if (result.exitCode != 0) {
+ throw new RuntimeException("Could not fetch configmap $mapName: ${result.stdErr}")
+ }
+
+ if (result.stdOut == "") {
+ throw new RuntimeException("Could not fetch $key within config-map $mapName")
+ }
+
+ return result.stdOut
+ }
+
+ String getCurrentContext() {
+ // When running inside a pod this might fail
+ def output = commandExecutor.execute('kubectl config current-context', false)
+ if (!output.stdOut) {
+ output.stdOut = '(current context not set)'
+ }
+ return output.stdOut
+ }
+
+ /**
+ * @param resource resource to get the annotation from
+ * @param name name of the resource, only one resource allowed!
+ * @param key key of the annotation
+ * @param namespace namespace of the resource (if not cluster wide)
+ *
+ * @return the value of the annotation
+ */
+ String getAnnotation(String resource, String name, String key, String namespace = '') {
+ List commandAsList = ["kubectl",
+ "get",
+ resource,
+ name,
+ "-o",
+ // jsonpath expects a single resource object
+ // some requests with multiple resources may result in a listed response
+ // that does not match the jsonpath
+ "jsonpath={.metadata.annotations}"]
+ if (namespace) {
+ commandAsList.add("-n $namespace" as String)
+ }
+ String[] command = commandAsList.toArray(new String[0])
+ def result = commandExecutor.execute(command, false)
+ if (!result.getStdErr().isEmpty()) {
+ throw new RuntimeException("Failed to fetch data from resource [$resource/$name] in namespace [$namespace]: ${result.stdErr}")
+ }
+ log.debug("getAnnotation returns = ${result.stdOut}")
+ def value = new JsonSlurper().parseText(result.stdOut) as Map
+ String myResult = value[key]
+ return myResult
+ }
+
+ private Kubectl kubectl(String... args) {
+ new Kubectl(args)
+ }
+
+ /**
+ * Patches the nodePort of a specified port in a service.
+ *
+ * @param serviceName The name of the service to patch.
+ * @param namespace The namespace of the service.
+ * @param portName The name of the port to patch.
+ * @param newNodePort The new nodePort value to set.
+ *
+ * @throws IllegalArgumentException if name, namespace, portName, and nodePort are invalid.
+ * @throws RuntimeException if an error occurs while patching the service (i.e. portName not found).
+ */
+ void patchServiceNodePort(String serviceName, String namespace, String portName, int newNodePort) {
+ validateInputForPatch(serviceName, namespace, portName, newNodePort)
+
+ // Get the current service spec to find the index of the port to patch
+ String[] getServiceCommand = new Kubectl("get", "service", serviceName)
+ .namespace(namespace)
+ .mandatory("-o", "json")
+ .build()
+ CommandExecutor.Output getServiceOutput = commandExecutor.execute(getServiceCommand)
+ def serviceSpec = new JsonSlurper().parseText(getServiceOutput.stdOut)
+ def ports = serviceSpec['spec']['ports']
+
+ // Find the index of the port to patch
+ def portIndex = ports.findIndexOf { it['name'] == portName }
+ if (portIndex == -1) {
+ throw new RuntimeException("Port with name ${portName} not found in service ${serviceName}.")
+ }
+
+ // Create the JSON patch for the specific port
+ def patch = [[op : "replace",
+ path : "/spec/ports/${portIndex}/nodePort",
+ value: newNodePort]]
+ String patchJson = new JsonBuilder(patch).toString()
+
+ // Apply the patch
+ String[] patchCommand = new Kubectl("patch", "service", serviceName)
+ .namespace(namespace)
+ .mandatory("--type", "json")
+ .mandatory("-p", patchJson)
+ .build()
+ CommandExecutor.Output patchOutput = commandExecutor.execute(patchCommand)
+ log.debug("Service ${serviceName} in namespace ${namespace} successfully patched with nodePort ${newNodePort} for port ${portName}.")
+ }
+
+ private static String mapToJson(Map kubectlJson, String debugPrefix) {
+ if (kubectlJson.isEmpty()) {
+ return ''
+ }
+
+ JsonBuilder json = new JsonBuilder(kubectlJson)
+ log.debug("${debugPrefix} JSON pretty printed:\n${json.toPrettyString()}")
+ // Note that toPrettyString() will lead to empty results in some shell, e.g. plain sh 🧐
+ return json.toString()
+ }
+
+ private void validateInputForPatch(String serviceName, String namespace, String portName, int newNodePort) {
+ if (!serviceName || !namespace || !portName || newNodePort <= 0) {
+ throw new IllegalArgumentException("Service name, namespace, port name, and valid nodePort must be provided")
+ }
+ }
+
+ /**
+ * Waits until the specified resource reaches the desired phase.
+ *
+ * @param resourceType The type of the Kubernetes resource (e.g., pod, deployment).
+ * @param resourceName The name of the specific resource.
+ * @param namespace The namespace of the resource.
+ * @param desiredPhase The desired phase to wait for (e.g., Running, Succeeded).
+ * @param timeoutSeconds The maximum time to wait for the desired phase in seconds.
+ * @param checkIntervalSeconds The interval between status checks in seconds.
+ *
+ * @throws IllegalArgumentException if Resource type, name, namespace, desired phase, Timeout and check interval are invalid.
+ * @throws RuntimeException if the desired phase is not reached within the timeout period.
+ */
+ void waitForResourcePhase(String resourceType, String resourceName, String namespace, String desiredPhase, int timeoutSeconds, int checkIntervalSeconds) {
+ validateInputForWaitPhase(resourceType, resourceName, namespace, desiredPhase, timeoutSeconds, checkIntervalSeconds)
+
+ long startTime = System.currentTimeMillis()
+ long endTime = startTime + (timeoutSeconds * 1000)
+
+ while (System.currentTimeMillis() < endTime) {
+ String[] command = new Kubectl("get", resourceType, resourceName)
+ .namespace(namespace)
+ .mandatory("-o", "jsonpath={.status.phase}")
+ .build()
+
+ def output = commandExecutor.execute(command)
+ String phase = output.stdOut.trim()
+ if (phase == desiredPhase) {
+ log.debug("Resource ${resourceType}/${resourceName} in namespace ${namespace} reached the desired phase: ${desiredPhase}")
+ return
+ }
+
+ log.debug("Current phase: ${phase}. Waiting for phase: ${desiredPhase}...")
+ sleep(checkIntervalSeconds * 1000)
+ }
+
+ // Never reached the desired Phase, so throw a RuntimeException and end the execution
+ throw new RuntimeException("Timeout reached. Resource ${resourceType}/${resourceName} in namespace ${namespace} did not reach the desired phase: ${desiredPhase} within ${timeoutSeconds} seconds.")
+ }
+
+ private void validateInputForWaitPhase(String resourceType, String resourceName, String namespace, String desiredPhase, int timeoutSeconds, int checkIntervalSeconds) {
+ if (!resourceType || !resourceName || !namespace || !desiredPhase) {
+ throw new IllegalArgumentException("Resource type, name, namespace, and desired phase must be provided")
+ }
+ if (timeoutSeconds <= 0 || checkIntervalSeconds <= 0) {
+ throw new IllegalArgumentException("Timeout and check interval must be greater than zero")
+ }
+ }
+
+ /**
+ * Waits for a specific resource to reach the desired phase with default timeout and interval.
+ *
+ * @param resourceType The type of the Kubernetes resource (e.g., pod, deployment).
+ * @param resourceName The name of the specific resource.
+ * @param namespace The namespace of the resource.
+ * @param desiredPhase The desired phase to wait for (e.g., Running, Succeeded).
+ *
+ * @see #waitForResourcePhase(String, String, String, String, int, int)
+ */
+ void waitForResourcePhase(String resourceType, String resourceName, String namespace, String desiredPhase) {
+ waitForResourcePhase(resourceType, resourceName, namespace, desiredPhase, 60, 1)
+ }
+
+ @Immutable
+ static class CustomResource {
+ String namespace
+ String name
+ }
+
+ private class Kubectl {
+ private List command = ['kubectl']
+
+ Kubectl(String... args) {
+ command.addAll(args)
+ }
+
+ Kubectl namespace(String namespace) {
+ if (namespace) {
+ this.command += ['-n', namespace]
+ }
+ return this
+ }
+
+ Kubectl mandatory(String paramName, String value) {
+ // Here we could assert that value != null. For historical reasons we don't, for now.
+ this.command += [paramName, value]
+ return this
+ }
+
+ Kubectl mandatory(String paramName, Tuple2... values) {
+ if (!values) {
+ throw new RuntimeException("Missing values for parameter '${paramName}' in command '${command.join(' ')}'")
+ }
+ values.each { command += [paramName, "${it.v1}=${it.v2 ? it.v2 : ''}".toString()] }
+ return this
+ }
+
+ Kubectl optional(String paramName, String value) {
+ if (value) {
+ this.command += [paramName, value]
+ }
+ return this
+ }
+
+ Kubectl optional(String... params) {
+ command.addAll(params)
+ return this
+ }
+
+ Kubectl dryRunOutputYaml() {
+ this.command += ['--dry-run=client', '-oyaml']
+ return this
+ }
+
+ String[] build() {
+ this.command
+ }
+ }
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/kubernetes/api/K8sJavaApiClient.groovy b/src/main/groovy/com/cloudogu/gitops/kubernetes/api/K8sJavaApiClient.groovy
index b03e51de3..3c3559736 100644
--- a/src/main/groovy/com/cloudogu/gitops/kubernetes/api/K8sJavaApiClient.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/kubernetes/api/K8sJavaApiClient.groovy
@@ -1,6 +1,7 @@
package com.cloudogu.gitops.kubernetes.api
import com.cloudogu.gitops.config.Credentials
+
import io.fabric8.kubernetes.api.model.IntOrString
import io.fabric8.kubernetes.api.model.Secret
import io.fabric8.kubernetes.api.model.Service
@@ -10,86 +11,80 @@ import io.fabric8.kubernetes.client.KubernetesClientBuilder
class K8sJavaApiClient {
- KubernetesClient client
+ KubernetesClient client
- K8sJavaApiClient(){
- this.client = new KubernetesClientBuilder().build()
- }
+ K8sJavaApiClient() {
+ this.client = new KubernetesClientBuilder().build()
+ }
- /**
- * Gets login credentials from a K8s secret
- */
- Credentials getCredentialsFromSecret(String secretname, String namespace, String usernameKey='username', String passwordKey='password') {
- try {
- Secret secret = this.client.secrets()
- .inNamespace(namespace)
- .withName(secretname)
- .get()
+ /**
+ * Gets login credentials from a K8s secret*/
+ Credentials getCredentialsFromSecret(String secretname, String namespace, String usernameKey = 'username', String passwordKey = 'password') {
+ try {
+ Secret secret = this.client.secrets()
+ .inNamespace(namespace)
+ .withName(secretname)
+ .get()
- def secretData = secret.getData()
- String username = new String(Base64.getDecoder().decode(secretData[usernameKey]))
- String password = new String(Base64.getDecoder().decode(secretData[passwordKey]))
- return new Credentials(username, password)
- } catch (Exception e) {
- throw new RuntimeException("Couldn't parse credentials from K8s secret: ${secretname} in namespace ${namespace}", e)
- }
- }
+ def secretData = secret.getData()
+ String username = new String(Base64.getDecoder().decode(secretData[usernameKey]))
+ String password = new String(Base64.getDecoder().decode(secretData[passwordKey]))
+ return new Credentials(username, password)
+ } catch (Exception e) {
+ throw new RuntimeException("Couldn't parse credentials from K8s secret: ${secretname} in namespace ${namespace}", e)
+ }
+ }
- Service createNodePortService(
- String namespace,
- String serviceName,
- Map selector,
- Integer port,
- Integer nodePort,
- String portName = 'custom-port'
- ) {
+ Service createNodePortService(String namespace,
+ String serviceName,
+ Map selector,
+ Integer port,
+ Integer nodePort,
+ String portName = 'custom-port') {
- def service = new ServiceBuilder()
- .withNewMetadata()
- .withName(serviceName)
- .withNamespace(namespace)
- .endMetadata()
- .withNewSpec()
- .withType("NodePort")
- .addToSelector(selector)
- .addNewPort()
- .withName(portName)
- .withPort(port)
- .withTargetPort(new IntOrString(port))
- .withNodePort(nodePort)
- .endPort()
- .endSpec()
- .build()
+ def service = new ServiceBuilder()
+ .withNewMetadata()
+ .withName(serviceName)
+ .withNamespace(namespace)
+ .endMetadata()
+ .withNewSpec()
+ .withType("NodePort")
+ .addToSelector(selector)
+ .addNewPort()
+ .withName(portName)
+ .withPort(port)
+ .withTargetPort(new IntOrString(port))
+ .withNodePort(nodePort)
+ .endPort()
+ .endSpec()
+ .build()
- client.services()
- .inNamespace(namespace)
- .resource(service)
- .create()
- }
+ client.services()
+ .inNamespace(namespace)
+ .resource(service)
+ .create()
+ }
- /**
- * Gets login credentials from a K8s secret
- */
- Credentials getCredentialsFromSecret(Credentials credentials) {
- try {
- Secret secret = this.client.secrets()
- .inNamespace(credentials.secretNamespace)
- .withName(credentials.secretName)
- .get()
+ /**
+ * Gets login credentials from a K8s secret*/
+ Credentials getCredentialsFromSecret(Credentials credentials) {
+ try {
+ Secret secret = this.client.secrets()
+ .inNamespace(credentials.secretNamespace)
+ .withName(credentials.secretName)
+ .get()
- def secretData = secret.getData()
- def usernameEncoded = secretData[credentials.usernameKey]
- String username = usernameEncoded != null
- ? new String(Base64.decoder.decode(usernameEncoded))
- : credentials.username
- String password = new String(Base64.getDecoder().decode(secretData[credentials.passwordKey]))
- Credentials credentialsNew = new Credentials(credentials)
- credentialsNew.username = username
- credentialsNew.password = password
+ def secretData = secret.getData()
+ def usernameEncoded = secretData[credentials.usernameKey]
+ String username = usernameEncoded != null ? new String(Base64.decoder.decode(usernameEncoded)) : credentials.username
+ String password = new String(Base64.getDecoder().decode(secretData[credentials.passwordKey]))
+ Credentials credentialsNew = new Credentials(credentials)
+ credentialsNew.username = username
+ credentialsNew.password = password
- return credentialsNew
- } catch (Exception e) {
- throw new RuntimeException("Couldn't parse credentials from K8s secret: ${credentials.secretName} in namespace ${credentials.secretNamespace}", e)
- }
- }
+ return credentialsNew
+ } catch (Exception e) {
+ throw new RuntimeException("Couldn't parse credentials from K8s secret: ${credentials.secretName} in namespace ${credentials.secretNamespace}", e)
+ }
+ }
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/kubernetes/argocd/ArgoApplication.groovy b/src/main/groovy/com/cloudogu/gitops/kubernetes/argocd/ArgoApplication.groovy
index 686653777..c767091bf 100644
--- a/src/main/groovy/com/cloudogu/gitops/kubernetes/argocd/ArgoApplication.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/kubernetes/argocd/ArgoApplication.groovy
@@ -1,67 +1,62 @@
package com.cloudogu.gitops.kubernetes.argocd
-
import com.cloudogu.gitops.git.GitRepo
import com.cloudogu.gitops.utils.TemplatingEngine
-import groovy.util.logging.Slf4j
import java.nio.file.Path
+import groovy.util.logging.Slf4j
@Slf4j
class ArgoApplication {
- final String ARGOCD = ("templates/kubernetes/argocd/application.ftl.yaml")
-
- String name
- String namespace
- String destinationNamespace
- String path
- String repoUrl
- String project
-
- private final TemplatingEngine templater = new TemplatingEngine()
-
- ArgoApplication(String name, String repoUrl, String namespace, String destinationNamespace, String path, String project = 'default') {
- this.name = name
- this.namespace = namespace
- this.destinationNamespace = destinationNamespace
- this.project = project
- this.repoUrl = repoUrl
- this.path = path
- }
-
- Map toTemplateParams() {
- return [
- name : this.name,
- namespace : this.namespace,
- project : this.project,
- path : this.path,
- destinationNamespace: this.destinationNamespace,
- repoUrl : this.repoUrl
- ]
- }
-
- File getTemplateFile() {
- return new File(ARGOCD)
- }
-
- File getOutputFile(File outputDir) {
- String filename = "argocd-application-${name}-${namespace}.yaml"
- return new File(outputDir, filename)
- }
-
- void generate(GitRepo repo, String subfolder) {
- log.debug("Generating ArgoCDApplication for name='${name}', namespace='${namespace}''")
-
- def outputDir = Path.of(repo.absoluteLocalRepoTmpDir, subfolder).toFile()
- outputDir.mkdirs()
-
- templater.template(
- this.getTemplateFile(),
- this.getOutputFile(outputDir),
- this.toTemplateParams()
- )
-
- }
+ final String ARGOCD = ("templates/kubernetes/argocd/application.ftl.yaml")
+
+ String name
+ String namespace
+ String destinationNamespace
+ String path
+ String repoUrl
+ String project
+
+ private final TemplatingEngine templater = new TemplatingEngine()
+
+ ArgoApplication(String name, String repoUrl, String namespace, String destinationNamespace, String path, String project = 'default') {
+ this.name = name
+ this.namespace = namespace
+ this.destinationNamespace = destinationNamespace
+ this.project = project
+ this.repoUrl = repoUrl
+ this.path = path
+ }
+
+ Map toTemplateParams() {
+ return [name : this.name,
+ namespace : this.namespace,
+ project : this.project,
+ path : this.path,
+ destinationNamespace: this.destinationNamespace,
+ repoUrl : this.repoUrl]
+ }
+
+ File getTemplateFile() {
+ return new File(ARGOCD)
+ }
+
+ File getOutputFile(File outputDir) {
+ String filename = "argocd-application-${name}-${namespace}.yaml"
+ return new File(outputDir, filename)
+ }
+
+ void generate(GitRepo repo, String subfolder) {
+ log.debug("Generating ArgoCDApplication for name='${name}', namespace='${namespace}''")
+
+ def outputDir = Path.of(repo.absoluteLocalRepoTmpDir, subfolder).toFile()
+ outputDir.mkdirs()
+
+ templater.template(this.getTemplateFile(),
+ this.getOutputFile(outputDir),
+ this.toTemplateParams())
+
+ }
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/kubernetes/rbac/RbacDefinition.groovy b/src/main/groovy/com/cloudogu/gitops/kubernetes/rbac/RbacDefinition.groovy
index 10ab03fe6..395536d82 100644
--- a/src/main/groovy/com/cloudogu/gitops/kubernetes/rbac/RbacDefinition.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/kubernetes/rbac/RbacDefinition.groovy
@@ -3,103 +3,99 @@ package com.cloudogu.gitops.kubernetes.rbac
import com.cloudogu.gitops.config.Config
import com.cloudogu.gitops.git.GitRepo
import com.cloudogu.gitops.utils.TemplatingEngine
-import groovy.util.logging.Slf4j
import java.nio.file.Path
+import groovy.util.logging.Slf4j
@Slf4j
class RbacDefinition {
- private final Role.Variant variant
- private String name
- private String namespace
- private List serviceAccounts = []
- private String subfolder = "rbac"
- private GitRepo repo
- private Config config
-
- private final TemplatingEngine templater = new TemplatingEngine()
-
- RbacDefinition(Role.Variant variant) {
- this.variant = variant
- }
-
- RbacDefinition withName(String name) {
- this.name = name
- return this
- }
-
- RbacDefinition withNamespace(String namespace) {
- this.namespace = namespace
- return this
- }
-
- RbacDefinition withServiceAccounts(List accounts) {
- this.serviceAccounts = accounts
- return this
- }
-
- RbacDefinition withServiceAccountsFrom(String saNamespace, List saNames) {
- return withServiceAccounts(ServiceAccountRef.fromNames(saNamespace, saNames))
- }
-
- RbacDefinition withSubfolder(String subfolder) {
- this.subfolder = subfolder
- return this
- }
-
- RbacDefinition withRepo(GitRepo repo) {
- this.repo = repo
- return this
- }
-
- RbacDefinition withConfig(Config config) {
- this.config = config
- return this
- }
-
- void generate() {
- if (!repo) {
- throw new IllegalStateException("SCMM repo must be set using withRepo() before calling generate()")
- }
-
- log.trace("Generating RBAC for name='${name}', namespace='${namespace}', subfolder='${subfolder}'")
-
- File outputDir = Path.of(repo.absoluteLocalRepoTmpDir, subfolder).toFile()
- outputDir.mkdirs()
-
- generateRole(outputDir)
-
- generateRoleBinding(outputDir)
- }
-
- private void generateRole(File outputDir) {
- if(variant == Role.Variant.CLUSTER_ADMIN) {
- log.trace("Skipping creation of ClusterRole cluster-admin")
- return
- }
-
- def role = new Role(name, namespace, variant, config)
-
- templater.template(
- role.getTemplateFile(),
- role.getOutputFile(outputDir),
- role.toTemplateParams()
- )
- }
-
- private void generateRoleBinding(File outputDir) {
- String roleName = name
- if(variant == Role.Variant.CLUSTER_ADMIN) {
- roleName = "cluster-admin"
- }
- def binding = new RoleBinding(name, namespace, roleName, serviceAccounts)
-
- templater.template(
- binding.getTemplateFile(),
- binding.getOutputFile(outputDir),
- binding.toTemplateParams()
- )
- }
+ private final Role.Variant variant
+ private String name
+ private String namespace
+ private List serviceAccounts = []
+ private String subfolder = "rbac"
+ private GitRepo repo
+ private Config config
+
+ private final TemplatingEngine templater = new TemplatingEngine()
+
+ RbacDefinition(Role.Variant variant) {
+ this.variant = variant
+ }
+
+ RbacDefinition withName(String name) {
+ this.name = name
+ return this
+ }
+
+ RbacDefinition withNamespace(String namespace) {
+ this.namespace = namespace
+ return this
+ }
+
+ RbacDefinition withServiceAccounts(List accounts) {
+ this.serviceAccounts = accounts
+ return this
+ }
+
+ RbacDefinition withServiceAccountsFrom(String saNamespace, List saNames) {
+ return withServiceAccounts(ServiceAccountRef.fromNames(saNamespace, saNames))
+ }
+
+ RbacDefinition withSubfolder(String subfolder) {
+ this.subfolder = subfolder
+ return this
+ }
+
+ RbacDefinition withRepo(GitRepo repo) {
+ this.repo = repo
+ return this
+ }
+
+ RbacDefinition withConfig(Config config) {
+ this.config = config
+ return this
+ }
+
+ void generate() {
+ if (!repo) {
+ throw new IllegalStateException("SCMM repo must be set using withRepo() before calling generate()")
+ }
+
+ log.trace("Generating RBAC for name='${name}', namespace='${namespace}', subfolder='${subfolder}'")
+
+ File outputDir = Path.of(repo.absoluteLocalRepoTmpDir, subfolder).toFile()
+ outputDir.mkdirs()
+
+ generateRole(outputDir)
+
+ generateRoleBinding(outputDir)
+ }
+
+ private void generateRole(File outputDir) {
+ if (variant == Role.Variant.CLUSTER_ADMIN) {
+ log.trace("Skipping creation of ClusterRole cluster-admin")
+ return
+ }
+
+ def role = new Role(name, namespace, variant, config)
+
+ templater.template(role.getTemplateFile(),
+ role.getOutputFile(outputDir),
+ role.toTemplateParams())
+ }
+
+ private void generateRoleBinding(File outputDir) {
+ String roleName = name
+ if (variant == Role.Variant.CLUSTER_ADMIN) {
+ roleName = "cluster-admin"
+ }
+ def binding = new RoleBinding(name, namespace, roleName, serviceAccounts)
+
+ templater.template(binding.getTemplateFile(),
+ binding.getOutputFile(outputDir),
+ binding.toTemplateParams())
+ }
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/kubernetes/rbac/Role.groovy b/src/main/groovy/com/cloudogu/gitops/kubernetes/rbac/Role.groovy
index 9e31cf129..b81e22f65 100644
--- a/src/main/groovy/com/cloudogu/gitops/kubernetes/rbac/Role.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/kubernetes/rbac/Role.groovy
@@ -3,54 +3,52 @@ package com.cloudogu.gitops.kubernetes.rbac
import com.cloudogu.gitops.config.Config
class Role {
- String name
- String namespace
- Variant variant
- Config config
-
- Role(String name, String namespace, Variant variant, Config config) {
- if (!name?.trim()) throw new IllegalArgumentException("Role name must not be blank")
- if (!namespace?.trim()) throw new IllegalArgumentException("Role namespace must not be blank")
- if (!variant) throw new IllegalArgumentException("Role variant must not be null")
- if (!config) throw new IllegalArgumentException("Config must not be null")
-
- this.name = name
- this.namespace = namespace
- this.variant = variant
- this.config = config
- }
-
- enum Variant {
- ARGOCD("templates/kubernetes/rbac/argocd-role.ftl.yaml"),
- CLUSTER_ADMIN("")
-
- final String templatePath
-
- Variant(String templatePath) {
- this.templatePath = templatePath
- }
- }
-
- Map toTemplateParams() {
- return [
- name : name,
- namespace: namespace,
- config : config
- ]
- }
-
- File getTemplateFile() {
- if(variant == Variant.CLUSTER_ADMIN) {
- throw new IllegalStateException("cluster-admin role shall not be created")
- }
- return new File(variant.getTemplatePath())
- }
-
- File getOutputFile(File outputDir) {
- if(variant == Variant.CLUSTER_ADMIN) {
- throw new IllegalStateException("cluster-admin role shall not be created")
- }
- String filename = "role-${name}-${namespace}.yaml"
- return new File(outputDir, filename)
- }
+ String name
+ String namespace
+ Variant variant
+ Config config
+
+ Role(String name, String namespace, Variant variant, Config config) {
+ if (!name?.trim()) throw new IllegalArgumentException("Role name must not be blank")
+ if (!namespace?.trim()) throw new IllegalArgumentException("Role namespace must not be blank")
+ if (!variant) throw new IllegalArgumentException("Role variant must not be null")
+ if (!config) throw new IllegalArgumentException("Config must not be null")
+
+ this.name = name
+ this.namespace = namespace
+ this.variant = variant
+ this.config = config
+ }
+
+ enum Variant {
+ ARGOCD("templates/kubernetes/rbac/argocd-role.ftl.yaml"),
+ CLUSTER_ADMIN("")
+
+ final String templatePath
+
+ Variant(String templatePath) {
+ this.templatePath = templatePath
+ }
+ }
+
+ Map toTemplateParams() {
+ return [name : name,
+ namespace: namespace,
+ config : config]
+ }
+
+ File getTemplateFile() {
+ if (variant == Variant.CLUSTER_ADMIN) {
+ throw new IllegalStateException("cluster-admin role shall not be created")
+ }
+ return new File(variant.getTemplatePath())
+ }
+
+ File getOutputFile(File outputDir) {
+ if (variant == Variant.CLUSTER_ADMIN) {
+ throw new IllegalStateException("cluster-admin role shall not be created")
+ }
+ String filename = "role-${name}-${namespace}.yaml"
+ return new File(outputDir, filename)
+ }
}
\ No newline at end of file
diff --git a/src/main/groovy/com/cloudogu/gitops/kubernetes/rbac/RoleBinding.groovy b/src/main/groovy/com/cloudogu/gitops/kubernetes/rbac/RoleBinding.groovy
index 5b7cd36f9..8a01ab27d 100644
--- a/src/main/groovy/com/cloudogu/gitops/kubernetes/rbac/RoleBinding.groovy
+++ b/src/main/groovy/com/cloudogu/gitops/kubernetes/rbac/RoleBinding.groovy
@@ -1,53 +1,51 @@
package com.cloudogu.gitops.kubernetes.rbac
class RoleBinding {
- String name
- String kind
- String namespace
- String roleName
- String roleKind
- List