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 1 commit
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
```

### `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
21 changes: 16 additions & 5 deletions lib/command/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,27 @@ class Base # rubocop:disable Metrics/ClassLength
WITH_INFO_HEADER = true
# Which validations to run before the command
VALIDATIONS = %w[config].freeze
SUBCOMMAND = nil

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
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..1].map(&:downcase).join("_")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you mean namespaces[1..]? Because namespaces[1..1] should be the same as namespaces[1], no?

Copy link
Contributor Author

@zzaakiirr zzaakiirr Aug 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, I meant namespaces[1..]. We don't need first namespace (which is Command)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's still namespaces[1..1] by the way.

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
25 changes: 25 additions & 0 deletions lib/command/terraform/generate.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# frozen_string_literal: true

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

def call
# TODO: Implement
end
end
end
end
42 changes: 33 additions & 9 deletions lib/cpflow.rb
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ 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 && (ARGV & subcommand_names).any?

matches.each do |match|
ARGV.delete(match)
ARGV.unshift(match)
Expand Down Expand Up @@ -149,6 +153,10 @@ def self.all_base_commands
::Command::Base.all_commands.merge(deprecated_commands)
end

def self.subcommand_names
::Command::Base.all_commands.values.map { |command| command::SUBCOMMAND }.compact
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 +165,17 @@ def self.process_option_params(params)
params
end

def self.klass_for(subcommand)
klass_name = subcommand.to_s.split("-").collect(&: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, "#{subcommand.capitalize} commands"
subcommand subcommand, subcommand_klass
end
end
private_class_method :klass_for

@commands_with_required_options = []
@commands_with_extra_options = []

Expand All @@ -181,28 +200,33 @@ def self.process_option_params(params)
hide = command_class::HIDE || deprecated
with_info_header = command_class::WITH_INFO_HEADER
validations = command_class::VALIDATIONS
subcommand = command_class::SUBCOMMAND

long_description += "\n#{examples}" if examples.length.positive?

# `handle_argument_error` does not exist in the context below,
# 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 ? klass_for(subcommand) : 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
if deprecated
normalized_old_name = ::Helpers.normalize_command_name(command_key)
::Shell.warn_deprecated("Command '#{normalized_old_name}' is deprecated, " \
Expand Down
15 changes: 15 additions & 0 deletions spec/cpflow_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,19 @@
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)

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

subcommand_result = run_cpflow_command(subcommand, "help")

expect(subcommand_result[:status]).to eq(0)
expect(subcommand_result[:stdout]).to include("#{package_name} #{subcommand} help [COMMAND]")
end
end
end
9 changes: 9 additions & 0 deletions spec/support/command_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,9 @@ def create_app_if_not_exists(app, deploy: false, image_before_deploy_count: 0, i
end

def run_cpflow_command(*args, raise_errors: false) # rubocop:disable Metrics/MethodLength
program_name_before = $PROGRAM_NAME
$PROGRAM_NAME = package_name

LogHelpers.write_command_to_log(args.join(" "))

result = {
Expand All @@ -182,6 +185,8 @@ def run_cpflow_command(*args, raise_errors: false) # rubocop:disable Metrics/Met
raise result.to_json if result[:status].nonzero? && raise_errors

result
ensure
$PROGRAM_NAME = program_name_before
end

def run_cpflow_command!(*args)
Expand Down Expand Up @@ -257,4 +262,8 @@ def spec_directory

File.dirname(current_directory)
end

def package_name
@package_name ||= Cpflow::Cli.instance_variable_get("@package_name")
end
end
Loading