cpflow 4.1.0 → 4.1.1
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 +4 -4
- data/.gitignore +4 -0
- data/CHANGELOG.md +4 -0
- data/Gemfile.lock +1 -1
- data/README.md +7 -7
- data/docs/commands.md +16 -0
- data/docs/terraform/details.md +415 -0
- data/docs/terraform/example/.controlplane/controlplane.yml +29 -0
- data/docs/terraform/example/.controlplane/templates/app.yml +38 -0
- data/docs/terraform/example/.controlplane/templates/postgres.yml +30 -0
- data/docs/terraform/example/.controlplane/templates/rails.yml +26 -0
- data/docs/terraform/overview.md +105 -0
- data/lib/command/base.rb +29 -5
- data/lib/command/base_sub_command.rb +15 -0
- data/lib/command/generate.rb +1 -1
- data/lib/command/ps.rb +1 -1
- data/lib/command/ps_stop.rb +2 -1
- data/lib/command/run.rb +1 -1
- data/lib/command/terraform/base.rb +35 -0
- data/lib/command/terraform/generate.rb +99 -0
- data/lib/command/terraform/import.rb +79 -0
- data/lib/core/controlplane.rb +3 -3
- data/lib/core/shell.rb +9 -4
- data/lib/core/terraform_config/agent.rb +31 -0
- data/lib/core/terraform_config/audit_context.rb +31 -0
- data/lib/core/terraform_config/base.rb +25 -0
- data/lib/core/terraform_config/dsl.rb +102 -0
- data/lib/core/terraform_config/generator.rb +184 -0
- data/lib/core/terraform_config/gvc.rb +63 -0
- data/lib/core/terraform_config/identity.rb +35 -0
- data/lib/core/terraform_config/local_variable.rb +30 -0
- data/lib/core/terraform_config/policy.rb +151 -0
- data/lib/core/terraform_config/provider.rb +22 -0
- data/lib/core/terraform_config/required_provider.rb +23 -0
- data/lib/core/terraform_config/secret.rb +138 -0
- data/lib/core/terraform_config/volume_set.rb +155 -0
- data/lib/core/terraform_config/workload/main.tf +316 -0
- data/lib/core/terraform_config/workload/required_providers.tf +8 -0
- data/lib/core/terraform_config/workload/variables.tf +263 -0
- data/lib/core/terraform_config/workload.rb +132 -0
- data/lib/cpflow/version.rb +1 -1
- data/lib/cpflow.rb +50 -9
- data/lib/generator_templates/templates/postgres.yml +1 -1
- data/lib/patches/array.rb +8 -0
- data/lib/patches/hash.rb +47 -0
- data/lib/patches/string.rb +34 -0
- data/script/update_command_docs +6 -2
- metadata +33 -3
@@ -0,0 +1,105 @@
|
|
1
|
+
# Terraform
|
2
|
+
|
3
|
+
## Overview
|
4
|
+
|
5
|
+
The Terraform feature in this project allows you to manage your Control Plane (CPLN) configurations using Terraform by:
|
6
|
+
1. Generating Terraform configuration files from existing CPLN YAML configuration files
|
7
|
+
2. Easily importing existing infrastructure into Terraform management
|
8
|
+
|
9
|
+
You can continue working with CPLN configuration files in YAML format and start using Terraform at any time.
|
10
|
+
|
11
|
+
## Benefits of Using Terraform Over YAML Configs
|
12
|
+
|
13
|
+
1. **State Management**: Terraform maintains a state file that tracks the current state of your infrastructure, making it easier to manage changes and updates.
|
14
|
+
2. **Dependency Management**: Terraform automatically handles dependencies between resources, ensuring that they are created or destroyed in the correct order.
|
15
|
+
3. **Multi-Cloud Support**: With Terraform, you can manage resources across multiple cloud providers seamlessly, allowing for a more flexible architecture.
|
16
|
+
4. **Plan and Apply**: Terraform provides a clear plan of what changes will be made before applying them, reducing the risk of unintended modifications.
|
17
|
+
|
18
|
+
## Usage
|
19
|
+
|
20
|
+
Let's take a look at how to deploy a [simple Rails application](https://github.com/shakacode/control-plane-flow/tree/main/docs/terraform/example/.controlplane/controlplane.yml) on CPLN using Terraform:
|
21
|
+
|
22
|
+
```
|
23
|
+
.controlplane/
|
24
|
+
├── templates/
|
25
|
+
│ ├── app.yml -- GVC config
|
26
|
+
│ ├── postgres.yml -- Workload config for PostgreSQL
|
27
|
+
│ └── rails.yml -- Workload config for Rails
|
28
|
+
└── controlplane.yml -- Configs for overall application
|
29
|
+
```
|
30
|
+
|
31
|
+
### Generating Terraform configurations
|
32
|
+
|
33
|
+
To generate Terraform configurations, run the following command from the project root:
|
34
|
+
|
35
|
+
```sh
|
36
|
+
cpflow terraform generate
|
37
|
+
```
|
38
|
+
|
39
|
+
Invoking this command will generate a new `terraform` folder with subfolders containing Terraform configurations for each application described in `controlplane.yml`:
|
40
|
+
|
41
|
+
```
|
42
|
+
terraform/
|
43
|
+
├── rails-app-production/ -- Terraform configurations for production environment
|
44
|
+
│ ├── gvc.tf -- GVC config in HCL
|
45
|
+
│ ├── identities.tf -- Identities config in HCL
|
46
|
+
│ ├── postgres.tf -- Postgres workload config in HCL
|
47
|
+
│ ├── postgres_envs.tf -- ENV variables for Postgres workload in HCL
|
48
|
+
│ ├── providers.tf -- Providers config in HCL
|
49
|
+
│ ├── rails.tf -- Rails workload config in HCL
|
50
|
+
│ ├── rails_envs.tf -- ENV variables for Rails workload in HCL
|
51
|
+
│ ├── required_providers.tf -- Required providers config in HCL
|
52
|
+
│ └── secrets.tf -- Secrets config in HCL
|
53
|
+
├── rails-app-staging/ -- Terraform configurations for staging environment
|
54
|
+
│ ├── gvc.tf -- GVC config in HCL
|
55
|
+
│ ├── identities.tf -- Identities config in HCL
|
56
|
+
│ ├── postgres.tf -- Postgres workload config in HCL
|
57
|
+
│ ├── postgres_envs.tf -- ENV variables for Postgres workload in HCL
|
58
|
+
│ ├── providers.tf -- Providers config in HCL
|
59
|
+
│ ├── rails.tf -- Rails workload config in HCL
|
60
|
+
│ ├── rails_envs.tf -- ENV variables for Rails workload in HCL
|
61
|
+
│ ├── required_providers.tf -- Required providers config in HCL
|
62
|
+
│ └── secrets.tf -- Secrets config in HCL
|
63
|
+
├── workload/ -- Terraform configurations for workload module
|
64
|
+
│ ├── main.tf -- Main config for workload resource in HCL
|
65
|
+
│ ├── required_providers.tf -- Required providers for Terraform in HCL
|
66
|
+
│ └── variables.tf -- Variables used to create config for workload resource in HCL
|
67
|
+
```
|
68
|
+
|
69
|
+
### Importing existing infrastructure
|
70
|
+
|
71
|
+
Now we need to import existing infrastructure into Terraform management because some resources can already exist on CPLN and Terraform needs to know about this:
|
72
|
+
|
73
|
+
```sh
|
74
|
+
cpflow terraform import
|
75
|
+
```
|
76
|
+
|
77
|
+
This command will initialize Terraform and import resources defined in your `controlplane.yml` and `templates` folder into the Terraform state for each application.
|
78
|
+
|
79
|
+
Please note that during the import process, you may encounter errors indicating that non-existing resources are being imported. This is expected behavior and can be safely ignored.
|
80
|
+
|
81
|
+
### Application deployment using Terraform
|
82
|
+
|
83
|
+
Preparations are complete, and now we can use Terraform commands directly to deploy our application.
|
84
|
+
|
85
|
+
1. **Navigate to the Application Folder**:
|
86
|
+
```sh
|
87
|
+
cd terraform/rails-app-staging
|
88
|
+
```
|
89
|
+
|
90
|
+
2. **Plan the Deployment**:
|
91
|
+
```sh
|
92
|
+
terraform plan
|
93
|
+
```
|
94
|
+
|
95
|
+
3. **Apply the Configuration**:
|
96
|
+
```sh
|
97
|
+
terraform apply
|
98
|
+
```
|
99
|
+
|
100
|
+
You can visit [Details](https://github.com/shakacode/control-plane-flow/tree/main/docs/terraform/details.md) to learn more about how CPLN templates in YAML format are transformed to Terraform configurations.
|
101
|
+
|
102
|
+
## References
|
103
|
+
|
104
|
+
- [Terraform Provider Plugin](https://shakadocs.controlplane.com/terraform/installation#terraform-provider-plugin)
|
105
|
+
- [Terraform - Control Plane Examples](https://github.com/controlplane-com/examples/tree/main/terraform)
|
data/lib/command/base.rb
CHANGED
@@ -12,6 +12,8 @@ module Command
|
|
12
12
|
VALIDATIONS_WITH_ADDITIONAL_OPTIONS = %w[templates].freeze
|
13
13
|
ALL_VALIDATIONS = VALIDATIONS_WITHOUT_ADDITIONAL_OPTIONS + VALIDATIONS_WITH_ADDITIONAL_OPTIONS
|
14
14
|
|
15
|
+
# Used to call the command (`cpflow SUBCOMMAND_NAME NAME`)
|
16
|
+
SUBCOMMAND_NAME = nil
|
15
17
|
# Used to call the command (`cpflow NAME`)
|
16
18
|
# NAME = ""
|
17
19
|
# Displayed when running `cpflow help` or `cpflow help NAME` (defaults to `NAME`)
|
@@ -43,11 +45,21 @@ module Command
|
|
43
45
|
@config = config
|
44
46
|
end
|
45
47
|
|
46
|
-
def self.all_commands
|
47
|
-
Dir["#{__dir__}
|
48
|
-
|
49
|
-
|
50
|
-
|
48
|
+
def self.all_commands # rubocop:disable Metrics/MethodLength
|
49
|
+
Dir["#{__dir__}/**/*.rb"].each_with_object({}) do |file, result|
|
50
|
+
content = File.read(file)
|
51
|
+
|
52
|
+
classname = content.match(/^\s+class (?!Base\b)(\w+) < (?:.*(?!Command::)Base)(?:$| .*$)/)&.captures&.first
|
53
|
+
next unless classname
|
54
|
+
|
55
|
+
namespaces = content.scan(/^\s+module (\w+)/).flatten
|
56
|
+
full_classname = [*namespaces, classname].join("::").prepend("::")
|
57
|
+
|
58
|
+
command_key = File.basename(file, ".rb")
|
59
|
+
prefix = namespaces[1..].map(&:downcase).join("_")
|
60
|
+
command_key.prepend(prefix.concat("_")) unless prefix.empty?
|
61
|
+
|
62
|
+
result[command_key.to_sym] = Object.const_get(full_classname)
|
51
63
|
end
|
52
64
|
end
|
53
65
|
|
@@ -453,6 +465,18 @@ module Command
|
|
453
465
|
}
|
454
466
|
}
|
455
467
|
end
|
468
|
+
|
469
|
+
def self.dir_option(required: false)
|
470
|
+
{
|
471
|
+
name: :dir,
|
472
|
+
params: {
|
473
|
+
banner: "DIR",
|
474
|
+
desc: "Output directory",
|
475
|
+
type: :string,
|
476
|
+
required: required
|
477
|
+
}
|
478
|
+
}
|
479
|
+
end
|
456
480
|
# rubocop:enable Metrics/MethodLength
|
457
481
|
|
458
482
|
def self.all_options
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Inspired by https://github.com/rails/thor/wiki/Subcommands
|
4
|
+
class BaseSubCommand < Thor
|
5
|
+
def self.banner(command, _namespace = nil, _subcommand = false) # rubocop:disable Style/OptionalBooleanParameter
|
6
|
+
"#{basename} #{subcommand_prefix} #{command.usage}"
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.subcommand_prefix
|
10
|
+
name
|
11
|
+
.gsub(/.*::/, "")
|
12
|
+
.gsub(/^[A-Z]/) { |match| match[0].downcase }
|
13
|
+
.gsub(/[A-Z]/) { |match| "-#{match[0].downcase}" }
|
14
|
+
end
|
15
|
+
end
|
data/lib/command/generate.rb
CHANGED
data/lib/command/ps.rb
CHANGED
data/lib/command/ps_stop.rb
CHANGED
@@ -75,7 +75,8 @@ module Command
|
|
75
75
|
|
76
76
|
step("Waiting for replica '#{replica}' to not be ready", retry_on_failure: true) do
|
77
77
|
result = cp.fetch_workload_replicas(workload, location: config.location)
|
78
|
-
|
78
|
+
items = result&.dig("items")
|
79
|
+
items && !items.include?(replica)
|
79
80
|
end
|
80
81
|
end
|
81
82
|
end
|
data/lib/command/run.rb
CHANGED
@@ -274,7 +274,7 @@ module Command
|
|
274
274
|
def wait_for_replica_for_job
|
275
275
|
step("Waiting for replica to start, which runs job '#{job}'", retry_on_failure: true) do
|
276
276
|
result = cp.fetch_workload_replicas(runner_workload, location: location)
|
277
|
-
@replica = result
|
277
|
+
@replica = result&.dig("items")&.find { |item| item.include?(job) }
|
278
278
|
|
279
279
|
replica || false
|
280
280
|
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Command
|
4
|
+
module Terraform
|
5
|
+
class Base < Command::Base
|
6
|
+
private
|
7
|
+
|
8
|
+
def templates
|
9
|
+
parser = TemplateParser.new(self)
|
10
|
+
template_files = Dir["#{parser.template_dir}/*.yml"]
|
11
|
+
|
12
|
+
if template_files.empty?
|
13
|
+
Shell.warn("No templates found in #{parser.template_dir}")
|
14
|
+
return []
|
15
|
+
end
|
16
|
+
|
17
|
+
parser.parse(template_files)
|
18
|
+
rescue StandardError => e
|
19
|
+
Shell.warn("Error parsing templates: #{e.message}")
|
20
|
+
[]
|
21
|
+
end
|
22
|
+
|
23
|
+
def terraform_dir
|
24
|
+
@terraform_dir ||= begin
|
25
|
+
full_path = config.options.fetch(:dir, Cpflow.root_path.join("terraform"))
|
26
|
+
Pathname.new(full_path).tap do |path|
|
27
|
+
FileUtils.mkdir_p(path)
|
28
|
+
rescue StandardError => e
|
29
|
+
Shell.abort("Invalid directory: #{e.message}")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Command
|
4
|
+
module Terraform
|
5
|
+
class Generate < Base
|
6
|
+
SUBCOMMAND_NAME = "terraform"
|
7
|
+
NAME = "generate"
|
8
|
+
OPTIONS = [
|
9
|
+
app_option,
|
10
|
+
dir_option
|
11
|
+
].freeze
|
12
|
+
DESCRIPTION = "Generates terraform configuration files"
|
13
|
+
LONG_DESCRIPTION = <<~DESC
|
14
|
+
- Generates terraform configuration files based on `controlplane.yml` and `templates/` config
|
15
|
+
DESC
|
16
|
+
WITH_INFO_HEADER = false
|
17
|
+
|
18
|
+
def call
|
19
|
+
Array(config.app || config.apps.keys).each do |app|
|
20
|
+
config.instance_variable_set(:@app, app.to_s)
|
21
|
+
generate_app_config
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def generate_app_config
|
28
|
+
copy_workload_module
|
29
|
+
|
30
|
+
terraform_app_dir = cleaned_terraform_app_dir
|
31
|
+
generate_provider_configs(terraform_app_dir)
|
32
|
+
|
33
|
+
templates.each do |template|
|
34
|
+
TerraformConfig::Generator.new(config: config, template: template).tf_configs.each do |filename, tf_config|
|
35
|
+
File.write(terraform_app_dir.join(filename), tf_config.to_tf, mode: "a+")
|
36
|
+
end
|
37
|
+
rescue TerraformConfig::Generator::InvalidTemplateError => e
|
38
|
+
Shell.warn(e.message)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def copy_workload_module
|
43
|
+
FileUtils.copy_entry(
|
44
|
+
Cpflow.root_path.join("lib/core/terraform_config/workload"),
|
45
|
+
terraform_dir.join("workload")
|
46
|
+
)
|
47
|
+
end
|
48
|
+
|
49
|
+
def generate_provider_configs(terraform_app_dir)
|
50
|
+
generate_required_providers(terraform_app_dir)
|
51
|
+
generate_providers(terraform_app_dir)
|
52
|
+
rescue StandardError => e
|
53
|
+
Shell.abort("Failed to generate provider config files: #{e.message}")
|
54
|
+
end
|
55
|
+
|
56
|
+
def generate_required_providers(terraform_app_dir)
|
57
|
+
File.write(terraform_app_dir.join("required_providers.tf"), required_cpln_provider.to_tf)
|
58
|
+
end
|
59
|
+
|
60
|
+
def generate_providers(terraform_app_dir)
|
61
|
+
cpln_provider = TerraformConfig::Provider.new(name: "cpln", org: config.org)
|
62
|
+
File.write(terraform_app_dir.join("providers.tf"), cpln_provider.to_tf)
|
63
|
+
end
|
64
|
+
|
65
|
+
def required_cpln_provider
|
66
|
+
TerraformConfig::RequiredProvider.new(
|
67
|
+
name: "cpln",
|
68
|
+
org: config.org,
|
69
|
+
source: "controlplane-com/cpln",
|
70
|
+
version: "~> 1.0"
|
71
|
+
)
|
72
|
+
end
|
73
|
+
|
74
|
+
def cleaned_terraform_app_dir
|
75
|
+
full_path = terraform_dir.join(config.app)
|
76
|
+
|
77
|
+
unless File.expand_path(full_path).include?(Cpflow.root_path.to_s)
|
78
|
+
Shell.abort("Directory to save terraform configuration files cannot be outside of current directory")
|
79
|
+
end
|
80
|
+
|
81
|
+
if Dir.exist?(full_path)
|
82
|
+
clean_terraform_app_dir(full_path)
|
83
|
+
else
|
84
|
+
FileUtils.mkdir_p(full_path)
|
85
|
+
end
|
86
|
+
|
87
|
+
full_path
|
88
|
+
end
|
89
|
+
|
90
|
+
def clean_terraform_app_dir(terraform_app_dir)
|
91
|
+
Dir.children(terraform_app_dir).each do |child|
|
92
|
+
next if child == ".terraform.lock.hcl"
|
93
|
+
|
94
|
+
FileUtils.rm_rf(terraform_app_dir.join(child))
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Command
|
4
|
+
module Terraform
|
5
|
+
class Import < Base
|
6
|
+
SUBCOMMAND_NAME = "terraform"
|
7
|
+
NAME = "import"
|
8
|
+
OPTIONS = [
|
9
|
+
app_option,
|
10
|
+
dir_option
|
11
|
+
].freeze
|
12
|
+
DESCRIPTION = "Imports terraform resources"
|
13
|
+
LONG_DESCRIPTION = <<~DESC
|
14
|
+
- Imports terraform resources from the generated configuration files
|
15
|
+
DESC
|
16
|
+
WITH_INFO_HEADER = false
|
17
|
+
|
18
|
+
def call
|
19
|
+
Array(config.app || config.apps.keys).each do |app|
|
20
|
+
config.instance_variable_set(:@app, app.to_s)
|
21
|
+
|
22
|
+
Dir.chdir(terraform_app_dir) do
|
23
|
+
run_terraform_init
|
24
|
+
|
25
|
+
resources.each do |resource|
|
26
|
+
run_terraform_import(resource[:address], resource[:id])
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def run_terraform_init
|
35
|
+
result = Shell.cmd("terraform", "init", capture_stderr: true)
|
36
|
+
|
37
|
+
if result[:success]
|
38
|
+
Shell.info(result[:output])
|
39
|
+
else
|
40
|
+
Shell.abort("Failed to initialize terraform - #{result[:output]}")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def run_terraform_import(address, id)
|
45
|
+
result = Shell.cmd("terraform", "import", address, id, capture_stderr: true)
|
46
|
+
Shell.info(result[:output])
|
47
|
+
end
|
48
|
+
|
49
|
+
def resources
|
50
|
+
tf_configs.filter_map do |tf_config|
|
51
|
+
next unless tf_config.importable?
|
52
|
+
|
53
|
+
{ address: tf_config.reference, id: resource_id(tf_config) }
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def tf_configs
|
58
|
+
templates.flat_map do |template|
|
59
|
+
TerraformConfig::Generator.new(config: config, template: template).tf_configs.values
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def resource_id(tf_config)
|
64
|
+
case tf_config
|
65
|
+
when TerraformConfig::Gvc, TerraformConfig::Policy,
|
66
|
+
TerraformConfig::Secret, TerraformConfig::Agent,
|
67
|
+
TerraformConfig::AuditContext
|
68
|
+
tf_config.name
|
69
|
+
else
|
70
|
+
"#{config.app}:#{tf_config.name}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def terraform_app_dir
|
75
|
+
terraform_dir.join(config.app)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
data/lib/core/controlplane.rb
CHANGED
@@ -192,7 +192,7 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
192
192
|
end
|
193
193
|
|
194
194
|
def fetch_workload_replicas(workload, location:)
|
195
|
-
cmd = "cpln workload replica get #{workload} #{gvc_org} --location #{location} -o yaml"
|
195
|
+
cmd = "cpln workload replica get #{workload} #{gvc_org} --location #{location} -o yaml 2> /dev/null"
|
196
196
|
perform_yaml(cmd)
|
197
197
|
end
|
198
198
|
|
@@ -213,8 +213,8 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
213
213
|
end
|
214
214
|
end
|
215
215
|
|
216
|
-
def workload_deployments_ready?(workload, location:, expected_status:)
|
217
|
-
deployed_replicas = fetch_workload_replicas(workload, location: location)
|
216
|
+
def workload_deployments_ready?(workload, location:, expected_status:) # rubocop:disable Metrics/CyclomaticComplexity
|
217
|
+
deployed_replicas = fetch_workload_replicas(workload, location: location)&.dig("items")&.length || 0
|
218
218
|
return deployed_replicas.zero? if expected_status == false
|
219
219
|
|
220
220
|
deployments = fetch_workload_deployments(workload)["items"]
|
data/lib/core/shell.rb
CHANGED
@@ -5,10 +5,6 @@ class Shell
|
|
5
5
|
attr_reader :tmp_stderr, :verbose
|
6
6
|
end
|
7
7
|
|
8
|
-
def self.shell
|
9
|
-
@shell ||= Thor::Shell::Color.new
|
10
|
-
end
|
11
|
-
|
12
8
|
def self.use_tmp_stderr
|
13
9
|
@tmp_stderr = Tempfile.create
|
14
10
|
|
@@ -35,6 +31,10 @@ class Shell
|
|
35
31
|
shell.yes?("#{message} (y/N)")
|
36
32
|
end
|
37
33
|
|
34
|
+
def self.info(message)
|
35
|
+
shell.say(message)
|
36
|
+
end
|
37
|
+
|
38
38
|
def self.warn(message)
|
39
39
|
Kernel.warn(color("WARNING: #{message}", :yellow))
|
40
40
|
end
|
@@ -97,4 +97,9 @@ class Shell
|
|
97
97
|
exit(ExitCode::INTERRUPT)
|
98
98
|
end
|
99
99
|
end
|
100
|
+
|
101
|
+
def self.shell
|
102
|
+
@shell ||= Thor::Shell::Color.new
|
103
|
+
end
|
104
|
+
private_class_method :shell
|
100
105
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TerraformConfig
|
4
|
+
class Agent < Base
|
5
|
+
attr_reader :name, :description, :tags
|
6
|
+
|
7
|
+
def initialize(name:, description: nil, tags: nil)
|
8
|
+
super()
|
9
|
+
|
10
|
+
@name = name
|
11
|
+
@description = description
|
12
|
+
@tags = tags
|
13
|
+
end
|
14
|
+
|
15
|
+
def importable?
|
16
|
+
true
|
17
|
+
end
|
18
|
+
|
19
|
+
def reference
|
20
|
+
"cpln_agent.#{name}"
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_tf
|
24
|
+
block :resource, :cpln_agent, name do
|
25
|
+
argument :name, name
|
26
|
+
argument :description, description, optional: true
|
27
|
+
argument :tags, tags, optional: true
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TerraformConfig
|
4
|
+
class AuditContext < Base
|
5
|
+
attr_reader :name, :description, :tags
|
6
|
+
|
7
|
+
def initialize(name:, description: nil, tags: nil)
|
8
|
+
super()
|
9
|
+
|
10
|
+
@name = name
|
11
|
+
@description = description
|
12
|
+
@tags = tags
|
13
|
+
end
|
14
|
+
|
15
|
+
def importable?
|
16
|
+
true
|
17
|
+
end
|
18
|
+
|
19
|
+
def reference
|
20
|
+
"cpln_audit_context.#{name}"
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_tf
|
24
|
+
block :resource, :cpln_audit_context, name do
|
25
|
+
argument :name, name
|
26
|
+
argument :description, description, optional: true
|
27
|
+
argument :tags, tags, optional: true
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "dsl"
|
4
|
+
|
5
|
+
module TerraformConfig
|
6
|
+
class Base
|
7
|
+
include Dsl
|
8
|
+
|
9
|
+
def importable?
|
10
|
+
false
|
11
|
+
end
|
12
|
+
|
13
|
+
def reference
|
14
|
+
raise NotImplementedError if importable?
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_tf
|
18
|
+
raise NotImplementedError
|
19
|
+
end
|
20
|
+
|
21
|
+
def locals
|
22
|
+
{}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TerraformConfig
|
4
|
+
module Dsl
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
EXPRESSION_PATTERN = /(var|local|cpln_\w+)\./.freeze
|
8
|
+
|
9
|
+
def_delegators :current_context, :put, :output
|
10
|
+
|
11
|
+
def block(name, *labels)
|
12
|
+
switch_context do
|
13
|
+
put("#{block_declaration(name, labels)} {\n")
|
14
|
+
yield
|
15
|
+
put("}\n")
|
16
|
+
end
|
17
|
+
|
18
|
+
# There is extra indent for whole output that needs to be removed
|
19
|
+
output.unindent
|
20
|
+
end
|
21
|
+
|
22
|
+
def argument(name, value, optional: false, raw: false)
|
23
|
+
return if value.nil? && optional
|
24
|
+
|
25
|
+
content =
|
26
|
+
if value.is_a?(Hash)
|
27
|
+
operator = raw ? ": " : " = "
|
28
|
+
"{\n#{value.map { |n, v| "#{n}#{operator}#{tf_value(v)}" }.join("\n").indent(2)}\n}\n"
|
29
|
+
else
|
30
|
+
"#{tf_value(value)}\n"
|
31
|
+
end
|
32
|
+
|
33
|
+
# remove quotes from expression values
|
34
|
+
content = content.gsub(/("#{EXPRESSION_PATTERN}.*")/) { ::Regexp.last_match(1)[1...-1] }
|
35
|
+
|
36
|
+
put("#{name} = #{content}", indent: 2)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def tf_value(value)
|
42
|
+
value = value.to_s if value.is_a?(Symbol)
|
43
|
+
|
44
|
+
case value
|
45
|
+
when String
|
46
|
+
tf_string_value(value)
|
47
|
+
when Hash
|
48
|
+
tf_hash_value(value)
|
49
|
+
else
|
50
|
+
value
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def tf_string_value(value)
|
55
|
+
return value if expression?(value)
|
56
|
+
return "\"#{value}\"" unless value.include?("\n")
|
57
|
+
|
58
|
+
"EOF\n#{value.indent(2)}\nEOF"
|
59
|
+
end
|
60
|
+
|
61
|
+
def tf_hash_value(value)
|
62
|
+
JSON.pretty_generate(value.crush)
|
63
|
+
.gsub(/"(\w+)":/) { "#{::Regexp.last_match(1)}:" } # remove quotes from keys
|
64
|
+
end
|
65
|
+
|
66
|
+
def expression?(value)
|
67
|
+
value.match?(/^#{EXPRESSION_PATTERN}/)
|
68
|
+
end
|
69
|
+
|
70
|
+
def block_declaration(name, labels)
|
71
|
+
result = name.to_s
|
72
|
+
return result unless labels.any?
|
73
|
+
|
74
|
+
result + " #{labels.map { |label| tf_value(label) }.join(' ')}"
|
75
|
+
end
|
76
|
+
|
77
|
+
class Context
|
78
|
+
attr_accessor :output
|
79
|
+
|
80
|
+
def initialize
|
81
|
+
@output = ""
|
82
|
+
end
|
83
|
+
|
84
|
+
def put(content, indent: 0)
|
85
|
+
@output += content.to_s.indent(indent)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def switch_context
|
90
|
+
old_context = current_context
|
91
|
+
@current_context = Context.new
|
92
|
+
yield
|
93
|
+
ensure
|
94
|
+
old_context.put(current_context.output, indent: 2)
|
95
|
+
@current_context = old_context
|
96
|
+
end
|
97
|
+
|
98
|
+
def current_context
|
99
|
+
@current_context ||= Context.new
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|