standard-procedure-anvil 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 826f4465c3c26af2077d355b53142e7755aa8897eceda3a030ef8dc024fa1a3a
4
- data.tar.gz: c2e780bd49774ce57fc357210b10f867bd8edb1e3694698a0886527ab72d5d38
3
+ metadata.gz: 3d027adf40008dfa46d97cee23b65a6a8468e935d11e56da74ee9f464b06ff68
4
+ data.tar.gz: 81aa2e0e4b54cc557338df3198a57bd69ff8da6e1cb53b26e9a437b8aafcc082
5
5
  SHA512:
6
- metadata.gz: 0d0fe31b85a435a86777fd01de0e0a4c79356dff5f831872a74f0eb552a32f03479a9f5a8a8a371b56aa646e3a2f71d3cdbbdc6d5f8c42479356403f7e163485
7
- data.tar.gz: f64d1bc0457cc69932acc4a8405206ee7e63c038e68655e2fb96ac10d31bb79a7dc221a5ea497c831cf701176a223bae828e8ce157635fcc12df579f8880f0e8
6
+ metadata.gz: 07fc89824892dbd33fc70dd33b3b8420fc8ac4c334a92c31e98a4b7f94a2f7071a9c5c58b02d9baf20ae3926d54a3fdf206c3cdfac29de48375b98a6847c4aee
7
+ data.tar.gz: 8b56e4d0f3c223b4225b587d218ac6694397aa528e69a5bef8e92ba3cb9148a89718cfb842e8076d0951ae60516269db55766dd653438694783801448ca394bc
@@ -3,20 +3,30 @@
3
3
  {
4
4
  "name": "Ruby",
5
5
  // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
6
- "image": "mcr.microsoft.com/devcontainers/ruby:0-3-bullseye"
7
-
6
+ "dockerFile": "../Dockerfile",
8
7
  // Features to add to the dev container. More info: https://containers.dev/features.
9
8
  // "features": {},
10
-
11
9
  // Use 'forwardPorts' to make a list of ports inside the container available locally.
12
10
  // "forwardPorts": [],
13
-
14
11
  // Use 'postCreateCommand' to run commands after the container is created.
15
- // "postCreateCommand": "ruby --version",
16
-
17
- // Configure tool-specific properties.
18
- // "customizations": {},
19
-
12
+ "postCreateCommand": "bundle install",
13
+ "customizations": {
14
+ "vscode": {
15
+ "extensions": [
16
+ "ms-azuretools.vscode-docker",
17
+ "donjayamanne.githistory",
18
+ "github.vscode-github-actions",
19
+ "GitHub.copilot-chat",
20
+ "GitHub.vscode-pull-request-github",
21
+ "aki77.rails-db-schema",
22
+ "rebornix.ruby",
23
+ "sibiraj-s.vscode-scss-formatter",
24
+ "testdouble.vscode-standard-ruby",
25
+ "bennycode.sort-everything",
26
+ "Thadeu.vscode-run-rspec-file",
27
+ ]
28
+ }
29
+ }
20
30
  // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
21
31
  // "remoteUser": "root"
22
32
  }
data/Dockerfile ADDED
@@ -0,0 +1,10 @@
1
+ FROM mcr.microsoft.com/devcontainers/ruby:0-3-bullseye
2
+
3
+ RUN gem update bundler
4
+ RUN gem install standardrb
5
+
6
+ WORKDIR /workspaces/standard-procedure-anvil
7
+
8
+ COPY . /workspaces/standard-procedure-anvil/
9
+ RUN bundle check || bundle install
10
+
@@ -0,0 +1,48 @@
1
+ #cloud-config
2
+ users:
3
+ - name: %{USER}
4
+ groups: users, admin, docker
5
+ sudo: ALL=(ALL) NOPASSWD:ALL
6
+ shell: /bin/bash
7
+ ssh_authorized_keys:
8
+ - %{PUBLIC_KEY}
9
+ packages:
10
+ - fail2ban
11
+ - ufw
12
+ - wget
13
+ - apt-transport-https
14
+ package_update: true
15
+ package_upgrade: true
16
+ runcmd:
17
+ # General server setup
18
+ - timedatectl set-timezone UTC
19
+ # Fail2Ban setup
20
+ - printf "[sshd]\nenabled = true\nbanaction = iptables-multiport" > /etc/fail2ban/jail.local
21
+ - systemctl enable fail2ban
22
+ # UFW and SSH setup
23
+ - ufw allow 22/tcp
24
+ - ufw allow 80/tcp
25
+ - ufw allow 443/tcp
26
+ - ufw enable
27
+ # Harden SSH
28
+ - sed -i -e '/^\(#\|\)PermitRootLogin/s/^.*$/PermitRootLogin no/' /etc/ssh/sshd_config
29
+ - sed -i -e '/^\(#\|\)PasswordAuthentication/s/^.*$/PasswordAuthentication no/' /etc/ssh/sshd_config
30
+ - sed -i -e '/^\(#\|\)X11Forwarding/s/^.*$/X11Forwarding no/' /etc/ssh/sshd_config
31
+ - sed -i -e '/^\(#\|\)MaxAuthTries/s/^.*$/MaxAuthTries 2/' /etc/ssh/sshd_config
32
+ - sed -i -e '/^\(#\|\)AllowTcpForwarding/s/^.*$/AllowTcpForwarding no/' /etc/ssh/sshd_config
33
+ - sed -i -e '/^\(#\|\)AllowAgentForwarding/s/^.*$/AllowAgentForwarding no/' /etc/ssh/sshd_config
34
+ - sed -i -e '/^\(#\|\)AuthorizedKeysFile/s/^.*$/AuthorizedKeysFile .ssh\/authorized_keys/' /etc/ssh/sshd_config
35
+ - sed -i '$a AllowUsers %{USER} dokku' /etc/ssh/sshd_config
36
+ # Dokku setup
37
+ - echo "dokku dokku/vhost_enable boolean true" | sudo debconf-set-selections
38
+ - wget https://dokku.com/install/v0.30.7/bootstrap.sh && sudo DOKKU_TAG=v0.30.7 bash bootstrap.sh
39
+ - cat /home/%{USER}/.ssh/authorized_keys | dokku ssh-keys:add admin
40
+ - dokku plugin:install https://github.com/dokku/dokku-cron-restart.git cron-restart
41
+ - dokku plugin:install https://github.com/dokku/dokku-maintenance.git maintenance
42
+ - dokku plugin:install https://github.com/dokku/dokku-redis.git redis
43
+ - dokku plugin:install https://github.com/dokku/dokku-mariadb.git mariadb
44
+ - dokku plugin:install https://github.com/dokku/dokku-memcached.git memcached
45
+ - dokku plugin:install https://github.com/dokku/dokku-letsencrypt.git letsencrypt
46
+ - dokku config:set --global DOKKU_LETSENCRYPT_EMAIL=sysadmin@echodek.co
47
+ - dokku git:set --global deploy-branch main
48
+ - reboot
@@ -0,0 +1,50 @@
1
+ #cloud-config
2
+ users:
3
+ - name: %{USER}
4
+ groups: users, admin, docker
5
+ sudo: ALL=(ALL) NOPASSWD:ALL
6
+ shell: /bin/bash
7
+ ssh_authorized_keys:
8
+ - %{PUBLIC_KEY}
9
+ packages:
10
+ - fail2ban
11
+ - ufw
12
+ - wget
13
+ - memcached
14
+ - logrotate
15
+ package_update: true
16
+ package_upgrade: true
17
+ runcmd:
18
+ # General server setup
19
+ - timedatectl set-timezone UTC
20
+ # Fail2Ban setup
21
+ - printf "[sshd]\nenabled = true\nbanaction = iptables-multiport" > /etc/fail2ban/jail.local
22
+ - systemctl enable fail2ban
23
+ # UFW and SSH setup
24
+ - ufw allow 22/tcp
25
+ - ufw allow 11211/tcp
26
+ - ufw enable
27
+ # Harden SSH
28
+ - sed -i -e '/^\(#\|\)PermitRootLogin/s/^.*$/PermitRootLogin no/' /etc/ssh/sshd_config
29
+ - sed -i -e '/^\(#\|\)PasswordAuthentication/s/^.*$/PasswordAuthentication no/' /etc/ssh/sshd_config
30
+ - sed -i -e '/^\(#\|\)X11Forwarding/s/^.*$/X11Forwarding no/' /etc/ssh/sshd_config
31
+ - sed -i -e '/^\(#\|\)MaxAuthTries/s/^.*$/MaxAuthTries 2/' /etc/ssh/sshd_config
32
+ - sed -i -e '/^\(#\|\)AllowTcpForwarding/s/^.*$/AllowTcpForwarding no/' /etc/ssh/sshd_config
33
+ - sed -i -e '/^\(#\|\)AllowAgentForwarding/s/^.*$/AllowAgentForwarding no/' /etc/ssh/sshd_config
34
+ - sed -i -e '/^\(#\|\)AuthorizedKeysFile/s/^.*$/AuthorizedKeysFile .ssh\/authorized_keys/' /etc/ssh/sshd_config
35
+ - sed -i '$a AllowUsers %{USER}' /etc/ssh/sshd_config
36
+ # Set up memcached
37
+ - sed -i 's/-m 64/-m 512/g' /etc/memcached.conf
38
+ - sed -i 's/-l 127.0.0.1/-l 0.0.0.0/g' /etc/memcached.conf
39
+ - systemctl restart memcached.service
40
+ - |
41
+ cat > /etc/logrotate.d/memcached << EOF
42
+ /var/log/redis/memcached*.log {
43
+ daily
44
+ missingok
45
+ rotate 7
46
+ compress
47
+ notifempty
48
+ }
49
+ EOF
50
+ - reboot
@@ -0,0 +1,57 @@
1
+ #cloud-config
2
+ users:
3
+ - name: %{USER}
4
+ groups: users, admin
5
+ sudo: ALL=(ALL) NOPASSWD:ALL
6
+ shell: /bin/bash
7
+ ssh_authorized_keys:
8
+ - %{PUBLIC_KEY}
9
+ packages:
10
+ - fail2ban
11
+ - mysql-client
12
+ - libmysqlclient-dev
13
+ - ufw
14
+ package_update: true
15
+ package_upgrade: true
16
+ runcmd:
17
+ # General server setup
18
+ - timedatectl set-timezone UTC
19
+ # Install MySQL
20
+ - echo "mysql-server mysql-server/root_password password root" | sudo debconf-set-selections
21
+ - echo "mysql-server mysql-server/root_password_again password root" | sudo debconf-set-selections
22
+ - sudo apt-get -y install mysql-server
23
+ - |
24
+ cat >> /etc/mysql/mysql.conf.d/utf8.cnf << CONF
25
+ [client]
26
+ default-character-set=utf8mb4
27
+
28
+ [mysql]
29
+ default-character-set=utf8mb4
30
+
31
+ [mysqld]
32
+ init_connect='SET collation_connection = utf8mb4_unicode_ci'
33
+ init_connect='SET NAMES utf8mb4'
34
+ character-set-server=utf8mb4
35
+ collation-server=utf8mb4_unicode_ci
36
+ skip-character-set-client-handshake
37
+ CONF
38
+ - sed -i -e '/^\(#\|\)bind-address/s/^.*$/bind-address = 0.0.0.0/' /etc/mysql/mysql.conf.d/mysqld.cnf
39
+ # Setup fail2ban
40
+ - printf "[sshd]\nenabled = true\nbanaction = iptables-multiport" > /etc/fail2ban/jail.local
41
+ - systemctl enable fail2ban
42
+ # Start MySQL
43
+ - systemctl start mysql.service
44
+ # Setup ufw
45
+ - ufw allow 22/tcp
46
+ - ufw allow 3306/tcp
47
+ - ufw enable
48
+ # Harden SSH
49
+ - sed -i -e '/^\(#\|\)PermitRootLogin/s/^.*$/PermitRootLogin no/' /etc/ssh/sshd_config
50
+ - sed -i -e '/^\(#\|\)PasswordAuthentication/s/^.*$/PasswordAuthentication no/' /etc/ssh/sshd_config
51
+ - sed -i -e '/^\(#\|\)X11Forwarding/s/^.*$/X11Forwarding no/' /etc/ssh/sshd_config
52
+ - sed -i -e '/^\(#\|\)MaxAuthTries/s/^.*$/MaxAuthTries 2/' /etc/ssh/sshd_config
53
+ - sed -i -e '/^\(#\|\)AllowTcpForwarding/s/^.*$/AllowTcpForwarding no/' /etc/ssh/sshd_config
54
+ - sed -i -e '/^\(#\|\)AllowAgentForwarding/s/^.*$/AllowAgentForwarding no/' /etc/ssh/sshd_config
55
+ - sed -i -e '/^\(#\|\)AuthorizedKeysFile/s/^.*$/AuthorizedKeysFile .ssh\/authorized_keys/' /etc/ssh/sshd_config
56
+ - sed -i '$a AllowUsers %{USER}' /etc/ssh/sshd_config
57
+ - reboot
@@ -0,0 +1,90 @@
1
+ #cloud-config
2
+ users:
3
+ - name: %{USER}
4
+ groups: users, admin, docker
5
+ sudo: ALL=(ALL) NOPASSWD:ALL
6
+ shell: /bin/bash
7
+ ssh_authorized_keys:
8
+ - %{PUBLIC_KEY}
9
+ packages:
10
+ - fail2ban
11
+ - ufw
12
+ - wget
13
+ - docker.io
14
+ - docker-compose
15
+ - apt-transport-https
16
+ package_update: true
17
+ package_upgrade: true
18
+ runcmd:
19
+ # General server setup
20
+ - timedatectl set-timezone UTC
21
+ # Prepare for OpenSearch
22
+ - swapoff -a
23
+ - echo "vm.max_map_count=262144" > /etc/sysctl.d/98-opensearch.conf
24
+ - sysctl -p /etc/sysctl.d/98-opensearch.conf
25
+ # Fail2Ban setup
26
+ - printf "[sshd]\nenabled = true\nbanaction = iptables-multiport" > /etc/fail2ban/jail.local
27
+ - systemctl enable fail2ban
28
+ # UFW and SSH setup
29
+ - ufw allow 22/tcp
30
+ - ufw allow 9200/tcp
31
+ - ufw allow 9600/tcp
32
+ - ufw enable
33
+ # Harden SSH
34
+ - sed -i -e '/^\(#\|\)PermitRootLogin/s/^.*$/PermitRootLogin no/' /etc/ssh/sshd_config
35
+ - sed -i -e '/^\(#\|\)PasswordAuthentication/s/^.*$/PasswordAuthentication no/' /etc/ssh/sshd_config
36
+ - sed -i -e '/^\(#\|\)X11Forwarding/s/^.*$/X11Forwarding no/' /etc/ssh/sshd_config
37
+ - sed -i -e '/^\(#\|\)MaxAuthTries/s/^.*$/MaxAuthTries 2/' /etc/ssh/sshd_config
38
+ - sed -i -e '/^\(#\|\)AllowTcpForwarding/s/^.*$/AllowTcpForwarding no/' /etc/ssh/sshd_config
39
+ - sed -i -e '/^\(#\|\)AllowAgentForwarding/s/^.*$/AllowAgentForwarding no/' /etc/ssh/sshd_config
40
+ - sed -i -e '/^\(#\|\)AuthorizedKeysFile/s/^.*$/AuthorizedKeysFile .ssh\/authorized_keys/' /etc/ssh/sshd_config
41
+ - sed -i '$a AllowUsers %{USER}' /etc/ssh/sshd_config
42
+ # OpenSearch setup
43
+ - mkdir -p /etc/opensearch
44
+ - docker pull opensearchproject/opensearch:latest
45
+ - docker pull opensearchproject/opensearch-dashboards:latest
46
+ - |
47
+ cat >> /etc/opensearch/docker-compose.yml << EOF
48
+ version: '3'
49
+ services:
50
+ search_db:
51
+ image: opensearchproject/opensearch:latest
52
+ container_name: search_db
53
+ environment:
54
+ - discovery.type=single-node
55
+ - node.name=search_db
56
+ - bootstrap.memory_lock=true
57
+ - plugins.security.disabled=true
58
+ - "OPENSEARCH_JAVA_OPTS=-Xms4096m -Xmx4096m"
59
+ ulimits:
60
+ memlock:
61
+ soft: -1
62
+ hard: -1
63
+ nofile:
64
+ soft: 65536
65
+ hard: 65536
66
+ volumes:
67
+ - opensearch_data:/usr/share/opensearch/data
68
+ ports:
69
+ - 9200:9200
70
+ - 9600:9600
71
+ volumes:
72
+ opensearch_data:
73
+ EOF
74
+ - |
75
+ cat >> /etc/systemd/system/opensearch.service << EOF
76
+ Description=OpenSearch container
77
+ Requires=docker.service
78
+ After=docker.service
79
+ [Service]
80
+ WorkingDirectory=/etc/opensearch
81
+ Restart=always
82
+ ExecStart=/usr/bin/docker-compose up
83
+ ExecStop=/usr/bin/docker-compose down
84
+ [Install]
85
+ WantedBy=multi-user.target
86
+ EOF
87
+ - systemctl daemon-reload
88
+ - systemctl enable opensearch.service
89
+ - service opensearch start
90
+ - reboot
@@ -0,0 +1,51 @@
1
+ #cloud-config
2
+ users:
3
+ - name: %{USER}
4
+ groups: users, admin, docker
5
+ sudo: ALL=(ALL) NOPASSWD:ALL
6
+ shell: /bin/bash
7
+ ssh_authorized_keys:
8
+ - %{PUBLIC_KEY}
9
+ packages:
10
+ - fail2ban
11
+ - ufw
12
+ - wget
13
+ - redis-server
14
+ - logrotate
15
+ package_update: true
16
+ package_upgrade: true
17
+ runcmd:
18
+ # General server setup
19
+ - timedatectl set-timezone UTC
20
+ # Fail2Ban setup
21
+ - printf "[sshd]\nenabled = true\nbanaction = iptables-multiport" > /etc/fail2ban/jail.local
22
+ - systemctl enable fail2ban
23
+ # UFW and SSH setup
24
+ - ufw allow 22/tcp
25
+ - ufw allow 6379/tcp
26
+ - ufw enable
27
+ # Harden SSH
28
+ - sed -i -e '/^\(#\|\)PermitRootLogin/s/^.*$/PermitRootLogin no/' /etc/ssh/sshd_config
29
+ - sed -i -e '/^\(#\|\)PasswordAuthentication/s/^.*$/PasswordAuthentication no/' /etc/ssh/sshd_config
30
+ - sed -i -e '/^\(#\|\)X11Forwarding/s/^.*$/X11Forwarding no/' /etc/ssh/sshd_config
31
+ - sed -i -e '/^\(#\|\)MaxAuthTries/s/^.*$/MaxAuthTries 2/' /etc/ssh/sshd_config
32
+ - sed -i -e '/^\(#\|\)AllowTcpForwarding/s/^.*$/AllowTcpForwarding no/' /etc/ssh/sshd_config
33
+ - sed -i -e '/^\(#\|\)AllowAgentForwarding/s/^.*$/AllowAgentForwarding no/' /etc/ssh/sshd_config
34
+ - sed -i -e '/^\(#\|\)AuthorizedKeysFile/s/^.*$/AuthorizedKeysFile .ssh\/authorized_keys/' /etc/ssh/sshd_config
35
+ - sed -i '$a AllowUsers %{USER}' /etc/ssh/sshd_config
36
+ # Set up Redis
37
+ - sed -i 's/supervised no/supervised systemd/g' /etc/redis/redis.conf
38
+ - sed -i 's/bind 127.0.0.1 ::1/# bind 127.0.0.1 ::1/g' /etc/redis/redis.conf
39
+ - sed -i 's/protected-mode yes/protected-mode no/g' /etc/redis/redis.conf
40
+ - systemctl restart redis.service
41
+ - |
42
+ cat > /etc/logrotate.d/redis-server << EOF
43
+ /var/log/redis/redis-server*.log {
44
+ daily
45
+ missingok
46
+ rotate 7
47
+ compress
48
+ notifempty
49
+ }
50
+ EOF
51
+ - reboot
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anvil
4
+ class App
5
+ require_relative "../configuration_reader"
6
+ class Env < Struct.new(:configuration, :host, :secrets)
7
+ include ConfigurationReader
8
+
9
+ def call
10
+ self.host ||= hosts.first
11
+ validate host
12
+ [env_vars_for(host), env_vars_for_app, secrets].compact.join(" ").strip
13
+ end
14
+
15
+ protected
16
+
17
+ def env_vars_for host
18
+ generate_from environment_for(host)
19
+ end
20
+
21
+ def env_vars_for_app
22
+ generate_from environment_for_app
23
+ end
24
+
25
+ def generate_from variables
26
+ variables&.join(" ")&.strip
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "standard_procedure/async"
4
+ module Anvil
5
+ require_relative "../logger"
6
+ require_relative "../ssh_executor"
7
+ require_relative "env"
8
+ class App
9
+ class HostInstaller < Struct.new(:configuration, :host, :secrets)
10
+ include StandardProcedure::Async::Actor
11
+
12
+ async :call do
13
+ Anvil::SshExecutor.new(host, user_for(host), logger).call do |ssh|
14
+ create_app ssh
15
+ set_environment ssh
16
+ set_dokku_options ssh
17
+ run_post_installation_scripts ssh
18
+ end
19
+ end
20
+
21
+ protected
22
+
23
+ def create_app ssh
24
+ ssh.exec! "dokku apps:create app", "create_app"
25
+ end
26
+
27
+ def set_environment ssh
28
+ ssh.exec! "dokku config:set app #{Anvil::App::Env.new(configuration, host, secrets).call}", "set_environment"
29
+ end
30
+
31
+ def set_dokku_options ssh
32
+ ssh.exec! "dokku docker-options:add app run \"--add-host=host.docker.internal:host-gateway\"", "set_dokku_options"
33
+ ssh.exec! "dokku domains:set app #{configuration_for_app["domain"]}", "set_dokku_options"
34
+ ssh.exec! "dokku proxy:ports-add app http:80:#{configuration_for_app["port"]}", "set_dokku_options"
35
+ ssh.exec! "dokku nginx:set app client-max-body-size 512m", "set_dokku_options"
36
+ ssh.exec! "dokku nginx:set app proxy-read-timeout 60s", "set_dokku_options"
37
+ ssh.exec! "dokku proxy:build-config app", "set_dokku_options"
38
+ end
39
+
40
+ def run_post_installation_scripts ssh
41
+ configuration_for_app.fetch("scripts")&.fetch("post_install")&.each do |script|
42
+ ssh.exec! script, "run_post_installation_scripts"
43
+ end
44
+ configuration_for(host).fetch("scripts")&.fetch("post_install")&.each do |script|
45
+ ssh.exec! script, "run_post_installation_scripts"
46
+ end
47
+ end
48
+
49
+ def logger
50
+ @logger ||= Anvil::Logger.new("HostInstaller - #{host}")
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anvil
4
+ class App
5
+ require_relative "host_installer"
6
+ require_relative "../configuration_reader"
7
+ class Install < Struct.new(:configuration, :secrets)
8
+ include ConfigurationReader
9
+ def call
10
+ hosts.each do |host|
11
+ HostInstaller.new(configuration, host, secrets).call
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
data/lib/anvil/app.rb ADDED
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "subcommand"
4
+ require "yaml"
5
+
6
+ module Anvil
7
+ class App < Anvil::SubCommandBase
8
+ require_relative "app/env"
9
+
10
+ desc "env", "Generate environment variables for an app"
11
+ long_desc <<-DESC
12
+ List the environment variables for an app (on a given host)
13
+
14
+ Example:
15
+ anvil app env /path/to/config
16
+
17
+ If the /path/to/config is not supplied, it defaults to deploy.yml
18
+
19
+ Options:
20
+
21
+ --host, -h: The server that the environment variables should be generated for - only required if multiple servers are configured
22
+
23
+ --secrets, -s: The path to a file containing secrets to be injected into the environment variables
24
+
25
+ --secrets-stdin, -S: Read secrets from STDIN instead of a file
26
+ DESC
27
+ option :host, type: :string, default: nil, aliases: "-h"
28
+ option :secrets, type: :string, default: nil, aliases: "-s"
29
+ option :secrets_stdin, type: :boolean, default: false, aliases: "-S"
30
+ def env filename = "deploy.yml"
31
+ configuration = YAML.load_file(filename)
32
+ secrets = read_secrets filename: options[:secrets], stdin: options[:secrets_stdin]
33
+ puts Anvil::App::Env.new(configuration, options[:host], secrets).call
34
+ end
35
+
36
+ desc "install", "Install an app"
37
+ long_desc <<-DESC
38
+ Install an app on the hosts specified in the configuration.
39
+
40
+ This logs in to each host in turn, using the user specified in the configuration file, it initialises the app, using dokku, then sets up the environment variables and other dokku options and finally runs any post-installation scripts.
41
+
42
+ In order to SSH in to the server correctly, it expects the private key to be available via your SSH agent. To test this, make sure you can `ssh user@host` without being prompted for a password.
43
+
44
+ Example:
45
+ anvil app install /path/to/config
46
+ If the /path/to/config is not supplied, it defaults to deploy.yml
47
+
48
+ Options:
49
+
50
+ --secrets, -s: The path to a file containing secrets to be injected into the environment variables
51
+
52
+ --secrets-stdin, -S: Read secrets from STDIN instead of a file
53
+ DESC
54
+ def install filename = "deploy.yml"
55
+ configuration = YAML.load_file(filename)
56
+ secrets = read_secrets filename: options[:secrets], stdin: options[:secrets_stdin]
57
+ Anvil::App::Install.new(configuration, secrets).call
58
+ end
59
+
60
+ protected
61
+
62
+ def read_secrets(filename: nil, stdin: false)
63
+ return nil if filename.nil? && !stdin
64
+ return $stdin.read if stdin
65
+ return File.read(filename) if File.exist?(filename)
66
+ end
67
+ end
68
+ end
data/lib/anvil/cli.rb CHANGED
@@ -1,56 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "thor"
4
- require_relative "installer"
5
- class Anvil::Cli < Thor
6
- desc "anvil install", "Perform an installation of dokku on a server"
7
- long_desc <<-DESC
8
- Perform an installation of dokku on a server in preparation for deploying apps.
4
+ require_relative "cloudinit"
5
+ require_relative "app"
9
6
 
10
- Example:
11
- `anvil install CONFIG`
7
+ module Anvil
8
+ class Cli < Thor
9
+ desc "cloudinit", "Generate a cloudinit configuration"
10
+ subcommand "cloudinit", Anvil::Cloudinit
12
11
 
13
- The default CONFIG file is `anvil.yml` in the current directory.
12
+ desc "app", "Install or deploy a dokku app"
13
+ subcommand "app", Anvil::App
14
14
 
15
- Options:
16
- --private_key, -k: The path to the key certificate file to use when connecting to the server.
17
- --passphrase, -p: The passphrase to use when connecting to the server.
18
- DESC
19
- option :private_key, type: :string, default: nil, aliases: "-k"
20
- option :passphrase, type: :string, default: nil, aliases: "-p"
21
- def install config = "anvil.yml", private_key = nil, passphrase = nil
22
- Anvil::Installer.new(configuration_from(config), private_key, passphrase).call
23
- end
24
-
25
- desc "anvil deploy", "Deploy the current app"
26
- long_desc <<-DESC
27
- Deploy the current app to the server to the servers
28
-
29
- Example:
30
- `anvil deploy CONFIG`
31
-
32
- The default CONFIG file is `anvil.yml` in the current directory.
33
- Options:
34
- --private_key, -k: The path to the key certificate file to use when connecting to the server.
35
- --passphrase, -p: The passphrase to use when connecting to the server.
36
- DESC
37
- option :private_key, type: :string, default: nil, aliases: "-k"
38
- option :passphrase, type: :string, default: nil, aliases: "-p"
39
- def deploy config = "anvil.yml", private_key = nil, passphrase = nil
40
- end
41
-
42
- desc "anvil version", "Print the version of the anvil gem"
43
- def version
44
- puts Anvil::VERSION
45
- end
15
+ desc "version", "Print the version of the anvil gem"
16
+ def version
17
+ puts Anvil::VERSION
18
+ end
46
19
 
47
- def self.exit_on_failure?
48
- true
49
- end
20
+ def self.exit_on_failure?
21
+ true
22
+ end
50
23
 
51
- protected
24
+ protected
52
25
 
53
- def configuration_from(file_name)
54
- @configuration ||= YAML.load_file(file_name)
26
+ def configuration_from(file_name)
27
+ @configuration ||= YAML.load_file(file_name)
28
+ end
55
29
  end
56
30
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anvil
4
+ class Cloudinit
5
+ class Generator < Struct.new(:filename, :user, :public_key_path)
6
+ def call
7
+ public_key = public_key_path.to_s.gsub("~", Dir.home)
8
+
9
+ if File.exist?(public_key)
10
+ puts File.read(filename).gsub("%{USER}", user).gsub("%{PUBLIC_KEY}", File.read(public_key))
11
+ else
12
+ puts "Cannot find public key file at #{public_key}"
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ # Compare this snippet from assets/cloudinit/mysql.ubuntu-22.yml:
20
+ # # frozen_string_literal: true
21
+ #
22
+ # ---
23
+ # packages:
24
+ # - mysql-server
25
+ # - mysql-client
26
+ # - libmysqlclient-dev
27
+ #
28
+ # users:
29
+ # - name: <%= user %>
30
+ # groups: sudo
31
+ # shell: /bin/bash
32
+ # sudo: ALL=(ALL) NOPASSWD:ALL
33
+ # ssh_authorized_keys:
34
+ # - <%= public_key %>
35
+ #
36
+ # files:
37
+ # - path: /etc/mysql/mysql.conf.d/mysqld.cnf
38
+ # content: |
39
+ # [mysqld]
40
+ # bind-address =
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "subcommand"
4
+
5
+ module Anvil
6
+ class Cloudinit < Anvil::SubCommandBase
7
+ require_relative "cloudinit/generator"
8
+
9
+ desc "list", "List cloudinit generators"
10
+ def list
11
+ Dir[File.dirname(__FILE__) + "/../../assets/cloudinit/*.yml"].each do |filename|
12
+ puts File.basename(filename.to_s, ".yml")
13
+ end
14
+ end
15
+
16
+ desc "generate", "Generate a cloudinit configuration"
17
+ long_desc <<-DESC
18
+ Generate a cloudinit configuration for a server
19
+
20
+ Example:
21
+ anvil cloudinit generate mysql.ubuntu-22 --user dbuser --public_key ~/.ssh/my_key.pub
22
+
23
+ Options:
24
+ --user, -u: The user to create on the server - defaults to app
25
+ --public_key, -k: The path to the public key file that will be installed for the user - default to ~/.ssh/id_rsa.pub
26
+ DESC
27
+ option :user, type: :string, default: "app", aliases: "-u"
28
+ option :public_key, type: :string, default: "~/.ssh/id_rsa.pub", aliases: "-k"
29
+ def generate configuration
30
+ filename = File.dirname(__FILE__) + "/../../assets/cloudinit/#{configuration}.yml"
31
+ Anvil::Cloudinit::Generator.new(filename, options[:user], options[:public_key]).call
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anvil
4
+ # A set of utility methods for reading configuration data
5
+ # It expects the implemneting class to have a `configuration` method
6
+ module ConfigurationReader
7
+ def hosts
8
+ configuration["hosts"].collect { |host_data| host_data.keys }.flatten
9
+ end
10
+
11
+ def validate hostname
12
+ raise ArgumentError.new("Host #{hostname} is not in the configuration hosts list") unless hosts.include? hostname
13
+ end
14
+
15
+ def configuration_for_app
16
+ configuration["app"]
17
+ end
18
+
19
+ def environment_for_app
20
+ configuration_for_app.fetch "environment", []
21
+ end
22
+
23
+ def configuration_for hostname
24
+ host_config = configuration["hosts"].find { |host_data| host_data.key?(hostname) ? host_data[hostname] : nil }
25
+ host_config&.fetch(hostname)
26
+ end
27
+
28
+ def environment_for hostname
29
+ configuration_for(hostname)&.fetch "environment", []
30
+ end
31
+
32
+ def user_for hostname
33
+ configuration_for(hostname)&.fetch "user", nil
34
+ end
35
+ end
36
+ end
@@ -7,31 +7,19 @@ require "net/ssh"
7
7
  # - without sudo it runs the scripts as supplied
8
8
  # - with sudo it creates a script on the remote server, runs it via sudo, and then deletes it
9
9
  # If supplied, it will also write the output of the script to a logger.
10
- class Anvil::SshExecutor < Struct.new(:hostname, :user, :use_sudo, :logger)
11
- def call &block
12
- @connection = Net::SSH.start hostname, user, use_agent: true
13
- block.call self
14
- end
10
+ module Anvil
11
+ class SshExecutor < Struct.new(:hostname, :user, :logger)
12
+ def call &block
13
+ @connection = Net::SSH.start hostname, user, use_agent: true
14
+ block.call self
15
+ end
15
16
 
16
- def exec! script, category = ""
17
- method = use_sudo ? :exec_with_sudo : :exec_without_sudo
18
- send(method, script, category) do |channel, stream, data|
19
- data.to_s.split("\n") do |line|
20
- logger&.info line, category
17
+ def exec! script, category = ""
18
+ @connection.exec! script do |channel, stream, data|
19
+ data.to_s.split("\n") do |line|
20
+ logger&.info line, category
21
+ end
21
22
  end
22
23
  end
23
24
  end
24
-
25
- protected
26
-
27
- def exec_without_sudo script, category = "", &block
28
- @connection.exec! script, &block
29
- end
30
-
31
- def exec_with_sudo script, category = "", &block
32
- @connection.exec! "cat >> exec.sh << SCRIPT\n#{script}\nSCRIPT", &block
33
- @connection.exec! "chmod 755 exec.sh", &block
34
- @connection.exec! "sudo ./exec.sh", &block
35
- @connection.exec! "rm exec.sh", &block
36
- end
37
25
  end
@@ -0,0 +1,13 @@
1
+ require "thor"
2
+
3
+ module Anvil
4
+ class SubCommandBase < Thor
5
+ def self.banner(command, namespace = nil, subcommand = false)
6
+ "#{basename} #{subcommand_prefix} #{command.usage}"
7
+ end
8
+
9
+ def self.subcommand_prefix
10
+ name.gsub(%r{.*::}, "").gsub(%r{^[A-Z]}) { |match| match[0].downcase }.gsub(%r{[A-Z]}) { |match| "-#{match[0].downcase}" }
11
+ end
12
+ end
13
+ end
data/lib/anvil/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Anvil
4
- VERSION = "0.1.2"
4
+ VERSION = "0.1.3"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: standard-procedure-anvil
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rahoul Baruah
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-06-21 00:00:00.000000000 Z
11
+ date: 2023-06-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: standard-procedure-async
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
69
83
  description: Tools for managing servers and apps built using dokku
70
84
  email:
71
85
  - rahoulb@standardprocedure.app
@@ -77,25 +91,28 @@ files:
77
91
  - ".rspec"
78
92
  - ".standard.yml"
79
93
  - CHANGELOG.md
94
+ - Dockerfile
80
95
  - LICENSE.txt
81
96
  - README.md
82
97
  - Rakefile
98
+ - assets/cloudinit/dokku.ubuntu-22.yml
99
+ - assets/cloudinit/memcached.ubuntu-22.yml
100
+ - assets/cloudinit/mysql.ubuntu-22.yml
101
+ - assets/cloudinit/opensearch.ubuntu-22.yml
102
+ - assets/cloudinit/redis.ubuntu-22.yml
83
103
  - exe/anvil
84
104
  - lib/anvil.rb
105
+ - lib/anvil/app.rb
106
+ - lib/anvil/app/env.rb
107
+ - lib/anvil/app/host_installer.rb
108
+ - lib/anvil/app/install.rb
85
109
  - lib/anvil/cli.rb
86
- - lib/anvil/installer.rb
110
+ - lib/anvil/cloudinit.rb
111
+ - lib/anvil/cloudinit/generator.rb
112
+ - lib/anvil/configuration_reader.rb
87
113
  - lib/anvil/logger.rb
88
- - lib/anvil/server_installer.rb
89
- - lib/anvil/server_installer/configure_docker.rb
90
- - lib/anvil/server_installer/configure_dokku.rb
91
- - lib/anvil/server_installer/configure_firewall.rb
92
- - lib/anvil/server_installer/configure_ssh_server.rb
93
- - lib/anvil/server_installer/create_user.rb
94
- - lib/anvil/server_installer/install_packages.rb
95
- - lib/anvil/server_installer/install_plugins.rb
96
- - lib/anvil/server_installer/set_hostname.rb
97
- - lib/anvil/server_installer/set_timezone.rb
98
114
  - lib/anvil/ssh_executor.rb
115
+ - lib/anvil/subcommand.rb
99
116
  - lib/anvil/version.rb
100
117
  - sig/standard/procedure/anvil.rbs
101
118
  homepage: https://github.com/standard-procedure/anvil
@@ -1,17 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "yaml"
4
- require_relative "server_installer"
5
-
6
- # The Installer reads the configuration and runs the ServerInstaller for each host.
7
- class Anvil::Installer < Struct.new(:configuration, :private_key, :passphrase)
8
- def call
9
- hosts.each do |host|
10
- Anvil::ServerInstaller.new(host, configuration, private_key, passphrase).call
11
- end
12
- end
13
-
14
- def hosts
15
- configuration["hosts"]
16
- end
17
- end
@@ -1,10 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Anvil::ServerInstaller::ConfigureDocker < Struct.new(:ssh_connection)
4
- def call
5
- script = <<~SCRIPT
6
- echo "15 0 3 * * /usr/bin/docker system prune -f" | crontab
7
- SCRIPT
8
- ssh_connection.exec! script, "ConfigureDocker"
9
- end
10
- end
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Anvil::ServerInstaller::ConfigureDokku < Struct.new(:ssh_connection, :hostname)
4
- def call
5
- script = <<~SCRIPT
6
- dokku domains:set-global #{hostname}
7
- dokku git:set --global deploy-branch main
8
- SCRIPT
9
- ssh_connection.exec! script, "ConfigureDokku"
10
- end
11
- end
@@ -1,10 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Anvil::ServerInstaller::ConfigureFirewall < Struct.new(:ssh_connection, :ports)
4
- def call
5
- ports.collect do |port|
6
- ssh_connection.exec! "ufw allow #{port}", "ConfigureFirewall"
7
- end
8
- ssh_connection.exec! "ufw --force enable", "ConfigureFirewall"
9
- end
10
- end
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Anvil::ServerInstaller::ConfigureSshServer < Struct.new(:ssh_connection)
4
- def call
5
- script = <<-SCRIPT
6
- sed -i 's/PermitRootLogin yes/PermitRootLogin no/g' /etc/ssh/sshd_config
7
- sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin no/g' /etc/ssh/sshd_config
8
- service sshd restart
9
- SCRIPT
10
- ssh_connection.exec! script, "ConfigureSshServer"
11
- end
12
- end
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Anvil::ServerInstaller::CreateUser < Struct.new(:ssh_connection, :user_name)
4
- def call
5
- script = <<~SCRIPT
6
- if id -u #{user_name} >/dev/null 2>&1; then
7
- echo "#{user_name} already exists"
8
- else
9
- echo "Adding #{user_name}"
10
- adduser --disabled-password --gecos "" #{user_name}
11
- usermod -aG sudo #{user_name}
12
- usermod -aG docker #{user_name}
13
- echo "#{user_name} ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
14
- fi
15
- SCRIPT
16
- ssh_connection.exec! script, "CreateUser"
17
- end
18
- end
@@ -1,40 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Anvil::ServerInstaller::InstallPackages < Struct.new(:ssh_connection, :public_key_file)
4
- def call
5
- public_key = File.read public_key_file
6
- script = <<~SCRIPT
7
- mkdir -p /root/.ssh
8
- echo "#{public_key}" > /root/.ssh/id_rsa.pub
9
- mkdir -p /etc/skel/.ssh
10
- cp /root/.ssh/id_rsa.pub /etc/skel/.ssh/authorized_keys
11
-
12
- echo "Installing packages"
13
- apt-get update -qq >/dev/null
14
- apt-get -qq -y --no-install-recommends install apt-transport-https
15
-
16
- if command -v "$@" > /dev/null 2>&1; then
17
- echo "Docker is already installed"
18
- else
19
- echo "Installing docker"
20
- wget -nv -O - https://get.docker.com/ | sh
21
-
22
- wget -qO- https://packagecloud.io/dokku/dokku/gpgkey | tee /etc/apt/trusted.gpg.d/dokku.asc
23
- DISTRO="$(awk -F= '$1=="ID" { print tolower($2) ;}' /etc/os-release)"
24
- OS_ID="$(awk -F= '$1=="VERSION_CODENAME" { print tolower($2) ;}' /etc/os-release)"
25
- echo "deb https://packagecloud.io/dokku/dokku/${DISTRO}/ ${OS_ID} main" | tee /etc/apt/sources.list.d/dokku.list
26
- fi
27
-
28
- echo "Installing dokku"
29
- apt-get update -qq >/dev/null
30
- apt-get -qq -y install dokku
31
-
32
- echo "Installing dependencies"
33
- dokku plugin:install-dependencies --core
34
-
35
- cat /root/.ssh/id_rsa.pub | dokku ssh-keys:add admin
36
- SCRIPT
37
-
38
- ssh_connection.exec! script, "InstallPackages"
39
- end
40
- end
@@ -1,14 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Anvil::ServerInstaller::InstallPlugins < Struct.new(:ssh_connection, :plugins)
4
- def call
5
- plugins.each do |name, config|
6
- scripts = ["dokku plugin:install #{config["url"]} #{name}"]
7
- plugin_config = config["config"] || []
8
- scripts += plugin_config.collect do |cmd|
9
- "dokku #{name}:#{cmd}"
10
- end
11
- ssh_connection.exec! scripts.join("\n"), "InstallPlugins"
12
- end
13
- end
14
- end
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Anvil::ServerInstaller::SetHostname < Struct.new(:ssh_connection, :hostname)
4
- def call
5
- script = <<-SCRIPT
6
- hostnamectl set-hostname #{hostname}
7
- mkdir -p /etc/environment.d
8
- echo "HOSTNAME=#{hostname}" > /etc/environment.d/99-hostname
9
- SCRIPT
10
- ssh_connection.exec! script, "SetHostname"
11
- end
12
- end
@@ -1,10 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Anvil::ServerInstaller::SetTimezone < Struct.new(:ssh_connection, :timezone)
4
- def call
5
- script = <<-SCRIPT
6
- timedatectl set-timezone #{timezone}
7
- SCRIPT
8
- ssh_connection.exec! script, "SetTimezone"
9
- end
10
- end
@@ -1,66 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "ssh_executor"
4
- require_relative "logger"
5
-
6
- # The server installer uses Net::SSH to connect to the server and then run the following steps:
7
- # - Sets the server hostname and timezone
8
- # - Installs various necessary packages, plus dokku itself
9
- # - Sets up the firewall
10
- # - Creates unix users for each app, adding them to the sudo and docker groups, and setting their authorized_keys files with the given public key
11
- # - Sets the dokku deployment branch to `main`
12
- # - Schedule a `docker system prune` once per week to clean up any dangling images or containers
13
- # - Configure nginx
14
- # - Install dokku plugins and run any configuration you have defined
15
- # - Disallows root and passwordless logins over SSH
16
- # You can specify a custom logger or SSH executor using the options hash.
17
- class Anvil::ServerInstaller < Struct.new(:hostname, :configuration, :private_key, :passphrase, :options)
18
- require_relative "server_installer/set_hostname"
19
- require_relative "server_installer/set_timezone"
20
- require_relative "server_installer/install_packages"
21
- require_relative "server_installer/create_user"
22
- require_relative "server_installer/configure_dokku"
23
- require_relative "server_installer/configure_docker"
24
- require_relative "server_installer/install_plugins"
25
- require_relative "server_installer/configure_firewall"
26
- require_relative "server_installer/configure_ssh_server"
27
-
28
- def call
29
- ssh_executor.call do |ssh_connection|
30
- logger.info "SetHostname"
31
- Anvil::ServerInstaller::SetHostname.new(ssh_connection, hostname).call
32
- logger.info "SetTimezone"
33
- Anvil::ServerInstaller::SetTimezone.new(ssh_connection, server_configuration["timezone"]).call
34
- logger.info "InstallPackages"
35
- Anvil::ServerInstaller::InstallPackages.new(ssh_connection, server_configuration["public_key"]).call
36
- logger.info "ConfigureDokku"
37
- Anvil::ServerInstaller::ConfigureDokku.new(ssh_connection, hostname).call
38
- logger.info "CreateUsers"
39
- Anvil::ServerInstaller::CreateUser.new(ssh_connection, server_configuration["app_user"]).call
40
- logger.info "InstallPlugins"
41
- Anvil::ServerInstaller::InstallPlugins.new(ssh_connection, server_configuration["plugins"]).call
42
- logger.info "ConfigureDocker"
43
- Anvil::ServerInstaller::ConfigureDocker.new(ssh_connection).call
44
- logger.info "ConfigureFirewall"
45
- Anvil::ServerInstaller::ConfigureFirewall.new(ssh_connection, server_configuration["ports"]).call
46
- logger.info "ConfigureSshServer"
47
- Anvil::ServerInstaller::ConfigureSshServer.new(ssh_connection).call
48
- end
49
- end
50
-
51
- def server_configuration
52
- configuration["server"]
53
- end
54
-
55
- def options
56
- super || {}
57
- end
58
-
59
- def logger
60
- options[:logger].nil? ? Anvil::Logger.new(hostname) : options[:logger]
61
- end
62
-
63
- def ssh_executor
64
- options[:ssh_executor].nil? ? Anvil::SshExecutor.new(hostname, server_configuration["install_user"], server_configuration["use_sudo"], logger) : options[:ssh_executor]
65
- end
66
- end