Files
canine/app/jobs/projects/deployment_job.rb
2025-11-27 14:44:31 -08:00

171 lines
5.6 KiB
Ruby

require "base64"
require "json"
class Projects::DeploymentJob < ApplicationJob
DEPLOYABLE_RESOURCES = %w[ConfigMap Secrets Deployment CronJob Service Ingress Pv Pvc]
class DeploymentFailure < StandardError; end
def perform(deployment, user)
@logger = deployment
@marked_resources = []
project = deployment.project
connection = K8::Connection.new(project, user, allow_anonymous: true)
kubectl = create_kubectl(deployment, connection)
kubectl.register_before_apply do |yaml_content|
deployment.add_manifest(yaml_content)
end
# Create namespace
if project.managed_namespace?
apply_namespace(project, kubectl)
end
# Upload container registry secrets
upload_registry_secrets(kubectl, deployment)
apply_config_map(project, kubectl)
apply_secrets(project, kubectl)
deploy_volumes(project, kubectl)
predeploy(project, kubectl, connection)
# For each of the projects services
deploy_services(project, kubectl)
sweep_unused_resources(project, user, connection)
# Kill all one off containers
kill_one_off_containers(project, kubectl)
postdeploy(project, kubectl, connection)
deployment.completed!
project.deployed!
rescue StandardError => e
@logger.error("Deployment failed: #{e.message}")
puts e.full_message
deployment.failed!
end
private
def deploy_volumes(project, kubectl)
project.volumes.each do |volume|
begin
apply_pv(volume, kubectl)
apply_pvc(volume, kubectl)
volume.deployed!
rescue StandardError => e
@logger.error("Volume deployment failed: #{e.message}")
volume.failed!
raise e
end
end
end
def _run_command(command, kubectl, project, type, connection)
@logger.info("Running command: `#{command}`...", color: :yellow)
command = K8::Stateless::Command.new(project, type, command).connect(connection)
command_yaml = command.to_yaml
command.delete_if_exists!
kubectl.apply_yaml(command_yaml)
command.wait_for_completion
end
def predeploy(project, kubectl, connection)
return unless project.predeploy_command.present?
_run_command(project.predeploy_command, kubectl, project, 'predeploy', connection)
end
def postdeploy(project, kubectl, connection)
return unless project.postdeploy_command.present?
_run_command(project.postdeploy_command, kubectl, project, 'postdeploy', connection)
end
def create_kubectl(deployment, connection)
runner = Cli::RunAndLog.new(deployment)
K8::Kubectl.new(connection, runner)
end
def deploy_services(project, kubectl)
project.services.each do |service|
deploy_service(service, kubectl)
end
end
def deploy_service(service, kubectl)
if service.background_service?
apply_deployment(service, kubectl)
restart_deployment(service, kubectl)
elsif service.cron_job?
apply_cron_job(service, kubectl)
elsif service.web_service?
apply_deployment(service, kubectl)
apply_service(service, kubectl)
if service.domains.any? && service.allow_public_networking?
apply_ingress(service, kubectl)
end
restart_deployment(service, kubectl)
end
service.healthy!
end
def kill_one_off_containers(project, kubectl)
kubectl.call("-n #{project.namespace} delete pods -l oneoff=true")
end
def apply_namespace(project, kubectl)
@logger.info("Creating namespace: #{project.namespace}", color: :yellow)
namespace_yaml = K8::Namespace.new(project).to_yaml
kubectl.apply_yaml(namespace_yaml)
end
def sweep_unused_resources(project, user, connection)
# Check deployments that need to be deleted
# Exclude Persistent Volumes
resources_to_sweep = DEPLOYABLE_RESOURCES.reject { |r| [ 'Pv' ].include?(r) }
kubectl = K8::Kubectl.new(connection)
resources_to_sweep.each do |resource_type|
results = YAML.safe_load(kubectl.call("get #{resource_type.downcase} -o yaml -n #{project.namespace}"))
results['items'].each do |resource|
if @marked_resources.select do |r|
r.is_a?(K8::Stateless.const_get(resource_type))
end.none? do |applied_resource|
applied_resource.name == resource['metadata']['name']
end && resource.dig('metadata', 'labels', 'caninemanaged') == 'true'
@logger.info("Deleting #{resource_type}: #{resource['metadata']['name']}", color: :yellow)
kubectl.call("delete #{resource_type.downcase} #{resource['metadata']['name']} -n #{project.namespace}")
end
end
end
end
DEPLOYABLE_RESOURCES.each do |resource_type|
define_method(:"apply_#{resource_type.underscore}") do |service, kubectl|
@logger.info("Creating #{resource_type}: #{service.name}", color: :yellow)
resource = K8::Stateless.const_get(resource_type).new(service)
resource_yaml = resource.to_yaml
kubectl.apply_yaml(resource_yaml)
@marked_resources << resource
end
end
def restart_deployment(service, kubectl)
@logger.info("Restarting deployment: #{service.name}", color: :yellow)
kubectl.call("-n #{service.project.namespace} rollout restart deployment/#{service.name}")
end
def upload_registry_secrets(kubectl, deployment)
project = deployment.project
@logger.info("Creating registry secret for #{project.container_image_reference}", color: :yellow)
provider = project.build_provider
result = Providers::GenerateConfigJson.execute(
provider:,
)
raise StandardError, result.message if result.failure?
secret_yaml = K8::Secrets::RegistrySecret.new(project, result.docker_config_json).to_yaml
kubectl.apply_yaml(secret_yaml)
end
end