Faaso deploy command

This commit is contained in:
Roberto Alsina 2024-07-08 11:16:03 -03:00
parent 5e7764ca9f
commit 8e6ec620aa
5 changed files with 72 additions and 9 deletions

View File

@ -31,7 +31,7 @@
* ✅ Fix proxy reload / Make it reload on file changes * ✅ Fix proxy reload / Make it reload on file changes
* Implement `faaso help command` * Implement `faaso help command`
* Fix `export examples/hello_crystal` it has a `template/` * Fix `export examples/hello_crystal` it has a `template/`
* Implement zero-downtime rollout (`faaso deploy`) * Implement zero-downtime rollout (`faaso deploy`)
* Cleanup `tmp/` after use unless `DEBUG` is set * Cleanup `tmp/` after use unless `DEBUG` is set
# Things to do but not before release # Things to do but not before release

41
src/commands/deploy.cr Normal file
View File

@ -0,0 +1,41 @@
module Faaso
module Commands
struct Deploy
# FIXME: local only for now
def run(options, funko_name : String) : Int32
Log.info { "Deploying #{funko_name}" }
funko = Funko::Funko.from_names([funko_name])[0]
# Get scale, check for out-of-date containers
current_scale = funko.scale
latest_image = funko.latest_image
containers = funko.containers
out_of_date = containers.count { |container| container.image_id != latest_image }
Log.info { "Need to update #{out_of_date} containers" }
Log.info { "Scaling from #{current_scale} to #{current_scale + out_of_date}" }
# Increase scale to get enough up-to-date containers
new_containers = funko.scale(current_scale + out_of_date)
# Wait for them to be healthy
begin
funko.wait_for(current_scale + out_of_date, 120, healthy: true)
rescue ex : Exception
# Failed to start, rollback
Log.error(exception: ex) { "Failed to scale, rolling back" }
docker_api = Docr::API.new(Docr::Client.new)
new_containers.each do |container|
docker_api.containers.stop(container.id)
docker_api.containers.delete(container.id)
end
return 1
end
Log.info { "Scaling down to #{current_scale}" }
# Decrease scale to the desired amount
funko.scale(current_scale)
funko.wait_for(current_scale, 30)
Log.info { "Deployed #{funko_name}" }
0
end
end
end
end

View File

@ -1,4 +1,5 @@
require "./commands/build.cr" require "./commands/build.cr"
require "./commands/deploy.cr"
require "./commands/export.cr" require "./commands/export.cr"
require "./commands/login.cr" require "./commands/login.cr"
require "./commands/new.cr" require "./commands/new.cr"

View File

@ -85,16 +85,18 @@ module Funko
end end
# Set the number of running instances of this funko # Set the number of running instances of this funko
def scale(new_scale : Int) # Returns the list of IDs started or stopped
def scale(new_scale : Int) : Array(String)
docker_api = Docr::API.new(Docr::Client.new) docker_api = Docr::API.new(Docr::Client.new)
current_scale = self.scale current_scale = self.scale
return if current_scale == new_scale result = [] of String
return result if current_scale == new_scale
Log.info { "Scaling #{name} from #{current_scale} to #{new_scale}" } Log.info { "Scaling #{name} from #{current_scale} to #{new_scale}" }
if new_scale > current_scale if new_scale > current_scale
(current_scale...new_scale).each { (current_scale...new_scale).each {
Log.info { "Adding instance" } Log.info { "Adding instance" }
id = create_container result << (id = create_container)
start(id) start(id)
sleep 0.1.seconds sleep 0.1.seconds
} }
@ -105,6 +107,7 @@ module Funko
}.each { |container| }.each { |container|
Log.info { "Removing instance" } Log.info { "Removing instance" }
docker_api.containers.stop(container.@id) docker_api.containers.stop(container.@id)
result << container.@id
current_scale -= 1 current_scale -= 1
break if current_scale == new_scale break if current_scale == new_scale
sleep 0.1.seconds sleep 0.1.seconds
@ -116,6 +119,8 @@ module Funko
Log.info { "Pruning dead instance" } Log.info { "Pruning dead instance" }
docker_api.containers.delete(container.@id) docker_api.containers.delete(container.@id)
} }
result
end end
# Setup the target directory `path` with all the files needed # Setup the target directory `path` with all the files needed
@ -184,7 +189,7 @@ module Funko
docker_api.containers.list(all: true).select { |container| docker_api.containers.list(all: true).select { |container|
container.@names.any?(&.starts_with?("/faaso-#{name}-")) && container.@names.any?(&.starts_with?("/faaso-#{name}-")) &&
container.@state == "running" container.@state == "running"
} } || [] of Docr::Types::ContainerSummary
end end
# A comprehensive status for the funko: # A comprehensive status for the funko:
@ -207,12 +212,26 @@ module Funko
end end
end end
# Wait up to `t` seconds for the funko to reach the requested `state` # Wait up to `t` seconds for the funko to reach the desired scale
def wait_for(new_scale : Int, t) # If `healthy` is true, it will wait for the container to be declared
# healthy by the healthcheck
def wait_for(new_scale : Int, t : Int, healthy : Bool = false)
docker_api = Docr::API.new(Docr::Client.new)
channel = Channel(Nil).new channel = Channel(Nil).new
spawn do spawn do
loop do loop do
channel.send(nil) if scale == new_scale if healthy
channel.send(nil) if containers.count { |container|
begin
container = docker_api.containers.inspect(container.@id)
channel.send(nil) if !container.nil? && (container.state.health.status == "healthy")
rescue ex : Docr::Errors::DockerAPIError
Log.error { "#{ex}" } unless ex.status_code == 304 # This just happens
end
} == new_scale
else
channel.send(nil) if scale == new_scale
end
sleep 0.2.seconds sleep 0.2.seconds
end end
end end
@ -221,7 +240,7 @@ module Funko
when channel.receive when channel.receive
Log.info { "Funko #{name} reached scale #{new_scale}" } Log.info { "Funko #{name} reached scale #{new_scale}" }
when timeout(t.seconds) when timeout(t.seconds)
Log.error { "Funko #{name} did not reach scale #{new_scale} in #{t} seconds" } raise Exception.new("Funko #{name} did not reach scale #{new_scale} in #{t} seconds")
end end
end end

View File

@ -42,6 +42,8 @@ status : Int32 = 0
case ans case ans
when .fetch("build", false) when .fetch("build", false)
status = Faaso::Commands::Build.new.run(ans, ans["FOLDER"].as(Array(String))) status = Faaso::Commands::Build.new.run(ans, ans["FOLDER"].as(Array(String)))
when .fetch("deploy", false)
status = Faaso::Commands::Deploy.new.run(ans, ans["FUNKO"].as(String))
when .fetch("export", false) when .fetch("export", false)
status = Faaso::Commands::Export.new.run(ans, ans["SOURCE"].as(String), ans["DESTINATION"].as(String)) status = Faaso::Commands::Export.new.run(ans, ans["SOURCE"].as(String), ans["DESTINATION"].as(String))
when .fetch("login", false) when .fetch("login", false)