From 11d7cf1f9f88503275e06abe862f6ccf3ff961c9 Mon Sep 17 00:00:00 2001 From: Roberto Alsina Date: Wed, 3 Jul 2024 11:19:34 -0300 Subject: [PATCH] Switched to docopt because all CLI things suck --- shard.lock | 8 ++-- shard.yml | 22 ++++----- src/faaso.cr | 85 ++++++---------------------------- src/funko.cr | 81 ++++++++++++++++++++++---------- src/main.cr | 127 +++++++++++++-------------------------------------- 5 files changed, 117 insertions(+), 206 deletions(-) diff --git a/shard.lock b/shard.lock index 48481d1..8e51532 100644 --- a/shard.lock +++ b/shard.lock @@ -4,10 +4,6 @@ shards: git: https://github.com/sija/backtracer.cr.git version: 1.2.2 - commander: - git: https://github.com/mrrooijen/commander.git - version: 0.4.0 - crest: git: https://github.com/mamantoha/crest.git version: 1.3.13 @@ -20,6 +16,10 @@ shards: git: https://github.com/naqvis/crystar.git version: 0.4.0 + docopt: + git: https://github.com/chenkovsky/docopt.cr.git + version: 0.2.0+git.commit.620fce4f334ff15d7321e5ecb6665ad258fe9297 + docr: git: https://github.com/ralsina/docr.git version: 0.1.1+git.commit.18f15cc7111b1d0c63347c7cca07aee9ec87a7a8 diff --git a/shard.yml b/shard.yml index be71f9c..da486cc 100644 --- a/shard.yml +++ b/shard.yml @@ -15,18 +15,18 @@ crystal: ">= 1.12.2" license: MIT dependencies: - docr: - github: ralsina/docr - branch: add_exposed_ports - commander: - github: mrrooijen/commander - kemal: - github: kemalcr/kemal - kemal-basic-auth: - github: kemalcr/kemal-basic-auth + crest: + github: mamantoha/crest crinja: github: straight-shoota/crinja crystar: github: naqvis/crystar - crest: - github: mamantoha/crest \ No newline at end of file + docopt: + github: chenkovsky/docopt.cr + docr: + github: ralsina/docr + branch: add_exposed_ports + kemal: + github: kemalcr/kemal + kemal-basic-auth: + github: kemalcr/kemal-basic-auth diff --git a/src/faaso.cr b/src/faaso.cr index 1291ce5..15bbed4 100644 --- a/src/faaso.cr +++ b/src/faaso.cr @@ -1,5 +1,4 @@ require "./funko.cr" -require "commander" require "crest" require "docr" require "docr/utils.cr" @@ -29,19 +28,10 @@ module Faaso module Commands # Build images for one or more funkos class Build - @arguments : Array(String) = [] of String - @options : Commander::Options + def run(options, folders : Array(String)) + funkos = Funko::Funko.from_paths(folders) - def initialize(options, arguments) - @options = options - @arguments = arguments - end - - def run - funkos = Funko::Funko.from_paths(@arguments) - local = @options.@bool["local"] - - if local + if options["--local"] funkos.each do |funko| # Create temporary build location tmp_dir = Path.new("tmp", UUID.random.to_s) @@ -112,7 +102,7 @@ module Faaso # If there are no images for the funko, it will fail to bring it up. class Up @arguments : Array(String) = [] of String - @options : Commander::Options + @options : Hash(String, Bool) def initialize(options, arguments) @options = options @@ -179,64 +169,17 @@ module Faaso end class Export - @arguments : Array(String) = [] of String - @options : Commander::Options - - def initialize(options, arguments) - @options = options - @arguments = arguments - end - - def run - funkos = Funko::Funko.from_paths(@arguments) - funkos.each do |funko| - # Create temporary build location - dst_path = Path.new("export", funko.name) - if File.exists? dst_path - Log.error { "#{dst_path} already exists, not exporting #{funko.path}" } - next - end - Log.info { "Exporting #{funko.path} to #{dst_path}" } - Dir.mkdir_p(dst_path) - funko.prepare_build dst_path - end - end - end - - class Down - @arguments : Array(String) = [] of String - @options : Commander::Options - - def initialize(options, arguments) - @options = options - @arguments = arguments - end - - def run - @arguments.each do |arg| - Log.info { "Stopping funko... #{arg}" } - # TODO: check if funko is running - # TODO: stop funko container - # TODO: delete funko container - # TODO: remove route from reverse proxy - end - end - end - - class Deploy - @arguments : Array(String) = [] of String - @options : Commander::Options - - def initialize(options, arguments) - @options = options - @arguments = arguments - end - - def run - @arguments.each do |arg| - Log.info { "Deploying funko... #{arg}" } - # TODO: Everything + def run(options, source : String, destination : String) + funko = Funko::Funko.from_paths([source])[0] + # Create temporary build location + dst_path = destination + if File.exists? dst_path + Log.error { "#{dst_path} already exists, not exporting #{funko.path}" } + return 1 end + Log.info { "Exporting #{funko.path} to #{dst_path}" } + Dir.mkdir_p(dst_path) + funko.prepare_build Path[dst_path] end end end diff --git a/src/funko.cr b/src/funko.cr index c7f03de..2d5b172 100644 --- a/src/funko.cr +++ b/src/funko.cr @@ -25,6 +25,10 @@ module Funko @[YAML::Field(ignore: true)] property path : String = "" + # Scale: how many instances of this funko should be running + @[YAML::Field(ignore: true)] + property scale = 0 + # Healthcheck properties property healthcheck_options : String = "--interval=1m --timeout=2s --start-period=2s --retries=3" property healthcheck_command : String = "curl --fail http://localhost:3000/ping || exit 1" @@ -60,37 +64,31 @@ module Funko } end - # Create an array of funkos just from names. These are limited in function - # and can't call `prepare_build` or some other functionality - def self.from_names(names : Array(String)) : Array(Funko) - names.map { |name| - Funko.from_yaml("name: #{name}") + # Get the number of running instances of this funko + def scale + docker_api = Docr::API.new(Docr::Client.new) + docker_api.containers.list.count { |container| + container.@name.starts_with? "faaso-#{name}-" && container.@state == "running" } end - # Get all the funkos docker knows about. - def self.from_docker : Array(Funko) + # Set the number of running instances of this funko + def scale(new_scale : Int) docker_api = Docr::API.new(Docr::Client.new) - names = [] of String + current_scale = self.scale + return if current_scale == new_scale - # Get all containers that look like funkos - docker_api.containers.list( - all: true, - ).each { |container| - container.@names.each { |name| - names << name.split("-", 2)[1].lstrip("/") if name.starts_with?("/faaso-") + if new_scale > current_scale + (current_scale...new_scale).each { start(create_container) } + else + containers.select { |contatiner| container.@state == "running" }.sort! { |i, j| + i.@created <=> j.@created + }.each { |container| + docker_api.containers.stop(container.@id) + current_scale -= 1 + break if current_scale == new_scale } - } - - # Now get all images that look like funkos, since - # we can start them just fine. - docker_api.images.list.each { |image| - next if image.@repo_tags.nil? - image.@repo_tags.as(Array(String)).each { |tag| - names << tag.split("-", 2)[1].split(":", 2)[0] if tag.starts_with?("faaso-") - } - } - from_names(names.to_set.to_a.sort!) + end end # Setup the target directory `path` with all the files needed @@ -280,5 +278,38 @@ module Funko docker_api.containers.start(response.@id) if autostart response.@id end + + # Create an array of funkos just from names. These are limited in function + # and can't call `prepare_build` or some other functionality + def self.from_names(names : Array(String)) : Array(Funko) + names.map { |name| + Funko.from_yaml("name: #{name}") + } + end + + # Get all the funkos docker knows about. + def self.from_docker : Array(Funko) + docker_api = Docr::API.new(Docr::Client.new) + names = [] of String + + # Get all containers that look like funkos + docker_api.containers.list( + all: true, + ).each { |container| + container.@names.each { |name| + names << name.split("-", 2)[1].lstrip("/") if name.starts_with?("/faaso-") + } + } + + # Now get all images that look like funkos, since + # we can start them just fine. + docker_api.images.list.each { |image| + next if image.@repo_tags.nil? + image.@repo_tags.as(Array(String)).each { |tag| + names << tag.split("-", 2)[1].split(":", 2)[0] if tag.starts_with?("faaso-") + } + } + from_names(names.to_set.to_a.sort!) + end end end diff --git a/src/main.cr b/src/main.cr index 7c53ff1..93b9fa6 100644 --- a/src/main.cr +++ b/src/main.cr @@ -1,6 +1,21 @@ require "./faaso.cr" require "colorize" -require "commander" +require "docopt" + +doc = <<-DOC +FaaSO CLI tool. + +Usage: + faaso build FOLDER ... [-l] [-v=] + faaso scale FUNKO_NAME SCALE [-l] [-v=] + faaso export SOURCE DESTINATION [-v=] + +Options: + -h --help Show this screen. + --version Show version. + -l --local Run commands locally instead of against a FaaSO server. + -v=level Control the logging verbosity, 0 to 5 [default: 3] +DOC # Log formatter for struct LogFormat < Log::StaticFormatter @@ -17,19 +32,15 @@ struct LogFormat < Log::StaticFormatter string "[#{Time.local}] #{@entry.severity.label}: #{@entry.message}".colorize(@@colors[@entry.severity.label]) end - def self.setup(quiet : Bool, verbosity) - if quiet - _verbosity = Log::Severity::Fatal - else - _verbosity = [ - Log::Severity::Fatal, - Log::Severity::Error, - Log::Severity::Warn, - Log::Severity::Info, - Log::Severity::Debug, - Log::Severity::Trace, - ][[verbosity, 5].min] - end + def self.setup(verbosity) + _verbosity = [ + Log::Severity::Fatal, + Log::Severity::Error, + Log::Severity::Warn, + Log::Severity::Info, + Log::Severity::Debug, + Log::Severity::Trace, + ][[verbosity, 5].min] Log.setup( _verbosity, Log::IOBackend.new(io: STDERR, formatter: LogFormat) @@ -37,86 +48,12 @@ struct LogFormat < Log::StaticFormatter end end -cli = Commander::Command.new do |cmd| - cmd.use = "faaso" - cmd.long = "Functions as a Service, Open" +ans = Docopt.docopt(doc, ARGV) +LogFormat.setup(ans["-v"].to_s.to_i) - cmd.flags.add do |flag| - flag.name = "local" - flag.short = "-l" - flag.long = "--local" - flag.description = "Run commands locally instead of against a FaaSO server." - flag.default = false - flag.persistent = true - end - - cmd.flags.add do |flag| - flag.name = "quiet" - flag.short = "-q" - flag.long = "--quiet" - flag.description = "Don't log anything" - flag.default = false - flag.persistent = true - end - - cmd.flags.add do |flag| - flag.name = "verbosity" - flag.short = "-v" - flag.long = "--verbosity" - flag.description = "Control the logging verbosity, 0 to 5 " - flag.default = 3 - flag.persistent = true - end - - cmd.commands.add do |command| - command.use = "build" - command.short = "Build a funko" - command.long = "Build a funko's Docker image and upload it to registry" - command.run do |options, arguments| - LogFormat.setup(options.@bool["quiet"], options.@int["verbosity"]) - Faaso::Commands::Build.new(options, arguments).run - end - end - - cmd.commands.add do |command| - command.use = "up" - command.short = "Ensure funkos are running" - command.long = "Start/unpause/create containers for requested funkos and ensure they are up." - command.run do |options, arguments| - LogFormat.setup(options.@bool["quiet"], options.@int["verbosity"]) - Faaso::Commands::Up.new(options, arguments).run - end - end - - cmd.commands.add do |command| - command.use = "deploy" - command.short = "Deploy latest images" - command.long = "Update containers for all funkos to latest image." - command.run do |options, arguments| - LogFormat.setup(options.@bool["quiet"], options.@int["verbosity"]) - Faaso::Commands::Deploy.new(options, arguments).run - end - end - - cmd.commands.add do |command| - command.use = "down" - command.short = "Stop a funko" - command.long = "Stop a funko in a container" - command.run do |options, arguments| - LogFormat.setup(options.@bool["quiet"], options.@int["verbosity"]) - Faaso::Commands::Down.new(options, arguments).run - end - end - - cmd.commands.add do |command| - command.use = "export" - command.short = "Export a funko to a directory" - command.long = "Exports a funko as a self-contained directory." - command.run do |options, arguments| - LogFormat.setup(options.@bool["quiet"], options.@int["verbosity"]) - Faaso::Commands::Export.new(options, arguments).run - end - end +case ans +when .fetch("build", false) + Faaso::Commands::Build.new.run(ans, ans["FOLDER"].as(Array(String))) +when .fetch("export", false) + Faaso::Commands::Export.new.run(ans, ans["SOURCE"].as(String), ans["DESTINATION"].as(String)) end - -Commander.run(cli, ARGV)