rails_template_18f 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/Gemfile.lock +1 -1
  4. data/exe/rails_template_18f +31 -0
  5. data/lib/generators/rails_template18f/active_storage/active_storage_generator.rb +135 -0
  6. data/lib/generators/rails_template18f/active_storage/templates/app/jobs/file_scan_job.rb +33 -0
  7. data/lib/generators/rails_template18f/active_storage/templates/app/models/file_upload.rb +25 -0
  8. data/lib/generators/rails_template18f/active_storage/templates/doc/adr/clamav.md.tt +30 -0
  9. data/lib/generators/rails_template18f/active_storage/templates/spec/jobs/file_scan_job_spec.rb +35 -0
  10. data/lib/generators/rails_template18f/active_storage/templates/spec/models/file_upload_spec.rb +38 -0
  11. data/lib/generators/rails_template18f/cloud_gov_config/cloud_gov_config_generator.rb +28 -0
  12. data/lib/generators/rails_template18f/cloud_gov_config/templates/app/models/cloud_gov_config.rb +15 -0
  13. data/lib/generators/rails_template18f/cloud_gov_config/templates/spec/models/cloud_gov_config_spec.rb +44 -0
  14. data/lib/generators/rails_template18f/i18n_js/i18n_js_generator.rb +60 -0
  15. data/lib/generators/rails_template18f/i18n_js/templates/lib/tasks/i18n.rake +9 -0
  16. data/lib/generators/rails_template18f/sidekiq/sidekiq_generator.rb +70 -0
  17. data/lib/generators/rails_template18f/sidekiq/templates/config/initializers/redis.rb +14 -0
  18. data/lib/generators/rails_template18f/terraform/templates/terraform/production/main.tf.tt +37 -5
  19. data/lib/generators/rails_template18f/terraform/templates/terraform/shared/clamav/main.tf.tt +50 -0
  20. data/lib/generators/rails_template18f/terraform/templates/terraform/shared/clamav/providers.tf +16 -0
  21. data/lib/generators/rails_template18f/terraform/templates/terraform/shared/clamav/variables.tf +47 -0
  22. data/lib/generators/rails_template18f/terraform/templates/terraform/shared/redis/main.tf.tt +23 -0
  23. data/lib/generators/rails_template18f/terraform/templates/terraform/shared/redis/providers.tf +16 -0
  24. data/lib/generators/rails_template18f/terraform/templates/terraform/shared/redis/variables.tf +42 -0
  25. data/lib/generators/rails_template18f/terraform/templates/terraform/staging/main.tf.tt +37 -5
  26. data/lib/generators/rails_template18f/terraform/terraform_generator.rb +0 -11
  27. data/lib/rails_template18f/app_updater.rb +19 -0
  28. data/lib/rails_template18f/generators/base.rb +22 -5
  29. data/lib/rails_template18f/generators/cloud_gov_options.rb +0 -4
  30. data/lib/rails_template18f/version.rb +1 -1
  31. data/template.rb +16 -0
  32. data/templates/config/deployment/staging.yml +1 -1
  33. data/templates/config/environments/ci.rb +0 -1
  34. data/templates/doc/compliance/apps/application.boundary.md.tt +0 -7
  35. metadata +22 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6c732c75ebc9f84d2f730c43042eb922392954f18e8a2500722e4f2e070f329a
4
- data.tar.gz: 0fca28e40b443de8032d7a76f38f889449238c62ffeb15444b78a73d9482eaf0
3
+ metadata.gz: c87267d368d3e90a500c0fc1bf23312a3f6edf437ba991a9a4c7b994fff4282d
4
+ data.tar.gz: ec24cf97ec587cf730bb2f63ff4cf0fd5b13ae64364b175041b0b0afd42a4321
5
5
  SHA512:
6
- metadata.gz: c3900e08a7ae9227bad8e4f341b867556ddbaae1614a526ab64be758cd0c78ad0b09e4be1587da1038623151a045f05aa7a7fe3ea82676479f29d54d4cc4d94f
7
- data.tar.gz: 7da9792ec9280f443a5bfa2bfc6ffca23a60cf984cb64c0773019dd27ea130b02fcd331647493b81ffcac434fbc8bb5cdf064c1d3e5a5714617a4bcac5335c90
6
+ metadata.gz: c2c05519936ff836d7c99dd16a5363755a6e86731c5d41454adbc0f6a7ada083fd1b636d0f5da3db446225866f095b42c2278100c85d3c10a3a2182100fcf550
7
+ data.tar.gz: b2e6da4f298a5c0054997c2c44244e4956044af83af81792937adf7d52716a68b3f8ebf8332f598943b9bad9a37bebf3c6dd4da0eb196ac119cc32455db00e57
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.4.0] - 2022-02-24
4
+
5
+ - helper script to run rails app:update
6
+ - cloud.gov configuration helper generator
7
+ - activestorage/clamav generator
8
+ - activejob/sidekiq generator
9
+ - i18n-js generator
10
+
3
11
  ## [0.3.0] - 2022-02-17
4
12
 
5
13
  - i18n generator
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rails_template_18f (0.3.0)
4
+ rails_template_18f (0.4.0)
5
5
  activesupport (~> 7.0.0)
6
6
  railties (~> 7.0.0)
7
7
  thor (~> 1.0)
@@ -2,6 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "thor"
5
+ require_relative "../lib/rails_template18f/version"
5
6
 
6
7
  class CLI < Thor
7
8
  include Thor::Actions
@@ -24,6 +25,36 @@ class CLI < Thor
24
25
  railsrc = options[:hotwire] ? "railsrc-hotwire" : "railsrc"
25
26
  run "rails new #{app_directory} --rc=#{File.join(gem_path, railsrc)} --template=#{File.join(gem_path, "template.rb")} #{rails_arguments.join(" ")}"
26
27
  end
28
+
29
+ desc "update", "Run rails app:update with some enhancements"
30
+ long_desc <<-LONGDESC
31
+ Run `rails app:update` with frameworks fully defined by what is commented out at the top
32
+ of config/application.rb
33
+
34
+ Example: to enable ActiveStorage
35
+
36
+ 1) Uncomment `require "active_storage/engine"` in `config/application.rb`
37
+
38
+ 2) Run `bin/rails active_storage:install`
39
+
40
+ 3) Run bundle exec rails_template_18f update
41
+
42
+ 4) Optional: run other rails_template18f generators that may be applicable
43
+ LONGDESC
44
+ def update
45
+ require_relative "../lib/rails_template18f/app_updater"
46
+ require "rails/command"
47
+ Rails::Command.invoke "app:update"
48
+ end
49
+
50
+ desc "version", "Output gem version"
51
+ def version
52
+ puts RailsTemplate18f::VERSION
53
+ end
54
+
55
+ def self.exit_on_failure?
56
+ true
57
+ end
27
58
  end
28
59
 
29
60
  CLI.start(ARGV)
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module RailsTemplate18f
6
+ module Generators
7
+ class ActiveStorageGenerator < ::Rails::Generators::Base
8
+ include Base
9
+
10
+ desc <<~DESC
11
+ Description:
12
+ Document use of Clamav as ActiveStorage scanner
13
+ DESC
14
+
15
+ def configure_active_storage
16
+ generate "rails_template18f:cloud_gov_config"
17
+ rails_command "active_storage:install"
18
+ comment_lines "config/environments/production.rb", /active_storage\.service/
19
+ insert_into_file "config/environments/production.rb", "\n config.active_storage.service = :amazon", after: /active_storage\.service.*$/
20
+ environment "config.active_storage.service = :local", env: "ci"
21
+ append_to_file "config/storage.yml", <<~EOYAML
22
+
23
+ amazon:
24
+ service: S3
25
+ access_key_id: <%= CloudGovConfig.dig(:s3, :credentials, :access_key_id) %>
26
+ secret_access_key: <%= CloudGovConfig.dig(:s3, :credentials, :secret_access_key) %>
27
+ region: us-gov-west-1
28
+ bucket: <%= CloudGovConfig.dig(:s3, :credentials, :bucket) %>
29
+ EOYAML
30
+ end
31
+
32
+ def install_gems
33
+ gem "faraday", "~> 2.2"
34
+ gem "faraday-multipart", "~> 1.0"
35
+ gem_group :production do
36
+ gem "aws-sdk-s3", "~> 1.112"
37
+ end
38
+ end
39
+
40
+ def create_scanned_upload_model_and_job
41
+ generate :migration, "CreateFileUploads", "file:attachment", "record:references{polymorphic}", "scan_status:string"
42
+ migration_file = Dir.glob(File.expand_path(File.join("db", "migrate", "[0-9]*_*.rb"), destination_root)).grep(/\d+_create_file_uploads.rb$/).first
43
+ unless migration_file.nil?
44
+ gsub_file migration_file, ":scan_status", ":scan_status, null: false, default: \"uploaded\""
45
+ end
46
+ directory "app"
47
+ directory "spec"
48
+ end
49
+
50
+ def configure_local_clamav_runner
51
+ append_to_file "Procfile.dev", "clamav: docker run --rm -p 9443:9443 ajilaag/clamav-rest:20211229\n"
52
+ end
53
+
54
+ def configure_clamav_env_var
55
+ append_to_file ".env", <<~EOM
56
+
57
+
58
+ # CLAMAV_API_URL tells FileScanJob where to send files for virus scans
59
+ CLAMAV_API_URL=https://localhost:9443
60
+ EOM
61
+ insert_into_file "manifest.yml", " CLAMAV_API_URL: \"https://#{app_name}-clamapi-((env)).apps.internal:9443\"\n", before: /^\s+processes:/
62
+ insert_into_file "manifest.yml", "\n - #{app_name}-s3-((env))", after: "services:"
63
+ end
64
+
65
+ def update_boundary_diagram
66
+ boundary_filename = "doc/compliance/apps/application.boundary.md"
67
+
68
+ insert_into_file boundary_filename, indent(<<~EOB, 16), after: /ContainerDb\(app_db.*$\n/
69
+ Container(clamav, "File Scanning API", "ClamAV", "Internal application for scanning user uploads")
70
+ ContainerDb(app_s3, "File Storage", "AWS S3", "User-uploaded file storage")
71
+ EOB
72
+ insert_into_file boundary_filename, <<~EOB, before: "@enduml"
73
+ Rel(app, app_s3, "reads/writes file data", "https (443)")
74
+ EOB
75
+ if has_active_job?
76
+ insert_into_file boundary_filename, <<~EOB, before: "@enduml"
77
+ Rel(worker, app_s3, "reads/writes file data", "https (443)")
78
+ Rel(worker, clamav, "scans files", "https (9443)")
79
+ EOB
80
+ end
81
+ end
82
+
83
+ def update_data_model_uml
84
+ insert_into_file "doc/compliance/apps/data.logical.md", data_model_uml, before: "@enduml"
85
+ end
86
+
87
+ def generate_adr
88
+ adr_dir = File.expand_path(File.join("doc", "adr"), destination_root)
89
+ if Dir.exist? adr_dir
90
+ @next_adr_id = `ls #{adr_dir} | tail -n 1 | awk -F '-' '{print $1}'`.strip.to_i + 1
91
+ template "doc/adr/clamav.md", "doc/adr/#{"%04d" % @next_adr_id}-clamav-file-scanning.md"
92
+ end
93
+ end
94
+
95
+ no_tasks do
96
+ def data_model_uml
97
+ <<~UML
98
+ class file_uploads {
99
+ * id : bigint <<generated>>
100
+ * scan_status : string
101
+ * record_id : bigint
102
+ * record_type : string
103
+ }
104
+ class active_storage_attachments {
105
+ * id : bigint <<generated>>
106
+ * name : string
107
+ * record_type : string
108
+ * record_id : bigint
109
+ * blob_id : bigint
110
+ * created_at : timestamp without time zone
111
+ }
112
+ class active_storage_blobs {
113
+ * id : bigint <<generated>>
114
+ * key : string
115
+ * filename : string
116
+ content_type : string
117
+ metadata : text
118
+ * service_name : string
119
+ * byte_size : bigint
120
+ checksum : string
121
+ * created_at : timestamp without time zone
122
+ }
123
+ class active_storage_variant_records {
124
+ * id : bigint <<generated>>
125
+ * variation_digest : string
126
+ }
127
+ file_uploads ||--|| active_storage_attachments
128
+ active_storage_attachments ||--|{ active_storage_blobs
129
+ active_storage_variant_records ||--|{ active_storage_blobs
130
+ UML
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,33 @@
1
+ class FileScanJob < ApplicationJob
2
+ queue_as :default
3
+
4
+ def perform(file_upload)
5
+ return if file_upload.nil? || file_upload.clean?
6
+ file_upload.open do |file|
7
+ payload = {file: Faraday::Multipart::FilePart.new(
8
+ file,
9
+ file_upload.content_type,
10
+ file_upload.filename
11
+ )}
12
+ response = connection.post("/scan", payload)
13
+ if response.success?
14
+ file_upload.update_columns scan_status: "scanned", updated_at: Time.now
15
+ else
16
+ logger.error "File Scan for #{file_upload.id} failed: #{response.body}"
17
+ file_upload.update_columns scan_status: "quarantined", updated_at: Time.now
18
+ end
19
+ end
20
+ rescue => ex
21
+ file_upload&.update_columns scan_status: "scan_failed", updated_at: Time.now
22
+ raise ex
23
+ end
24
+
25
+ def connection
26
+ @connection ||= Faraday.new(
27
+ url: ENV["CLAMAV_API_URL"],
28
+ ssl: {verify: false}
29
+ ) do |f|
30
+ f.request :multipart
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,25 @@
1
+ class FileUpload < ApplicationRecord
2
+ belongs_to :record, polymorphic: true
3
+ has_one_attached :file
4
+
5
+ delegate :open, :content_type, to: :file
6
+
7
+ validates_presence_of :file
8
+ validates_inclusion_of :scan_status, in: %w[uploaded scan_failed scanned quarantined]
9
+
10
+ after_commit :scan
11
+
12
+ def clean?
13
+ scan_status == "scanned"
14
+ end
15
+
16
+ def filename
17
+ file.filename.to_s
18
+ end
19
+
20
+ private
21
+
22
+ def scan
23
+ FileScanJob.perform_later self
24
+ end
25
+ end
@@ -0,0 +1,30 @@
1
+ # <%= @next_adr_id %>. ClamAV File Scanning
2
+
3
+ Date: <%= Date.today.iso8601 %>
4
+
5
+ ## Status
6
+
7
+ Accepted
8
+
9
+ ## Context
10
+
11
+ In order to satisfy the [RA-5](https://nvd.nist.gov/800-53/Rev4/control/RA-5)
12
+ control around vulnerability scanning, we wish to scan all user-uploaded files
13
+ with a malware detection service. These files are user-supplied, and thus
14
+ cannot fit into our other security controls built into the CI/CD pipeline.
15
+
16
+ ## Decision
17
+
18
+ We will run a ClamAV daemon, fronted by a REST API that we will pass all user-uploaded
19
+ files through as part of the upload process.
20
+ <% if terraform_dir_exists? %>
21
+ The ClamAV app is deployed along with other infrastructure by terraform.
22
+ <% else %>
23
+ The ClamAV app is based on a [separate government-controlled repository](https://github.com/18f/clamav-api-cg-app)
24
+ <% end %>
25
+
26
+ ## Consequences
27
+
28
+ While our user-supplied files will now have vulnerability protection, this architecture
29
+ does not provide scanning of the application itself. Therefore we must find other solutions
30
+ to addressing SI-3 or RA-5 in the context of the application.
@@ -0,0 +1,35 @@
1
+ require "rails_helper"
2
+
3
+ RSpec.describe FileScanJob, type: :job do
4
+ subject { described_class.new }
5
+ let(:scanned_file) { double(clean?: true) }
6
+ let(:unscanned_file) { double(id: 1, clean?: false, content_type: "text/plain", filename: "test.txt") }
7
+ let(:success_response) { double(success?: true) }
8
+ let(:error_response) { double(success?: false, body: "Error response body") }
9
+
10
+ it "deals with a nil argument" do
11
+ expect { subject.perform nil }.to_not raise_error
12
+ end
13
+
14
+ it "returns quickly if the file is already scanned" do
15
+ expect { subject.perform scanned_file }.to_not raise_error
16
+ end
17
+
18
+ it "updates the scan_status after scanning the file" do
19
+ now = Time.now
20
+ allow(Time).to receive(:now).and_return now
21
+ allow(unscanned_file).to receive(:open).and_yield __FILE__
22
+ expect(unscanned_file).to receive(:update_columns).with scan_status: "scanned", updated_at: Time.now
23
+ allow(subject).to receive(:connection).and_return double(post: success_response)
24
+ subject.perform unscanned_file
25
+ end
26
+
27
+ it "marks the file as quarantined when dirty" do
28
+ now = Time.now
29
+ allow(Time).to receive(:now).and_return now
30
+ allow(unscanned_file).to receive(:open).and_yield __FILE__
31
+ expect(unscanned_file).to receive(:update_columns).with scan_status: "quarantined", updated_at: Time.now
32
+ allow(subject).to receive(:connection).and_return double(post: error_response)
33
+ subject.perform unscanned_file
34
+ end
35
+ end
@@ -0,0 +1,38 @@
1
+ require "rails_helper"
2
+
3
+ RSpec.describe FileUpload, type: :model do
4
+ subject { described_class.new }
5
+
6
+ describe "validations" do
7
+ before do
8
+ subject.file.attach(io: File.open(__FILE__), filename: "file_upload_spec.rb")
9
+ end
10
+
11
+ %w[uploaded scan_failed scanned quarantined].each do |valid_status|
12
+ it "allows scan_status=#{valid_status}" do
13
+ pending "#{described_class.name} cannot be valid without a record to belong_to"
14
+ subject.scan_status = valid_status
15
+ expect(subject).to be_valid
16
+ end
17
+ end
18
+
19
+ it "is invalid with a bad scan_status" do
20
+ subject.scan_status = "invalid"
21
+ expect(subject).to_not be_valid
22
+ end
23
+ end
24
+
25
+ describe "#clean?" do
26
+ it "returns true when scan_status is scanned" do
27
+ subject.scan_status = "scanned"
28
+ expect(subject).to be_clean
29
+ end
30
+
31
+ it "returns false when scan_status is not scanned" do
32
+ subject.scan_status = "uploaded"
33
+ expect(subject).to_not be_clean
34
+ subject.scan_status = "quarantined"
35
+ expect(subject).to_not be_clean
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module RailsTemplate18f
6
+ module Generators
7
+ class CloudGovConfigGenerator < ::Rails::Generators::Base
8
+ include Base
9
+
10
+ desc <<~DESC
11
+ Description:
12
+ Install a helper class to retrieve configuration from ENV["VCAP_SERVICES"]
13
+ DESC
14
+
15
+ def install_climate_control
16
+ return if file_content("Gemfile").match?(/gem "climate_control"/)
17
+ gem_group :test do
18
+ gem "climate_control", "~> 1.0"
19
+ end
20
+ end
21
+
22
+ def install_model_and_test
23
+ copy_file "app/models/cloud_gov_config.rb"
24
+ copy_file "spec/models/cloud_gov_config_spec.rb"
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CloudGovConfig
4
+ ENV_VARIABLE = "VCAP_SERVICES"
5
+
6
+ def self.dig(*path)
7
+ return nil if ENV[ENV_VARIABLE].blank?
8
+ first, *rest = path
9
+ vcap_services[first]&.first&.dig(*rest)
10
+ end
11
+
12
+ def self.vcap_services
13
+ @vcap_services ||= JSON.parse(ENV[ENV_VARIABLE]).with_indifferent_access
14
+ end
15
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails_helper"
4
+
5
+ RSpec.describe CloudGovConfig, type: :model do
6
+ subject { described_class }
7
+
8
+ describe ".dig" do
9
+ context "VCAP_SERVICES is blank" do
10
+ it "returns nil" do
11
+ expect(subject.dig(:s3, :credentials, :bucket)).to be_nil
12
+ end
13
+ end
14
+
15
+ context "VCAP_SERVICES is set" do
16
+ let(:bucket_name) { "bucket-name" }
17
+ let(:vcap) {
18
+ {
19
+ s3: [
20
+ {
21
+ credentials: {
22
+ bucket: bucket_name
23
+ }
24
+ }
25
+ ]
26
+ }
27
+ }
28
+
29
+ around do |example|
30
+ ClimateControl.modify VCAP_SERVICES: vcap.to_json do
31
+ example.run
32
+ end
33
+ end
34
+
35
+ it "can find a path" do
36
+ expect(subject.dig(:s3, :credentials, :bucket)).to eq bucket_name
37
+ end
38
+
39
+ it "returns nil for a missing path" do
40
+ expect(subject.dig(:s3, :credentials, :other)).to be_nil
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "bundler"
5
+
6
+ module RailsTemplate18f
7
+ module Generators
8
+ class I18nJsGenerator < ::Rails::Generators::Base
9
+ include Base
10
+
11
+ desc <<~DESC
12
+ Description:
13
+ Install and configure i18n-js gem to provide translations to JS code.
14
+
15
+ By default, will only export translations with keys that match `*.js.*`
16
+ DESC
17
+
18
+ def install_gem_and_tasks
19
+ return if file_content("Gemfile").match?(/gem "i18n-js"/)
20
+ gem "i18n-js", "~> 3.9"
21
+ Bundler.with_original_env do
22
+ in_root do
23
+ run "bundle install"
24
+ run "yarn add i18n-js"
25
+ generate "i18n:js:config"
26
+ end
27
+ end
28
+ append_to_file "config/i18n-js.yml", <<~EOYAML
29
+ # remove `only` to include all translations
30
+ translations:
31
+ - file: "app/assets/builds/translations.js"
32
+ only: "*.js.*"
33
+ EOYAML
34
+ end
35
+
36
+ def configure_asset_pipeline
37
+ copy_file "lib/tasks/i18n.rake"
38
+ environment "config.middleware.use I18n::JS::Middleware", env: :development
39
+ insert_into_file "app/views/layouts/application.html.erb", indent(<<~EOHTML, 4), after: /<%= stylesheet_link_tag "application".*$\n/
40
+ <%= javascript_include_tag "i18n", "data-turbo-track": "reload" %>
41
+ <%= javascript_include_tag "translations", "data-turbo-track": "reload" %>
42
+ EOHTML
43
+ append_to_file "app/assets/config/manifest.js", <<~EOJS
44
+ //= link i18n.js
45
+ //= link translations.js
46
+ EOJS
47
+ end
48
+
49
+ def ignore_generated_file
50
+ unless skip_git?
51
+ append_to_file ".gitignore", <<~EOM
52
+
53
+ # Generated by i18n-js
54
+ /public/javascripts/i18n.js
55
+ EOM
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,9 @@
1
+ # export translations as part of asset precompile
2
+
3
+ Rake::Task["assets:precompile"].enhance(["i18n:js:export"])
4
+
5
+ if Rake::Task.task_defined?("test:prepare")
6
+ Rake::Task["test:prepare"].enhance(["i18n:js:export"])
7
+ elsif Rake::Task.task_defined?("db:test:prepare")
8
+ Rake::Task["db:test:prepare"].enhance(["i18n:js:export"])
9
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module RailsTemplate18f
6
+ module Generators
7
+ class SidekiqGenerator < ::Rails::Generators::Base
8
+ include Base
9
+
10
+ desc <<~DESC
11
+ Description:
12
+ Install Sidekiq and configure it as the ActiveJob backend
13
+ DESC
14
+
15
+ def install_gem
16
+ gem "sidekiq", "~> 6.4"
17
+ end
18
+
19
+ def configure_server_runner
20
+ append_to_file "Procfile.dev", "worker: bundle exec sidekiq\n"
21
+ insert_into_file "manifest.yml", indent(<<~EOYAML), after: /processes:$\n/
22
+ - type: worker
23
+ instances: ((worker_instances))
24
+ memory: ((worker_memory))
25
+ command: bundle exec sidekiq
26
+ EOYAML
27
+ insert_into_file "manifest.yml", "\n - #{app_name}-redis-((env))", after: "services:"
28
+ inside "config/deployment" do
29
+ append_to_file "staging.yml", <<~EOYAML
30
+ worker_instances: 1
31
+ worker_memory: 256M
32
+ EOYAML
33
+ append_to_file "production.yml", <<~EOYAML
34
+ worker_instances: 1
35
+ worker_memory: 512M
36
+ EOYAML
37
+ end
38
+ end
39
+
40
+ def configure_active_job
41
+ generate "rails_template18f:cloud_gov_config"
42
+ copy_file "config/initializers/redis.rb"
43
+ application "config.active_job.queue_adapter = :sidekiq"
44
+ end
45
+
46
+ def configure_sidekiq_ui
47
+ prepend_to_file "config/routes.rb", "require \"sidekiq/web\"\n\n"
48
+ route <<~EOR
49
+ if Rails.env.development?
50
+ mount Sidekiq::Web => "/sidekiq"
51
+ end
52
+ EOR
53
+ end
54
+
55
+ def update_boundary_diagram
56
+ boundary_filename = "doc/compliance/apps/application.boundary.md"
57
+
58
+ insert_into_file boundary_filename, indent(<<~EOB, 16), after: /ContainerDb\(app_db.*$\n/
59
+ Container(worker, "<&layers> Sidekiq workers", "Ruby #{ruby_version}, Sidekiq", "Perform background work and data processing")
60
+ ContainerDb(redis, "Redis Database", "AWS ElastiCache (Redis)", "Background job queue")
61
+ EOB
62
+ insert_into_file boundary_filename, <<~EOB, before: "@enduml"
63
+ Rel(app, redis, "enqueue job parameters", "redis")
64
+ Rel(worker, redis, "dequeues job parameters", "redis")
65
+ Rel(worker, app_db, "reads/writes primary data", "psql (5432)")
66
+ EOB
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ Rails.application.config.to_prepare do
4
+ redis_url = CloudGovConfig.dig "aws-elasticache-redis", "credentials", "uri"
5
+ if redis_url.present?
6
+ Sidekiq.configure_server do |config|
7
+ config.redis = {url: redis_url, ssl: true}
8
+ end
9
+
10
+ Sidekiq.configure_client do |config|
11
+ config.redis = {url: redis_url, ssl: true}
12
+ end
13
+ end
14
+ end
@@ -16,17 +16,49 @@ module "database" {
16
16
  recursive_delete = local.recursive_delete
17
17
  rds_plan_name = "TKTK-production-rds-plan"
18
18
  }
19
+ <% if has_active_job? %>
20
+ module "redis" {
21
+ source = "../shared/redis"
19
22
 
23
+ cf_user = var.cf_user
24
+ cf_password = var.cf_password
25
+ cf_org_name = local.cf_org_name
26
+ cf_space_name = local.cf_space_name
27
+ env = local.env
28
+ recursive_delete = local.recursive_delete
29
+ redis_plan_name = "TKTK-production-redis-plan"
30
+ }
31
+ <% end %>
20
32
  <% if has_active_storage? %>
21
33
  module "s3" {
22
34
  source = "../shared/s3"
23
35
 
24
- cf_user = var.cf_user
25
- cf_password = var.cf_password
26
- cf_org_name = local.cf_org_name
27
- cf_space_name = local.cf_space_name
28
- s3_service_name = "<%= app_name %>-s3-${local.env}"
36
+ cf_user = var.cf_user
37
+ cf_password = var.cf_password
38
+ cf_org_name = local.cf_org_name
39
+ cf_space_name = local.cf_space_name
40
+ recursive_delete = local.recursive_delete
41
+ s3_service_name = "<%= app_name %>-s3-${local.env}"<% if cloud_gov_organization == "sandbox-gsa" %>
42
+ s3_plan_name = "basic-sandbox"<% end %>
29
43
  }
44
+
45
+ ###########################################################################
46
+ # The following lines need to be commented out for the initial `terraform apply`
47
+ # It can be re-enabled after:
48
+ # 1) the app has first been deployed
49
+ # 2) Your organization has sufficient memory. Each clamav app requires 3GB
50
+ ###########################################################################
51
+ # module "clamav" {
52
+ # source = "../shared/clamav"
53
+ #
54
+ # cf_user = var.cf_user
55
+ # cf_password = var.cf_password
56
+ # cf_org_name = local.cf_org_name
57
+ # cf_space_name = local.cf_space_name
58
+ # env = local.env
59
+ # clamav_image = "ajilaag/clamav-rest:20211229"
60
+ # max_file_size = "30M"
61
+ # }
30
62
  <% end %>
31
63
 
32
64
  ###########################################################################
@@ -0,0 +1,50 @@
1
+ ###
2
+ # Target space/org
3
+ ###
4
+
5
+ data "cloudfoundry_space" "space" {
6
+ org_name = var.cf_org_name
7
+ name = var.cf_space_name
8
+ }
9
+
10
+ data "cloudfoundry_domain" "internal" {
11
+ name = "apps.internal"
12
+ }
13
+
14
+ data "cloudfoundry_app" "app" {
15
+ name_or_id = "<%= app_name %>-${var.env}"
16
+ space = data.cloudfoundry_space.space.id
17
+ }
18
+
19
+ ###
20
+ # ClamAV API app
21
+ ###
22
+
23
+ resource "cloudfoundry_route" "clamav_route" {
24
+ space = data.cloudfoundry_space.space.id
25
+ domain = data.cloudfoundry_domain.internal.id
26
+ hostname = "<%= app_name %>-clamapi-${var.env}"
27
+ }
28
+
29
+ resource "cloudfoundry_app" "clamav_api" {
30
+ name = "<%= app_name %>-clamav-api-${var.env}"
31
+ space = data.cloudfoundry_space.space.id
32
+ memory = var.clamav_memory
33
+ disk_quota = 2048
34
+ timeout = 600
35
+ docker_image = var.clamav_image
36
+ routes {
37
+ route = cloudfoundry_route.clamav_route.id
38
+ }
39
+ environment = {
40
+ MAX_FILE_SIZE = var.max_file_size
41
+ }
42
+ }
43
+
44
+ resource "cloudfoundry_network_policy" "clamav_routing" {
45
+ policy {
46
+ source_app = data.cloudfoundry_app.app.id
47
+ destination_app = cloudfoundry_app.clamav_api.id
48
+ port = "9443"
49
+ }
50
+ }
@@ -0,0 +1,16 @@
1
+ terraform {
2
+ required_version = "~> 1.0"
3
+ required_providers {
4
+ cloudfoundry = {
5
+ source = "cloudfoundry-community/cloudfoundry"
6
+ version = "0.15.0"
7
+ }
8
+ }
9
+ }
10
+
11
+ provider "cloudfoundry" {
12
+ api_url = var.cf_api_url
13
+ user = var.cf_user
14
+ password = var.cf_password
15
+ app_logs_max = 30
16
+ }
@@ -0,0 +1,47 @@
1
+ variable "cf_api_url" {
2
+ type = string
3
+ description = "cloud.gov api url"
4
+ default = "https://api.fr.cloud.gov"
5
+ }
6
+
7
+ variable "cf_user" {
8
+ type = string
9
+ description = "cloud.gov deployer account user"
10
+ }
11
+
12
+ variable "cf_password" {
13
+ type = string
14
+ description = "secret; cloud.gov deployer account password"
15
+ sensitive = true
16
+ }
17
+
18
+ variable "cf_org_name" {
19
+ type = string
20
+ description = "cloud.gov organization name"
21
+ }
22
+
23
+ variable "cf_space_name" {
24
+ type = string
25
+ description = "cloud.gov space name (staging or prod)"
26
+ }
27
+
28
+ variable "env" {
29
+ type = string
30
+ description = "deployment environment (staging, production)"
31
+ }
32
+
33
+ variable "clamav_image" {
34
+ type = string
35
+ description = "Docker image to deploy the clamav api app"
36
+ }
37
+
38
+ variable "clamav_memory" {
39
+ type = number
40
+ description = "Memory in MB to allocate to clamav app"
41
+ default = 3072
42
+ }
43
+
44
+ variable "max_file_size" {
45
+ type = string
46
+ description = "Maximum file size the API will accept for scanning"
47
+ }
@@ -0,0 +1,23 @@
1
+ ###
2
+ # Target space/org
3
+ ###
4
+
5
+ data "cloudfoundry_space" "space" {
6
+ org_name = var.cf_org_name
7
+ name = var.cf_space_name
8
+ }
9
+
10
+ ###
11
+ # RDS instance
12
+ ###
13
+
14
+ data "cloudfoundry_service" "redis" {
15
+ name = "aws-elasticache-redis"
16
+ }
17
+
18
+ resource "cloudfoundry_service_instance" "redis" {
19
+ name = "<%= app_name %>-redis-${var.env}"
20
+ space = data.cloudfoundry_space.space.id
21
+ service_plan = data.cloudfoundry_service.redis.service_plans[var.redis_plan_name]
22
+ recursive_delete = var.recursive_delete
23
+ }
@@ -0,0 +1,16 @@
1
+ terraform {
2
+ required_version = "~> 1.0"
3
+ required_providers {
4
+ cloudfoundry = {
5
+ source = "cloudfoundry-community/cloudfoundry"
6
+ version = "0.15.0"
7
+ }
8
+ }
9
+ }
10
+
11
+ provider "cloudfoundry" {
12
+ api_url = var.cf_api_url
13
+ user = var.cf_user
14
+ password = var.cf_password
15
+ app_logs_max = 30
16
+ }
@@ -0,0 +1,42 @@
1
+ variable "cf_api_url" {
2
+ type = string
3
+ description = "cloud.gov api url"
4
+ default = "https://api.fr.cloud.gov"
5
+ }
6
+
7
+ variable "cf_user" {
8
+ type = string
9
+ description = "cloud.gov deployer account user"
10
+ }
11
+
12
+ variable "cf_password" {
13
+ type = string
14
+ description = "secret; cloud.gov deployer account password"
15
+ sensitive = true
16
+ }
17
+
18
+ variable "cf_org_name" {
19
+ type = string
20
+ description = "cloud.gov organization name"
21
+ }
22
+
23
+ variable "cf_space_name" {
24
+ type = string
25
+ description = "cloud.gov space name (staging or prod)"
26
+ }
27
+
28
+ variable "env" {
29
+ type = string
30
+ description = "deployment environment (staging, production)"
31
+ }
32
+
33
+ variable "recursive_delete" {
34
+ type = bool
35
+ description = "when true, deletes service bindings attached to the resource (not recommended for production)"
36
+ default = false
37
+ }
38
+
39
+ variable "redis_plan_name" {
40
+ type = string
41
+ description = "name of the service plan name to create"
42
+ }
@@ -16,15 +16,47 @@ module "database" {
16
16
  recursive_delete = local.recursive_delete
17
17
  rds_plan_name = "micro-psql"
18
18
  }
19
+ <% if has_active_job? %>
20
+ module "redis" {
21
+ source = "../shared/redis"
19
22
 
23
+ cf_user = var.cf_user
24
+ cf_password = var.cf_password
25
+ cf_org_name = local.cf_org_name
26
+ cf_space_name = local.cf_space_name
27
+ env = local.env
28
+ recursive_delete = local.recursive_delete
29
+ redis_plan_name = "redis-dev"
30
+ }
31
+ <% end %>
20
32
  <% if has_active_storage? %>
21
33
  module "s3" {
22
34
  source = "../shared/s3"
23
35
 
24
- cf_user = var.cf_user
25
- cf_password = var.cf_password
26
- cf_org_name = local.cf_org_name
27
- cf_space_name = local.cf_space_name
28
- s3_service_name = "<%= app_name %>-s3-${local.env}"
36
+ cf_user = var.cf_user
37
+ cf_password = var.cf_password
38
+ cf_org_name = local.cf_org_name
39
+ cf_space_name = local.cf_space_name
40
+ recursive_delete = local.recursive_delete
41
+ s3_service_name = "<%= app_name %>-s3-${local.env}"<% if cloud_gov_organization == "sandbox-gsa" %>
42
+ s3_plan_name = "basic-sandbox"<% end %>
29
43
  }
44
+
45
+ ###########################################################################
46
+ # The following lines need to be commented out for the initial `terraform apply`
47
+ # It can be re-enabled after:
48
+ # 1) the app has first been deployed
49
+ # 2) Your organization has sufficient memory. Each clamav app requires 3GB
50
+ ###########################################################################
51
+ # module "clamav" {
52
+ # source = "../shared/clamav"
53
+ #
54
+ # cf_user = var.cf_user
55
+ # cf_password = var.cf_password
56
+ # cf_org_name = local.cf_org_name
57
+ # cf_space_name = local.cf_space_name
58
+ # env = local.env
59
+ # clamav_image = "ajilaag/clamav-rest:20211229"
60
+ # max_file_size = "30M"
61
+ # }
30
62
  <% end %>
@@ -79,17 +79,6 @@ module RailsTemplate18f
79
79
  EOM
80
80
  end
81
81
  end
82
-
83
- private
84
-
85
- def terraform_dir_exists?
86
- # prevents cloud_gov_* helpers from trying to read non-existant .tf files
87
- false
88
- end
89
-
90
- def has_active_storage?
91
- defined?(::ActiveStorage)
92
- end
93
82
  end
94
83
  end
95
84
  end
@@ -0,0 +1,19 @@
1
+ require "rails/app_updater"
2
+
3
+ module AppUpdaterOptions
4
+ extend ActiveSupport::Concern
5
+
6
+ class_methods do
7
+ def generator_options
8
+ options = super
9
+ # These options all end up hardcoded to true in the default `rails app:update`
10
+ options[:skip_active_job] = !defined?(ActiveJob::Railtie)
11
+ options[:skip_action_mailbox] = !defined?(ActionMailbox::Engine)
12
+ options[:skip_action_text] = !defined?(ActionText::Engine)
13
+ options[:skip_test] = !defined?(Rails::TestUnitRailtie)
14
+ options
15
+ end
16
+ end
17
+ end
18
+
19
+ Rails::AppUpdater.prepend(AppUpdaterOptions)
@@ -6,10 +6,6 @@ module RailsTemplate18f
6
6
  extend ActiveSupport::Concern
7
7
  include ::Rails::Generators::AppName
8
8
 
9
- included do
10
- self.source_path = RailsTemplate18f::Generators.const_source_location(name).first
11
- end
12
-
13
9
  class_methods do
14
10
  attr_accessor :source_path
15
11
 
@@ -18,19 +14,40 @@ module RailsTemplate18f
18
14
  end
19
15
  end
20
16
 
17
+ included do
18
+ self.source_path = RailsTemplate18f::Generators.const_source_location(name).first
19
+ end
20
+
21
21
  private
22
22
 
23
23
  def file_content(filename)
24
- File.read(File.expand_path(filename, destination_root))
24
+ file_path = File.expand_path(filename, destination_root)
25
+ if File.exist? file_path
26
+ File.read(file_path)
27
+ else
28
+ ""
29
+ end
25
30
  end
26
31
 
27
32
  def ruby_version
28
33
  RUBY_VERSION
29
34
  end
30
35
 
36
+ def terraform_dir_exists?
37
+ Dir.exist? File.expand_path("terraform", destination_root)
38
+ end
39
+
31
40
  def skip_git?
32
41
  !Dir.exist?(File.expand_path(".git", destination_root))
33
42
  end
43
+
44
+ def has_active_job?
45
+ defined?(::ActiveJob)
46
+ end
47
+
48
+ def has_active_storage?
49
+ defined?(::ActiveStorage)
50
+ end
34
51
  end
35
52
  end
36
53
  end
@@ -48,10 +48,6 @@ module RailsTemplate18f
48
48
  end
49
49
  "prod"
50
50
  end
51
-
52
- def terraform_dir_exists?
53
- Dir.exist? File.expand_path("terraform", destination_root)
54
- end
55
51
  end
56
52
  end
57
53
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsTemplate18f
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
data/template.rb CHANGED
@@ -10,6 +10,10 @@ def skip_git?
10
10
  !!options[:skip_git]
11
11
  end
12
12
 
13
+ def skip_active_job?
14
+ !!options[:skip_active_job]
15
+ end
16
+
13
17
  def webpack?
14
18
  adjusted_javascript_option == "webpack"
15
19
  end
@@ -339,6 +343,18 @@ if terraform
339
343
  register_announcement("Terraform", "Run the bootstrap script and update the appropriate CI/CD environment variables defined in the Deployment section of the README")
340
344
  end
341
345
 
346
+ if !skip_active_job?
347
+ after_bundle do
348
+ generate "rails_template18f:sidekiq"
349
+ end
350
+ end
351
+
352
+ if !skip_active_storage?
353
+ after_bundle do
354
+ generate "rails_template18f:active_storage"
355
+ end
356
+ end
357
+
342
358
  if @github_actions
343
359
  after_bundle do
344
360
  generator_arguments = [
@@ -1,3 +1,3 @@
1
1
  env: staging
2
2
  web_instances: 1
3
- web_memory: 512M
3
+ web_memory: 256M
@@ -1,7 +1,6 @@
1
1
  require_relative "./production"
2
2
 
3
3
  Rails.application.configure do
4
- config.assets.compile = true
5
4
  config.public_file_server.enabled = true
6
5
 
7
6
  logger = ActiveSupport::Logger.new($stdout)
@@ -30,15 +30,11 @@ Boundary(aws, "AWS GovCloud") {
30
30
  System_Boundary(inventory, "Application") {
31
31
  Container(app, "<&layers> <%= app_name.titleize %>", "Ruby <%= @ruby_version %>, Rails <%= Rails.version %>", "TKTK Application Description")
32
32
  ContainerDb(app_db, "Application DB", "AWS RDS (PostgreSQL)", "Primary data storage")
33
- <% if !skip_active_storage? %>
34
- ContainerDb(app_s3, "File Storage", "AWS S3", "User-uploaded file storage")
35
- <% end %>
36
33
  }
37
34
  }
38
35
  }
39
36
  }
40
37
 
41
-
42
38
  Boundary(gsa_saas, "GSA-authorized SaaS") {
43
39
  }
44
40
 
@@ -49,9 +45,6 @@ Rel(browser, aws_alb, "request info, submit requests", "https GET/POST (443)")
49
45
  Rel(aws_alb, cloudgov_router, "proxies requests", "https GET/POST (443)")
50
46
  Rel(cloudgov_router, app, "proxies requests", "https GET/POST (443)")
51
47
  Rel(app, app_db, "reads/writes primary data", "psql (5432)")
52
- <% if !skip_active_storage? %>
53
- Rel(app, app_s3, "reads/writes file data", "https (443)")
54
- <% end %>
55
48
  @enduml
56
49
  ```
57
50
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_template_18f
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Ahearn
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-02-17 00:00:00.000000000 Z
11
+ date: 2022-02-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: railties
@@ -114,11 +114,20 @@ files:
114
114
  - bin/console
115
115
  - bin/setup
116
116
  - exe/rails_template_18f
117
+ - lib/generators/rails_template18f/active_storage/active_storage_generator.rb
118
+ - lib/generators/rails_template18f/active_storage/templates/app/jobs/file_scan_job.rb
119
+ - lib/generators/rails_template18f/active_storage/templates/app/models/file_upload.rb
120
+ - lib/generators/rails_template18f/active_storage/templates/doc/adr/clamav.md.tt
121
+ - lib/generators/rails_template18f/active_storage/templates/spec/jobs/file_scan_job_spec.rb
122
+ - lib/generators/rails_template18f/active_storage/templates/spec/models/file_upload_spec.rb
117
123
  - lib/generators/rails_template18f/circleci/circleci_generator.rb
118
124
  - lib/generators/rails_template18f/circleci/templates/Dockerfile.tt
119
125
  - lib/generators/rails_template18f/circleci/templates/bin/ci-server-start
120
126
  - lib/generators/rails_template18f/circleci/templates/circleci/config.yml.tt
121
127
  - lib/generators/rails_template18f/circleci/templates/docker-compose.ci.yml
128
+ - lib/generators/rails_template18f/cloud_gov_config/cloud_gov_config_generator.rb
129
+ - lib/generators/rails_template18f/cloud_gov_config/templates/app/models/cloud_gov_config.rb
130
+ - lib/generators/rails_template18f/cloud_gov_config/templates/spec/models/cloud_gov_config_spec.rb
122
131
  - lib/generators/rails_template18f/dap/dap_generator.rb
123
132
  - lib/generators/rails_template18f/github_actions/github_actions_generator.rb
124
133
  - lib/generators/rails_template18f/github_actions/templates/github/actions/run-server/action.yml
@@ -139,8 +148,12 @@ files:
139
148
  - lib/generators/rails_template18f/i18n/templates/config/locales/es.yml
140
149
  - lib/generators/rails_template18f/i18n/templates/config/locales/fr.yml
141
150
  - lib/generators/rails_template18f/i18n/templates/config/locales/zh.yml
151
+ - lib/generators/rails_template18f/i18n_js/i18n_js_generator.rb
152
+ - lib/generators/rails_template18f/i18n_js/templates/lib/tasks/i18n.rake
142
153
  - lib/generators/rails_template18f/newrelic/newrelic_generator.rb
143
154
  - lib/generators/rails_template18f/newrelic/templates/config/newrelic.yml.tt
155
+ - lib/generators/rails_template18f/sidekiq/sidekiq_generator.rb
156
+ - lib/generators/rails_template18f/sidekiq/templates/config/initializers/redis.rb
144
157
  - lib/generators/rails_template18f/terraform/templates/terraform/README.md.tt
145
158
  - lib/generators/rails_template18f/terraform/templates/terraform/bootstrap/import.sh
146
159
  - lib/generators/rails_template18f/terraform/templates/terraform/bootstrap/main.tf.tt
@@ -153,12 +166,18 @@ files:
153
166
  - lib/generators/rails_template18f/terraform/templates/terraform/production/main.tf.tt
154
167
  - lib/generators/rails_template18f/terraform/templates/terraform/production/providers.tf.tt
155
168
  - lib/generators/rails_template18f/terraform/templates/terraform/production/variables.tf
169
+ - lib/generators/rails_template18f/terraform/templates/terraform/shared/clamav/main.tf.tt
170
+ - lib/generators/rails_template18f/terraform/templates/terraform/shared/clamav/providers.tf
171
+ - lib/generators/rails_template18f/terraform/templates/terraform/shared/clamav/variables.tf
156
172
  - lib/generators/rails_template18f/terraform/templates/terraform/shared/database/main.tf.tt
157
173
  - lib/generators/rails_template18f/terraform/templates/terraform/shared/database/providers.tf
158
174
  - lib/generators/rails_template18f/terraform/templates/terraform/shared/database/variables.tf
159
175
  - lib/generators/rails_template18f/terraform/templates/terraform/shared/domain/main.tf.tt
160
176
  - lib/generators/rails_template18f/terraform/templates/terraform/shared/domain/providers.tf
161
177
  - lib/generators/rails_template18f/terraform/templates/terraform/shared/domain/variables.tf
178
+ - lib/generators/rails_template18f/terraform/templates/terraform/shared/redis/main.tf.tt
179
+ - lib/generators/rails_template18f/terraform/templates/terraform/shared/redis/providers.tf
180
+ - lib/generators/rails_template18f/terraform/templates/terraform/shared/redis/variables.tf
162
181
  - lib/generators/rails_template18f/terraform/templates/terraform/shared/s3/main.tf
163
182
  - lib/generators/rails_template18f/terraform/templates/terraform/shared/s3/providers.tf
164
183
  - lib/generators/rails_template18f/terraform/templates/terraform/shared/s3/variables.tf
@@ -166,6 +185,7 @@ files:
166
185
  - lib/generators/rails_template18f/terraform/templates/terraform/staging/providers.tf.tt
167
186
  - lib/generators/rails_template18f/terraform/templates/terraform/staging/variables.tf
168
187
  - lib/generators/rails_template18f/terraform/terraform_generator.rb
188
+ - lib/rails_template18f/app_updater.rb
169
189
  - lib/rails_template18f/generators.rb
170
190
  - lib/rails_template18f/generators/base.rb
171
191
  - lib/rails_template18f/generators/cloud_gov_options.rb