diff --git a/src/config.cr b/src/config.cr new file mode 100644 index 0000000..2a6dfc0 --- /dev/null +++ b/src/config.cr @@ -0,0 +1,15 @@ + +require "yaml" +require "./config/local" +require "./config/remote" +require "./config/deployment" + +class Config + YAML.mapping( + locals: Array(LocalConfig), + remotes: Array(RemoteConfig), + deployments: Array(DeploymentConfig) + ) +end + + diff --git a/src/config/deployment.cr b/src/config/deployment.cr new file mode 100644 index 0000000..23c0af5 --- /dev/null +++ b/src/config/deployment.cr @@ -0,0 +1,10 @@ + +require "yaml" + +class DeploymentConfig + YAML.mapping( + local: String, + remote: String, + type: String, + ) +end diff --git a/src/config/local.cr b/src/config/local.cr new file mode 100644 index 0000000..6098af9 --- /dev/null +++ b/src/config/local.cr @@ -0,0 +1,21 @@ + +require "yaml" + +enum LocalType + DOCKER_IMAGE = 1 + MYSQL_DUMP = 2 + + def to_yaml(io) + to_s(io) + end +end + +class LocalConfig + YAML.mapping( + name: String, + type: LocalType, # enum ? + docker_image: String | Nil, + path: String | Nil + ) +end + diff --git a/src/config/remote.cr b/src/config/remote.cr new file mode 100644 index 0000000..8963a7e --- /dev/null +++ b/src/config/remote.cr @@ -0,0 +1,11 @@ + +require "yaml" + +class RemoteConfig + YAML.mapping( + name: String, + user: String, + host: String + ) +end + diff --git a/src/deployment.cr b/src/deployment.cr new file mode 100644 index 0000000..8a58c95 --- /dev/null +++ b/src/deployment.cr @@ -0,0 +1,4 @@ + +require "./deployment/docker_image_to_dokku_app" +require "./deployment/mysql_dump_to_dokku_mariadb" + diff --git a/src/deployment/docker_image_to_dokku_app.cr b/src/deployment/docker_image_to_dokku_app.cr new file mode 100644 index 0000000..8ffb905 --- /dev/null +++ b/src/deployment/docker_image_to_dokku_app.cr @@ -0,0 +1,109 @@ + +require "colorize" + +class DockerImageToDokkuApp + def self.handler + "docker_image_to_dokku_app" + end + + def initialize(@local : LocalConfig, @remote : RemoteConfig, @deployment : DeploymentConfig) + end + + def run + image_meta = image_tag(@local.docker_image, config["app"]) + image_push(@remote.host, image_meta["tag_name_version"]) + image_deploy(@remote.host, image_meta["app"], image_meta["version"]) + end + + # private def image_tag(docker_compose_yml : String, service : String, app : String) + # version = `date +"v%Y%m%d_%H%M"`.strip + # tag_name = "dokku/#{app}" + # tag_name_version = "#{tag_name}:#{version}" + # image = `docker-compose -f #{docker_compose_yml} images -q #{service} `.strip + # Process.run "docker", ["tag", image, tag_name_version] + + # res = { + # app: app, + # version: version, + # tag_name_version: tag_name_version + # } + # puts YAML.dump({ image_tag: res }) + # puts "---" + # return res + # end + + private def image_tag(docker_image : String, app : String) + version = `date +"v%Y%m%d_%H%M"`.strip + tag_name = "dokku/#{app}" + tag_name_version = "#{tag_name}:#{version}" + Process.run "docker", ["tag", docker_image, tag_name_version] + + res = { + app: app, + version: version, + tag_name_version: tag_name_version + } + puts YAML.dump({ image_tag: res }) + puts "---" + return res + end + + private def image_push(host, tag_name_version) + # docker save "$TAG_NAME_VERSION" \ + # | gzip \ + # | ssh "$HOST_REMOTE" "gunzip | docker load" + + pipe1_reader, pipe1_writer = IO.pipe(true) + pipe2_reader, pipe2_writer = IO.pipe(true) + + p3_out = IO::Memory.new + puts "Pushing image...".colorize(:yellow) + p3 = Process.new "ssh", [host, "gunzip | docker load"], + input: pipe2_reader, output: p3_out, error: STDERR + + p2 = Process.new "gzip", + input: pipe1_reader, + output: pipe2_writer, + error: STDERR + + p1 = Process.new "docker", ["save", tag_name_version], + output: pipe1_writer, + error: STDERR + + status = p1.wait + pipe1_writer.close + if status.success? + puts "-----> Docker image successfully exported" + else + STDERR.puts "Error (code #{status.exit_status}) when exporting docker image!" + exit 1 + end + + status = p2.wait + pipe1_reader.close + pipe2_writer.close + if ! status.success? + STDERR.puts "Error (code #{status.exit_status}) when gzipping image!" + end + + status = p3.wait + pipe2_reader.close + if status.success? + puts "-----> Docker image successfully imported on #{host}" + else + STDERR.puts "Error (code #{status.exit_status}) when importing docker image!" + end + puts "Image pushed successfully!".colorize(:green) + end + + private def image_deploy(host, app, version) + puts "Deploying image #{app}:#{version}...".colorize(:yellow) + status = Process.run "ssh", [host, "dokku tags:deploy #{app} #{version}"], + output: STDOUT, error: STDOUT + if status.success? + puts "Image deployed successfully!".colorize(:green) + else + STDERR.puts "Error (code #{status.exit_status}) when deploying image!" + end + end +end diff --git a/src/deployment/mysql_dump_to_dokku_mariadb.cr b/src/deployment/mysql_dump_to_dokku_mariadb.cr new file mode 100644 index 0000000..538096c --- /dev/null +++ b/src/deployment/mysql_dump_to_dokku_mariadb.cr @@ -0,0 +1,13 @@ + +class MysqlDumpToDokkuMariadb + def self.handler + "mysql_dump_to_dokku_mariadb" + end + + def initialize(@local : LocalConfig, @remote : RemoteConfig, @deployment : DeploymentConfig) + end + + def run + end +end + diff --git a/src/pushokku.cr b/src/pushokku.cr index e9439bb..a0f6b55 100644 --- a/src/pushokku.cr +++ b/src/pushokku.cr @@ -3,157 +3,108 @@ require "option_parser" require "yaml" require "colorize" +require "./config" +require "./deployment" - -class Pushokku - alias Options = { - config_file: String, - docker_compose_yml: String, - environment: String - } - - alias Config = { - host: String, - service: String, - app: String - } - - def parse_options(args) : Options - config_file = ".pushokku.yml" - docker_compose_yml = "docker-compose.yml" - environment = "production" - - OptionParser.parse(args) do |parser| - parser.banner = "Welcome to Pushokku!" - - parser.on "-c CONFIG", "--config=CONFIG", "Use the following config file" do |file| - config_file = file - end - - parser.on "-f DOCKER_COMPOSE_YML", "--config=DOCKER_COMPOSE_YML", "Use the following docker-compose file" do |file| - docker_compose_yml = file - end - - parser.on "-v", "--version", "Show version" do - puts "version 1.0" - exit - end - parser.on "-h", "--help", "Show help" do - puts parser - exit - end - end - return { - docker_compose_yml: docker_compose_yml, - config_file: config_file, - environment: environment +module Pushokku + class Cli + alias Options = { + config_file: String, + docker_compose_yml: String, + environment: String } - end - def load_config(config_file : String) : Config - puts "Loading configuration...".colorize(:yellow) - if ! File.exists? config_file - STDERR.puts "ERROR: Unable to read configuration file '#{config_file}'" - exit 1 + def parse_options(args) : Options + config_file = ".pushokku.yml" + docker_compose_yml = "docker-compose.yml" + environment = "production" + + OptionParser.parse(args) do |parser| + parser.banner = "Welcome to Pushokku!" + + parser.on "-c CONFIG", "--config=CONFIG", "Use the following config file" do |file| + config_file = file + end + + parser.on "-f DOCKER_COMPOSE_YML", "--config=DOCKER_COMPOSE_YML", "Use the following docker-compose file" do |file| + docker_compose_yml = file + end + + parser.on "-v", "--version", "Show version" do + puts "version 1.0" + exit + end + parser.on "-h", "--help", "Show help" do + puts parser + exit + end + end + return { + docker_compose_yml: docker_compose_yml, + config_file: config_file, + environment: environment + } end - yaml = File.open(config_file) do |file| - YAML.parse(file) + def load_config(config_file : String) : Config + puts "Loading configuration...".colorize(:yellow) + if ! File.exists? config_file + STDERR.puts "ERROR: Unable to read configuration file '#{config_file}'" + exit 1 + end + + yaml_str = File.read(config_file) + config = Config.from_yaml(yaml_str) + # yaml = YAML.parse(yaml_str) + + if config.nil? + STDERR.puts "ERROR: Invalid YAML content in '#{config_file}'" + exit 1 + end + + return config end - { - host: yaml["host"].to_s, - service: yaml["service"].to_s, - app: yaml["app"].to_s - } - end - def image_tag(docker_compose_yml : String, service : String, app : String) - version = `date +"v%Y%m%d_%H%M"`.strip - tag_name = "dokku/#{app}" - tag_name_version = "#{tag_name}:#{version}" - image = `docker-compose -f #{docker_compose_yml} images -q #{service} `.strip - Process.run "docker", ["tag", image, tag_name_version] + def self.run(args) + app = Cli.new + opts = app.parse_options(args) + config = app.load_config(opts["config_file"]) + # env_config = App.get_config(config, opts["environment"]) - res = { - app: app, - version: version, - tag_name_version: tag_name_version - } - puts YAML.dump({ image_tag: res }) - puts "---" - return res - end + deployment_classes = [ + DockerImageToDokkuApp, + MysqlDumpToDokkuMariadb + ] - def image_push(host, tag_name_version) - # docker save "$TAG_NAME_VERSION" \ - # | gzip \ - # | ssh "$HOST_REMOTE" "gunzip | docker load" + config.deployments.each do |deployment| + local = config.locals.select { |l| l.name == deployment.local }.first + remote = config.remotes.select { |r| r.name == deployment.remote }.first + if local.nil? + puts "Unknown local #{deployment.local}. Exiting." + exit 2 + end + if remote.nil? + puts "Unknown remote #{deployment.remote}. Exiting." + exit 2 + end - pipe1_reader, pipe1_writer = IO.pipe(true) - pipe2_reader, pipe2_writer = IO.pipe(true) + deployment_handler = "#{local.type}_to_#{deployment.type}" + deployment_class = deployment_classes.select {|c| c.handler == deployment_handler }.first + if deployment_class.nil? + puts "Unknown deloyment class for #{deployment_handler}. Exiting." + exit 2 + end - p3_out = IO::Memory.new - puts "Pushing image...".colorize(:yellow) - p3 = Process.new "ssh", [host, "gunzip | docker load"], - input: pipe2_reader, output: p3_out, error: STDERR + deployment = deployment_class.new(local, remote, deployment) + deployment.run + # puts deployment.inspect + end - p2 = Process.new "gzip", - input: pipe1_reader, - output: pipe2_writer, - error: STDERR - - p1 = Process.new "docker", ["save", tag_name_version], - output: pipe1_writer, - error: STDERR - - status = p1.wait - pipe1_writer.close - if status.success? - puts "-----> Docker image successfully exported" - else - STDERR.puts "Error (code #{status.exit_status}) when exporting docker image!" - exit 1 + exit 2 end - - status = p2.wait - pipe1_reader.close - pipe2_writer.close - if ! status.success? - STDERR.puts "Error (code #{status.exit_status}) when gzipping image!" - end - - status = p3.wait - pipe2_reader.close - if status.success? - puts "-----> Docker image successfully imported on #{host}" - else - STDERR.puts "Error (code #{status.exit_status}) when importing docker image!" - end - puts "Image pushed successfully!".colorize(:green) - end - - def image_deploy(host, app, version) - puts "Deploying image #{app}:#{version}...".colorize(:yellow) - status = Process.run "ssh", [host, "dokku tags:deploy #{app} #{version}"], - output: STDOUT, error: STDOUT - if status.success? - puts "Image deployed successfully!".colorize(:green) - else - STDERR.puts "Error (code #{status.exit_status}) when deploying image!" - end - end - - def self.run(args) - app = Pushokku.new - opts = app.parse_options(args) - config = app.load_config(opts["config_file"]) - # env_config = App.get_config(config, opts["environment"]) - image_meta = app.image_tag(opts["docker_compose_yml"], config["service"], config["app"]) - app.image_push(config["host"], image_meta["tag_name_version"]) - app.image_deploy(config["host"], image_meta["app"], image_meta["version"]) end end -Pushokku.run(ARGV) +Pushokku::Cli.run(ARGV)