renuo-cli 4.22.0 → 4.23.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 13ccfc20af1ba9cda74f9ffb3a9b8f24cbbf34808a4d94e5baab28f6da384d26
4
- data.tar.gz: ac1d41b13a258f07268bb78ed0f6b697eaf4abd2ffe4ae324b4c41880bb253df
3
+ metadata.gz: d49d5760372b8bb7bd5ada89efeb64580a32f62997a701597a48a5ce7fda72ed
4
+ data.tar.gz: 384d94a32debe72aab7d4d68b0c3b4241ee4260e74caeaae63751daf8b044fba
5
5
  SHA512:
6
- metadata.gz: 72fa17d236d416cb18c8f99d1e463ee8e90c6740102348df812090269c996337fad30a48c953f94ad31fc710f835d311dbf45a8190eb9e982398f7d404d88358
7
- data.tar.gz: d0cf0de0355ae2b5bef63e1209b74fd818a362072054386106c495035df5c67053b3e1e18a79eecb480812dfe801acc1974b9f8e3d97dc9e198231c42db0ad0d
6
+ metadata.gz: e9a012730f76d8428624dbe15a56f78bab6b9c1f0109d30b1eebc0cd31434ebb1657d1704c4a5d887d6d4b8c1ec5157c1eb2dc0ab54f426a3a2f2d72317b90aa
7
+ data.tar.gz: cdf2265362bedb6f2172ca1058ccfdcf06f5823ea244a26aff46d1bf9bb68a537828cc4f571780eafdd41daeec3ae00e82b4d7f77869ed472233edaa5e30a9e8
data/README.md CHANGED
@@ -30,7 +30,8 @@ After checking out the repo, run `bin/setup` to install dependencies.
30
30
 
31
31
  Run `rake spec` to run the tests.
32
32
 
33
- Run `bundle exec ruby -Ilib ./bin/renuo` to run the executable. (e.g. `ruby -Ilib ./bin/renuo -v`)
33
+ Run `bin/renuo` to run the executable against the working tree. (e.g. `bin/renuo -v`)
34
+ The shipped executable lives in `exe/renuo`; `bin/renuo` is a bundler binstub that loads it with the local code.
34
35
 
35
36
  You can also run `bin/console` for an interactive prompt that will allow you to experiment.
36
37
 
@@ -46,7 +47,7 @@ This binds the `renuo` command to your local version, so you can run commands di
46
47
 
47
48
  ```shell
48
49
  renuo -h
49
- renuo generate_github_settings
50
+ renuo generate-password
50
51
  ```
51
52
 
52
53
  To install this gem onto your local machine, run `bundle exec rake install`.
data/exe/renuo ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "renuo/cli"
5
+ Renuo::Cli.new.start
@@ -2,9 +2,10 @@
2
2
 
3
3
  # :nocov:
4
4
  module Renuo::Cli::Commands::Ci
5
- class CheckDeploioStatus
5
+ class CheckDeploioStatus # rubocop:disable Metrics/ClassLength
6
6
  TIMEOUT_IN_SECONDS = 600
7
7
  INTERVAL_IN_SECONDS = 5
8
+ LOG_DRAIN_SECONDS = 2
8
9
 
9
10
  command "ci check-deploio-status" do |c|
10
11
  c.syntax = "renuo ci check-deploio-status [options] <app-name>"
@@ -17,11 +18,11 @@ module Renuo::Cli::Commands::Ci
17
18
 
18
19
  def run
19
20
  puts "(1/2) Checking build status for revision #{@revision}..."
20
- poll "build"
21
+ with_streamed_logs("build") { poll "build" }
21
22
  abort "build check timed out after #{TIMEOUT_IN_SECONDS} seconds" if build.nil?
22
23
 
23
24
  puts "(2/2) Checking release status for build #{build_name}..."
24
- poll "release"
25
+ with_streamed_logs("app") { poll "release" }
25
26
  abort "release check timed out after #{TIMEOUT_IN_SECONDS} seconds" if release.nil?
26
27
  end
27
28
 
@@ -49,12 +50,24 @@ module Renuo::Cli::Commands::Ci
49
50
  Psych.load_stream stdout
50
51
  end
51
52
 
52
- def fetch_build_logs
53
- command = "nctl logs build -a #{@app} -p #{@project} -l 5000 --no-labels"
54
- stdout, stderr, status = Open3.capture3 command
53
+ def with_streamed_logs(type)
54
+ pid = Process.spawn(
55
+ "nctl logs application #{@app} -p #{@project} --type=#{type} -f --lines=100 --since 15m --no-labels",
56
+ out: $stdout, err: $stdout
57
+ )
58
+ yield
59
+ ensure
60
+ sleep LOG_DRAIN_SECONDS
61
+ stop_streaming(pid)
62
+ end
63
+
64
+ def stop_streaming(pid)
65
+ return unless pid
55
66
 
56
- puts "error fetching build logs: #{stderr}" unless status.success?
57
- stdout
67
+ Process.kill("KILL", pid)
68
+ Process.wait(pid)
69
+ rescue Errno::ESRCH, Errno::ECHILD
70
+ nil
58
71
  end
59
72
 
60
73
  def build
@@ -82,7 +95,7 @@ module Renuo::Cli::Commands::Ci
82
95
  end
83
96
 
84
97
  def succeeded?(status, type) # rubocop:disable Metrics/MethodLength
85
- time = "[#{Time.now.utc.iso8601}]"
98
+ time = Time.current.iso8601.to_s
86
99
 
87
100
  case status
88
101
  when "available", "success"
@@ -94,7 +107,6 @@ module Renuo::Cli::Commands::Ci
94
107
  when "paused"
95
108
  abort "#{time} app is paused"
96
109
  when "error", "failed", "failure"
97
- puts fetch_build_logs
98
110
  abort "#{time} #{type} failed"
99
111
  else
100
112
  puts "#{time} #{type} status is #{status}, waiting..."
@@ -15,7 +15,7 @@ module Renuo::Cli::Commands::Ci
15
15
  end
16
16
 
17
17
  def run
18
- system! "nctl update app #{@app} -p #{@project} --env APP_REVISION=#{@git_revision} " \
18
+ system! "nctl update app #{@app} -p #{@project} " \
19
19
  "#{"--build-env RUBY_VERSION=#{File.read(".ruby-version").strip} " if @docker}" \
20
20
  "--git-revision #{@git_revision} --skip-repo-access-check"
21
21
  end
@@ -96,11 +96,11 @@ class Renuo::Cli::Commands::CreateDeploioApp # rubocop:disable Metrics/ClassLeng
96
96
 
97
97
  def set_postgres_version
98
98
  loop do
99
- @postgres_version = ask("Enter Postgres version (e.g., 17) or leave empty to skip database creation: ")
99
+ @postgres_version = ask("Enter Postgres version (e.g., 18) or leave empty to skip database creation: ")
100
100
  break if @postgres_version.empty? || @postgres_version.match?(/^\d+$/)
101
101
 
102
102
  say ">> The postgres version is invalid. Only major versions are allowed. " \
103
- "For example, use 17 instead of 17.4. Please try again.".colorize(:red)
103
+ "For example, use 18 instead of 18.4. Please try again.".colorize(:red)
104
104
  end
105
105
  end
106
106
 
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../helpers/environments"
4
+
5
+ class Renuo::Cli::Commands::CreateNineCloudvm
6
+ SSH_KEY_FILE_NAME = "temporary_cloudvm_ssh_key"
7
+ SSH_ALGORITHM = "Ed25519"
8
+ DEFAULT_LOCATION = "nine-es34"
9
+ DEFAULT_MACHINE_TYPE = "nine-small-1"
10
+ DEFAULT_OS = "debian13"
11
+ VPS_SETUP_GUIDE_REPO = "https://github.com/renuo/vps-setup-guide"
12
+
13
+ command "create-nine-cloudvm" do |c|
14
+ c.syntax = "renuo create-nine-cloudvm [project-name]"
15
+ c.summary = "Generates the script necessary to setup a cloud VM on Nine."
16
+ c.description = <<~DESC
17
+ Generates the script necessary to setup a cloud VM on Nine.
18
+ This command will:
19
+ - Prompt for the organization, project name, location, machine type, OS, and 1Password vault name.
20
+ - Validate the provided inputs.
21
+ - Generate an SSH key (stored in 1Password) to access the cloud VM.
22
+ - Generate the commands to create a cloud VM for each environment on Nine.
23
+ DESC
24
+ c.option "-o", "--org <organization>", String, "The organization name"
25
+ c.example "renuo create-nine-cloudvm my-app",
26
+ "Prompts the user for the necessary information and generates the commands " \
27
+ "to create the cloud VMs on Nine."
28
+ c.action { |args, options| new.run(args, options) }
29
+ end
30
+
31
+ def run(args, options = {})
32
+ @project_name, = args
33
+ @options = options
34
+ parse_arguments
35
+ setup_ssh_key
36
+ setup_environments
37
+ say_configuration_note
38
+ ensure
39
+ cleanup
40
+ end
41
+
42
+ private
43
+
44
+ def valid_name?(name)
45
+ name&.length&.between?(3, 63) && name.match?(/^[a-z0-9-]+$/)
46
+ end
47
+
48
+ def parse_arguments
49
+ set_organization
50
+ set_project_name
51
+ set_location
52
+ set_machine_type
53
+ set_os
54
+ set_vault_name
55
+ end
56
+
57
+ def set_project_name
58
+ loop do
59
+ break if valid_name?(@project_name)
60
+
61
+ say ">> Project name must be between 3 and 63 characters and can only contain lowercase letters, " \
62
+ "numbers, and hyphens. Please try again.".colorize(:red)
63
+
64
+ @project_name = ask("Enter the project name (e.g: my-app): ")
65
+ end
66
+
67
+ @project_name_with_org_prefix = "#{@organization}-#{@project_name}"
68
+ end
69
+
70
+ def set_organization
71
+ @organization = @options.org if @options.org
72
+
73
+ loop do
74
+ break if valid_name?(@organization)
75
+
76
+ say ">> Organization name must be between 3 and 63 characters and can only contain lowercase letters, " \
77
+ "numbers, and hyphens. Please try again.".colorize(:red)
78
+
79
+ @organization = ask("Enter the organization name (default: renuo): ") { |q| q.default = "renuo" }
80
+ end
81
+ end
82
+
83
+ def set_location
84
+ @location = ask("Enter the location (default: #{DEFAULT_LOCATION}): ") { |q| q.default = DEFAULT_LOCATION }
85
+ end
86
+
87
+ def set_machine_type
88
+ @machine_type = ask("Enter the machine type (default: #{DEFAULT_MACHINE_TYPE}): ") do |q|
89
+ q.default = DEFAULT_MACHINE_TYPE
90
+ end
91
+ end
92
+
93
+ def set_os
94
+ @os = ask("Enter the operating system (default: #{DEFAULT_OS}): ") { |q| q.default = DEFAULT_OS }
95
+ end
96
+
97
+ def set_vault_name
98
+ @vault_name = ask("An SSH key to access the cloud VM will be stored on 1Password. " \
99
+ "Enter the 1Password vault name: ") do |q|
100
+ q.default = "Deploio"
101
+ end
102
+ end
103
+
104
+ def setup_ssh_key
105
+ say "\n# Generate the SSH key to access the cloud VM\n".bold
106
+ generate_ssh_key(SSH_KEY_FILE_NAME)
107
+ end
108
+
109
+ def setup_environments
110
+ ENVIRONMENTS.each do |environment|
111
+ say "\n# Commands to create #{environment} cloud VM \n".bold
112
+ say_cloudvm_creation(environment)
113
+ end
114
+ end
115
+
116
+ def say_configuration_note
117
+ say "\n# Configure the cloud VMs\n".bold
118
+ say <<~OUTPUT
119
+ Once the cloud VMs are up, configure them (SSH access, monitoring, backups) with the
120
+ Ansible VPS setup guide: #{VPS_SETUP_GUIDE_REPO}
121
+ OUTPUT
122
+ end
123
+
124
+ def cleanup
125
+ say "\n# Cleanup temporary files\n".bold
126
+ say "rm #{SSH_KEY_FILE_NAME} #{SSH_KEY_FILE_NAME}.pub"
127
+ end
128
+
129
+ def say_cloudvm_creation(environment)
130
+ say <<~OUTPUT
131
+ nctl create cloudvm #{environment} \\
132
+ --project #{@project_name_with_org_prefix} \\
133
+ --wait \\
134
+ --location="#{@location}" \\
135
+ --machine-type="#{@machine_type}" \\
136
+ --hostname="#{hostname(environment)}" \\
137
+ --os="#{@os}" \\
138
+ --ssh-keys-from-files=#{SSH_KEY_FILE_NAME}.pub
139
+ OUTPUT
140
+ end
141
+
142
+ def hostname(environment)
143
+ "#{@project_name_with_org_prefix}-#{environment}"
144
+ end
145
+
146
+ def generate_ssh_key(file_name)
147
+ say <<~OUTPUT
148
+ # Create SSH key item in 1Password directly
149
+ op item create \\
150
+ --account renuo.1password.com \\
151
+ --category ssh \\
152
+ --ssh-generate-key #{SSH_ALGORITHM.downcase} \\
153
+ --title "#{@project_name_with_org_prefix}-cloudvm-key" \\
154
+ --vault #{@vault_name} \\
155
+ --tags #{@project_name_with_org_prefix}
156
+
157
+ # Extract keys to temp files for cloud VM usage
158
+ op item get "#{@project_name_with_org_prefix}-cloudvm-key" --vault #{@vault_name} \\
159
+ --account renuo.1password.com \\
160
+ --reveal --fields "public key" > #{file_name}.pub
161
+ op item get "#{@project_name_with_org_prefix}-cloudvm-key" --vault #{@vault_name} --reveal \\
162
+ --account renuo.1password.com \\
163
+ --fields "private key" --format json | jq -r '.ssh_formats.openssh.value' > #{file_name}
164
+ OUTPUT
165
+ end
166
+ end
@@ -1,10 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Renuo::Cli::Commands::SetupUptimerobot
4
- CONTACT_GROUP_IDS = "2811620_0_0-6298845_0_0-5223624_0_0"
5
- HTTPS_MONITORING = 1
6
- STATUS_PAUSED = 0
7
-
8
4
  command "setup-uptimerobot" do |c|
9
5
  c.syntax = "renuo setup-uptimerobot"
10
6
  c.summary = "Sets up uptimerobot for the given project"
@@ -17,8 +13,8 @@ class Renuo::Cli::Commands::SetupUptimerobot
17
13
 
18
14
  def initialize(args)
19
15
  abort("No project name given.") if args.nil?
20
- @api_key = ask_for_api_key
21
16
  @url = validate_url(args)
17
+ @api_key = read_api_key
22
18
  end
23
19
 
24
20
  def run
@@ -30,10 +26,18 @@ class Renuo::Cli::Commands::SetupUptimerobot
30
26
 
31
27
  private
32
28
 
33
- def ask_for_api_key
29
+ LIST_API_KEY_CMD = "op item list --tags renuo-cli-uptimerobot-api-key --format=json"
30
+ EXTRACT_API_KEY_CMD = "op item get --reveal --field credential"
31
+ def read_api_key
34
32
  say "This command will configure uptimerobot for your project."
35
- say "Please give in the api_key for uptimerobot"
36
- ask("API-key: ")
33
+ say "Fetching the uptimerobot api_key from 1Password..."
34
+ stdout, stderr, status = Open3.capture3("#{LIST_API_KEY_CMD} | #{EXTRACT_API_KEY_CMD}")
35
+ abort("Failed to fetch the uptimerobot api_key from 1Password: #{stderr.strip}") unless status.success?
36
+
37
+ api_key = stdout.strip
38
+ abort("No 1Password item tagged 'renuo-cli-uptimerobot-api-key' was found.") if api_key.empty?
39
+
40
+ api_key
37
41
  end
38
42
 
39
43
  def validate_url(args)
@@ -42,13 +46,19 @@ class Renuo::Cli::Commands::SetupUptimerobot
42
46
  URI.parse(args.first)
43
47
  end
44
48
 
49
+ HTTPS_MONITORING = 1
50
+ STATUS_PAUSED = 0
51
+ DOMAIN_EXPIRY_NOTIFICATIONS_DISABLED = 1
52
+ # Format is "id_threshold_recurrence": notify #uptime after 10 minutes of downtime, without re-notifying.
53
+ ALERT_CONTACTS = "7783511_10_0"
45
54
  def create_robot_params
46
55
  {
47
56
  friendly_name: @url.host.downcase,
48
57
  url: @url.to_s,
49
58
  type: HTTPS_MONITORING,
50
59
  interval: 300,
51
- alert_contacts: CONTACT_GROUP_IDS
60
+ alert_contacts: ALERT_CONTACTS,
61
+ disable_domain_expire_notifications: DOMAIN_EXPIRY_NOTIFICATIONS_DISABLED
52
62
  }
53
63
  end
54
64
 
@@ -24,7 +24,7 @@ global_job_config:
24
24
  - source .semaphore/bin/cache_restore rails
25
25
  - bundle config set deployment 'true'
26
26
  - bundle config set path 'vendor/bundle'
27
- - bundle install
27
+ - bundle install --retry 3
28
28
 
29
29
  blocks:
30
30
  - name: linting
@@ -49,7 +49,7 @@ blocks:
49
49
  value: test
50
50
  prologue:
51
51
  commands:
52
- - sem-service start postgres 17
52
+ - sem-service start postgres 18
53
53
  - cp .env.example .env
54
54
  - bundle exec rails db:create db:schema:load
55
55
  jobs:
@@ -3,7 +3,7 @@
3
3
  # :nocov:
4
4
  module Renuo
5
5
  class Cli
6
- VERSION = "4.22.0"
6
+ VERSION = "4.23.0"
7
7
  NAME = "renuo-cli"
8
8
  end
9
9
  end
metadata CHANGED
@@ -1,11 +1,11 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: renuo-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.22.0
4
+ version: 4.23.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Renuo AG
8
- bindir: bin
8
+ bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
@@ -62,9 +62,8 @@ extra_rdoc_files: []
62
62
  files:
63
63
  - LICENSE.txt
64
64
  - README.md
65
- - bin/renuo
65
+ - exe/renuo
66
66
  - lib/renuo/cli.rb
67
- - lib/renuo/cli/commands/check_deploio_status.rb
68
67
  - lib/renuo/cli/commands/ci/check_deploio_status.rb
69
68
  - lib/renuo/cli/commands/ci/setup.rb
70
69
  - lib/renuo/cli/commands/ci/update_deploio_app.rb
@@ -77,6 +76,7 @@ files:
77
76
  - lib/renuo/cli/commands/create_deploio_object_storage.rb
78
77
  - lib/renuo/cli/commands/create_heroku_app.rb
79
78
  - lib/renuo/cli/commands/create_new_logins.rb
79
+ - lib/renuo/cli/commands/create_nine_cloudvm.rb
80
80
  - lib/renuo/cli/commands/create_pr.rb
81
81
  - lib/renuo/cli/commands/create_slidev_presentation.rb
82
82
  - lib/renuo/cli/commands/debug.rb
@@ -132,7 +132,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
132
132
  - !ruby/object:Gem::Version
133
133
  version: '0'
134
134
  requirements: []
135
- rubygems_version: 4.0.8
135
+ rubygems_version: 4.0.15
136
136
  specification_version: 4
137
137
  summary: The Renuo CLI automates some common workflows.
138
138
  test_files: []
data/bin/renuo DELETED
@@ -1,4 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require 'renuo/cli'
4
- Renuo::Cli.new.start
@@ -1,120 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # :nocov:
4
- class Renuo::Cli::Commands::CheckDeploioStatus
5
- TIMEOUT_IN_SECONDS = 600
6
- INTERVAL_IN_SECONDS = 5
7
-
8
- command "check-deploio-status" do |c|
9
- c.syntax = "renuo check-deploio-status"
10
- c.summary = "DEPRECATED: Use 'renuo ci check-deploio-status' instead."
11
- c.description = "Checks the build and release status of the deployment."
12
- c.option "-a", "--app <name>", String, "The name of the app"
13
- c.option "-p", "--project <name>", String, "The name of the project"
14
- c.option "-g", "--git-revision <revision>", String, "The git revision to check"
15
- c.action { |_, options| new(options).run }
16
- end
17
-
18
- def initialize(options)
19
- @revision = options.git_revision || `git rev-parse HEAD`.strip
20
- @app = options.app || ENV.fetch("DEPLOIO_APP_NAME", nil)
21
- @project = options.project || ENV.fetch("DEPLOIO_PROJECT", nil)
22
-
23
- abort "missing app name" if @app.nil?
24
- abort "missing project name" if @project.nil?
25
- end
26
-
27
- def run
28
- warn "DEPRECATED: Use 'renuo ci check-deploio-status' instead."
29
- puts "(1/2) Checking build status for revision #{@revision}..."
30
- poll "build"
31
- abort "build check timed out after #{TIMEOUT_IN_SECONDS} seconds" if build.nil?
32
-
33
- puts "(2/2) Checking release status for build #{build_name}..."
34
- poll "release"
35
- abort "release check timed out after #{TIMEOUT_IN_SECONDS} seconds" if release.nil?
36
- end
37
-
38
- private
39
-
40
- def system!(cmd)
41
- abort unless system cmd
42
- end
43
-
44
- def fetch(type)
45
- command = "nctl get #{type} -a #{@app} -p #{@project} -o yaml"
46
- stdout, stderr, status = Open3.capture3 command
47
-
48
- abort "error fetching #{type} information: #{stderr}" unless status.success?
49
- Psych.load_stream stdout
50
- end
51
-
52
- def fetch_build_logs
53
- command = "nctl logs build -a #{@app} -p #{@project} -l 5000 --no-labels"
54
- stdout, stderr, status = Open3.capture3 command
55
-
56
- puts "error fetching build logs: #{stderr}" unless status.success?
57
- stdout
58
- end
59
-
60
- def build
61
- @build ||= fetch("builds").find do |build|
62
- build.dig("spec", "forProvider", "sourceConfig", "git", "revision") == @revision
63
- end
64
- end
65
-
66
- def build_name
67
- @build_name ||= build.dig("metadata", "name")
68
- end
69
-
70
- def build_status
71
- build.dig("status", "atProvider", "buildStatus")
72
- end
73
-
74
- def release
75
- @release ||= fetch("releases").find do |release|
76
- release.dig("spec", "forProvider", "build", "name") == build_name
77
- end
78
- end
79
-
80
- def release_status
81
- release.dig("status", "atProvider", "releaseStatus")
82
- end
83
-
84
- def succeeded?(status, type) # rubocop:disable Metrics/MethodLength
85
- time = "[#{Time.now.utc.iso8601}]"
86
-
87
- case status
88
- when "available", "success"
89
- puts "#{time} #{type} succeeded"
90
- true
91
- when "superseded"
92
- puts "#{time} release was superseded"
93
- true
94
- when "paused"
95
- abort "#{time} app is paused"
96
- when "error", "failed", "failure"
97
- puts fetch_build_logs
98
- abort "#{time} #{type} failed"
99
- else
100
- puts "#{time} #{type} status is #{status}, waiting..."
101
- instance_variable_set :"@#{type}", nil
102
- end
103
- end
104
-
105
- def poll(type)
106
- elapsed = 0
107
-
108
- while elapsed < TIMEOUT_IN_SECONDS
109
- if send type
110
- break if succeeded?(send(:"#{type}_status"), type)
111
- else
112
- puts "no matching #{type} found, waiting..."
113
- end
114
-
115
- sleep INTERVAL_IN_SECONDS
116
- elapsed += INTERVAL_IN_SECONDS
117
- end
118
- end
119
- end
120
- # :nocov: