From c4497155147c2ff4efc011e87fc9070332b8c035 Mon Sep 17 00:00:00 2001 From: Konstantin Yarovoy Date: Mon, 14 Oct 2024 10:08:43 +0000 Subject: [PATCH] installation: Create new installation process Create new CNF installation process, that will support multiple deployments, be more comprehensible and easier to maintain. Don't replace the old one yet, both installation methods should exist simultaneously Refs: #2161 Signed-off-by: Konstantin Yarovoy --- spec/utils/utils_spec.cr | 25 +-- src/tasks/cnf_setup.cr | 21 +++ src/tasks/constants.cr | 3 + src/tasks/utils/cnf_installation/config.cr | 4 + .../deployment_manager_common.cr | 15 ++ .../helm_deployment_manager.cr | 109 ++++++++++++ .../manifest_deployment_manager.cr | 37 +++++ .../utils/cnf_installation/install_common.cr | 155 ++++++++++++++++-- 8 files changed, 330 insertions(+), 39 deletions(-) create mode 100644 src/tasks/utils/cnf_installation/deployment_management/deployment_manager_common.cr create mode 100644 src/tasks/utils/cnf_installation/deployment_management/helm_deployment_manager.cr create mode 100644 src/tasks/utils/cnf_installation/deployment_management/manifest_deployment_manager.cr diff --git a/spec/utils/utils_spec.cr b/spec/utils/utils_spec.cr index 0a8dacb47..c99521338 100644 --- a/spec/utils/utils_spec.cr +++ b/spec/utils/utils_spec.cr @@ -118,30 +118,9 @@ describe "Utils" do it "'single_task_runner' should put a 1 in the results file if it has an exception", tags: ["task_runner"] do CNFManager::Points.clean_results_yml - args = Sam::Args.new(["cnf-config=./cnf-testsuite.yml"]) + args = Sam::Args.new() task_response = CNFManager::Task.single_task_runner(args) do - cdir = FileUtils.pwd() - response = String::Builder.new - config = CNFInstall::Config.parse_cnf_config_from_file(CNFManager.ensure_cnf_testsuite_yml_path(args.named["cnf-config"].as(String))) - helm_directory = config.deployments.get_deployment_param(:helm_directory) - if File.directory?(CNFManager.ensure_cnf_testsuite_dir(args.named["cnf-config"].as(String)) + helm_directory) - Dir.cd(CNFManager.ensure_cnf_testsuite_dir(args.named["cnf-config"].as(String)) + helm_directory) - Process.run("grep -r -P '^(?!.+0\.0\.0\.0)(?![[:space:]]*0\.0\.0\.0)(?!#)(?![[:space:]]*#)(?!\/\/)(?![[:space:]]*\/\/)(?!\/\\*)(?![[:space:]]*\/\\*)(.+([0-9]{1,3}[\.]){3}[0-9]{1,3})'", shell: true) do |proc| - while line = proc.output.gets - response << line - end - end - Dir.cd(cdir) - if response.to_s.size > 0 - resp = upsert_failed_task("ip_addresses","✖️ FAILED: IP addresses found", Time.utc) - else - resp = upsert_passed_task("ip_addresses", "✔️ PASSED: No IP addresses found", Time.utc) - end - resp - else - Dir.cd(cdir) - resp = upsert_passed_task("ip_addresses", "✔️ PASSED: No IP addresses found", Time.utc) - end + raise Exception.new() end yaml = File.open("#{CNFManager::Points::Results.file}") do |file| YAML.parse(file) diff --git a/src/tasks/cnf_setup.cr b/src/tasks/cnf_setup.cr index b04b71941..ba57384ec 100644 --- a/src/tasks/cnf_setup.cr +++ b/src/tasks/cnf_setup.cr @@ -96,6 +96,27 @@ task "sample_generic_cnf_cleanup" do |_, args| CNFManager.sample_cleanup(config_file: "sample-cnfs/sample-generic-cnf", verbose: true) end +task "new_cnf_setup" do |_, args| + if CNFManager.cnf_installed? + stdout_warning "A CNF is already set up. Setting up multiple CNFs is not allowed." + stdout_warning "To set up a new CNF, clean up the existing one by running: cnf_cleanup cnf-path=#{CNFManager.cnf_config_list.first}" + exit 0 + end + if ClusterTools.install + stdout_success "ClusterTools installed" + else + stdout_failure "The ClusterTools installation timed out. Please check the status of the cluster-tools pods." + exit 1 + end + stdout_success "CNF installation start." + CNFInstall.install_cnf(args) + stdout_success "CNF installation complete." +end + +task "new_cnf_cleanup" do |_, args| + CNFInstall.uninstall_cnf() +end + def interactively_create_config new_config = { config_version: CNFInstall::Config::ConfigVersion::Latest.to_s, diff --git a/src/tasks/constants.cr b/src/tasks/constants.cr index 13ddea251..2874f1671 100644 --- a/src/tasks/constants.cr +++ b/src/tasks/constants.cr @@ -2,9 +2,12 @@ require "./utils/embedded_file_manager.cr" ESSENTIAL_PASSED_THRESHOLD = 15 CNF_DIR = "cnfs" +DEPLOYMENTS_DIR = File.join(CNF_DIR, "deployments") +CNF_TEMP_FILES_DIR = File.join(CNF_DIR, "temp_files") CONFIG_FILE = "cnf-testsuite.yml" BASE_CONFIG = "./config.yml" COMMON_MANIFEST_FILE_PATH = "#{CNF_DIR}/common_manifest.yml" +DEPLOYMENT_MANIFEST_FILE_NAME = "deployment_manifest.yml" PASSED = "passed" FAILED = "failed" SKIPPED = "skipped" diff --git a/src/tasks/utils/cnf_installation/config.cr b/src/tasks/utils/cnf_installation/config.cr index 9ca3da499..e1b789541 100644 --- a/src/tasks/utils/cnf_installation/config.cr +++ b/src/tasks/utils/cnf_installation/config.cr @@ -10,6 +10,10 @@ module CNFInstall end def self.parse_cnf_config_from_file(path_to_config) + if !File.exists?(path_to_config) + stdout_failure "No config found at #{path_to_config}." + exit 1 + end yaml_content = File.read(path_to_config) config_dir = CNFManager.ensure_cnf_testsuite_dir(path_to_config) begin diff --git a/src/tasks/utils/cnf_installation/deployment_management/deployment_manager_common.cr b/src/tasks/utils/cnf_installation/deployment_management/deployment_manager_common.cr new file mode 100644 index 000000000..aa5ba4124 --- /dev/null +++ b/src/tasks/utils/cnf_installation/deployment_management/deployment_manager_common.cr @@ -0,0 +1,15 @@ +module CNFInstall + abstract class DeploymentManager + property deployment_name : String + + abstract def install + abstract def uninstall + abstract def generate_manifest + + def initialize(deployment_name) + @deployment_name = deployment_name + end + end + + +end \ No newline at end of file diff --git a/src/tasks/utils/cnf_installation/deployment_management/helm_deployment_manager.cr b/src/tasks/utils/cnf_installation/deployment_management/helm_deployment_manager.cr new file mode 100644 index 000000000..add2b91cc --- /dev/null +++ b/src/tasks/utils/cnf_installation/deployment_management/helm_deployment_manager.cr @@ -0,0 +1,109 @@ +require "../config_versions/config_versions.cr" +require "./deployment_manager_common.cr" + +module CNFInstall + abstract class HelmDeploymentManager < DeploymentManager + def initialize(deployment_name) + super(deployment_name) + end + + abstract def get_deployment_config() : ConfigV2::HelmDeploymentConfig + + def get_deployment_name() + helm_deployment_config = get_deployment_config() + helm_deployment_config.name() + end + + def get_deployment_namespace() + helm_deployment_config = get_deployment_config() + helm_deployment_config.namespace.empty? ? DEFAULT_CNF_NAMESPACE : helm_deployment_config.namespace + end + + def install_from_folder(chart_path, helm_namespace, helm_values) + begin + CNFManager.ensure_namespace_exists!(helm_namespace) + #TODO (kosstennbl) fix Helm install to add -n to namespace and remove it there + response = Helm.install(release_name: @deployment_name, helm_chart: chart_path, namespace: "-n #{helm_namespace}", values: helm_values) + if !response[:status].success? + stdout_failure "Helm installation failed, stderr:" + stdout_failure "\t#{response[:error]}" + exit 1 + end + rescue e : Helm::InstallationFailed + stdout_failure "Helm installation failed with message:" + stdout_failure "\t#{e.message}" + exit 1 + rescue e : Helm::CannotReuseReleaseNameError + stdout_failure "Helm deployment \"#{@deployment_name}\" already exists in \"#{helm_namespace}\" namespace." + stdout_failure "Change deployment name in CNF configuration or uninstall existing deployment." + exit 1 + end + end + + def uninstall() + deployment_name = get_deployment_name() + helm_uninstall_cmd = "#{deployment_name} -n #{get_deployment_namespace()}" + result = Helm.uninstall(helm_uninstall_cmd) + if result[:status].success? + stdout_success "Successfully uninstalled helm deployment \"#{deployment_name}\"." + end + end + + def generate_manifest() + namespace = get_deployment_namespace() + generated_manifest = Helm.generate_manifest(get_deployment_name(), namespace) + generated_manifest_with_namespaces = Manifest.add_namespace_to_resources(generated_manifest, namespace) + end + end + + class HelmChartDeploymentManager < HelmDeploymentManager + @helm_chart_config : ConfigV2::HelmChartConfig + + def initialize(helm_chart_config) + super(helm_chart_config.name) + @helm_chart_config = helm_chart_config + end + + def install() + helm_repo_url = @helm_chart_config.helm_repo_url + helm_repo_name = @helm_chart_config.helm_repo_name + helm_chart_name = @helm_chart_config.helm_chart_name + + if !helm_repo_url.empty? + Helm.helm_repo_add(helm_repo_name, helm_repo_url) + end + helm_pull_destination = File.join(DEPLOYMENTS_DIR, @deployment_name) + helm_pull_cmd = "#{helm_repo_name}/#{helm_chart_name} --untar --destination #{helm_pull_destination}" + pull_response = Helm.pull(helm_pull_cmd) + if !pull_response[:status].success? + stdout_failure "Helm pull failed for deployment \"#{get_deployment_name()}\": #{pull_response[:error]}" + exit 1 + end + chart_path = File.join(helm_pull_destination, helm_chart_name) + install_from_folder(chart_path, get_deployment_namespace(), @helm_chart_config.helm_values) + end + + def get_deployment_config() : ConfigV2::HelmDeploymentConfig + @helm_chart_config + end + end + + class HelmDirectoryDeploymentManager < HelmDeploymentManager + @helm_directory_config : ConfigV2::HelmDirectoryConfig + + def initialize(helm_directory_config) + super(helm_directory_config.name) + @helm_directory_config = helm_directory_config + end + + def install() + chart_path = File.join(DEPLOYMENTS_DIR, @deployment_name, @helm_directory_config.helm_directory) + install_from_folder(chart_path, get_deployment_namespace(), @helm_directory_config.helm_values) + end + + + def get_deployment_config() : ConfigV2::HelmDeploymentConfig + @helm_directory_config + end + end +end \ No newline at end of file diff --git a/src/tasks/utils/cnf_installation/deployment_management/manifest_deployment_manager.cr b/src/tasks/utils/cnf_installation/deployment_management/manifest_deployment_manager.cr new file mode 100644 index 000000000..063f8a6a8 --- /dev/null +++ b/src/tasks/utils/cnf_installation/deployment_management/manifest_deployment_manager.cr @@ -0,0 +1,37 @@ +require "../config_versions/config_versions.cr" +require "./deployment_manager_common.cr" + + +module CNFInstall + class ManifestDeploymentManager < DeploymentManager + @manifest_config : ConfigV2::ManifestDirectoryConfig + @manifest_directory_path : String + + def initialize(manifest_config) + super(manifest_config.name) + @manifest_config = manifest_config + @manifest_directory_path = File.join(DEPLOYMENTS_DIR, @deployment_name, @manifest_config.manifest_directory) + end + + def install() + KubectlClient::Apply.file(@manifest_directory_path) + end + + def uninstall() + result = KubectlClient::Delete.file(@manifest_directory_path, wait: true) + if result[:status].success? + stdout_success "Successfully uninstalled manifest deployment \"#{@manifest_config.name}\"" + end + end + + def generate_manifest() + deployment_manifest = "" + list_of_manifests = Manifest.manifest_file_list(@manifest_directory_path) + list_of_manifests.each do |manifest_path| + manifest = File.read(manifest_path) + deployment_manifest = deployment_manifest + manifest + "\n" + end + deployment_manifest + end + end +end \ No newline at end of file diff --git a/src/tasks/utils/cnf_installation/install_common.cr b/src/tasks/utils/cnf_installation/install_common.cr index 783e7b361..d7aca0fe3 100644 --- a/src/tasks/utils/cnf_installation/install_common.cr +++ b/src/tasks/utils/cnf_installation/install_common.cr @@ -8,24 +8,147 @@ module CNFInstall Invalid end - def self.install_method_by_config_src(config_src : String) : InstallMethod - Log.info { "install_method_by_config_src" } - Log.info { "config_src: #{config_src}" } - helm_chart_file = "#{config_src}/#{Helm::CHART_YAML}" - Log.info { "looking for potential helm_chart_file: #{helm_chart_file}: file exists?: #{File.exists?(helm_chart_file)}" } - - if !Dir.exists?(config_src) - Log.info { "install_method_by_config_src helm_chart selected" } - InstallMethod::HelmChart - elsif File.exists?(helm_chart_file) - Log.info { "install_method_by_config_src helm_directory selected" } - InstallMethod::HelmDirectory - elsif Dir.exists?(config_src) - Log.info { "install_method_by_config_src manifest_directory selected" } - InstallMethod::ManifestDirectory + def self.install_cnf(cli_args) + parsed_args = parse_cli_args(cli_args) + cnf_config_path = parsed_args[:config_path] + if cnf_config_path.empty? + stdout_failure "cnf-config or cnf-path parameter with valid CNF configuration should be provided." + exit(1) + end + config = Config.parse_cnf_config_from_file(cnf_config_path) + ensure_cnf_dir_structure() + FileUtils.cp(cnf_config_path, CNF_DIR) + + prepare_deployment_directories(config, cnf_config_path) + + deployment_managers = create_deployment_manager_list(config) + install_deployments(parsed_args: parsed_args, deployment_managers: deployment_managers) + end + + def self.parse_cli_args(cli_args) + Log.for("cnf_setup").debug { "cli_args = #{cli_args.inspect}" } + cnf_config_path = "" + timeout = 1800 + skip_wait_for_install = cli_args.raw.includes? "skip_wait_for_install" + + if cli_args.named.keys.includes? "cnf-config" + cnf_config_path = cli_args.named["cnf-config"].as(String) + elsif cli_args.named.keys.includes? "cnf-path" + cnf_config_path = cli_args.named["cnf-path"].as(String) + end + cnf_config_path = self.ensure_cnf_config_path_file(cnf_config_path) + + if cli_args.named.keys.includes? "timeout" + timeout = cli_args.named["timeout"].to_i + end + parsed_args = {config_path: cnf_config_path, timeout: timeout, skip_wait_for_install: skip_wait_for_install} + Log.for("cnf_setup").debug { "parsed_cli_args = #{parsed_args}"} + parsed_args + end + + def self.ensure_cnf_config_path_file(path) + if CNFManager.path_has_yml?(path) + yml = path else - puts "Error: #{config_src} is neither a helm_chart, helm_directory, or manifest_directory.".colorize(:red) + yml = File.join(path, CONFIG_FILE) + end + end + + def self.ensure_cnf_dir_structure() + FileUtils.mkdir_p(CNF_DIR) + FileUtils.mkdir_p(DEPLOYMENTS_DIR) + FileUtils.mkdir_p(CNF_TEMP_FILES_DIR) + end + + def self.prepare_deployment_directories(config, cnf_config_path) + # Deployment names are expected to be unique (ensured in config) + config.deployments.helm_charts.each do |helm_chart_config| + FileUtils.mkdir_p(File.join(DEPLOYMENTS_DIR, helm_chart_config.name)) + end + config.deployments.helm_dirs.each do |helm_directory_config| + source_dir = File.join(Path[cnf_config_path].dirname, helm_directory_config.helm_directory) + destination_dir = File.join(DEPLOYMENTS_DIR, helm_directory_config.name) + FileUtils.mkdir_p(destination_dir) + FileUtils.cp_r(source_dir, destination_dir) + end + config.deployments.manifests.each do |manifest_config| + source_dir = File.join(Path[cnf_config_path].dirname, manifest_config.manifest_directory) + destination_dir = File.join(DEPLOYMENTS_DIR, manifest_config.name) + FileUtils.mkdir_p(destination_dir) + FileUtils.cp_r(source_dir, destination_dir) + end + end + + def self.create_deployment_manager_list(config) + deployment_managers = [] of DeploymentManager + config.deployments.helm_charts.each do |helm_chart_config| + deployment_managers << HelmChartDeploymentManager.new(helm_chart_config) + end + config.deployments.helm_dirs.each do |helm_directory_config| + deployment_managers << HelmDirectoryDeploymentManager.new(helm_directory_config) + end + config.deployments.manifests.each do |manifest_config| + deployment_managers << ManifestDeploymentManager.new(manifest_config) + end + deployment_managers + end + + def self.install_deployments(parsed_args, deployment_managers) + deployment_managers.each do |deployment_manager| + deployment_name = deployment_manager.deployment_name + + stdout_success "Installing deployment #{deployment_name}" + deployment_manager.install() + + generated_deployment_manifest = deployment_manager.generate_manifest() + deployment_manifest_path = File.join(DEPLOYMENTS_DIR, deployment_name, DEPLOYMENT_MANIFEST_FILE_NAME) + Manifest.add_manifest_to_file(deployment_name, generated_deployment_manifest, deployment_manifest_path) + Manifest.add_manifest_to_file(deployment_name, generated_deployment_manifest, COMMON_MANIFEST_FILE_PATH) + + if !parsed_args[:skip_wait_for_install] + wait_for_deployment_resources(deployment_name, generated_deployment_manifest, parsed_args[:timeout]) + end + end + end + + def self.wait_for_deployment_resources(deployment_name, deployment_manifest, timeout) + resources_info = Helm.workload_resource_kind_names(Manifest.manifest_string_to_ymls(deployment_manifest)) + workload_resources_info = resources_info.select { |resource_info| + ["replicaset", "deployment", "statefulset", "pod", "daemonset"].includes?(resource_info[:kind].downcase) + } + total_resource_count = workload_resources_info.size() + current_resource_number = 1 + workload_resources_info.each do | resource_info | + stdout_success "Waiting for resource for \"#{deployment_name}\" deployment (#{current_resource_number}/#{total_resource_count}): [#{resource_info[:kind]}] #{resource_info[:name]}", same_line: true + ready = KubectlClient::Get.resource_wait_for_install(resource_info[:kind], resource_info[:name], wait_count: timeout, namespace: resource_info[:namespace]) + if !ready + stdout_failure "\"#{deployment_name}\" deployment setup has timed-out, [#{resource_info[:kind]}] #{resource_info[:name]} is not ready after #{timeout} seconds.", same_line: true + stdout_failure "Recommended course of actions would be to investigate the resource in cluster, then call cnf_cleanup and try to reinstall the CNF." + exit 1 + end + current_resource_number += 1 + end + stdout_success "All \"#{deployment_name}\" deployment resources are up.", same_line: true + end + + def self.uninstall_cnf() + cnf_config_path = File.join(CNF_DIR, CONFIG_FILE) + if !File.exists?(cnf_config_path) + stdout_failure "No CNF config found in #{CNF_DIR} directory. Nothing to uninstall" exit 1 end + config = Config.parse_cnf_config_from_file(cnf_config_path) + + deployment_managers = create_deployment_manager_list(config) + uninstall_deployments(deployment_managers) + + FileUtils.rm_rf(CNF_DIR) + end + + def self.uninstall_deployments(deployment_managers) + deployment_managers.each do |deployment_manager| + deployment_manager.uninstall() + end + stdout_success "All CNF deployments were uninstalled, some time might be needed for all resources to be down." end end \ No newline at end of file