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

Introduce terraform subcommand #221

Merged
merged 2 commits into from
Aug 6, 2024
Merged
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
8 changes: 8 additions & 0 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,14 @@ cpflow run -a $APP_NAME --entrypoint /app/alternative-entrypoint.sh -- rails db:
cpflow setup-app -a $APP_NAME
```

### `terraform generate`

- Generates terraform configuration files based on `controlplane.yml` and `templates/` config

```sh
cpflow terraform generate
```

### `version`

- Displays the current version of the CLI
Expand Down
22 changes: 17 additions & 5 deletions lib/command/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ class Base # rubocop:disable Metrics/ClassLength
VALIDATIONS_WITH_ADDITIONAL_OPTIONS = %w[templates].freeze
ALL_VALIDATIONS = VALIDATIONS_WITHOUT_ADDITIONAL_OPTIONS + VALIDATIONS_WITH_ADDITIONAL_OPTIONS

# Used to call the command (`cpflow SUBCOMMAND_NAME NAME`)
SUBCOMMAND_NAME = nil
# Used to call the command (`cpflow NAME`)
# NAME = ""
# Displayed when running `cpflow help` or `cpflow help NAME` (defaults to `NAME`)
Expand Down Expand Up @@ -43,11 +45,21 @@ def initialize(config)
@config = config
end

def self.all_commands
Dir["#{__dir__}/*.rb"].each_with_object({}) do |file, result|
filename = File.basename(file, ".rb")
classname = File.read(file).match(/^\s+class (\w+) < Base($| .*$)/)&.captures&.first
result[filename.to_sym] = Object.const_get("::Command::#{classname}") if classname
def self.all_commands # rubocop:disable Metrics/MethodLength
rafaelgomesxyz marked this conversation as resolved.
Show resolved Hide resolved
Dir["#{__dir__}/**/*.rb"].each_with_object({}) do |file, result|
content = File.read(file)

classname = content.match(/^\s+class (\w+) < (?:.*Base)(?:$| .*$)/)&.captures&.first
next unless classname

namespaces = content.scan(/^\s+module (\w+)/).flatten
full_classname = [*namespaces, classname].join("::").prepend("::")

command_key = File.basename(file, ".rb")
prefix = namespaces[1..].map(&:downcase).join("_")
command_key.prepend(prefix.concat("_")) unless prefix.empty?

result[command_key.to_sym] = Object.const_get(full_classname)
end
end

Expand Down
15 changes: 15 additions & 0 deletions lib/command/base_sub_command.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

# Inspired by https://github.com/rails/thor/wiki/Subcommands
class BaseSubCommand < Thor
def self.banner(command, _namespace = nil, _subcommand = false) # rubocop:disable Style/OptionalBooleanParameter
"#{basename} #{subcommand_prefix} #{command.usage}"
end

def self.subcommand_prefix
name
.gsub(/.*::/, "")
.gsub(/^[A-Z]/) { |match| match[0].downcase }
.gsub(/[A-Z]/) { |match| "-#{match[0].downcase}" }
end
end
20 changes: 20 additions & 0 deletions lib/command/terraform/generate.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

module Command
module Terraform
class Generate < Base
SUBCOMMAND_NAME = "terraform"
NAME = "generate"
DESCRIPTION = "Generates terraform configuration files"
LONG_DESCRIPTION = <<~DESC
- Generates terraform configuration files based on `controlplane.yml` and `templates/` config
DESC
WITH_INFO_HEADER = false
VALIDATIONS = [].freeze

def call
# TODO: Implement
end
rafaelgomesxyz marked this conversation as resolved.
Show resolved Hide resolved
end
end
end
47 changes: 38 additions & 9 deletions lib/cpflow.rb
Original file line number Diff line number Diff line change
Expand Up @@ -113,12 +113,21 @@ def self.check_cpflow_version # rubocop:disable Metrics/MethodLength
def self.fix_help_option
help_mappings = Thor::HELP_MAPPINGS + ["help"]
matches = help_mappings & ARGV

# Help option works correctly for subcommands
return if matches && subcommand?

matches.each do |match|
ARGV.delete(match)
ARGV.unshift(match)
end
end

def self.subcommand?
(subcommand_names & ARGV).any?
end
private_class_method :subcommand?

# Needed to silence deprecation warning
def self.exit_on_failure?
true
Expand Down Expand Up @@ -149,6 +158,10 @@ def self.all_base_commands
::Command::Base.all_commands.merge(deprecated_commands)
end

def self.subcommand_names
Dir["#{__dir__}/command/*"].filter_map { |name| File.basename(name) if File.directory?(name) }
end

def self.process_option_params(params)
# Ensures that if no value is provided for a non-boolean option (e.g., `cpflow command --option`),
# it defaults to an empty string instead of the option name (which is the default Thor behavior)
Expand All @@ -157,6 +170,17 @@ def self.process_option_params(params)
params
end

def self.klass_for(subcommand_name)
klass_name = subcommand_name.to_s.split("-").map(&:capitalize).join
return Cpflow.const_get(klass_name) if Cpflow.const_defined?(klass_name)

Cpflow.const_set(klass_name, Class.new(BaseSubCommand)).tap do |subcommand_klass|
desc(subcommand_name, "#{subcommand_name.capitalize} commands")
subcommand(subcommand_name, subcommand_klass)
end
end
private_class_method :klass_for

@commands_with_required_options = []
@commands_with_extra_options = []

Expand All @@ -169,6 +193,7 @@ def self.process_option_params(params)
deprecated = deprecated_commands[command_key]

name = command_class::NAME
subcommand_name = command_class::SUBCOMMAND_NAME
name_for_method = deprecated ? command_key : name.tr("-", "_")
usage = command_class::USAGE.empty? ? name : command_class::USAGE
requires_args = command_class::REQUIRES_ARGS
Expand All @@ -188,21 +213,25 @@ def self.process_option_params(params)
# so we store it here to be able to use it
raise_args_error = ->(*args) { handle_argument_error(commands[name_for_method], ArgumentError, *args) }

desc(usage, description, hide: hide)
long_desc(long_description)

command_options.each do |option|
params = process_option_params(option[:params])
method_option(option[:name], **params)
end

# We'll handle required options manually in `Config`
required_options = command_options.select { |option| option[:params][:required] }.map { |option| option[:name] }
@commands_with_required_options.push(name_for_method.to_sym) if required_options.any?

@commands_with_extra_options.push(name_for_method.to_sym) if accepts_extra_options

define_method(name_for_method) do |*provided_args| # rubocop:disable Metrics/BlockLength, Metrics/MethodLength
klass = subcommand_name ? klass_for(subcommand_name) : self

klass.class_eval do
desc(usage, description, hide: hide)
long_desc(long_description)

command_options.each do |option|
params = process_option_params(option[:params])
method_option(option[:name], **params)
end
end

klass.define_method(name_for_method) do |*provided_args| # rubocop:disable Metrics/BlockLength, Metrics/MethodLength
zzaakiirr marked this conversation as resolved.
Show resolved Hide resolved
if deprecated
normalized_old_name = ::Helpers.normalize_command_name(command_key)
::Shell.warn_deprecated("Command '#{normalized_old_name}' is deprecated, " \
Expand Down
8 changes: 6 additions & 2 deletions script/update_command_docs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,16 @@ commands.keys.sort.each do |command_key|
next if command_class::HIDE

name = command_class::NAME
usage = command_class::USAGE.empty? ? name : command_class::USAGE
subcommand_name = command_class::SUBCOMMAND_NAME

full_command = [subcommand_name, name].compact.join(" ")

usage = command_class::USAGE.empty? ? full_command : command_class::USAGE
options = command_class::OPTIONS
long_description = command_class::LONG_DESCRIPTION
examples = command_class::EXAMPLES

command_str = "### `#{name}`\n\n"
command_str = "### `#{full_command}`\n\n"
command_str += "#{long_description.strip}\n\n"

if examples.empty?
Expand Down
18 changes: 18 additions & 0 deletions spec/cpflow_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,22 @@
expect(result[:stderr]).to include("No value provided for option --#{option[:name].to_s.tr('_', '-')}")
end
end

it "handles subcommands correctly" do
result = run_cpflow_command("--help")

expect(result[:status]).to eq(0)

# Temporary solution, will be fixed with https://github.com/rails/thor/issues/742
basename = Cpflow::Cli.send(:basename)

Cpflow::Cli.subcommand_names.each do |subcommand|
expect(result[:stdout]).to include("#{basename} #{subcommand}")

subcommand_result = run_cpflow_command(subcommand, "--help")

expect(subcommand_result[:status]).to eq(0)
expect(subcommand_result[:stdout]).to include("#{basename} #{subcommand} help [COMMAND]")
end
end
end
Loading