From 4cef7d944f1dca1e84fd0a15c8809b6d4c82f1e7 Mon Sep 17 00:00:00 2001 From: Chris Date: Sun, 16 Nov 2025 18:35:24 -0800 Subject: [PATCH] proof of concept for helm packaging & deployment --- app/jobs/projects/helm_deployment_job.rb | 77 ++++++++++++++++++++++++ app/models/deployment.rb | 5 +- app/services/k8/base.rb | 11 ++++ app/services/k8/helm/chart_builder.rb | 47 +++++++++++++++ app/services/k8/helm/client.rb | 4 +- resources/helm/Chart.yaml | 5 ++ 6 files changed, 143 insertions(+), 6 deletions(-) create mode 100644 app/jobs/projects/helm_deployment_job.rb create mode 100644 app/services/k8/helm/chart_builder.rb create mode 100644 resources/helm/Chart.yaml diff --git a/app/jobs/projects/helm_deployment_job.rb b/app/jobs/projects/helm_deployment_job.rb new file mode 100644 index 00000000..3b3987b7 --- /dev/null +++ b/app/jobs/projects/helm_deployment_job.rb @@ -0,0 +1,77 @@ +class Projects::HelmDeploymentJob < ApplicationJob + DEPLOYABLE_RESOURCES = %w[ConfigMap Secrets Deployment CronJob Service Ingress Pv Pvc] + def perform(deployment, user) + project = deployment.project + connection = K8::Connection.new(project, user, allow_anonymous: true) + chart_builder = K8::Helm::ChartBuilder.new( + project.name, + deployment, + ).connect( + K8::Connection.new(project, user, allow_anonymous: true) + ) + + #chart_builder << apply_namespace(project) + chart_builder << upload_registry_secrets(deployment) + chart_builder << apply_config_map(project) + chart_builder << apply_secrets(project) + + deploy_volumes(project, chart_builder) + #chart_builder << predeploy(project, connection) + deploy_services(project, chart_builder) + chart_builder.install_chart(project.name) + end + + def apply_namespace(project) + K8::Namespace.new(project) + end + + def upload_registry_secrets(deployment) + project = deployment.project + provider = project.build_provider + result = Providers::GenerateConfigJson.execute( + provider:, + ) + raise StandardError, result.message if result.failure? + + K8::Secrets::RegistrySecret.new(project, result.docker_config_json) + end + + DEPLOYABLE_RESOURCES.each do |resource_type| + define_method(:"apply_#{resource_type.underscore}") do |service| + K8::Stateless.const_get(resource_type).new(service) + end + end + + def deploy_volumes(project, chart_builder) + project.volumes.each do |volume| + begin + chart_builder << apply_pv(volume) + chart_builder << apply_pvc(volume) + volume.deployed! + rescue StandardError => e + volume.failed! + raise e + end + end + end + + def deploy_services(project, chart_builder) + project.services.each do |service| + deploy_service(service, chart_builder) + end + end + + def deploy_service(service, chart_builder) + if service.background_service? + chart_builder << apply_deployment(service) + elsif service.cron_job? + chart_builder << apply_cron_job(service) + elsif service.web_service? + chart_builder << apply_deployment(service) + chart_builder << apply_service(service) + if service.domains.any? && service.allow_public_networking? + chart_builder << apply_ingress(service) + end + end + end +end \ No newline at end of file diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 352d44b5..572088be 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -27,10 +27,7 @@ class Deployment < ApplicationRecord end def add_manifest(yaml) - manifest = YAML.safe_load(yaml) - kind = manifest["kind"]&.downcase - name = manifest.dig("metadata", "name") - manifest_key = "#{kind}/#{name}" + manifest_key = K8::Base.manifest_key(yaml) self.manifests ||= {} self.manifests[manifest_key] = yaml diff --git a/app/services/k8/base.rb b/app/services/k8/base.rb index 6aa6680f..cb246594 100644 --- a/app/services/k8/base.rb +++ b/app/services/k8/base.rb @@ -29,6 +29,17 @@ class K8::Base result.gsub(/\n\s*\n/, "\n") end + def self.manifest_key(yaml) + manifest = YAML.safe_load(yaml) + kind = manifest["kind"]&.downcase + name = manifest.dig("metadata", "name") + manifest_key = "#{kind}/#{name}" + end + + def suggested_file_name + "#{K8::Base.manifest_key(to_yaml).gsub("/", "_").underscore}.yaml" + end + def client raise "Client not connected" unless connected? @client ||= K8::Client.new(connection) diff --git a/app/services/k8/helm/chart_builder.rb b/app/services/k8/helm/chart_builder.rb new file mode 100644 index 00000000..1afd8b9c --- /dev/null +++ b/app/services/k8/helm/chart_builder.rb @@ -0,0 +1,47 @@ +class K8::Helm::ChartBuilder < K8::Base + attr_reader :chart_name, :resources, :logger, :client + + def initialize(chart_name, logger) + @chart_name = chart_name + @logger = logger + @resources = [] + end + + def connect(connection) + @client = K8::Helm::Client.connect(connection, Cli::RunAndLog.new(logger)) + super(connection) + end + + def <<(resource) + resources << resource + end + + def chart_yaml + <<-YAML + apiVersion: v2 + name: <%= chart_name %> + version: 1.0.0 + type: application + appVersion: "1.0.0" + YAML + end + + def install_chart(namespace) + Dir.mktmpdir do |chart_directory| + logger.info("Creating chart directory #{chart_directory}...") + # Create /templates directory + FileUtils.mkdir_p(File.join(chart_directory, "templates")) + + logger.info("Creating Chart.yaml") + File.write(File.join(chart_directory, "Chart.yaml"), chart_yaml) + + resources.each do |resource| + logger.info("Writing template #{resource.suggested_file_name}...") + File.write(File.join(chart_directory, "templates", resource.suggested_file_name), resource.to_yaml) + end + + logger.info("Installing chart #{chart_name} in namespace #{namespace}...") + client.install(chart_name, chart_directory, namespace: namespace) + end + end +end \ No newline at end of file diff --git a/app/services/k8/helm/client.rb b/app/services/k8/helm/client.rb index 35836f42..c791d851 100644 --- a/app/services/k8/helm/client.rb +++ b/app/services/k8/helm/client.rb @@ -78,7 +78,7 @@ class K8::Helm::Client self.class.add_repo(repository_name, repository_url, runner) end - def install(name, chart_url, values: {}, namespace: 'default') + def install(name, chart_url, values: {}, namespace: 'default', dry_run: false) return StandardError.new("Can't install helm chart if not connected") unless connected? with_kube_config do |kubeconfig_file| @@ -88,7 +88,7 @@ class K8::Helm::Client values_file.write(values.to_yaml) values_file.flush - command = "helm upgrade --install #{name} #{chart_url} -f #{values_file.path} --namespace #{namespace} --timeout=#{DEFAULT_TIMEOUT}" + command = "helm upgrade --install #{name} #{chart_url} -f #{values_file.path} --namespace #{namespace} --timeout=#{DEFAULT_TIMEOUT} #{dry_run ? '--dry-run' : ''}" exit_status = runner.(command, envs: { "KUBECONFIG" => kubeconfig_file.path }) raise "`#{command}` failed with exit status #{exit_status}" unless exit_status.success? exit_status diff --git a/resources/helm/Chart.yaml b/resources/helm/Chart.yaml new file mode 100644 index 00000000..c0f3357d --- /dev/null +++ b/resources/helm/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v2 +name: <%= project.name %> +version: 1.0.0 +type: application +appVersion: "1.0.0"