Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add dev console #7

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions bin/dev
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ gemfile do
gem 'tty-command', '~> 0.10.1'
gem 'tty-prompt'
gem 'pastel'
gem 'tty-option'
gem 'aws-sdk-ecs'
gem 'aws-sdk-ssm'
gem 'nokogiri'
end

require_relative "../lib/state"
Expand All @@ -36,10 +40,11 @@ Commands:
deploy Deploy wrapper for fly
setup_network Install systemd networking
clean Clean the current project
console Launch a prod console for the current project

HEREDOC

TOP_COMMANDS=%w{compose update post_update unset_docker_env mkcert provision_secrets clean deploy setup_network}
TOP_COMMANDS=%w{compose update post_update unset_docker_env mkcert provision_secrets clean deploy setup_network console}

CONFIG_DIR = Pathname.new("~/.config/dev").expand_path
SHARED_CONTAINERS_DIR = Pathname.new("/opt/shared_containers")
Expand Down Expand Up @@ -186,8 +191,8 @@ HEREDOC

def mkcert(*args)
options = {}
OptionParser.new do |opts|
opts.on("--domain=DOMAIN")
OptionParser.new do |parser|
parser.on("--domain=DOMAIN")
end.order_recognized!(*args, into: options)

%w[mkcert cacerts certs].each do |dir|
Expand Down Expand Up @@ -383,6 +388,13 @@ HEREDOC
setup_network
end

def console(*args)
require_relative "../lib/console"
cmd = Console.new
cmd.parse(*args)
cmd.run
end

private

def state
Expand Down
239 changes: 239 additions & 0 deletions lib/console.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
require_relative "state"

class Console
include TTY::Option
include State::Methods

usage do
program "dev"
end

option :cluster do
required
desc "ECS cluster name"
long "--cluster string"
end

option :task_family do
required
desc "ECS task definition family"
long "--task-family string"
end

option :container_name do
required
desc "Container to target in the task definition"
long "--container-name string"
end

option :log_level do
long "--log-level string"
desc "Sets the log level"
default :info
end

flag :help do
short "-h"
long "--help"
desc "Print usage"
end

flag :debug do
long "--debug"
desc "Shorthand to set the log level to DEBUG"
end

def run
if params[:help]
print help
exit
elsif !params.valid?
puts params.errors.summary
puts
print help
exit
else
if params[:debug]
params[:log_level] = :debug
end

interactive_console
end
end

private

def interactive_console
find_latest_task_def
start_task
connect_to_instance
ensure
stop_task
end

def find_latest_task_def
resp =
ecs_client.describe_task_definition(
task_definition: params[:task_family]
)

@task_def = "#{params[:task_family]}:#{resp.task_definition.revision}"

log.info "Using #{@task_def}"
end

def start_task
overrides = {
container_overrides: [
{
name: params[:container_name],
command: ["sleep", "infinity"]
}
]
}

whoami = `whoami`

log.info "Starting task..."

run_task_args = {
cluster: params[:cluster],
task_definition: @task_def,
network_configuration: {
awsvpc_configuration: {
subnets: awsvpc_private_subnet_ids,
security_groups: awsvpc_security_group_ids
},
},
overrides: overrides,
started_by: "user-console/#{whoami}",
propagate_tags: "TASK_DEFINITION",
}

log.debug(run_task_args)

resp =
ecs_client.run_task(run_task_args)

log.debug(resp)
tasks = resp.tasks

@task_arn = tasks.first.task_arn

@container_instance_arn = tasks.first.container_instance_arn
printed = false

while @container_instance_arn.nil?
if !printed
printed = true
puts "Waiting for ECS task to populate container instances..."
end

resp =
ecs_client.describe_tasks(
tasks: [
@task_arn
]
)
log.debug(resp)

sleep 1
@container_instance_arn = tasks.first.container_instance_arn
end
end

def stop_task
return if @task_arn.nil?

log.info "Stopping task..."

resp =
ecs_client.stop_task(
cluster: params[:cluster],
task: @task_arn,
reason: "user-console/stop"
)

log.info "Done."
end

def connect_to_instance
resp =
ecs_client.describe_container_instances(
cluster: params[:cluster],
container_instances: [@container_instance_arn]
)

instance_id = resp.container_instances.first.ec2_instance_id

script = <<~SCRIPT
docker run -it --rm \
--pull always \
--net host \
-v /var/run/docker.sock:/var/run/docker.sock \
-e TASK_ARN=#{@task_arn} \
-e CONTAINER_NAME=#{params[:container_name]} \
-e DEBUG=#{ENV.fetch("DEBUG", "false")} \
-e BASH_SHELL=#{ENV.fetch("BASH_SHELL", "false")} \
-e TASK_STATUS=#{ENV.fetch("TASK_STATUS", "true")} \
ryansch/console-helper:latest
SCRIPT

args = [
"ssh",
"-t",
instance_id,
"sudo",
"sheltie"
]

args += script.lines

begin
run_it
.subprocess(
args,
)
rescue Subprocess::NonZeroExit => e
puts e.message
rescue Interrupt
end
end

def awsvpc_private_subnet_ids
resp =
ssm_client.get_parameter(
name: "/console/#{params[:cluster]}/private_subnet_ids"
)
JSON.parse(resp.parameter.value)
end

def awsvpc_security_group_ids
resp =
ssm_client.get_parameter(
name: "/console/#{params[:cluster]}/client_nodes_security_group_ids"
)
JSON.parse(resp.parameter.value)
end

def ecs_client
return @ecs_client if defined?(@ecs_client)

@ecs_client =
Aws::ECS::Client.new
end

def ssm_client
return @ssm_client if defined?(@ssm_client)

@ssm_client =
Aws::SSM::Client.new
end

def state
return @state if defined?(@state)

@state =
State.new(log_level: params[:log_level])
end
end