From 2447eb42d06174511746cb5c1cbdf41e755a5eb4 Mon Sep 17 00:00:00 2001 From: "Glenn Y. Rolland" Date: Mon, 13 Apr 2020 16:12:55 +0200 Subject: [PATCH] Add basic support for TransferDeploymentConfig: DockerImageEndpointConfig --> DokkuAppEndpointConfig --- src/config.cr | 39 ++++++ src/config/endpoint_settings.cr | 8 ++ src/config/host.cr | 7 + src/config/host_settings.cr | 4 + src/deployment.cr | 62 ++++++++- src/deployment/docker_image_to_dokku_app.cr | 107 --------------- src/deployment/run_script_to_dokku_app.cr | 15 +++ .../transfer_docker_image_to_dokku_app.cr | 125 ++++++++++++++++++ ...> transfer_mysql_dump_to_dokku_mariadb.cr} | 16 ++- src/pushokku.cr | 64 +-------- src/validation.cr | 14 ++ 11 files changed, 284 insertions(+), 177 deletions(-) delete mode 100644 src/deployment/docker_image_to_dokku_app.cr create mode 100644 src/deployment/run_script_to_dokku_app.cr create mode 100644 src/deployment/transfer_docker_image_to_dokku_app.cr rename src/deployment/{mysql_dump_to_dokku_mariadb.cr => transfer_mysql_dump_to_dokku_mariadb.cr} (77%) create mode 100644 src/validation.cr diff --git a/src/config.cr b/src/config.cr index a95e580..d5ac536 100644 --- a/src/config.cr +++ b/src/config.cr @@ -10,4 +10,43 @@ class Config filters: Array(FilterConfig), deployments: Array(DeploymentConfig) ) + + alias FindableConfig = + Array(HostConfig) | + Array(EndpointConfig) | + Array(FilterConfig) | + Array(DeploymentConfig) + + class MissingItemError < Exception + end + + class MultipleItemsError < Exception + end + + def find(list : FindableConfig, name : String, str : String) + matches = list.select { |item| item.name == name } + if matches.size > 1 + raise MultipleItemsError.new "Multiple #{str} have the name #{str}" + end + if matches.size < 1 + raise MissingItemError.new "No #{str} found for name #{name}" + end + return matches.first + end + + def find_host(name : String) : HostConfig + find(self.hosts, name, "hosts") + end + + def find_endpoint(name : String) : EndpointConfig + find(self.endpoints, name, "endpoints") + end + + def find_filter(name : String) : FilterConfig + find(self.filters, name, "filters") + end + + def find_deployment(name : String) : DeploymentConfig + find(self.deployments, name, "deployments") + end end diff --git a/src/config/endpoint_settings.cr b/src/config/endpoint_settings.cr index 27e4309..cb5c1cd 100644 --- a/src/config/endpoint_settings.cr +++ b/src/config/endpoint_settings.cr @@ -15,12 +15,20 @@ end class DockerImageEndpointConfigSettings YAML.mapping( + name: String, tag: { type: String, nilable: false, default: "latest" } ) + + def initialize(@name : String, @tag : String) + end + + def to_s + "#{name}:#{tag}" + end end class MysqlDumpEndpointConfigSettings diff --git a/src/config/host.cr b/src/config/host.cr index 56de618..01d5b55 100644 --- a/src/config/host.cr +++ b/src/config/host.cr @@ -7,6 +7,10 @@ class LocalHostConfig name: String, localhost: Hash(String, YAML::Any) ) + + def to_s + "LocalHost[#{name}]" + end end @@ -15,6 +19,9 @@ class SshHostConfig name: String, ssh: SshHostConfigSettings ) + def to_s + "SshHost[#{name}]" + end end alias HostConfig = diff --git a/src/config/host_settings.cr b/src/config/host_settings.cr index 703fc9a..e0006f5 100644 --- a/src/config/host_settings.cr +++ b/src/config/host_settings.cr @@ -6,4 +6,8 @@ class SshHostConfigSettings user: String, host: String ) + + def to_s + "#{user}@#{host}" + end end diff --git a/src/deployment.cr b/src/deployment.cr index 8a58c95..b7094a7 100644 --- a/src/deployment.cr +++ b/src/deployment.cr @@ -1,4 +1,62 @@ -require "./deployment/docker_image_to_dokku_app" -require "./deployment/mysql_dump_to_dokku_mariadb" +require "./deployment/*" +# run_script_to_dokku_app" +# require "./deployment/transfer_docker_image_to_dokku_app" +# require "./deployment/transfer_mysql_dump_to_dokku_mariadb" + + +class Deployment + def self.apply_config!(config) + config.deployments.each do |deployment_config| + Deployment.apply_deployment!(config, deployment_config) + end + end + + def self.apply_deployment!(config : Config, deployment_config : TransferDeploymentConfig) + src = config.find_endpoint(deployment_config.transfer.src) + dest = config.find_endpoint(deployment_config.transfer.dest) + + puts "Trying TransferDeploymentConfig: #{src.class} --> #{dest.class}..." + self.apply_transfer!(config, src, dest) + end + + def self.apply_deployment!(config : Config, deployment_config : RunDeploymentConfig) + src = config.find_endpoint(deployment_config.run.src) + dest = config.find_endpoint(deployment_config.run.dest) + puts "Trying RunDeploymentConfig: #{src.class} --> #{dest.class}..." + + self.apply_run!(config, src, dest) + end + + def self.apply_run!(config, src, dest) + puts "WARNING: run #{src.class} --> #{dest.class} missing!".colorize(:yellow) + end + + def self.apply_transfer!(config, src, dest) + puts "WARNING: transfer #{src.class} --> #{dest.class} missing!".colorize(:yellow) + end + + def something + deployment = + case deployment_config + when DokkuAppDeploymentConfig then + DockerImageToDokkuAppDeployment.new( + local.as(DockerImageLocalConfig), + remote, + deployment_config.as(DokkuAppDeploymentConfig) + ) + when MysqlDumpToDokkuMariadbDeployment then + DeploymentMariadb.new( + local.as(MysqlDumpLocalConfig), + remote, + deployment_config.as(DokkuMariadbDeploymentConfig) + ) + when Nil + nil + end + + next if deployment.nil? + deployment.run + end +end diff --git a/src/deployment/docker_image_to_dokku_app.cr b/src/deployment/docker_image_to_dokku_app.cr deleted file mode 100644 index e684437..0000000 --- a/src/deployment/docker_image_to_dokku_app.cr +++ /dev/null @@ -1,107 +0,0 @@ - -require "colorize" - -class DockerImageToDokkuAppDeployment - def initialize(@local : DockerImageLocalConfig, @remote : RemoteConfig, @deployment : DokkuAppDeploymentConfig) - end - - def run - dokku_app = @deployment.as(DeploymentAppConfig).dokku_app - app = dokku_app.name - image_meta = image_tag(@local.docker_image, 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/run_script_to_dokku_app.cr b/src/deployment/run_script_to_dokku_app.cr new file mode 100644 index 0000000..dd53338 --- /dev/null +++ b/src/deployment/run_script_to_dokku_app.cr @@ -0,0 +1,15 @@ + +require "colorize" + +class Deployment + def self.apply_run!( + config : Config, + src : ScriptEndpointConfig, + dest : DokkuAppEndpointConfig + ) + dokku_app = dest.name + # image_meta = image_tag(@local.docker_image, dokku_app) + # image_push(@remote.host, image_meta["tag_name_version"]) + # image_deploy(@remote.host, image_meta["app"], image_meta["version"]) + end +end diff --git a/src/deployment/transfer_docker_image_to_dokku_app.cr b/src/deployment/transfer_docker_image_to_dokku_app.cr new file mode 100644 index 0000000..520f345 --- /dev/null +++ b/src/deployment/transfer_docker_image_to_dokku_app.cr @@ -0,0 +1,125 @@ + +require "colorize" + +class Deployment + # puts YAML.dump({ image_tag: res }) + # puts "---" + + def self.apply_transfer!( + config : Config, + src : DockerImageEndpointConfig, + dest : DokkuAppEndpointConfig + ) + src_host = config.find_host(src.host) + dest_host = config.find_host(dest.host) + + pp src + dest_docker_image = build_docker_image_ref_timestamp(dest.name) + docker_image_tag!(src, dest_docker_image) + docker_image_push!(dest, src, dest_docker_image, dest_host) + # docker_image_deploy!(dest_host, dest_docker_image, dest_host) + end + + private def self.build_docker_image_ref_timestamp( + dokku_app : String + ) + dest_ref = DockerImageEndpointConfigSettings.new( + name: "dokku/#{dokku_app}", + tag: `TZ=UTC date +"v%Y%m%d_%H%M"`.strip + ) + return dest_ref + end + + + # private def self.docker_image_push!( + # ) + # STDERR.puts "ERROR: pushing docker images to local host is not implemented" + # exit 2 + # end + + + private def self.docker_image_tag!( + src : DockerImageEndpointConfig, + dest_docker_image : DockerImageEndpointConfigSettings, + ) + puts "Tagging image...".colorize(:yellow) + puts YAML.dump({ + src: src.docker_image.to_s, + dest: dest_docker_image.to_s + }) + end + + # docker tag ... dokku/...:tag + # docker save "$TAG_NAME_VERSION" \ + # | gzip \ + # | ssh "$HOST_REMOTE" "gunzip | docker load" + private def self.docker_image_push!( + dest : DokkuAppEndpointConfig, + src : DockerImageEndpointConfig, + dest_docker_image : DockerImageEndpointConfigSettings, + dest_host : HostConfig, + ) + if dest_host.is_a? LocalHostConfig + return + end + + Process.run "docker", ["tag", src.docker_image.to_s, dest_docker_image.to_s] + + 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", [dest_host.ssh.to_s, "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", src.docker_image.to_s], + 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 #{dest_host.ssh.to_s}" + else + STDERR.puts "Error (code #{status.exit_status}) when importing docker image!" + end + puts "Image pushed successfully!".colorize(:green) + end + + private def self.docker_image_deploy!( + src : DockerImageEndpointConfig, + dest : DockerImageEndpointConfig, + dest_docker_image : DockerImageEndpointConfigSettings, + ) + puts "Deploying image #{app}:#{version}...".colorize(:yellow) + status = Process.run "ssh", [dest.host.to_s, "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/transfer_mysql_dump_to_dokku_mariadb.cr similarity index 77% rename from src/deployment/mysql_dump_to_dokku_mariadb.cr rename to src/deployment/transfer_mysql_dump_to_dokku_mariadb.cr index 60005f1..8f2b870 100644 --- a/src/deployment/mysql_dump_to_dokku_mariadb.cr +++ b/src/deployment/transfer_mysql_dump_to_dokku_mariadb.cr @@ -1,13 +1,15 @@ -class MysqlDumpToDokkuMariadbDeployment - def initialize(@local : LocalFileConfig, @remote : RemoteConfig, @deployment : DeploymentMariadbConfig) - end +class Deployment - def run - dokku_mariadb = @deployment.dokku_mariadb.as(DokkuMariadbConfig) - local_path = @local.path + def self.apply_transfer!( + config : Config, + src : MysqlDumpEndpointConfig, + dest : DokkuMariadbEndpointConfig + ) + dokku_mariadb = dest.name + # local_path = @local.path # puts @local.inspect - file_push(@remote.host, local_path, dokku_mariadb.name) + # file_push(@remote.host, local_path, dokku_mariadb.name) end private def file_push(host, local_path, dokku_mariadb_name) diff --git a/src/pushokku.cr b/src/pushokku.cr index c04a723..a56b1f4 100644 --- a/src/pushokku.cr +++ b/src/pushokku.cr @@ -4,6 +4,7 @@ require "yaml" require "colorize" require "./config" +require "./validation" require "./deployment" module Pushokku @@ -75,73 +76,14 @@ module Pushokku end - def self.handle_deployment(config : Config, deployment_config : DeploymentConfig) - local = config.locals.select { |l| l.name == deployment_config.local }.first - remote = config.remotes.select { |r| r.name == deployment_config.remote }.first - - if local.nil? - puts "Unknown local #{deployment_config.local}. Exiting." - exit 2 - end - - if remote.nil? - puts "Unknown remote #{deployment_config.remote}. Exiting." - exit 2 - end - - deployment = - case deployment_config - when DokkuAppDeploymentConfig then - DockerImageToDokkuAppDeployment.new( - local.as(DockerImageLocalConfig), - remote, - deployment_config.as(DokkuAppDeploymentConfig) - ) - when MysqlDumpToDokkuMariadbDeployment then - DeploymentMariadb.new( - local.as(MysqlDumpLocalConfig), - remote, - deployment_config.as(DokkuMariadbDeploymentConfig) - ) - when Nil - nil - end - - next if deployment.nil? - deployment.run - end - - - def self.find_filter(config, name) - matches = config.filters.select { |filter| filter.name == name } - if matches.size > 1 - raise "Multiple filters have the same name (unicity)" - end - return matches.first - end - - def self.validate_config!(config) - pp config - #config.deployments.each do |deployment_config| - # handle_deployment(config, deployment_config) - #end - end - - def self.apply_config!(config) - config.deployments.each do |deployment_config| - # handle_deployment(config, deployment_config) - end - end - 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"]) - validate_config!(config) - apply_config!(config) - + Validation.validate_config!(config) + Deployment.apply_config!(config) exit 0 end end diff --git a/src/validation.cr b/src/validation.cr new file mode 100644 index 0000000..268b1f1 --- /dev/null +++ b/src/validation.cr @@ -0,0 +1,14 @@ + +class Validation + def self.validate_config!(config) + pp config + config.deployments.each do |deployment_config| + Validation.validate_deployment!(config, deployment_config) + end + end + + def self.validate_deployment!(config : Config, deployment : DeploymentConfig) + STDERR.puts "WARNING: validation for #{deployment.class} not yet implemented".colorize(:yellow) + end +end +