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 cb1676267..5a56e37a3 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..f2dc493c6 --- /dev/null +++ b/src/tasks/utils/cnf_installation/deployment_management/helm_deployment_manager.cr @@ -0,0 +1,103 @@ +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_name() + abstract def get_deployment_namespace() + + def install_from_folder(chart_path, helm_namespace, helm_values) + begin + #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? + raise Helm::InstallationFailed.new("Helm pull failed: #{pull_response[:error]}") + 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_name() + @helm_chart_config.name + end + + def get_deployment_namespace() + @helm_chart_config.namespace.empty? ? DEFAULT_CNF_NAMESPACE : @helm_chart_config.namespace + 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_name() + @helm_directory_config.name + end + + def get_deployment_namespace() + @helm_directory_config.namespace.empty? ? DEFAULT_CNF_NAMESPACE : @helm_directory_config.namespace + 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