heighliner 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Heighliner
4
+ module CliOptions
5
+ def option(*option)
6
+ @options ||= []
7
+ @options << option
8
+ end
9
+
10
+ def options
11
+ @options || []
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Heighliner
4
+ module Cmds
5
+ class Attach < Cli
6
+ def usage
7
+ <<~EOS
8
+ Shuts down the application container and starts it up again with the current directory bind mounted inside. This way the application will run from the source code in the current directory and any edits you make will immediately show up inside the container. This is ideal for development.
9
+
10
+ Once the attached container exits (through the use of control+c for example) it will be replaced by a regular non-attached version of the app container.
11
+
12
+ USAGE: heighliner attach
13
+ EOS
14
+ end
15
+
16
+ def execute(_opts)
17
+ ensure_setup
18
+ attach_app
19
+ start_app
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Heighliner
4
+ module Cmds
5
+ class DbLoad < Cli
6
+ def usage
7
+ <<~EOS
8
+ Shuts down the database docker container, *replaces* the database with the backup provided and brings the container up again.
9
+
10
+ The database will be restored from a tarball saved as \`~/.heighliner/<ENV_NAME>/<current_github_branch_name>/<DB_BACKUP_FILENAME>.tar.bz\`
11
+
12
+ Alternatively you can also load it from your current directory.
13
+
14
+ If no database name was provided, the default database stored at \`~/.heighliner/<ENV_NAME>/<current_github_branch_name>/default.tar.bz\` will be used.
15
+
16
+ USAGE: heighliner db_load
17
+ heighliner db_load DB_BACKUP_FILENAME
18
+ heighliner db_load ./my_database.tar.bz
19
+ EOS
20
+ end
21
+
22
+ def execute(_opts)
23
+ ensure_setup
24
+ name = ARGV.shift || 'default'
25
+ load_db(name)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Heighliner
4
+ module Cmds
5
+ class DbReset < Cli
6
+ def usage
7
+ <<~EOS
8
+ Shuts down the database docker container, *replaces* the database with the default database image stored at \`~/.heighliner/<ENV_NAME>/<current_github_branch_name>/default.tar.bz\` and brings the container up again.
9
+
10
+ This is the same as running \`heighliner db_load\` with no arguments.
11
+
12
+ USAGE: heighliner db_reset
13
+ EOS
14
+ end
15
+
16
+ def execute(_opts)
17
+ ensure_setup
18
+ load_db('default')
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Heighliner
4
+ module Cmds
5
+ class DbResetHard < Cli
6
+ def usage
7
+ <<~EOS
8
+ Shuts down the database docker container, deletes the default database image stored at \`~/.heighliner/<ENV_NAME>/<current_github_branch_name>/default.tar.bz\`, rebuilds the docker volume if needed and the default database image from scratch and then brings the container up again.
9
+
10
+ USAGE: heighliner db_reset_hard
11
+ EOS
12
+ end
13
+
14
+ def execute(_opts)
15
+ ensure_setup
16
+ FileUtils.rm db_image_path('default') if File.exist?(db_image_path('default'))
17
+ setup_db
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Heighliner
4
+ module Cmds
5
+ class DbSave < Cli
6
+ def usage
7
+ <<~EOS
8
+ Shuts down the database docker container, backs up the database and brings the container back up.
9
+
10
+ The database will be saved as a tarball to \`~/.heighliner/<ENV_NAME>/<current_github_branch_name>/<DB_BACKUP_FILENAME>.tar.bz\`
11
+
12
+ Alternatively you can also save it to your current directory.
13
+
14
+ USAGE: heighliner db_save DB_BACKUP_FILENAME
15
+ heighliner db_save ./my_database.tar.bz
16
+ EOS
17
+ end
18
+
19
+ def execute(_opts)
20
+ ensure_setup
21
+ name = ARGV.shift || 'default'
22
+ save_db(name)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Heighliner
4
+ module Cmds
5
+ class Deinit < Cli
6
+ def usage
7
+ <<~EOS
8
+ Removes the Heighliner environment from \`~/.heighliner/config.yml\`. This also runs \`heighliner down\` to stop and delete your app containers and database volume. It does not delete the \`~/.heighliner/databases/<ENV_NAME>\` directory.
9
+
10
+ USAGE: heighliner deinit
11
+ EOS
12
+ end
13
+
14
+ def execute(_opts)
15
+ down
16
+ Config.config[:envs].delete(envname)
17
+ Config.config[:envnames].delete(Config.work_dir)
18
+ save_config
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Heighliner
4
+ module Cmds
5
+ class Down < Cli
6
+ def usage
7
+ <<~EOS
8
+ Stops and removes your app and database containers, and deletes the database volume. This does **not** affect the shared infrastructure (nginx, DNS, Chrome) — use \`heighliner shutdown\` for that.
9
+
10
+ USAGE: heighliner down
11
+ EOS
12
+ end
13
+
14
+ def execute(_opts)
15
+ stop_db
16
+ stop_app
17
+ delete_db_volume
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Heighliner
4
+ module Cmds
5
+ class Init < Cli
6
+ def usage
7
+ <<~EOS
8
+ Initializes a Heighliner environment and assigns ports for it in \`~/.heighliner/config.yml\`. When running \`heighliner up\` later the directory \`~/.heighliner/databases/<ENV_NAME>\` will get created.
9
+
10
+ If the current directory is already initialized, you will get an error telling you the existing environment name.
11
+
12
+ USAGE: heighliner init ENV_NAME
13
+ EOS
14
+ end
15
+
16
+ def execute(_opts)
17
+ return Optimist.die "Already initialized as #{envname}. Use 'heighliner deinit' to remove this environment first." if envname
18
+
19
+ name = ARGV.shift
20
+ return Optimist.die 'Needs environment name' if name.nil?
21
+
22
+ init_config_for_env(name)
23
+ save_config
24
+ end
25
+
26
+ def init_config_for_env(name)
27
+ Config.config[:envnames][Config.work_dir] = name
28
+ Config.config[:envs][name] = {
29
+ app_port: (largest_port + 1).to_s,
30
+ db_port: (largest_port + 2).to_s
31
+ }
32
+ Config.config[:largest_port] = Config.config[:largest_port] + 2
33
+ end
34
+
35
+ def largest_port
36
+ Config.config[:largest_port]
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Heighliner
4
+ module Cmds
5
+ class Login < Cli
6
+ def usage
7
+ <<~EOS
8
+ Executes a command on the application docker container. By executing the command \`sh\` you can get a login shell.
9
+
10
+ USAGE: heighliner login COMMAND
11
+ EOS
12
+ end
13
+
14
+ def execute(_opts)
15
+ ensure_setup
16
+ cmd = (ARGV || []).join(' ')
17
+ exec "docker exec -ti #{app_container_name} #{cmd}"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Heighliner
4
+ module Cmds
5
+ class Logs < Cli
6
+ def usage
7
+ <<~EOS
8
+ Continuously monitors the application container's logs.
9
+
10
+ USAGE: heighliner logs
11
+ EOS
12
+ end
13
+
14
+ def execute(_opts)
15
+ exec "docker logs -f #{app_container_name}"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Heighliner
4
+ module Cmds
5
+ class Root < Cli
6
+ def usage
7
+ <<~USAGE
8
+ Executes a command on the application docker container as a root user.
9
+
10
+ USAGE: heighliner root COMMAND
11
+ USAGE
12
+ end
13
+
14
+ def execute(_opts)
15
+ ensure_setup
16
+ cmd = (ARGV || []).join(' ')
17
+ exec "docker exec -ti --user root #{app_container_name} #{cmd}"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Heighliner
6
+ module Cmds
7
+ class Set < Cli
8
+ def usage
9
+ <<~EOS
10
+ This command lets you set up special variables that configure heighliner's behavior for you.
11
+
12
+ Available subcommands:
13
+
14
+ http-suffix - Sets the domain suffix for the reverse proxy to use (defaults to lvh.me)
15
+ cert-url - Sets up a URL from which HTTPS certificates can be downloaded.
16
+ cert-folder - Sets up a folder from which HTTPS certificates can be copied.
17
+ cert-1password - Sets up a 1Password item from which HTTPS certificates can be downloaded.
18
+ cert-1password-fields - Sets custom 1Password field names (JSON object)
19
+ help-https - Shows the HTTPS notes.
20
+
21
+ USAGE: heighliner set cert-url
22
+ heighliner set cert-folder
23
+ heighliner set cert-1password
24
+ heighliner set cert-1password-fields
25
+ heighliner set http-suffix
26
+ heighliner set help-https
27
+ EOS
28
+ end
29
+
30
+ def initialize
31
+ super
32
+ @use_steerfile = false
33
+ end
34
+
35
+ def execute(_opts)
36
+ cmd = ARGV.shift
37
+
38
+ case cmd
39
+ when 'cert-url'
40
+ Config.config[:cert_source] = { url: ARGV.shift }
41
+ when 'cert-folder'
42
+ Config.config[:cert_source] = { folder: ARGV.shift }
43
+ when 'cert-1password'
44
+ handle_cert_1password
45
+ when 'cert-1password-fields'
46
+ Config.config[:cert_source]['1password-fields'] = JSON.parse(ARGV.shift)
47
+ when 'http-suffix'
48
+ Config.config[:http_suffix] = ARGV.shift
49
+ when 'help-https'
50
+ puts help_https
51
+ else
52
+ Optimist.die "Unknown subcommand: '#{cmd}'"
53
+ end
54
+
55
+ save_config
56
+ end
57
+
58
+ private
59
+
60
+ def handle_cert_1password
61
+ if ARGV.empty?
62
+ puts help_1password
63
+ else
64
+ Config.config[:cert_source] = { '1password': ARGV.shift }
65
+ end
66
+ end
67
+
68
+ def help_https
69
+ <<~SET_HELP
70
+ Notes on HTTPS:
71
+
72
+ You need to set suffix and either cert-url or cert-folder to enable HTTPS.
73
+
74
+ cert-url and cert-folder are mutually exclusive. If you set one of them the other will be erased.
75
+
76
+ The cert-url and cert-folder must satisfy the following requirements to work:
77
+
78
+ The strings must be the root of certificates named after the suffix. For example,
79
+
80
+ if cert-url is https://mydomain.com/certs and your suffix is local.mydomain.com, the following
81
+ url need to be the certificate files:
82
+
83
+ https://mydomain.com/certs/local.mydomain.com.chain.pem
84
+ https://mydomain.com/certs/local.mydomain.com.crt
85
+ https://mydomain.com/certs/local.mydomain.com.key
86
+
87
+ Another example:
88
+
89
+ If you use suffix of localme.com and cert-folder is /home/me/https, The following files need to exist:
90
+
91
+ /home/me/https/localme.com.chain.pem
92
+ /home/me/https/localme.com.crt
93
+ /home/me/https/localme.com.key
94
+ SET_HELP
95
+ end
96
+
97
+ def help_1password
98
+ <<~SET_HELP
99
+ Notes on 1Password certificates:
100
+
101
+ You need to set suffix and cert-1password to enable HTTPS with 1Password.
102
+
103
+ The cert-1password value should be in the format 'Vault/Item'.
104
+
105
+ By default, the field names in the 1Password item should match the file extensions:
106
+ - key → <suffix>.key
107
+ - crt → <suffix>.crt
108
+ - chain → <suffix>.chain.pem
109
+
110
+ You can customize field names with:
111
+ heighliner set cert-1password-fields '{"key":"private-key","crt":"certificate","chain":"ca-bundle"}'
112
+
113
+ Example:
114
+ heighliner set cert-1password Vault/Dev-Certs
115
+ heighliner set http-suffix local.mydomain.com
116
+
117
+ This will read:
118
+ op read "op://Vault/Dev-Certs/key" → local.mydomain.com.key
119
+ op read "op://Vault/Dev-Certs/crt" → local.mydomain.com.crt
120
+ op read "op://Vault/Dev-Certs/chain" → local.mydomain.com.chain.pem
121
+
122
+ Make sure the `op` CLI is installed and authenticated on your machine.
123
+ SET_HELP
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Heighliner
4
+ module Cmds
5
+ class Show < Cli
6
+ def usage
7
+ <<~EOS
8
+ Subcommand that shows information about the environment such as the TCP ports or the certificate used for HTTPS.
9
+
10
+ USAGE: heighliner show ports
11
+ heighliner show cert-source
12
+ heighliner show http-suffix
13
+ EOS
14
+ end
15
+
16
+ def execute(_opts)
17
+ ensure_setup
18
+ cmd = ARGV.shift
19
+ valid_cmds = 'ports cert-source http-suffix'
20
+ return Optimist.die "Available things to show: #{valid_cmds}" unless cmd
21
+
22
+ case cmd
23
+ when 'ports'
24
+ Config.info_out.puts "app: #{app_port}"
25
+ Config.info_out.puts "db: #{db_port}"
26
+ when 'cert-source'
27
+ unless Config.config[:cert_source]
28
+ Optimist.die 'No certificate source set.
29
+ see heighliner set help'
30
+ end
31
+
32
+ source = Config.config[:cert_source][:url] || Config.config[:cert_source][:folder]
33
+ Config.info_out.puts source
34
+ when 'http-suffix'
35
+ Config.info_out.puts http_suffix
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Heighliner
4
+ module Cmds
5
+ class Shutdown < Cli
6
+ def usage
7
+ <<~EOS
8
+ Shuts down the shared infrastructure containers used by Heighliner (nginx reverse proxy, DNS resolver, and Selenium Chrome). This does **not** stop your app or database containers — use \`heighliner down\` for that.
9
+
10
+ NOTE: This command is destructive — it removes the shared Docker network and certs volume, which will affect all Heighliner environments on this machine.
11
+
12
+ USAGE: heighliner shutdown
13
+ EOS
14
+ end
15
+
16
+ def initialize
17
+ super
18
+ @use_steerfile = false
19
+ end
20
+
21
+ def execute(_opts)
22
+ Config.load(Dir.pwd, use_steerfile: false)
23
+
24
+ shared_names = Config.config[:shared_names] || {}
25
+ networkname = Config.config[:networkname] || 'heighliner_net'
26
+
27
+ shared_names.each_value do |container_name|
28
+ killrm container_name
29
+ end
30
+
31
+ CommandRunner.run Config.out, "docker network rm #{networkname}"
32
+ CommandRunner.run Config.out, "docker volume rm #{shared_names[:certs]}"
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Heighliner
4
+ module Cmds
5
+ class Up < Cli
6
+ option :attach,
7
+ 'Bind mount the current source code directory in the app container',
8
+ short: '-a'
9
+
10
+ def usage
11
+ <<~EOS
12
+ Boots up the application in docker as defined in the \`Steerfile\` in its source code. Usually this will create two docker containers \`<ENV_NAME>-db\` and \`<ENV_NAME>-app\` running your database and application respectively.
13
+
14
+ A backup of the default database is created and saved to \`~/.heighliner/<ENV_NAME>/<current_github_branch_name>/default.tar.bz\`. This can be restored at any time using the \`db_reset\` command.
15
+
16
+ USAGE: heighliner up
17
+ EOS
18
+ end
19
+
20
+ def execute(opts)
21
+ ensure_setup
22
+ setup_app
23
+ setup_db
24
+
25
+ if opts[:attach]
26
+ attach_app
27
+ else
28
+ start_app
29
+ end
30
+ end
31
+
32
+ def build_cmd
33
+ platform_args = ''
34
+ platform_args = "--platform=#{force_platform}" unless force_platform.empty?
35
+ build_args = docker_build_args.map { |k, v| "--build-arg #{k}=#{v}" }
36
+ [
37
+ 'docker build',
38
+ "-t heighliner:#{envname}-#{current_branch}",
39
+ "-f #{tmp_dockerfile_name} #{Config.work_dir}",
40
+ platform_args,
41
+ build_args.join(' ').to_s
42
+ ]
43
+ end
44
+
45
+ def setup_app
46
+ Config.info_out.puts 'Setting up application'
47
+ File.write(tmp_dockerfile_name, docker_file_contents)
48
+
49
+ CommandRunner.run! Config.out, build_cmd.join("\n\t"), env_vars: {
50
+ 'DOCKER_BUILDKIT' => '1',
51
+ 'BUILDKIT_PROGRESS' => 'plain'
52
+ }
53
+ FileUtils.rm(tmp_dockerfile_name)
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pty'
4
+ require 'English'
5
+
6
+ module Heighliner
7
+ # This is the command runner
8
+ # it abstracts away the complicated syntax required to deal with
9
+ # PTY and to pass the lines programmatically to the host application
10
+ # as well as to capture the return code at the end.
11
+ class CommandRunner
12
+ def self.run(out, cmd, env_vars: {}, &block)
13
+ out.puts "> #{cmd}"
14
+ CommandRunner.new(out, cmd, env_vars).run_command(&block)
15
+ end
16
+
17
+ def self.run!(out, cmd, env_vars: {}, &block)
18
+ status = run(out, cmd, env_vars: env_vars, &block)
19
+ raise Heighliner::CmdError.new(cmd, status) if status.to_s != '0'
20
+ end
21
+
22
+ def initialize(out, cmd, env_vars)
23
+ @out = out
24
+ @cmd = cmd.tr "\n", ' '
25
+ @env_vars = env_vars
26
+ end
27
+
28
+ def print_and_return_status(status = 0)
29
+ @out.puts "$? = #{status}"
30
+ @out.flush
31
+ status
32
+ end
33
+
34
+ def print_lines(lines)
35
+ lines.each do |line|
36
+ @out.print line
37
+ @out.flush
38
+ yield line.chomp if block_given?
39
+ end
40
+ rescue Errno::EIO
41
+ # Happens when `lines` stream is closed
42
+ end
43
+
44
+ def run_command(&block)
45
+ PTY.spawn(@env_vars, "#{@cmd} 2>&1") do |stdout, _stdin, pid|
46
+ print_lines(stdout, &block)
47
+ Process.wait(pid)
48
+ end
49
+ print_and_return_status $CHILD_STATUS.exitstatus
50
+ rescue PTY::ChildExited => e
51
+ print_and_return_status(e.status)
52
+ end
53
+ end
54
+ end