├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE.md
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── documentation.md
│ ├── feature_request.md
│ └── question.md
└── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── .rspec
├── CHANGELOG.md
├── CONDUCT.md
├── CONTRIBUTING.md
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── backers.md
├── exe
└── jets
├── jets.gemspec
├── lib
├── jets.rb
└── jets
│ ├── api.rb
│ ├── api
│ ├── agree.rb
│ ├── base.rb
│ ├── client.rb
│ ├── config.rb
│ ├── error.rb
│ ├── ping.rb
│ ├── project.rb
│ ├── release.rb
│ ├── response.rb
│ ├── sig.rb
│ └── stack.rb
│ ├── autoloaders.rb
│ ├── autoloaders
│ ├── gem.rb
│ └── main.rb
│ ├── aws_services.rb
│ ├── aws_services
│ ├── aws_helpers.rb
│ ├── aws_info.rb
│ ├── global_memoist.rb
│ ├── s3_bucket.rb
│ └── stack_status.rb
│ ├── bundle.rb
│ ├── camelizer.rb
│ ├── cfn
│ ├── base.rb
│ ├── bootstrap.rb
│ ├── builder
│ │ ├── interface.rb
│ │ ├── parent
│ │ │ └── genesis.rb
│ │ └── post_process.rb
│ ├── delete.rb
│ ├── deploy.rb
│ ├── iam
│ │ ├── managed_policy.rb
│ │ └── policy.rb
│ ├── resource.rb
│ ├── resource
│ │ ├── associated.rb
│ │ ├── associated_outputs.rb
│ │ ├── codebuild
│ │ │ ├── fleet.rb
│ │ │ ├── iam_role.rb
│ │ │ └── project
│ │ │ │ ├── base.rb
│ │ │ │ ├── ec2.rb
│ │ │ │ ├── env.rb
│ │ │ │ ├── format_env.rb
│ │ │ │ └── lambda.rb
│ │ ├── replacer.rb
│ │ ├── s3
│ │ │ ├── bucket.rb
│ │ │ └── jets_bucket.rb
│ │ └── standardizer.rb
│ ├── rollback.rb
│ ├── stack.rb
│ ├── stack
│ │ ├── deployable.rb
│ │ ├── rollback.rb
│ │ ├── template.rb
│ │ └── yamler.rb
│ ├── status.rb
│ ├── teardown.rb
│ └── teardown
│ │ └── bucket.rb
│ ├── cli.rb
│ ├── cli
│ ├── base.rb
│ ├── bootstrap.rb
│ ├── build.rb
│ ├── call.rb
│ ├── ci.rb
│ ├── ci
│ │ ├── base.rb
│ │ ├── build.rb
│ │ ├── delete.rb
│ │ ├── deploy.rb
│ │ ├── info.rb
│ │ ├── init.rb
│ │ ├── logs.rb
│ │ ├── start.rb
│ │ ├── status.rb
│ │ ├── stop.rb
│ │ ├── tailer.rb
│ │ └── templates
│ │ │ └── ci.rb.tt
│ ├── clean.rb
│ ├── concurrency.rb
│ ├── concurrency
│ │ ├── base.rb
│ │ ├── get.rb
│ │ ├── info.rb
│ │ ├── set.rb
│ │ └── unset.rb
│ ├── curl.rb
│ ├── curl
│ │ ├── adapter
│ │ │ ├── base.rb
│ │ │ ├── cookies
│ │ │ │ ├── jar.rb
│ │ │ │ └── parser.rb
│ │ │ └── lambda.rb
│ │ └── request.rb
│ ├── delete.rb
│ ├── deploy.rb
│ ├── dotenv.rb
│ ├── dotenv
│ │ ├── base.rb
│ │ ├── get.rb
│ │ ├── list.rb
│ │ ├── set.rb
│ │ ├── ssm.rb
│ │ └── unset.rb
│ ├── env.rb
│ ├── env
│ │ ├── base.rb
│ │ ├── get.rb
│ │ ├── list.rb
│ │ ├── parse.rb
│ │ ├── set.rb
│ │ └── unset.rb
│ ├── exec.rb
│ ├── exec
│ │ ├── command.rb
│ │ ├── repl.rb
│ │ └── repl
│ │ │ └── history.rb
│ ├── functions.rb
│ ├── generate.rb
│ ├── generate
│ │ ├── event.rb
│ │ └── templates
│ │ │ ├── application_event.rb.tt
│ │ │ └── event_types
│ │ │ ├── application_event.rb
│ │ │ ├── dynamodb.rb.tt
│ │ │ ├── iot.rb.tt
│ │ │ ├── kinesis.rb.tt
│ │ │ ├── log.rb.tt
│ │ │ ├── rule.rb.tt
│ │ │ ├── s3.rb.tt
│ │ │ ├── scheduled.rb.tt
│ │ │ ├── sns.rb.tt
│ │ │ └── sqs.rb.tt
│ ├── git.rb
│ ├── git
│ │ └── push.rb
│ ├── group
│ │ ├── actions.rb
│ │ ├── base.rb
│ │ └── helpers.rb
│ ├── help.rb
│ ├── help
│ │ ├── build.md
│ │ ├── call.md
│ │ ├── concurrency
│ │ │ └── info.md
│ │ ├── curl.md
│ │ ├── delete.md
│ │ ├── deploy.md
│ │ ├── dotenv
│ │ │ ├── get.md
│ │ │ ├── list.md
│ │ │ ├── set.md
│ │ │ └── unset.md
│ │ ├── exec.md
│ │ ├── generate
│ │ │ └── event.md
│ │ ├── logs.md
│ │ ├── projects.md
│ │ ├── release
│ │ │ └── history.md
│ │ ├── rollback.md
│ │ ├── schedule
│ │ │ └── validate.md
│ │ ├── stacks.md
│ │ ├── status.md
│ │ └── url.md
│ ├── init.rb
│ ├── init
│ │ └── templates
│ │ │ ├── config
│ │ │ └── jets
│ │ │ │ ├── bootstrap.rb.tt
│ │ │ │ ├── deploy.rb.tt
│ │ │ │ └── project.rb.tt
│ │ │ └── env
│ │ │ └── .env.tt
│ ├── lambda
│ │ ├── checks.rb
│ │ ├── function.rb
│ │ ├── functions.rb
│ │ └── lookup.rb
│ ├── login.rb
│ ├── logout.rb
│ ├── logs.rb
│ ├── maintenance.rb
│ ├── maintenance
│ │ ├── base.rb
│ │ ├── mode.rb
│ │ ├── web.rb
│ │ ├── worker.rb
│ │ └── worker
│ │ │ ├── base.rb
│ │ │ ├── restorer.rb
│ │ │ ├── saver.rb
│ │ │ └── zeroer.rb
│ ├── package.rb
│ ├── package
│ │ └── dockerfile.rb
│ ├── ping.rb
│ ├── projects.rb
│ ├── release.rb
│ ├── release
│ │ ├── base.rb
│ │ ├── history.rb
│ │ ├── info.rb
│ │ └── rollback.rb
│ ├── schedule.rb
│ ├── schedule
│ │ ├── base.rb
│ │ ├── translate.rb
│ │ └── validate.rb
│ ├── stacks.rb
│ ├── stop.rb
│ ├── teardown.rb
│ ├── tip.rb
│ ├── url.rb
│ ├── waf.rb
│ └── waf
│ │ ├── base.rb
│ │ ├── build.rb
│ │ ├── delete.rb
│ │ ├── deploy.rb
│ │ ├── info.rb
│ │ ├── init.rb
│ │ └── templates
│ │ └── waf.rb.tt
│ ├── code.rb
│ ├── code
│ ├── copy
│ │ ├── base.rb
│ │ ├── full.rb
│ │ ├── git_copy.rb
│ │ ├── git_inline.rb
│ │ └── rsync.rb
│ ├── dummy.rb
│ ├── stager.rb
│ └── user.rb
│ ├── core.rb
│ ├── core
│ ├── booter.rb
│ └── config
│ │ ├── base.rb
│ │ ├── bootstrap.rb
│ │ ├── bootstrap
│ │ ├── cfn.rb
│ │ ├── code.rb
│ │ ├── codebuild.rb
│ │ └── s3_bucket.rb
│ │ ├── helpers.rb
│ │ ├── helpers
│ │ └── ssm.rb
│ │ ├── info.rb
│ │ └── project.rb
│ ├── core_ext.rb
│ ├── core_ext
│ ├── bundler.rb
│ ├── file.rb
│ └── symbol.rb
│ ├── dotenv.rb
│ ├── dotenv
│ ├── convention.rb
│ ├── ssm.rb
│ └── var.rb
│ ├── event
│ ├── base.rb
│ ├── dsl.rb
│ ├── dsl
│ │ ├── dynamodb_event.rb
│ │ ├── iot_event.rb
│ │ ├── kinesis_event.rb
│ │ ├── log_event.rb
│ │ ├── rate_expression.rb
│ │ ├── s3_event.rb
│ │ ├── scheduled_event.rb
│ │ ├── sns_event.rb
│ │ └── sqs_event.rb
│ ├── helpers
│ │ ├── kinesis_event.rb
│ │ ├── log_event.rb
│ │ ├── s3_event.rb
│ │ ├── sns_event.rb
│ │ └── sqs_event.rb
│ └── s3.rb
│ ├── exception_reporting.rb
│ ├── framework.rb
│ ├── git.rb
│ ├── git
│ ├── azure.rb
│ ├── base.rb
│ ├── bitbucket.rb
│ ├── circleci.rb
│ ├── codebuild.rb
│ ├── custom.rb
│ ├── git_cli.rb
│ ├── github.rb
│ ├── gitlab.rb
│ ├── info.rb
│ ├── local.rb
│ ├── saved.rb
│ └── user.rb
│ ├── lambda
│ ├── definition.rb
│ ├── dsl.rb
│ ├── function.rb
│ ├── function_constructor.rb
│ └── functions.rb
│ ├── names.rb
│ ├── prewarm.rb
│ ├── rdoc.rb
│ ├── remote
│ ├── base.rb
│ ├── download.rb
│ ├── runner.rb
│ └── tailer.rb
│ ├── shim.rb
│ ├── shim
│ ├── adapter
│ │ ├── alb.rb
│ │ ├── apigw.rb
│ │ ├── base.rb
│ │ ├── command.rb
│ │ ├── event.rb
│ │ ├── fallback.rb
│ │ ├── lambda.rb
│ │ ├── prewarm.rb
│ │ └── web.rb
│ ├── config.rb
│ ├── handler.rb
│ ├── maintenance.rb
│ ├── maintenance
│ │ └── maintenance.html
│ └── response
│ │ ├── alb.rb
│ │ ├── apigw.rb
│ │ ├── base.rb
│ │ ├── lambda.rb
│ │ └── web.rb
│ ├── stack.rb
│ ├── stack
│ ├── dsl.rb
│ ├── dsl
│ │ ├── main.rb
│ │ ├── main
│ │ │ ├── base.rb
│ │ │ ├── cloudwatch.rb
│ │ │ ├── iam.rb
│ │ │ ├── kinesis.rb
│ │ │ ├── lambda.rb
│ │ │ ├── s3.rb
│ │ │ ├── sns.rb
│ │ │ └── sqs.rb
│ │ ├── output.rb
│ │ ├── parameter.rb
│ │ └── resource.rb
│ └── outputs.rb
│ ├── thor
│ ├── auth.rb
│ ├── base.rb
│ ├── help.rb
│ ├── project_check.rb
│ ├── shared_options.rb
│ └── version_check.rb
│ ├── util
│ ├── call_line.rb
│ ├── camelize.rb
│ ├── format_time.rb
│ ├── git.rb
│ ├── logging.rb
│ ├── pretty.rb
│ ├── sh.rb
│ ├── sure.rb
│ ├── truthy.rb
│ └── yamler.rb
│ └── version.rb
└── spec
├── fixtures
└── shim
│ ├── events
│ ├── alb.json
│ ├── apigw.json
│ └── lambda.json
│ ├── frameworks
│ ├── rails
│ │ └── config.ru
│ ├── sinatra
│ │ └── config.ru
│ └── sinatra_modular
│ │ ├── app.rb
│ │ └── config.ru
│ ├── handlers
│ └── controller.rb
│ └── rack
│ ├── rack-apigw.txt
│ ├── rails-apigw.txt
│ └── rails-local.txt
├── lib
└── jets
│ ├── cli
│ └── curl
│ │ └── request_spec.rb
│ └── shim
│ ├── adapter
│ ├── alb_spec.rb
│ ├── apigw_spec.rb
│ ├── lambda_spec.rb
│ └── response
│ │ ├── alb_spec.rb
│ │ └── lambda_spec.rb
│ └── config_spec.rb
└── spec_helper.rb
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: boltops-tools
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | Please fill out one of the templates on https://github.com/tongueroo/jets/issues/new/choose
2 |
3 | If you want to ask a question please do so on the Jets Community forum: https://community.boltops.com
4 |
5 | To be sensitive to everyone's time, we may close issues asking questions without comment. Posting your questions in the Jets community forum is the best place. It also benefits others by making the questions easier to find. Here are some additional options also http://rubyonjets.com/docs/support/ 👌
6 |
7 | Thank you!
8 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/documentation.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Documentation
3 | about: Found a typo or something that isn't crystal clear in the docs?
4 | title: ''
5 | labels: docs
6 | assignees: ''
7 |
8 | ---
9 |
10 |
17 |
18 | ## Motivation
19 |
20 |
21 |
22 |
23 | ## Suggestion
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: New Feature Suggestion
3 | about: Want to add a feature to Jets?
4 | title: ''
5 | labels: feature
6 | assignees: ''
7 |
8 | ---
9 |
10 |
15 |
16 | ## Summary
17 |
18 |
21 |
22 | ## Motivation
23 |
24 |
29 |
30 | ## Guide-level explanation
31 |
32 |
41 |
42 | ## Reference-level explanation
43 |
44 |
53 |
54 | ## Drawbacks
55 |
56 |
59 |
60 | ## Unresolved Questions
61 |
62 |
65 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/question.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Question
3 | about: Have any questions about how Jets works?
4 | title: ''
5 | labels: 'question'
6 | assignees: ''
7 |
8 | ---
9 |
10 | The Jets issue tracker IS NOT for usage questions! Please post your question on our dedicated forum at https://community.boltops.com
11 |
12 | To be sensitive to everyone's time, we may close issues asking questions without comment. If you repeatedly post questions in the issues tracker, you may be blocked from ever submitting issues to Jets again. Please use your best judgment. 👍
13 |
14 | Posting your questions in the Jets community forum benefits others by grouping questions in a dedicated place. Here are some additional options also http://rubyonjets.com/docs/support/ 😁
15 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
6 |
7 |
10 |
11 |
12 |
13 |
14 |
15 |
20 |
21 | - [ ] I've added tests (if it's a bug, feature or enhancement)
22 | - [ ] I've adjusted the documentation (if it's a feature or enhancement)
23 | - [ ] The test suite passes (run `bundle exec rspec` to verify this)
24 |
25 | ## Summary
26 |
27 |
30 |
31 | ## Context
32 |
33 |
36 |
37 | ## How to Test
38 |
39 |
42 |
43 |
44 | ## Version Changes
45 |
46 |
50 |
51 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | *.rbc
3 | .bundle
4 | .byebug_history
5 | .config
6 | .yardoc
7 | _yardoc
8 | coverage
9 | doc/
10 | docs/
11 | InstalledFiles
12 | lib/bundler/man
13 | pkg
14 | rdoc
15 | spec/reports
16 | test/tmp
17 | test/version_tmp
18 | tmp
19 | node_modules
20 |
21 | .codebuild/definitions
22 | /html
23 | /demo
24 | Gemfile.lock
25 | spec/fixtures/demo/dynamodb/migrate
26 | spec/fixtures/demo/public/assets
27 | spec/fixtures/project/handlers
28 | yarn.lock
29 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --color
2 | --exclude-pattern spec/fixtures/apps/**/*
3 | --format documentation
4 | --require spec_helper
5 |
--------------------------------------------------------------------------------
/CONDUCT.md:
--------------------------------------------------------------------------------
1 | http://rubyonjets.com/docs/conduct/
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | https://rubyonjets.com/docs/more/contributing/
2 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | # Specify your gem dependencies in jets.gemspec
4 | gemspec
5 |
6 | # required here for specs
7 | group :development, :test do
8 | gem "dynomite", ">= 2.0.0"
9 | gem "mysql2", ">= 0.5.2"
10 | end
11 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) Tung Nguyen
2 |
3 | MIT License
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | "Software"), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
4 |
5 | [](https://badge.fury.io/rb/jets)
6 |
7 | [](https://www.boltops.com)
8 |
9 | Please **watch/star** this repo to help grow and support the project.
10 |
11 | ## What is Jets?
12 |
13 | [Jets](https://www.rubyonjets.com/) is a Serverless Deployment Service. Jets deploys Serverless infrastructure resources to your AWS account, where you can control the concurrency and scale resources as much as you want.
14 |
15 | Jets makes it easy to deploy and run your app on Serverless. It packages up your code and runs it on [AWS Lambda](https://aws.amazon.com/lambda/). Jets can deploy Rails, Sinatra, and any Rack app.
16 |
17 | ## Quick Start
18 |
19 | Add to your project.
20 |
21 | Gemfile
22 |
23 | ```ruby
24 | gem "jets-rails", ">= 1.0"
25 | gem "jets", ">= 6.0"
26 | ```
27 |
28 | And run
29 |
30 | jets init
31 | jets deploy
32 |
33 | That's it.
34 |
35 | ## Docs
36 |
37 | Official Docs: [docs.rubyonjets.com](https://docs.rubyonjets.com)
38 |
39 | Getting Started Learn Guides:
40 |
41 | * [Rails](https://docs.rubyonjets.com/docs/learn/rails/)
42 | * [Sinatra](https://docs.rubyonjets.com/docs/learn/sinatra/)
43 | * [Rack](https://docs.rubyonjets.com/docs/learn/rack/)
44 | * [Events](https://docs.rubyonjets.com/docs/learn/events/)
45 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/setup"
2 | Bundler.setup
3 | require "bundler/gem_tasks"
4 | require "rspec/core/rake_task"
5 |
6 | task default: :spec
7 |
8 | RSpec::Core::RakeTask.new
9 |
10 | require_relative "lib/jets"
11 |
12 | # Thanks: https://docs.ruby-lang.org/en/2.1.0/RDoc/Task.html
13 | require "rdoc/task"
14 | require "jets/rdoc"
15 |
16 | RDoc::Task.new do |rdoc|
17 | rdoc.options += Jets::Rdoc.options
18 | end
19 |
--------------------------------------------------------------------------------
/backers.md:
--------------------------------------------------------------------------------
1 | Backers
2 |
3 | [Jets](http://rubyonjets.com/) is an MIT-licensed open source project. It's an independent project with its ongoing development made possible entirely thanks to the support by these awesome [backers](https://github.com/tongueroo/jets/blob/master/backers.md). If you'd like to join them, please consider:
4 |
5 | - [Become a backer or sponsor on Patreon](https://www.patreon.com/tongueroo).
6 |
7 | Funds donated via Patreon go directly to support Tung Nguyen's full-time work on Jets.
8 |
9 |
10 |
11 | Backers via Patreon
12 |
13 |
14 | - Theron Welch
15 |
16 |
--------------------------------------------------------------------------------
/exe/jets:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | $:.unshift(File.expand_path("../lib", __dir__))
4 | require "jets"
5 | require "jets/cli"
6 | Jets::CLI.start(ARGV)
7 |
--------------------------------------------------------------------------------
/lib/jets.rb:
--------------------------------------------------------------------------------
1 | $stdout.sync = true unless ENV["JETS_STDOUT_SYNC"] == "0"
2 | $:.unshift(File.expand_path("../", __FILE__))
3 |
4 | require "active_support"
5 | require "active_support/concern"
6 | require "active_support/core_ext"
7 | require "active_support/dependencies"
8 | require "active_support/ordered_hash"
9 | require "active_support/ordered_options"
10 | require "cfn_camelizer"
11 | require "cfn_status"
12 | require "cli-format"
13 | require "fileutils"
14 | require "json"
15 | require "memoist"
16 | require "rainbow/ext/string"
17 |
18 | CliFormat.default_format = "table"
19 |
20 | require "jets/core_ext"
21 | require "jets/autoloaders"
22 | Jets::Autoloaders.gem.setup
23 |
24 | module Jets
25 | class Error < StandardError; end
26 | extend Core # root, logger, etc
27 | end
28 |
--------------------------------------------------------------------------------
/lib/jets/api.rb:
--------------------------------------------------------------------------------
1 | module Jets
2 | module Api
3 | extend Memoist
4 | extend self
5 |
6 | def api
7 | Jets::Api::Client.new
8 | end
9 | memoize :api
10 |
11 | def api_key
12 | Jets::Api::Config.instance.api_key
13 | end
14 | end
15 | end
16 |
17 | require "jets/api/error" # load all error classes
18 |
--------------------------------------------------------------------------------
/lib/jets/api/agree.rb:
--------------------------------------------------------------------------------
1 | module Jets::Api
2 | class Agree
3 | def initialize
4 | @agree_file = "#{ENV["HOME"]}/.jets/agree"
5 | end
6 |
7 | # Only prompts if hasnt prompted before and saved a ~/.jets/agree file
8 | def prompt
9 | return if bypass_prompt
10 | return if File.exist?(@agree_file) && File.mtime(@agree_file) > Time.parse("2021-04-12")
11 |
12 | puts <<~EOL
13 | To use jets you must agree to the terms of service.
14 |
15 | Jets Terms: https://www.rubyonjets.com/terms
16 |
17 | Is it okay to send your gem data to Jets Api? (Y/n)?
18 | EOL
19 |
20 | answer = $stdin.gets.strip
21 | value = /y/i.match?(answer) ? "yes" : "no"
22 |
23 | write_file(value)
24 | end
25 |
26 | # Allow user to bypass prompt with JETS_AGREE=1 JETS_AGREE=yes etc
27 | # Useful for CI/CD pipelines.
28 | def bypass_prompt
29 | agree = ENV["JETS_AGREE"]
30 | return false unless agree
31 |
32 | if %w[1 yes true].include?(agree.downcase)
33 | write_file("yes")
34 | else
35 | write_file("no")
36 | end
37 |
38 | true
39 | end
40 |
41 | def yes?
42 | File.exist?(@agree_file) && IO.read(@agree_file).strip == "yes"
43 | end
44 |
45 | def no?
46 | File.exist?(@agree_file) && IO.read(@agree_file).strip == "no"
47 | end
48 |
49 | def yes!
50 | write_file("yes")
51 | end
52 |
53 | def no!
54 | write_file("no")
55 | end
56 |
57 | def write_file(content)
58 | FileUtils.mkdir_p(File.dirname(@agree_file))
59 | IO.write(@agree_file, content)
60 | end
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/lib/jets/api/base.rb:
--------------------------------------------------------------------------------
1 | module Jets::Api
2 | class Base
3 | include Jets::Api
4 | class << self
5 | include Jets::Api
6 | end
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/lib/jets/api/ping.rb:
--------------------------------------------------------------------------------
1 | module Jets::Api
2 | class Ping < Base
3 | class << self
4 | def create(params = {})
5 | api.post("/pings", params)
6 | end
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/jets/api/project.rb:
--------------------------------------------------------------------------------
1 | module Jets::Api
2 | class Project < Base
3 | class << self
4 | def list(params = {})
5 | api.get("/projects", params)
6 | end
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/jets/api/release.rb:
--------------------------------------------------------------------------------
1 | module Jets::Api
2 | class Release < Base
3 | class << self
4 | def list(params = {})
5 | api.get("/releases", params)
6 | end
7 |
8 | def retrieve(id, params = {})
9 | api.get("/releases/#{id}", params)
10 | end
11 |
12 | def create(params = {})
13 | api.post("/releases", params)
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/jets/api/response.rb:
--------------------------------------------------------------------------------
1 | require "hashie"
2 |
3 | module Jets::Api
4 | class Response
5 | attr_reader(
6 | :http_resp,
7 | :http_body,
8 | :http_headers,
9 | :http_status,
10 | :request_id
11 | )
12 | def initialize(http_resp)
13 | @http_resp = http_resp
14 | @http_body = http_resp.body
15 | @http_headers = http_resp.to_hash
16 | @http_status = http_resp.code.to_i
17 | @request_id = http_resp["request-id"]
18 | end
19 |
20 | def data
21 | data = JSON.parse(@http_resp.body, symbolize_names: true)
22 | Hashie::Mash.new(data)
23 | rescue JSON::ParserError
24 | raise Jets::Api::Error, http_resp
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/lib/jets/api/sig.rb:
--------------------------------------------------------------------------------
1 | module Jets::Api
2 | class Sig < Base
3 | class << self
4 | def create(params = {})
5 | api.post("/sigs", params)
6 | end
7 |
8 | def update(id, params = {})
9 | api.put("sigs/#{id}", params)
10 | end
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/jets/api/stack.rb:
--------------------------------------------------------------------------------
1 | module Jets::Api
2 | class Stack < Base
3 | class << self
4 | def list(params = {})
5 | api.get("/stacks", params)
6 | end
7 |
8 | def retrieve(id, params = {})
9 | id = Jets.project.namespace if id == :current
10 | api.get("/stacks/#{id}", params)
11 | end
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/lib/jets/autoloaders.rb:
--------------------------------------------------------------------------------
1 | require "jets/bundle"
2 | Jets::Bundle.setup
3 | require "zeitwerk"
4 | require_relative "autoloaders/gem"
5 | require_relative "autoloaders/main"
6 |
7 | module Jets
8 | class Autoloaders
9 | class << self
10 | extend Memoist
11 |
12 | def main
13 | Main
14 | end
15 | memoize :main
16 |
17 | def gem
18 | Gem
19 | end
20 | memoize :gem
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/lib/jets/autoloaders/gem.rb:
--------------------------------------------------------------------------------
1 | module Jets
2 | class Autoloaders
3 | # for jets gem itself
4 | class Gem
5 | class << self
6 | extend Memoist
7 |
8 | def loader
9 | loader = Zeitwerk::Loader.new
10 | loader.tag = "jets.gem"
11 | loader.inflector = Inflector.new
12 | loader.push_dir(lib)
13 | loader.do_not_eager_load(do_not_eager_load)
14 | loader.ignore(ignore_paths) # loader.ignore requires full dir or path
15 | # loader.log!
16 | loader
17 | end
18 | memoize :loader
19 |
20 | def setup
21 | loader.setup
22 | end
23 |
24 | def lib
25 | File.expand_path("#{__dir__}/../..") # jets/lib
26 | end
27 |
28 | def do_not_eager_load
29 | paths = %w[]
30 | paths.map { |path| "#{lib}/#{path}" } # do_not_eager_load requires full dir or path
31 | end
32 |
33 | def ignore_paths
34 | # commands
35 | paths = %w[
36 | jets/cli/generate/templates
37 | jets/cli/init/templates
38 | jets/core_ext
39 | jets/core_ext.rb
40 | jets/overrides
41 | jets/shim/template
42 | ]
43 | paths.map { |path| "#{lib}/#{path}" }
44 | end
45 | end
46 |
47 | class Inflector < Zeitwerk::Inflector
48 | def camelize(basename, _abspath)
49 | map = {
50 | cli: "CLI",
51 | io: "IO",
52 | version: "VERSION"
53 | }
54 | map[basename.to_sym] || super
55 | end
56 | end
57 | end
58 | end
59 | end
60 |
--------------------------------------------------------------------------------
/lib/jets/aws_services/aws_helpers.rb:
--------------------------------------------------------------------------------
1 | module Jets::AwsServices
2 | module AwsHelpers # :nodoc:
3 | include Jets::AwsServices
4 |
5 | def find_stack(stack_name)
6 | resp = cfn.describe_stacks(stack_name: stack_name)
7 | resp.stacks.first
8 | rescue Aws::CloudFormation::Errors::ValidationError => e
9 | # example: Stack with id demo-dev does not exist
10 | if e.message =~ /Stack with/ && e.message =~ /does not exist/
11 | nil
12 | else
13 | raise
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/jets/aws_services/s3_bucket.rb:
--------------------------------------------------------------------------------
1 | module Jets::AwsServices
2 | class S3Bucket
3 | include Jets::AwsServices
4 |
5 | def self.ensure_exists(bucket_name)
6 | new(bucket_name).ensure_exists
7 | end
8 |
9 | def initialize(name)
10 | @name = name
11 | end
12 |
13 | def ensure_exists
14 | s3.create_bucket(bucket: @name) unless exists?
15 | rescue Aws::S3::Errors::BucketAlreadyExists => e
16 | puts "ERROR #{e.class}: #{e.message}".color(:red)
17 | puts "Bucket name: #{@name}"
18 | exit 1
19 | end
20 |
21 | def exists?
22 | s3.head_bucket(bucket: @name)
23 | true
24 | rescue Aws::S3::Errors::BucketAlreadyOwnedByYou, Aws::S3::Errors::Http301Error => e
25 | # These exceptions indicate bucket already exists
26 | # Aws::S3::Errors::Http301Error could be inaccurate but compromising for simplicity
27 | true
28 | rescue
29 | false
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/lib/jets/aws_services/stack_status.rb:
--------------------------------------------------------------------------------
1 | module Jets::AwsServices
2 | module StackStatus
3 | # Only cache if it is true because the initial deploy checks live status until a stack exists.
4 | @@stack_exists_cache = [] # helps with CloudFormation rate limit
5 | def stack_exists?(stack_name)
6 | return false if Jets.env.test?
7 | return true if ENV["JETS_NO_INTERNET"]
8 | return true if @@stack_exists_cache.include?(stack_name)
9 |
10 | exist = nil
11 | begin
12 | # When the stack does not exist an exception is raised. Example:
13 | # Aws::CloudFormation::Errors::ValidationError: Stack with id blah does not exist
14 | cfn.describe_stacks(stack_name: stack_name)
15 | @@stack_exists_cache << stack_name
16 | exist = true
17 | rescue Aws::CloudFormation::Errors::ValidationError => e
18 | if /does not exist/.match?(e.message)
19 | exist = false
20 | elsif e.message.include?("'stackName' failed to satisfy constraint")
21 | # Example of e.message when describe_stack with invalid stack name
22 | # "1 validation error detected: Value 'instance_and_route53' at 'stackName' failed to satisfy constraint: Member must satisfy regular expression pattern: [a-zA-Z][-a-zA-Z0-9]*|arn:[-a-zA-Z0-9:/._+]*"
23 | log.info "Invalid stack name: #{stack_name}"
24 | log.info "Full error message: #{e.message}"
25 | exit 1
26 | else
27 | raise # re-raise exception because unsure what other errors can happen
28 | end
29 | end
30 | exist
31 | end
32 |
33 | def output_value(outputs, key)
34 | out = outputs.find { |o| o.output_key == key }
35 | out&.output_value
36 | end
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/lib/jets/camelizer.rb:
--------------------------------------------------------------------------------
1 | # Wrapper to CfnCamelizer gem
2 | module Jets
3 | Camelizer = CfnCamelizer
4 | end
5 |
--------------------------------------------------------------------------------
/lib/jets/cfn/bootstrap.rb:
--------------------------------------------------------------------------------
1 | module Jets::Cfn
2 | class Bootstrap
3 | def initialize(options = {})
4 | @options = options.merge(bootstrap: true)
5 | end
6 |
7 | def run
8 | Builder::Parent::Genesis.new(@options).build
9 | success = Deploy.new(@options).sync # returns true if success
10 | abort("Bootstrap deploy failed") unless success
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/jets/cfn/builder/post_process.rb:
--------------------------------------------------------------------------------
1 | # post process the text so that
2 | # "!Ref IamRole" => !Ref IamRole
3 | # We strip the surrounding quotes
4 | module Jets::Cfn::Builder
5 | class PostProcess
6 | def initialize(text)
7 | @text = text
8 | end
9 |
10 | def process
11 | results = @text.split("\n").map do |line|
12 | if line.include?(': "!') # IE: IamRole: "!Ref IamRole",
13 | # IamRole: "!Ref IamRole" => IamRole: !Ref IamRole
14 | line.sub(/: "(.*)"/, ': \1')
15 | elsif line.include?('- "!') # IE: - "!GetAtt Foo.Arn"
16 | # IamRole: - "!GetAtt Foo.Arn" => - !GetAtt Foo.Arn
17 | line.sub(/- "(.*)"/, '- \1')
18 | else
19 | line
20 | end
21 | end
22 | results.join("\n") + "\n"
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/lib/jets/cfn/delete.rb:
--------------------------------------------------------------------------------
1 | module Jets::Cfn
2 | class Delete < Stack
3 | def run
4 | bootstrap_if_needed
5 | Jets::Remote::Runner.new(@options.merge(dummy: true, command: "delete")).run
6 | Teardown.new(@options).run
7 | end
8 |
9 | # In case user has deleted stack already and needs to delete the Jets API deployment record.
10 | def bootstrap_if_needed
11 | stack_name = Jets.project.namespace
12 | return if stack_exists?(stack_name)
13 | log.info "Creating dummy stack for deletion: #{stack_name}"
14 | Jets::Cfn::Bootstrap.new(@options).run
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/jets/cfn/iam/managed_policy.rb:
--------------------------------------------------------------------------------
1 | module Jets::Cfn::Iam
2 | # Examples:
3 | # config.codebuild.iam.managed_policies = [AmazonSSMReadOnlyAccess]
4 | class ManagedPolicy
5 | def initialize(policies)
6 | @policies = policies.compact.flatten.uniq
7 | end
8 |
9 | def standardize
10 | return if @policies.nil? || @policies.empty?
11 |
12 | @policies.map do |policy|
13 | if policy.include?("arn:")
14 | policy
15 | else
16 | "arn:aws:iam::aws:policy/#{policy}"
17 | end
18 | end
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/jets/cfn/resource.rb:
--------------------------------------------------------------------------------
1 | module Jets::Cfn
2 | class Resource < Base
3 | attr_reader :definition
4 | def initialize(definition, replacements)
5 | @definition, @replacements = definition, replacements
6 | end
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/lib/jets/cfn/resource/associated.rb:
--------------------------------------------------------------------------------
1 | # Does not do full expansion, mainly a container that holds the definition and
2 | # standardizes it without camelizing it.
3 | class Jets::Cfn::Resource
4 | class Associated
5 | extend Memoist
6 |
7 | attr_reader :definition
8 | attr_accessor :multiple_resources
9 | def initialize(*definition)
10 | @definition = definition.flatten
11 | # Some associated resources require multiple resources for a single Lambda function. For
12 | # example `sqs_event` can create a `SQS::Queue` and `Lambda::EventSourceMapping`. We set
13 | # a `multiple` flag so `add_logical_id_counter` can use it to avoid adding counter ids to
14 | # these type of resources. The `multiple` flag allows us to handle both:
15 | #
16 | # 1. Associated resources that contain multiple resources for a single Lambda function
17 | # 2. A single Lambda function with multiple events. In this case, a counter is added
18 | #
19 | # Setting `multiple` to true means the counter id will not be added.
20 | @multiple_resources = false
21 | end
22 |
23 | def logical_id
24 | standardized.keys.first
25 | end
26 |
27 | def attributes
28 | standardized.values.first
29 | end
30 |
31 | def standardized
32 | standardizer = Standardizer.new(definition)
33 | standardizer.standarize(definition) # doesnt camelize keys yet
34 | end
35 | memoize :standardized
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/lib/jets/cfn/resource/associated_outputs.rb:
--------------------------------------------------------------------------------
1 | class Jets::Cfn::Resource
2 | class AssociatedOutputs
3 | extend Memoist
4 |
5 | def initialize(outputs = {}, replacements = {})
6 | @outputs = outputs
7 | @replacements = replacements
8 | end
9 |
10 | def replacer
11 | Replacer.new(@replacements)
12 | end
13 | memoize :replacer
14 |
15 | def outputs
16 | outputs = replacer.replace_placeholders(@outputs)
17 | outputs.transform_values! { |value| value.camelize }
18 | outputs.transform_keys! { |key| replacer.replace_value(key) }
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/jets/cfn/resource/codebuild/fleet.rb:
--------------------------------------------------------------------------------
1 | module Jets::Cfn::Resource::Codebuild
2 | class Fleet < Jets::Cfn::Base
3 | def definition
4 | {
5 | CodebuildFleet: {
6 | Type: "AWS::CodeBuild::Fleet",
7 | Properties: props
8 | }
9 | }
10 | end
11 |
12 | def props
13 | {
14 | BaseCapacity: base_capacity, # Integer
15 | ComputeType: compute_type, # String
16 | EnvironmentType: environment_type # String
17 | # Name: "", # String
18 | # Tags: "", # [ Tag, ... ]
19 | }
20 | end
21 |
22 | def base_capacity
23 | Jets.bootstrap.config.codebuild.fleet.base_capacity
24 | end
25 |
26 | def compute_type
27 | codebuild_properties[:Environment][:ComputeType]
28 | end
29 |
30 | def environment_type
31 | codebuild_properties[:Environment][:Type]
32 | end
33 |
34 | def outputs
35 | {
36 | "CodebuildFleet" => "!Ref CodebuildFleet"
37 | }
38 | end
39 |
40 | private
41 |
42 | def codebuild_properties
43 | project = Jets::Cfn::Resource::Codebuild::Project::Ec2.new
44 | project.properties
45 | end
46 | memoize :codebuild_properties
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/lib/jets/cfn/resource/codebuild/project/base.rb:
--------------------------------------------------------------------------------
1 | module Jets::Cfn::Resource::Codebuild::Project
2 | class Base < Jets::Cfn::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/lib/jets/cfn/resource/codebuild/project/env.rb:
--------------------------------------------------------------------------------
1 | module Jets::Cfn::Resource::Codebuild::Project
2 | class Env
3 | include FormatEnv
4 |
5 | # config/jets/bootstrap.rb
6 | #
7 | # Jets.bootstrap.configure do
8 | # config.codebuild.project.env.vars
9 | #
10 | def vars
11 | vars = Jets.bootstrap.config.codebuild.project.env.vars.symbolize_keys!
12 | standardize_env_vars(vars)
13 | end
14 |
15 | # Used for codebuild.start_build in runner.rb
16 | def pass_vars(overrides = {})
17 | # config/jets/bootstrap.rb defined ENV vars
18 | env = Jets.bootstrap.config.codebuild.project.env
19 |
20 | vars = {}
21 | pass = (env.default_pass + env.pass).uniq
22 |
23 | # pass vars from your local machine to the codebuild remote runner
24 | pass.each do |x|
25 | ENV.each do |k, v|
26 | k = k.to_s
27 | match = x.is_a?(Regexp) ? k =~ x : k == x
28 | if match && v.is_a?(String)
29 | vars[k.to_sym] = v
30 | end
31 | end
32 | end
33 |
34 | # block gets the final say
35 | vars.reject! do |k, v|
36 | k = k.to_s
37 | env.block.any? do |x|
38 | x.is_a?(Regexp) ? k =~ x : k == x
39 | end
40 | end
41 |
42 | vars.merge!(overrides)
43 |
44 | standardize_env_vars(vars, casing: :underscore_keys)
45 | end
46 |
47 | def always_block
48 | %w[
49 | JETS_APP_SRC
50 | JETS_SIG
51 | JETS_TEMPLATES
52 | ]
53 | end
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/lib/jets/cfn/resource/codebuild/project/format_env.rb:
--------------------------------------------------------------------------------
1 | module Jets::Cfn::Resource::Codebuild::Project
2 | module FormatEnv
3 | def standardize_env_vars(vars, casing: :camelcase_keys)
4 | map = {
5 | PARAMETER_STORE: "PARAMETER_STORE",
6 | SECRET: "SECRETS_MANAGER",
7 | SECRETS_MANAGER: "SECRETS_MANAGER",
8 | SSM: "PARAMETER_STORE"
9 | }
10 |
11 | vars = vars.reject { |k, v| v.nil? }
12 |
13 | # There's no map! method. So using map and then assigning to vars
14 | vars = vars.map do |k, v|
15 | starts_with = v.to_s.split(":").first
16 | value = if map.key?(starts_with.upcase.to_sym)
17 | v.to_s.sub("#{starts_with}:", "")
18 | else
19 | v
20 | end
21 | type = map[starts_with.upcase.to_sym] || "PLAINTEXT"
22 | {
23 | Name: k.to_s,
24 | Value: value,
25 | Type: type
26 | }
27 | end
28 | vars = vars.sort_by { |h| h[:Name].to_s }
29 | if casing == :underscore_keys
30 | vars.map! { |h| h.transform_keys { |k| k.to_s.underscore.to_sym } }
31 | end
32 | vars
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/lib/jets/cfn/resource/codebuild/project/lambda.rb:
--------------------------------------------------------------------------------
1 | module Jets::Cfn::Resource::Codebuild::Project
2 | class Lambda < Ec2
3 | def codebuild_logical_id
4 | "CodebuildLambda"
5 | end
6 |
7 | def project_name
8 | "#{Jets.project.namespace}-remote-lambda"
9 | end
10 |
11 | def compute_type
12 | config.codebuild.lambda.project.compute_type
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/jets/cfn/resource/s3/bucket.rb:
--------------------------------------------------------------------------------
1 | module Jets::Cfn::Resource::S3
2 | class Bucket < Jets::Cfn::Base
3 | attr_reader :bucket_logical_id
4 | def initialize(props = {})
5 | @props = props # associated_properties from dsl.rb
6 | @bucket_logical_id = props.delete(:logical_id) || "{namespace}_s3_bucket"
7 | end
8 |
9 | def definition
10 | {
11 | bucket_logical_id => {
12 | Type: "AWS::S3::Bucket",
13 | Properties: @props
14 | }
15 | }
16 | end
17 |
18 | def outputs
19 | {
20 | bucket_logical_id => "!Ref #{bucket_logical_id.to_s.camelize}"
21 | }
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/lib/jets/cfn/resource/standardizer.rb:
--------------------------------------------------------------------------------
1 | class Jets::Cfn::Resource
2 | class Standardizer
3 | include Jets::Util::Camelize
4 |
5 | attr_reader :definition
6 | def initialize(*definition)
7 | @definition = definition.flatten
8 | end
9 |
10 | def template
11 | camelize(standarize(@definition))
12 | end
13 |
14 | def standarize(definition)
15 | definition = camelize(definition)
16 | first, second, third, _ = definition
17 | if definition.size == 1 && first.is_a?(Hash) # long form
18 | attrs = first.values.first
19 | attrs.delete(:Properties) if attrs[:Properties].blank?
20 | first # pass through
21 | elsif definition.size == 2 && second.is_a?(Hash) # medium form
22 | logical_id, attributes = first, second
23 | attributes.delete(:Properties) if attributes[:Properties].blank?
24 | {logical_id => attributes}
25 | elsif definition.size == 2 && second.is_a?(String) # short form
26 | logical_id, type = first, second
27 | {logical_id => {
28 | Type: type
29 | }}
30 | elsif definition.size == 3 && (second.is_a?(String) || second.is_a?(NilClass)) # short form
31 | logical_id, type, properties = first, second, third
32 | template = {logical_id => {
33 | Type: type
34 | }}
35 | attributes = template.values.first
36 | attributes[:Properties] = properties unless properties.empty?
37 | template
38 | else # Dont understand this form
39 | raise "Invalid form provided. definition #{definition.inspect}"
40 | end
41 | end
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/lib/jets/cfn/rollback.rb:
--------------------------------------------------------------------------------
1 | module Jets::Cfn
2 | class Rollback < Stack
3 | def run
4 | check_deployable!
5 | Jets::Remote::Runner.new(@options.merge(dummy: true, command: "rollback")).run
6 | end
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/lib/jets/cfn/stack.rb:
--------------------------------------------------------------------------------
1 | module Jets::Cfn
2 | class Stack
3 | extend Memoist
4 | include Jets::AwsServices
5 | include Jets::Util::Logging
6 | include Rollback # delete_rollback_complete!
7 | include Deployable # check_deployable!
8 |
9 | def initialize(options)
10 | @options = options
11 | end
12 |
13 | def stack_name
14 | Jets.project.namespace
15 | end
16 |
17 | def check_stack_exist!
18 | stack = find_stack(stack_name)
19 | return if stack
20 | puts "ERROR: Stack #{stack_name} does not exist".color(:red)
21 | exit 1
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/lib/jets/cfn/stack/deployable.rb:
--------------------------------------------------------------------------------
1 | class Jets::Cfn::Stack
2 | module Deployable
3 | # All CloudFormation states listed here: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-describing-stacks.html
4 | def check_deployable!
5 | return if !stack_exists?(stack_name)
6 |
7 | resp = cfn.describe_stacks(stack_name: stack_name)
8 | status = resp.stacks[0].stack_status
9 | if /_IN_PROGRESS$/.match?(status)
10 | log.error "ERROR: The '#{stack_name}' stack status is #{status}".color(:red)
11 | log.error "Please wait until the stack is ready and try again."
12 | exit 1
13 | end
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/lib/jets/cfn/stack/template.rb:
--------------------------------------------------------------------------------
1 | class Jets::Cfn::Stack
2 | class Template
3 | extend Memoist
4 | include Jets::AwsServices
5 |
6 | delegate :s3_bucket, to: "Jets.project"
7 |
8 | attr_reader :path, :stack_name
9 | def initialize(options = {})
10 | @options = options
11 | @path = Jets::Names.parent_template_path
12 | @stack_name = Jets::Names.parent_stack_name
13 | end
14 |
15 | def template_option
16 | if upload_to_s3?
17 | {template_url: url}
18 | else
19 | {template_body: body}
20 | end
21 | end
22 |
23 | # uploads to s3 lazily on first call
24 | def url
25 | s3_key = "jets/cfn/#{File.basename(path)}"
26 | object = s3_resource.bucket(s3_bucket).object(s3_key)
27 | object.upload_file(path)
28 | "https://s3.amazonaws.com/#{s3_bucket}/#{s3_key}"
29 | end
30 | memoize :url
31 |
32 | # Only use filesystem on initial bootstrap
33 | def body
34 | IO.read(path)
35 | end
36 | memoize :body
37 |
38 | private
39 |
40 | # Should not upload template to s3 and always use local template for bootstrap deploy.
41 | # This is because for finale deletion stack, uploading the parent.yml to s3
42 | # prevents a clean deletion of the s3 bucket resource since it's not empty.
43 | def upload_to_s3?
44 | !@options[:bootstrap]
45 | end
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/lib/jets/cfn/stack/yamler.rb:
--------------------------------------------------------------------------------
1 | require "yaml"
2 |
3 | class Jets::Cfn::Stack
4 | class Yamler
5 | def self.load(text)
6 | new(text).load
7 | end
8 |
9 | def initialize(text)
10 | @text = text
11 | end
12 |
13 | def load
14 | add_domain_types!
15 | YAML.load(@text)
16 | end
17 |
18 | private
19 |
20 | def add_domain_types!
21 | intrinsic_functions.each do |name|
22 | YAML.add_domain_type("", name) do |type, val|
23 | key = type.split("::").last
24 | key = "Fn::" + key unless name == "Ref"
25 | {key => val}
26 | end
27 | end
28 | end
29 |
30 | def intrinsic_functions
31 | %w[
32 | And
33 | Base64
34 | Cidr
35 | Equals
36 | FindInMap
37 | GetAtt
38 | GetAZs
39 | If
40 | If
41 | ImportValue
42 | Join
43 | Not
44 | Or
45 | Ref
46 | Select
47 | Split
48 | Sub
49 | Transform
50 | ]
51 | end
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/lib/jets/cfn/teardown.rb:
--------------------------------------------------------------------------------
1 | module Jets::Cfn
2 | class Teardown < Stack
3 | def run
4 | check_stack_exist!
5 | log.info "Final Delete Phase"
6 | Bucket.new.empty!
7 | remaining_resources
8 | end
9 |
10 | def remaining_resources
11 | stack_resources = cfn.describe_stack_resources(stack_name: stack_name).stack_resources
12 | resources = stack_resources.map(&:logical_resource_id).join(", ")
13 | log.debug "Remaining resources: #{resources}"
14 | log.debug "Final delete #{Jets.project.namespace.color(:green)}"
15 | cfn.delete_stack(stack_name: stack_name)
16 | cfn_status = Jets::Cfn::Status.new
17 | cfn_status.wait
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/lib/jets/cli/base.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI
2 | class Base
3 | extend Memoist
4 | include Jets::Api
5 | include Jets::AwsServices
6 | include Jets::Util::Logging
7 | include Jets::Util::Sure
8 |
9 | attr_reader :options
10 | def initialize(options = {})
11 | @options = options
12 | Jets.boot
13 | end
14 |
15 | def paging_params
16 | params = {}
17 | params[:limit] = @options[:limit] if @options[:limit]
18 | params[:order] = @options[:order] if @options[:order]
19 | params[:page] = @options[:page] if @options[:page]
20 | params
21 | end
22 |
23 | def paginate(resp)
24 | return unless resp[:total_pages] > 1
25 | warn "\npage #{resp[:current_page]} of #{resp[:total_pages]}"
26 | end
27 |
28 | class << self
29 | def rescue_api_error(*methods)
30 | methods = [:run] if methods.empty?
31 | mod = Module.new do
32 | methods.each do |method_name|
33 | define_method(method_name) do |*args, &block|
34 | super(*args, &block)
35 | rescue Jets::Api::Error => e
36 | warn "Jets API Error. #{e.message}".color(:red)
37 | log.debug e.backtrace.join("\n")
38 | exit 1
39 | end
40 | end
41 | end
42 | prepend mod
43 | end
44 | end
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/lib/jets/cli/bootstrap.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI
2 | class Bootstrap < Base
3 | def run
4 | are_you_sure?
5 | Jets::Cfn::Bootstrap.new(@options).run
6 | end
7 |
8 | def are_you_sure?
9 | return if @options[:yes]
10 | sure?("Will bootstrap #{stack_name.color(:green)}")
11 | end
12 |
13 | def stack_name
14 | Jets::Names.parent_stack_name
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/jets/cli/build.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI
2 | class Build < Base
3 | def run
4 | Jets::Cfn::Bootstrap.new(@options).run
5 | Jets::Remote::Runner.new(@options.merge(command: "build")).run
6 | end
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/lib/jets/cli/ci.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI
2 | class Ci < Jets::Thor::Base
3 | Init.cli_options.each { |args| option(*args) }
4 | register(Init, "init", "init", "CI init creates config/jets/ci.rb")
5 |
6 | desc "build", "CI build cfn template"
7 | def build
8 | Build.new(options).run
9 | end
10 |
11 | desc "deploy", "CI deploy cfn stack"
12 | yes_option
13 | def deploy
14 | Deploy.new(options).run
15 | end
16 |
17 | desc "delete", "CI delete cfn stack"
18 | yes_option
19 | def delete
20 | Delete.new(options).run
21 | end
22 |
23 | desc "info", "CI info"
24 | format_option(default: "info")
25 | def info
26 | Info.new(options).run
27 | end
28 |
29 | desc "start", "CI start build"
30 | yes_option
31 | option :buildspec_override, desc: "Path to buildspec override file"
32 | option :branch, aliases: "b", desc: "git branch" # Default is nil. Will use what's configured on AWS CodeBuild project settings.
33 | option :env_vars, aliases: "e", type: :array, desc: "env var overrides. IE: KEY1=VALUE1 KEY2=VALUE2"
34 | def start
35 | Start.new(options).run
36 | end
37 |
38 | desc "status", "CI status of build"
39 | def status
40 | Status.new(options).run
41 | end
42 |
43 | desc "stop", "CI stop build"
44 | yes_option
45 | def stop
46 | Stop.new(options).run
47 | end
48 |
49 | desc "logs", "CI logs"
50 | yes_option
51 | def logs
52 | Logs.new(options).run
53 | end
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/lib/jets/cli/ci/base.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI::Ci
2 | class Base < Jets::CLI::Base
3 | def stack_name
4 | "#{Jets.project.namespace}-ci"
5 | end
6 | alias_method :project_name, :stack_name
7 |
8 | def run_with_exception_handling
9 | yield
10 | rescue Aws::CodeBuild::Errors::ResourceNotFoundException => e
11 | puts "ERROR: #{e.class}: #{e.message}".color(:red)
12 | puts "CodeBuild project #{project_name} not found."
13 | rescue Aws::CodeBuild::Errors::InvalidInputException => e
14 | puts "ERROR: #{e.class}: #{e.message}".color(:red)
15 | end
16 |
17 | def stop_build
18 | build = codebuild.batch_get_builds(ids: [build_id]).builds.first
19 | if build.build_status == "IN_PROGRESS"
20 | codebuild.stop_build(id: build_id)
21 | true
22 | else
23 | log.info "Not in progress. Status is #{build.build_status}. Cannot stop: #{build_id}"
24 | false
25 | end
26 | end
27 |
28 | def build_id
29 | return @options[:build_id] if @options[:build_id]
30 | find_build
31 | end
32 | memoize :build_id
33 |
34 | def find_build
35 | resp = codebuild.list_builds_for_project(project_name: project_name)
36 | resp.ids.first # most recent build_id
37 | rescue Aws::CodeBuild::Errors::ResourceNotFoundException => e
38 | logger.error "ERROR: #{e.class} #{e.message}".color(:red)
39 | exit 1
40 | end
41 |
42 | def check_build_id!
43 | return if build_id
44 | puts "WARN: No builds found for #{project_name.color(:green)} project"
45 | exit
46 | end
47 |
48 | def show_console_log_url(build_id)
49 | log.info "Console Log Url:"
50 | build_id = build_id.split(":").last
51 | log.info "https://#{Jets.aws.region}.console.aws.amazon.com/codesuite/codebuild/projects/#{project_name}/build/#{project_name}%3A#{build_id}/log"
52 | end
53 | end
54 | end
55 |
--------------------------------------------------------------------------------
/lib/jets/cli/ci/build.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI::Ci
2 | class Build < Base
3 | def run
4 | Jets::Cfn::Bootstrap.new(@options).run
5 | Jets::Remote::Runner.new(@options.merge(command: "ci:build")).run
6 | end
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/lib/jets/cli/ci/delete.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI::Ci
2 | class Delete < Base
3 | def run
4 | are_you_sure?
5 | check_exist!
6 | Jets::Cfn::Bootstrap.new(@options).run
7 | Jets::Remote::Runner.new(@options.merge(command: "ci:delete")).run
8 | end
9 |
10 | private
11 |
12 | def are_you_sure?
13 | unless @options[:yes]
14 | sure?("Will delete #{stack_name.color(:green)}")
15 | end
16 | end
17 |
18 | def check_exist!
19 | unless stack_exists?(stack_name)
20 | puts "Stack does not exist: #{stack_name.color(:green)}"
21 | exit 1
22 | end
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/lib/jets/cli/ci/deploy.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI::Ci
2 | class Deploy < Base
3 | def run
4 | are_you_sure?
5 | Jets::Cfn::Bootstrap.new(@options).run
6 | Jets::Remote::Runner.new(@options.merge(command: "ci:deploy")).run
7 | end
8 |
9 | def are_you_sure?
10 | sure? <<~EOL
11 | Will deploy stack #{stack_name.color(:green)}
12 |
13 | Uses remote runner to deploy a separate stack for CI resources.
14 | EOL
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/jets/cli/ci/info.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI::Ci
2 | class Info < Base
3 | include Jets::AwsServices
4 |
5 | def run
6 | resp = codebuild.batch_get_projects(names: [project_name])
7 | project = resp.projects.first
8 | present(project)
9 | end
10 |
11 | private
12 |
13 | def present(project)
14 | presenter = CliFormat::Presenter.new(@options)
15 | presenter.empty_message = "Project not found: #{project_name}"
16 |
17 | data = if project.nil?
18 | []
19 | else
20 | [
21 | ["Name", project.name],
22 | ["Arn", project.arn],
23 | ["Description", project.description],
24 | ["Service Role", project.service_role],
25 | ["Source", project.source.type],
26 | ["Environment", project.environment.type],
27 | ["Compute Type", project.environment.compute_type],
28 | ["Image", project.environment.image],
29 | ["Timeout", "#{project.timeout_in_minutes} minutes"],
30 | ["Created", project.created]
31 | ]
32 | end
33 |
34 | data.each do |row|
35 | presenter.rows << row
36 | end
37 | presenter.show
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/lib/jets/cli/ci/logs.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI::Ci
2 | class Logs < Base
3 | def run
4 | check_build_id!
5 | run_with_exception_handling do
6 | puts "Logs for #{build_id}"
7 | show_console_log_url(build_id)
8 | Tailer.new(@options, build_id).run
9 | end
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/lib/jets/cli/ci/status.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI::Ci
2 | class Status < Base
3 | include Jets::Util::FormatTime
4 |
5 | def run
6 | check_build_id!
7 | run_with_exception_handling do
8 | puts "Build id: #{build_id}"
9 | resp = codebuild.batch_get_builds(ids: [build_id])
10 | build = resp.builds.first
11 | puts "Build end time: #{pretty_time(build.end_time)}"
12 | puts "Build status: #{colored(build.build_status)}"
13 | end
14 | end
15 |
16 | private
17 |
18 | def colored(status)
19 | # one of SUCCEEDED FAILED FAULT TIMED_OUT IN_PROGRESS STOPPED
20 | case status
21 | when "SUCCEEDED"
22 | status.color(:green)
23 | when "FAILED", "FAULT", "TIMED_OUT"
24 | status.color(:red)
25 | when "IN_PROGRESS"
26 | status.color(:yellow)
27 | else
28 | status
29 | end
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/lib/jets/cli/ci/stop.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI::Ci
2 | class Stop < Base
3 | def run
4 | are_you_sure?
5 | check_build_id!
6 | run_with_exception_handling do
7 | stopped = stop_build
8 | log.info "Build has been stopped: #{build_id}" if stopped
9 | show_console_log_url(build_id)
10 | end
11 | end
12 |
13 | private
14 |
15 | def are_you_sure?
16 | message = "Will stop build for project #{project_name.color(:green)} build_id #{build_id.color(:green)}"
17 | if @options[:yes]
18 | logger.info message
19 | else
20 | sure?(message)
21 | end
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/lib/jets/cli/ci/tailer.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI::Ci
2 | class Tailer < Jets::Remote::Tailer
3 | def show_if
4 | return true unless Jets.bootstrap.config.codebuild.logging.show == "filtered"
5 |
6 | start_marker = "Entering phase BUILD"
7 | end_marker = "Phase complete: BUILD"
8 | proc do |event|
9 | @display_showing ||= event.message.include?(start_marker)
10 | if @display_showing && event.message.include?(end_marker)
11 | @display_showing = false
12 | end
13 | @display_showing && !event.message.include?(start_marker)
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/jets/cli/ci/templates/ci.rb.tt:
--------------------------------------------------------------------------------
1 | Jets.deploy.configure do
2 | # CI Docs: https://docs.rubyonjets.com/docs/ci/getting-started/
3 | config.ci.source = {
4 | Type: "<%= repo_type %>",
5 | Location: "<%= repo_location %>"
6 | }
7 | config.ci.source_version = "<%= git_default_branch %>"
8 | # triggers means that the CI git push to these branches
9 | config.ci.triggers = ["<%= git_default_branch %>"]
10 |
11 | # Note: JETS_API_KEY is required. The CI runner needs it to run: jets deploy
12 | config.ci.env.vars = {
13 | # BUNDLE_GITHUB__COM: "SSM:/#{ssm_env}/BUNDLE_GITHUB__COM",
14 | JETS_API_KEY: "SSM:/#{ssm_env}/JETS_API_KEY"
15 | }
16 | # config.ci.schedule.enable = true
17 | # config.ci.schedule.rate = "1d"
18 | # config.ci.schedule.cron = "8 0 * * ? *" # every day at 12:08am UTC
19 | end
20 |
--------------------------------------------------------------------------------
/lib/jets/cli/clean.rb:
--------------------------------------------------------------------------------
1 | require "fileutils"
2 |
3 | class Jets::CLI
4 | class Clean < Base
5 | def run
6 | FileUtils.rm_rf(Jets.build_root)
7 | puts "Removed #{Jets.build_root}"
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/lib/jets/cli/concurrency.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI
2 | class Concurrency < Jets::Thor::Base
3 | desc "info", "Concurrency info"
4 | format_option(default: "table")
5 | def info
6 | Info.new(options).run
7 | end
8 |
9 | desc "get", "Get concurrency for function"
10 | function_name_option
11 | def get
12 | Get.new(options).run
13 | end
14 |
15 | desc "set", "Set concurrency for function"
16 | function_name_option
17 | option :reserved, type: :numeric, desc: "Reserved concurrency"
18 | option :provisioned, type: :numeric, desc: "Provisioned concurrency"
19 | yes_option
20 | def set
21 | Set.new(options).run
22 | end
23 |
24 | desc "unset", "Unset concurrency for function"
25 | function_name_option
26 | option :reserved, type: :boolean, desc: "Reserved concurrency"
27 | option :provisioned, type: :boolean, desc: "Provisioned concurrency"
28 | yes_option
29 | def unset
30 | Unset.new(options).run
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/lib/jets/cli/concurrency/base.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI::Concurrency
2 | class Base < Jets::CLI::Base
3 | include Jets::CLI::Lambda::Checks
4 | include Jets::Util::Truthy
5 |
6 | def initialize(options = {})
7 | super
8 | check_deployed!
9 | end
10 |
11 | def account_limit
12 | response = lambda_client.get_account_settings
13 | response.account_limit
14 | end
15 | memoize :account_limit
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/jets/cli/concurrency/get.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI::Concurrency
2 | class Get < Base
3 | def initialize(options = {})
4 | super
5 | function_name = Jets::CLI::Lambda::Lookup.function(@options[:function])
6 | @lambda_function = Jets::CLI::Lambda::Function.new(function_name)
7 | end
8 |
9 | def run
10 | puts <<~EOL
11 | Settings for Function: #{@lambda_function.name}
12 | Reserved concurrency: #{reserved_concurrency}
13 | Provisioned concurrency: #{provisioned_concurrency}
14 | EOL
15 | end
16 |
17 | def provisioned_concurrency
18 | info = @lambda_function.provisioned_concurrency_info
19 |
20 | if @lambda_function.provisioned_concurrency.nil?
21 | "not set"
22 | elsif info[:status] == "IN_PROGRESS"
23 | "#{info[:allocated]}/#{info[:requested]} (In progress)"
24 | else
25 | @lambda_function.provisioned_concurrency
26 | end
27 | end
28 |
29 | def reserved_concurrency
30 | if @lambda_function.reserved_concurrency.nil?
31 | "not set. Will scale to unreserved limit: #{account_limit.unreserved_concurrent_executions}"
32 | else
33 | @lambda_function.reserved_concurrency
34 | end
35 | end
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/lib/jets/cli/concurrency/set.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI::Concurrency
2 | class Set < Get
3 | def run
4 | sure? "Will update the concurrency settings for #{@lambda_function.name}"
5 | puts "Updating concurrency settings for #{@lambda_function.name}"
6 |
7 | if @options[:reserved]
8 | @lambda_function.reserved_concurrency = @options[:reserved]
9 | puts "Set reserved concurrency to #{@options[:reserved]}"
10 | end
11 |
12 | if @options[:provisioned]
13 | success = set_provisioned_concurrency(@options[:provisioned])
14 | puts "Set provisioned concurrency to #{@options[:provisioned]}" if success
15 | end
16 |
17 | Jets::CLI::Tip.show(:concurrency_change)
18 | end
19 |
20 | def set_provisioned_concurrency(value)
21 | @lambda_function.provisioned_concurrency = value
22 | true # success
23 | rescue Aws::Lambda::Errors::ResourceNotFoundException => e
24 | # Can happen for Events Lambda Lambda Functions where Wersion and Alias resources are only created when specified in config/jets
25 | # For controller Lambda Function the Alias and Version resource is always created.
26 | if e.message.include?("Cannot find alias")
27 | puts "ERROR: The live alias does not exist for the function. Please deploy the function an initial provisioned concurrency first.".color(:red)
28 | end
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/lib/jets/cli/concurrency/unset.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI::Concurrency
2 | class Unset < Set
3 | def run
4 | sure? "Will unset the concurrency settings for #{@lambda_function.name}"
5 | puts "Unsetting concurrency settings for #{@lambda_function.name}"
6 |
7 | if @options[:reserved]
8 | @lambda_function.reserved_concurrency = nil
9 | puts "Removed reserved concurrency"
10 | puts "Will scale to your AWS account unreserved limit. Currently: #{account_limit.unreserved_concurrent_executions}"
11 | end
12 |
13 | if @options[:provisioned]
14 | success = set_provisioned_concurrency(0)
15 | puts "Removed provisioned concurrency" if success
16 | end
17 |
18 | Jets::CLI::Tip.show(:concurrency_change)
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/jets/cli/curl.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI
2 | class Curl < Base
3 | def run
4 | result = Request.new(options).run
5 | # only thing that goes to stdout. so can pipe to commands like jq
6 | puts JSON.pretty_generate(result)
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/jets/cli/curl/adapter/base.rb:
--------------------------------------------------------------------------------
1 | module Jets::CLI::Curl::Adapter
2 | class Base
3 | extend Memoist
4 | def initialize(options)
5 | @options = options
6 | end
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/lib/jets/cli/curl/adapter/cookies/jar.rb:
--------------------------------------------------------------------------------
1 | module Jets::CLI::Curl::Adapter::Cookies
2 | class Jar
3 | include Jets::Util::Logging
4 |
5 | def initialize(result, filename)
6 | @result, @filename = result, filename
7 | end
8 |
9 | def write_to_file
10 | cookies = @result[:cookies]
11 | if cookies.nil? || cookies.empty?
12 | log.debug "No cookies found in the result."
13 | return
14 | end
15 |
16 | File.open(@filename, "w") do |file|
17 | cookies.each do |cookie|
18 | file.puts("# HTTP Cookie File")
19 | file.puts("# Created by jets curl #{Jets::VERSION}")
20 | file.puts("# Date: #{Time.now}\n\n")
21 | file.puts("#{cookie}\n")
22 | end
23 | end
24 |
25 | log.debug "Cookies written to #{@filename}."
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/lib/jets/cli/curl/adapter/cookies/parser.rb:
--------------------------------------------------------------------------------
1 | module Jets::CLI::Curl::Adapter::Cookies
2 | class Parser
3 | def initialize(cookie_string)
4 | @cookie_string = cookie_string
5 | end
6 |
7 | def parse
8 | if @cookie_string.include?("=")
9 | parse_inline_cookies
10 | else
11 | parse_cookies_from_file
12 | end
13 | end
14 |
15 | private
16 |
17 | def skip_line?(line)
18 | line.empty? || line.start_with?("#")
19 | end
20 |
21 | def parse_inline_cookies
22 | cookies = []
23 |
24 | @cookie_string.split(";").each do |cookie|
25 | cookie = cookie.strip
26 | cookies << cookie unless skip_line?(cookie)
27 | end
28 |
29 | cookies
30 | end
31 |
32 | def parse_cookies_from_file
33 | cookies = []
34 |
35 | if File.exist?(@cookie_string)
36 | File.open(@cookie_string, "r").each_line do |line|
37 | line = line.chomp.strip
38 | cookies << line unless skip_line?(line)
39 | end
40 | else
41 | warn "Error: File '#{@cookie_string}' not found."
42 | exit 1
43 | end
44 |
45 | cookies
46 | end
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/lib/jets/cli/curl/request.rb:
--------------------------------------------------------------------------------
1 | require "active_support"
2 | require "active_support/core_ext/string/filters"
3 |
4 | class Jets::CLI::Curl
5 | class Request < Jets::CLI::Call
6 | TRIM_MAX = ENV["JETS_CURL_TRIM_MAX"] || 64
7 | def run
8 | warn "Calling Lambda function #{function_name}"
9 | show_body
10 | result = invoke
11 |
12 | if result[:cookies] && @options[:cookie_jar]
13 | cookie_jar = Adapter::Cookies::Jar.new(result, @options[:cookie_jar])
14 | cookie_jar.write_to_file
15 | end
16 | if @options[:trim] || ENV["JETS_CURL_TRIM"]
17 | trim!(result, TRIM_MAX)
18 | else
19 | result
20 | end
21 | end
22 |
23 | def show_body
24 | return unless @options[:verbose] && @options[:data]
25 | hash = JSON.parse(payload)
26 | body = hash["body"]
27 | text = begin
28 | JSON.pretty_generate(JSON.parse(body))
29 | rescue JSON::ParserError
30 | body
31 | end
32 |
33 | warn "Request Body:"
34 | warn text
35 | end
36 |
37 | # interface method: override to convert cli curl-like options to Call payload
38 | def payload
39 | adapter.convert
40 | end
41 |
42 | def trim!(hash, max_length)
43 | hash.transform_values! do |value|
44 | if value.is_a?(String) && value.length > max_length
45 | value.truncate(max_length)
46 | elsif value.is_a?(Hash)
47 | trim!(value, max_length)
48 | elsif value.is_a?(Array)
49 | value.map! { |v| (v.is_a?(String) && v.length > max_length) ? v.truncate(max_length) : v }
50 | else
51 | value
52 | end
53 | end
54 | end
55 |
56 | def adapter
57 | # Only support Lambda URL for now.
58 | Adapter::Lambda.new(@options)
59 | end
60 | memoize :adapter
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/lib/jets/cli/delete.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI
2 | class Delete < Base
3 | def run
4 | are_you_sure?
5 | Jets::Cfn::Bootstrap.new(@options).run
6 | Jets::Cfn::Delete.new(options).run
7 | end
8 |
9 | def are_you_sure?
10 | stack_name = Jets.project.namespace
11 | message = <<~EOL
12 | Will delete #{stack_name.color(:green)}
13 |
14 | Uses remote runner to delete the stack and resources.
15 | EOL
16 | unless stack_exists?(stack_name)
17 | message << <<~EOL
18 |
19 | Note: It looks like the stack #{stack_name} has already been deleted.
20 | Jets will create a dummy stack to delete the API deployment record.
21 | The dummy stack will be deleted immediately after.
22 | EOL
23 | end
24 | sure?(message)
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/lib/jets/cli/deploy.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI
2 | class Deploy < Base
3 | def run
4 | dev_mode_check!
5 | sure?("Will deploy #{Jets.project.namespace.color(:green)}")
6 | Tip.show(:faster_deploy)
7 |
8 | Jets::Cfn::Bootstrap.new(@options).run
9 | Jets::Remote::Runner.new(@options.merge(command: "deploy")).run
10 | end
11 |
12 | def dev_mode_check!
13 | if File.exist?("#{Jets.root}/dev.mode") && !ENV["JETS_SKIP_DEV_MODE_CHECK"]
14 | abort "The dev.mode file exists. Please removed it and run bundle update before you deploy.".color(:red)
15 | end
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/lib/jets/cli/dotenv.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI
2 | class Dotenv < Jets::Thor::Base
3 | desc "list", "Parse and list dotenv vars"
4 | format_option(default: "dotenv")
5 | option :reveal, type: :boolean, default: false, desc: "Reveal values also"
6 | def list
7 | List.new(options).run
8 | end
9 |
10 | desc "get NAME", "Get env var from local files and SSM"
11 | def get(name)
12 | Get.new(options.merge(name: name)).run
13 | end
14 |
15 | desc "set VALUES", "Set SSM env vars for function"
16 | yes_option
17 | option :secure, type: :boolean, default: true, desc: "Whether or not to use SSM parameter type SecureString or String"
18 | def set(*values)
19 | Set.new(options.merge(values: values)).run
20 | end
21 |
22 | desc "unset NAMES", "Unset SSM env vars for function"
23 | yes_option
24 | def unset(*names)
25 | Unset.new(options.merge(names: names)).run
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/lib/jets/cli/dotenv/base.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI::Dotenv
2 | class Base < Jets::CLI::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/lib/jets/cli/dotenv/get.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI::Dotenv
2 | class Get < Base
3 | def run
4 | vars = Jets::Dotenv.parse
5 | vars.find do |name, value|
6 | if name == @options[:name]
7 | puts value
8 | exit # success
9 | end
10 | end
11 | exit 1 # not found
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/lib/jets/cli/dotenv/list.rb:
--------------------------------------------------------------------------------
1 | require "shellwords"
2 |
3 | class Jets::CLI::Dotenv
4 | class List < Base
5 | def run
6 | presenter = CliFormat::Presenter.new(@options)
7 | warn "# Env from config/jets/env files and SSM parameters"
8 | warn "# Values are not used locally. They are only used for the Lambda Function"
9 | unless @options[:reveal]
10 | warn "# To show values also, use the --reveal option"
11 | end
12 | presenter.empty_message = "# No env vars found"
13 | unless @options[:format] == "dotenv"
14 | header = ["Name"]
15 | header << "Value" if @options[:reveal]
16 | presenter.header = header
17 | end
18 | vars = Jets::Dotenv.parse
19 | vars.each do |key, value|
20 | v = inspect?(value) ? value.inspect : value
21 | row = [key]
22 | row << v if @options[:reveal]
23 | presenter.rows << row
24 | end
25 | presenter.show
26 | end
27 |
28 | def inspect?(value)
29 | value.include?("\n") || Shellwords.escape(value) != value
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/lib/jets/cli/dotenv/set.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI::Dotenv
2 | class Set < Base
3 | include Jets::CLI::Env::Parse
4 |
5 | def run
6 | sure? sure_message
7 | puts "Setting SSM vars for #{Jets.project.namespace}"
8 |
9 | perform # interface method
10 | Jets::CLI::Tip.show(:ssm_change)
11 | end
12 |
13 | def perform
14 | ssm_manager.set(vars)
15 | end
16 |
17 | def vars
18 | parse_cli_env_values(@options[option_key])
19 | end
20 |
21 | # interface method
22 | def option_key
23 | :values
24 | end
25 |
26 | def ssm_method
27 | name = self.class.name.demodulize # Set or Unset
28 | (name == "Set") ? :set : :delete
29 | end
30 |
31 | def names
32 | vars.keys.map(&:to_s)
33 | end
34 |
35 | def sure_message
36 | <<~EOL
37 | Will #{ssm_method} the SSM vars for #{Jets.project.namespace}
38 | Note: SSM changes do not update the Lambda function env vars.
39 | You will need run jets deploy to update the env vars.
40 |
41 | #{ssm_manager.preview_list(names)}
42 | EOL
43 | end
44 |
45 | def ssm_manager
46 | Jets::CLI::Dotenv::Ssm.new(@options)
47 | end
48 | memoize :ssm_manager
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/lib/jets/cli/dotenv/unset.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI::Dotenv
2 | class Unset < Set
3 | def perform
4 | ssm_manager.delete(names)
5 | end
6 |
7 | def names
8 | @options[:names]
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/lib/jets/cli/env.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI
2 | class Env < Jets::Thor::Base
3 | class_option :function, aliases: :n, default: "controller", desc: "Lambda Function name"
4 |
5 | desc "list", "List and show env vars"
6 | format_option(default: "dotenv")
7 | option :reveal, type: :boolean, default: false, desc: "Reveal values also"
8 | def list
9 | List.new(options).run
10 | end
11 |
12 | desc "get NAME", "Get env vars for function"
13 | def get(name)
14 | Get.new(options.merge(key: name)).run
15 | end
16 |
17 | desc "set VALUES", "Set env vars for function"
18 | yes_option
19 | def set(*values)
20 | Set.new(options.merge(values: values)).run
21 | end
22 |
23 | desc "unset NAMES", "Unset env vars for function"
24 | yes_option
25 | def unset(*names)
26 | Unset.new(options.merge(names: names)).run
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/lib/jets/cli/env/base.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI::Env
2 | class Base < Jets::CLI::Base
3 | include Jets::CLI::Lambda::Checks
4 | include Jets::Util::Truthy
5 |
6 | def initialize(options = {})
7 | super
8 | check_deployed!
9 | function_name = Jets::CLI::Lambda::Lookup.function(options[:function])
10 | @lambda_function = Jets::CLI::Lambda::Function.new(function_name)
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/jets/cli/env/get.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI::Env
2 | class Get < Base
3 | def run
4 | @lambda_function.environment_variables.find do |key, value|
5 | if key == @options[:key]
6 | puts value
7 | exit # success
8 | end
9 | end
10 | exit 1 # not found
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/jets/cli/env/list.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI::Env
2 | class List < Base
3 | def run
4 | warn "# Env Variables for #{Jets.project.namespace}"
5 | unless @options[:reveal]
6 | warn "# To show values also, use the --reveal option"
7 | end
8 | vars = @lambda_function.environment_variables
9 |
10 | presenter = CliFormat::Presenter.new(@options)
11 | vars.each do |key, value|
12 | row = [key]
13 | row << value if @options[:reveal]
14 | presenter.rows << row
15 | end
16 | presenter.show
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/jets/cli/env/parse.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI::Env
2 | module Parse
3 | def parse_cli_env_values(pairs)
4 | # Use each_with_object to iterate over the pairs and insert them into a hash
5 | pairs.each_with_object({}) do |pair, hash|
6 | key, value = pair.split("=")
7 | hash[key] = value
8 | end
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/lib/jets/cli/env/set.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI::Env
2 | class Set < Base
3 | include Parse
4 |
5 | def run
6 | are_you_sure?
7 | puts "Setting env vars for #{@lambda_function.name}"
8 |
9 | @lambda_function.environment_variables = environment_variables
10 | Jets::CLI::Tip.show(:env_change)
11 | end
12 |
13 | def environment_variables
14 | parse_cli_env_values(@options[:values])
15 | end
16 |
17 | def are_you_sure?
18 | name = self.class.to_s.demodulize.underscore.humanize.downcase
19 | sure? <<~EOL
20 | Will #{name} env vars for #{@lambda_function.name}
21 | The Lambda Function will immediately use the new env vars.
22 | EOL
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/lib/jets/cli/env/unset.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI::Env
2 | class Unset < Set
3 | def run
4 | are_you_sure?
5 | puts "Unsetting env vars for #{@lambda_function.name}"
6 |
7 | @lambda_function.environment_variables = environment_variables
8 | Jets::CLI::Tip.show(:env_change)
9 | end
10 |
11 | def environment_variables
12 | @options[:names].each_with_object({}) do |name, hash|
13 | hash[name] = nil
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/jets/cli/exec.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI
2 | class Exec < Base
3 | def run
4 | if @options[:command].empty?
5 | Repl.new(options).start
6 | else
7 | Command.new(options).run
8 | end
9 | rescue Jets::CLI::Call::Error => e
10 | puts "ERROR: #{e.message}".color(:red)
11 | abort "Unable to find the function. Please check the function name and try again."
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/lib/jets/cli/exec/repl/history.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI::Exec::Repl
2 | class History
3 | MAX_SIZE = 10_000
4 |
5 | attr_reader :list
6 | def initialize(options = {})
7 | @options = options
8 | @file = "#{ENV["HOME"]}/.jets/history"
9 | @list = load
10 | end
11 |
12 | def add(cmd)
13 | @list << cmd
14 | @list.shift if @list.size > MAX_SIZE
15 | end
16 |
17 | def display(num = nil)
18 | num ||= 20
19 | num = (num == "all") ? @list.length : num.to_i
20 | num = [@list.length, num].min
21 | start_index = [@list.length - num, 0].max
22 | @list[start_index..].each_with_index { |cmd, index| puts "#{start_index + index + 1}: #{cmd}" }
23 | end
24 |
25 | def load
26 | history = if File.exist?(@file)
27 | File.readlines(@file).map(&:chomp)
28 | else
29 | []
30 | end
31 | history.each { |cmd| Readline::HISTORY << cmd }
32 | history
33 | end
34 |
35 | def save
36 | FileUtils.mkdir_p(File.dirname(@file))
37 | File.open(@file, "w") do |file|
38 | @list.each { |cmd| file.puts cmd }
39 | end
40 | end
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/lib/jets/cli/functions.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI
2 | class Functions < Base
3 | def run
4 | functions = all
5 | puts functions.sort
6 | end
7 |
8 | def all
9 | functions = []
10 | nested_stack_resources.each do |resource|
11 | stack_name = resource.physical_resource_id
12 | # Custom resource stacks may not have output with function name.
13 | # So use describe_stack_resources to get the function names.
14 | resources = cfn.describe_stack_resources(stack_name: stack_name).stack_resources
15 | resources.each do |r|
16 | if r.resource_type == "AWS::Lambda::Function"
17 | functions << r.physical_resource_id if r.physical_resource_id # race condition. can be nil for a brief moment while provisioning
18 | end
19 | end
20 | end
21 | unless @options[:full]
22 | functions = functions.map { |f| f.sub("#{Jets.project.namespace}-", "") }
23 | end
24 | functions
25 | end
26 |
27 | def nested_stack_resources
28 | stack_name = Jets::Names.parent_stack_name
29 | resp = cfn.describe_stack_resources(stack_name: stack_name)
30 | resp.stack_resources.select { |r| r.resource_type == "AWS::CloudFormation::Stack" }
31 | rescue Aws::CloudFormation::Errors::ValidationError => e
32 | if e.message.include?("does not exist")
33 | abort "The stack #{stack_name} does not exist. Have you deployed yet?".color(:red)
34 | else
35 | raise
36 | end
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/lib/jets/cli/generate.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI
2 | class Generate < Jets::Thor::Base
3 | Event.cli_options.each { |args| option(*args) }
4 | register(Event, "event", "event NAME", "Generate event app code")
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/lib/jets/cli/generate/event.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI::Generate
2 | class Event < Jets::CLI::Group::Base
3 | argument :name, required: true, desc: "Event name. Example: cool"
4 |
5 | def self.cli_options
6 | [
7 | [:force, aliases: :f, type: :boolean, desc: "Bypass overwrite are you sure prompt for existing files"],
8 | [:method, aliases: :m, desc: "Method name", default: "handle"],
9 | [:trigger, aliases: :t, desc: "Event trigger", default: "scheduled"]
10 | ]
11 | end
12 | cli_options.each { |args| class_option(*args) }
13 |
14 | source_root "#{__dir__}/templates/event_types"
15 |
16 | public
17 |
18 | def application_event
19 | template "application_event.rb", "app/events/application_event.rb", skip: true
20 | end
21 |
22 | def event
23 | trigger = options[:trigger]
24 | trigger = "scheduled" if trigger == "schedule" # allow both to work
25 | template_path = "#{trigger}.rb.tt"
26 | template template_path, "app/events/#{underscore_name}_event.rb"
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/lib/jets/cli/generate/templates/application_event.rb.tt:
--------------------------------------------------------------------------------
1 | <% module_namespacing do -%>
2 | class ApplicationEvent < Jets::Event::Base
3 | # Adjust default timeout for all Event classes
4 | class_timeout 15.minutes
5 | end
6 | <% end -%>
7 |
--------------------------------------------------------------------------------
/lib/jets/cli/generate/templates/event_types/application_event.rb:
--------------------------------------------------------------------------------
1 | class ApplicationEvent < Jets::Event::Base
2 | # Adjust default timeout for all Event classes
3 | class_timeout 15.minutes
4 | end
5 |
--------------------------------------------------------------------------------
/lib/jets/cli/generate/templates/event_types/dynamodb.rb.tt:
--------------------------------------------------------------------------------
1 | class <%= class_name %>Event < ApplicationEvent
2 | dynamodb_event "test-table" # existing namespaced table: IE: demo-dev-test-table
3 | def <%= options[:method] %>
4 | puts "event #{JSON.dump(event)}"
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/lib/jets/cli/generate/templates/event_types/iot.rb.tt:
--------------------------------------------------------------------------------
1 | class <%= class_name %>Event < ApplicationEvent
2 | iot_event "SELECT * FROM 'my/topic'"
3 | def <%= options[:method] %>
4 | puts "event #{JSON.dump(event)}"
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/lib/jets/cli/generate/templates/event_types/kinesis.rb.tt:
--------------------------------------------------------------------------------
1 | class <%= class_name %>Event < ApplicationEvent
2 | kinesis_event "my-stream" # existing stream
3 | def <%= options[:method] %>
4 | puts "event #{JSON.dump(event)}"
5 | puts "kinesis_data #{JSON.dump(kinesis_data)}"
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/lib/jets/cli/generate/templates/event_types/log.rb.tt:
--------------------------------------------------------------------------------
1 | class <%= class_name %>Event < ApplicationEvent
2 | log_event "/aws/lambda/hello"
3 | def <%= options[:method] %>
4 | puts "event #{JSON.dump(event)}"
5 | puts "log_event #{JSON.dump(log_event)}"
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/lib/jets/cli/generate/templates/event_types/rule.rb.tt:
--------------------------------------------------------------------------------
1 | class <%= class_name %>Event < ApplicationEvent
2 | rule_event(
3 | description: "Checks for security group changes",
4 | detail_type: ["AWS API Call via CloudTrail"],
5 | detail: {
6 | event_source: ["ec2.amazonaws.com"],
7 | event_name: [
8 | "AuthorizeSecurityGroupIngress",
9 | "AuthorizeSecurityGroupEgress",
10 | "RevokeSecurityGroupIngress",
11 | "RevokeSecurityGroupEgress",
12 | "CreateSecurityGroup",
13 | "DeleteSecurityGroup"
14 | ]
15 | }
16 | )
17 | def <%= options[:method] %>
18 | puts "event: #{JSON.dump(event)}" # event is available
19 | # your logic
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/jets/cli/generate/templates/event_types/s3.rb.tt:
--------------------------------------------------------------------------------
1 | class <%= class_name %>Event < ApplicationEvent
2 | # Please read the Considerations section before using s3_event
3 | s3_event "my-bucket" # <= CHANGE ME: new or existing bucket
4 | def <%= options[:method] %>
5 | puts "event #{JSON.dump(event)}"
6 | puts "s3_events #{JSON.dump(s3_events)}"
7 | puts "s3_objects #{JSON.dump(s3_objects)}"
8 | # s3_files.each do |file|
9 | # puts "file.filename #{file.filename}"
10 | # puts "file.content #{file.content}"
11 | # end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/jets/cli/generate/templates/event_types/scheduled.rb.tt:
--------------------------------------------------------------------------------
1 | class <%= class_name %>Event < ApplicationEvent
2 | rate "10 hours"
3 | def <%= options[:method] %>
4 | puts "Do something with event #{JSON.dump(event)}"
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/lib/jets/cli/generate/templates/event_types/sns.rb.tt:
--------------------------------------------------------------------------------
1 | class <%= class_name %>Event < ApplicationEvent
2 | sns_event "hello-topic"
3 | def <%= options[:method] %>
4 | puts "event #{JSON.dump(event)}"
5 | puts "sns_events #{JSON.dump(sns_events)}"
6 | puts "sns_events? #{JSON.dump(sns_events?)}"
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/lib/jets/cli/generate/templates/event_types/sqs.rb.tt:
--------------------------------------------------------------------------------
1 | class <%= class_name %>Event < ApplicationEvent
2 | class_timeout 30 # Lambda Function timeout must be less than or equal to the SQS Visibility timeout
3 | sqs_event "hello-queue"
4 | def <%= options[:method] %>
5 | puts "event #{JSON.dump(event)}"
6 | puts "sqs_events #{JSON.dump(sqs_events)}"
7 | puts "sqs_events? #{JSON.dump(sqs_events?)}"
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/jets/cli/git.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI
2 | class Git < Jets::Thor::Base
3 | desc "push", "Runs git push and jets ci:logs"
4 | def push(*args)
5 | Push.new(options.merge(args: args)).run
6 | end
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/lib/jets/cli/git/push.rb:
--------------------------------------------------------------------------------
1 | require "open3"
2 |
3 | class Jets::CLI::Git
4 | class Push < Jets::CLI::Base
5 | def initialize(options = {})
6 | super
7 | @args = options[:args] || []
8 | end
9 |
10 | def run
11 | args = ["push"] + @args
12 | puts "=> git #{args.join(" ")}"
13 |
14 | IO.popen(["git", *args]) do |io|
15 | io.each do |line|
16 | puts line
17 | end
18 | end
19 |
20 | return unless $?.success?
21 |
22 | command = [env_vars, "jets ci:logs"].compact.join(" ")
23 | Kernel.exec(command)
24 | end
25 |
26 | def env_vars
27 | env_vars = Jets.project.config.git.push.branch[push_branch]
28 | return unless env_vars
29 | # IE: branch_name = {JETS_ENV: "xxx", AWS_PROFILE: "xxx"}
30 | env_vars.map do |k, v|
31 | "#{k}=#{v}"
32 | end.sort.join(" ")
33 | end
34 |
35 | # man git-push
36 | # git push
37 | # git push origin
38 | # git push origin :
39 | # git push origin master
40 | # git push origin HEAD
41 | # git push mothership master:satellite/master dev:satellite/dev
42 | # git push origin HEAD:master
43 | # git push origin master:refs/heads/experimental
44 | # git push origin :experimental
45 | # git push origin +dev:master
46 | def push_branch
47 | args = @args.reject { |arg| arg.start_with?("-") } # remove options
48 | case args.size
49 | when 0
50 | local.git_default_branch
51 | when 1
52 | local.git_current_branch
53 | when 2
54 | args.last
55 | else
56 | raise "ERROR: Too many arguments. Usage: jets git:push [REMOTE] [BRANCH]"
57 | end
58 | end
59 |
60 | def local
61 | Jets::Git::Local.new
62 | end
63 | memoize :local
64 | end
65 | end
66 |
--------------------------------------------------------------------------------
/lib/jets/cli/group/base.rb:
--------------------------------------------------------------------------------
1 | require "fileutils"
2 | require "thor"
3 |
4 | module Jets::CLI::Group
5 | class Base < Thor::Group
6 | include Thor::Actions
7 | include Actions
8 | include Helpers
9 |
10 | add_runtime_options! # force, pretend, quiet, skip options
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/lib/jets/cli/group/helpers.rb:
--------------------------------------------------------------------------------
1 | module Jets::CLI::Group
2 | module Helpers
3 | extend Memoist
4 |
5 | def class_name
6 | name.camelize
7 | end
8 |
9 | def underscore_name
10 | name.underscore
11 | end
12 |
13 | def init_project_name
14 | # inferred from the folder name
15 | Dir.pwd.split("/").last.gsub(/[^a-zA-Z0-9_]/, "-").squeeze("-")
16 | end
17 |
18 | def framework
19 | Jets::Framework.name
20 | end
21 |
22 | def package_type
23 | (framework == "rails") ? "image" : "zip"
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/jets/cli/help.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI
2 | module Help
3 | extend self
4 | def text(namespaced_command)
5 | file = namespaced_command.to_s.tr(":", "/")
6 | path = File.expand_path("../help/#{file}.md", __FILE__)
7 | return IO.read(path) if File.exist?(path)
8 |
9 | # Also look up for a help folder within the current command folder
10 | called_from = caller(1..1).first.split(":").first
11 | unnamespaced_command = namespaced_command.to_s.split(":").last
12 | path = File.expand_path("../help/#{unnamespaced_command}.md", called_from)
13 | IO.read(path) if File.exist?(path)
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/lib/jets/cli/help/build.md:
--------------------------------------------------------------------------------
1 | Build the package and CloudFormation templates but does not deploy it. This can be helpful for development.
--------------------------------------------------------------------------------
/lib/jets/cli/help/call.md:
--------------------------------------------------------------------------------
1 | ## Remote mode
2 |
3 | Invoke the lambda function on AWS.
4 |
5 | ## Examples Cheatsheet
6 |
7 | jets call -n cool_event-party -e '{"test":1}'
8 | jets call -n cool_event-party -e '{"test":1}' | jq .
9 | jets call -n cool_event-party -e '{"test":1}' --verbose | jq
10 | jets call -n cool_event-party file://event.json | jq . # load event with a file
11 | jets call -n jets-prewarm_event-handle -e '{"invocation_type": "RequestResponse"}'
12 |
13 | The equivalent AWS Lambda CLI command:
14 |
15 | aws lambda invoke --function-name demo-dev-cool_event-party --payload -e '{"test":1}' outfile.txt
16 | cat outfile.txt | jq '.'
17 |
18 | ## Logs
19 |
20 | The `jets call` command can also print out the last 4KB of the lambda logs with the `--verbose` option. The logging output is directed to stderr and the response output from the lambda function itself is directed to stdout so you can safely pipe the results of the call command to other tools like `jq`.
21 |
22 | ## Controller Note
23 |
24 | You can directly call a controller but you must provide it with a event payload that it understands. IE: The event payload needs to come from Lambda Fucntion URL, APIGW, or ALB.
25 |
26 | jets call -n controller --event file://lambda.json
27 |
28 | The `jets curl` handles this more automatically is recommended over the `jets call` command for calling Jets controller.
29 |
--------------------------------------------------------------------------------
/lib/jets/cli/help/concurrency/info.md:
--------------------------------------------------------------------------------
1 | ## Example
2 |
3 | ❯ jets concurrency:info
4 | Concurrency for demo-dev
5 | +---------------------------+----------+
6 | | Function | Reserved |
7 | +---------------------------+----------+
8 | | controller | 25 |
9 | | jets-prewarm_event-handle | 2 |
10 | | total | 27 |
11 | +---------------------------+----------+
12 | Account Limits
13 | Concurrent Executions: 1000
14 | Unreserved Concurrent Executions: 730
15 |
--------------------------------------------------------------------------------
/lib/jets/cli/help/delete.md:
--------------------------------------------------------------------------------
1 | Deletes the Jets deployment and all resources associated with it.
2 |
3 | ## Examples
4 |
5 | $ jets delete
6 | Deleting project...
7 | Are you sure you want to want to delete the 'demo-dev' project? (y/N)
8 | y
9 | Emptying s3 bucket demo-dev-s3bucket-89jrrj60c7bj
10 | Deleting demo-dev...
11 | 05:14:09AM DELETE_IN_PROGRESS AWS::CloudFormation::Stack demo-dev User Initiated
12 | ...
13 | 05:14:23AM DELETE_IN_PROGRESS AWS::CloudFormation::Stack PostsController
14 | 05:15:31AM DELETE_COMPLETE AWS::S3::Bucket S3Bucket
15 | Stack demo-dev deleted.
16 | Time took for deletion: 1m 27s.
17 | Project demo-dev deleted!
18 | $
19 |
20 | You can bypass the are you sure prompt with the `-y` flag.
21 |
22 | $ jets delete --y
23 |
--------------------------------------------------------------------------------
/lib/jets/cli/help/deploy.md:
--------------------------------------------------------------------------------
1 | Deploys project to AWS Lambda.
2 |
3 | ## Example
4 |
5 | $ jets deploy
6 |
--------------------------------------------------------------------------------
/lib/jets/cli/help/dotenv/get.md:
--------------------------------------------------------------------------------
1 | ## Example
2 |
3 | ❯ jets dotenv:get NAME1
4 | value1
5 |
--------------------------------------------------------------------------------
/lib/jets/cli/help/dotenv/list.md:
--------------------------------------------------------------------------------
1 | ## Example
2 |
3 | ❯ jets dotenv:list
4 | NAME1=value1
5 | NAME2=value2
--------------------------------------------------------------------------------
/lib/jets/cli/help/dotenv/set.md:
--------------------------------------------------------------------------------
1 | ## Examples
2 |
3 | An SSM Parameter name is conventionally used based on JETS_ENV and the Jets project in `config/jets/project.rb`. Example:
4 |
5 | ❯ jets dotenv:set NAME1=value1 NAME2=value2
6 | Will set the SSM vars for demo-dev
7 |
8 | /demo/dev/NAME1
9 | /demo/dev/NAME2
10 |
11 | Are you sure? (y/N) y
12 | Setting SSM vars for demo-dev
13 | SSM Parameter set: /demo/dev/NAME1
14 | SSM Parameter set: /demo/dev/NAME2
15 |
16 | If the env var includes a / then the SSM parameter is assumed to be fully qualified, there is conventional name expansion, and it is used as it.
17 |
18 | ❯ jets dotenv:set /dev/NAME1=value1
19 | Will set the SSM vars for sinatra-dev
20 |
21 | /dev/NAME1
22 |
23 | Are you sure? (y/N) y
24 | Setting SSM vars for sinatra-dev
25 | SSM Parameter set: /dev/NAME1
26 |
--------------------------------------------------------------------------------
/lib/jets/cli/help/dotenv/unset.md:
--------------------------------------------------------------------------------
1 | ## Example
2 |
3 | ❯ jets dotenv:unset NAME1 NAME2
4 | Will delete the SSM vars for demo-dev
5 |
6 | /demo/dev/NAME1
7 | /demo/dev/NAME2
8 |
9 | Are you sure? (y/N) y
10 | Setting SSM vars for demo-dev
11 | SSM Parameter deleted: /demo/dev/NAME1
12 | SSM Parameter deleted: /demo/dev/NAME2
13 |
--------------------------------------------------------------------------------
/lib/jets/cli/help/exec.md:
--------------------------------------------------------------------------------
1 | Execute commands on AWS Lambda environment.
2 |
3 | ## REPL
4 |
5 | When no command is provided a REPL is started.
6 |
7 | ❯ jets exec
8 | > pwd
9 | /var/task
10 | > whoami
11 | sbx_user1051
12 | > echo $AWS_REGION
13 | us-west-2
14 | > env | sort
15 |
16 | More examples:
17 |
18 | ❯ jets exec
19 | > env | sort
20 | > cat /etc/os-release
21 | > du -sh * | sort -sh
22 |
23 | ## Execute Commands
24 |
25 | ❯ jets exec uname -a
26 | Linux ... GNU/Linux
27 |
28 | ## Status and Result
29 |
30 | You can see the status and result of the last command executed on AWS Lambda. Example:
31 |
32 | ❯ jets exec
33 | > whoami
34 | sbx_user1051
35 | > status
36 | Last command had a status of success (0).
37 | > result
38 | Last result:
39 | {
40 | "stdout": "sbx_user1051\n",
41 | "stderr": "",
42 | "status": 0
43 | }
44 | > echo "This is an error message" >&2
45 | This is an error message
46 | > result
47 | Last result:
48 | {
49 | "stdout": "",
50 | "stderr": "This is an error message\n",
51 | "status": 0
52 | }
53 | >
--------------------------------------------------------------------------------
/lib/jets/cli/help/generate/event.md:
--------------------------------------------------------------------------------
1 | ## Examples
2 |
3 | jets generate:event log --trigger log --method report
4 | jets generate:event cool --trigger scheduled
5 | jets generate:event security --trigger rule --method detect_security_group_changes
6 | jets generate:event clerk --trigger dynamodb --method file
7 | jets generate:event thermostat --trigger iot --method measure
8 | jets generate:event data --trigger kinesis --method file
9 | jets generate:event upload --trigger s3
10 | jets generate:event messenger --trigger sns --method deliver
11 | jets generate:event waiter --trigger sqs --method order
12 |
--------------------------------------------------------------------------------
/lib/jets/cli/help/logs.md:
--------------------------------------------------------------------------------
1 | Show logs from Lambda function CloudWatch log group.
2 |
3 | This defaults to the controller Lambda function. Example:
4 |
5 | ❯ jets logs
6 | Showing logs for /aws/lambda/demo-dev-controller
7 |
8 | If you want to follow the logs use the `-f` flag.
9 |
10 | ❯ jets logs -f
11 | Tailing logs for /aws/lambda/demo-dev-controller
12 |
13 | If you want to see the production logs:
14 |
15 | ❯ JETS_ENV=prod jets logs -f
16 | Tailing logs for /aws/lambda/demo-prod-controller
17 |
18 | If you want to see logs for a job, specify the job and method.
19 |
20 | ❯ jets logs -f -n hard_job-dig
21 | Tailing logs for /aws/lambda/demo-dev-hard_job-dig
22 |
--------------------------------------------------------------------------------
/lib/jets/cli/help/projects.md:
--------------------------------------------------------------------------------
1 | ## Examples
2 |
3 | ❯ jets projects
4 | api
5 | backend
6 | demo
7 |
--------------------------------------------------------------------------------
/lib/jets/cli/help/release/history.md:
--------------------------------------------------------------------------------
1 | ## Examples
2 |
3 | $ jets release:history
4 | Releases for stack: demo-dev
5 | +---------+-----------------+--------------+---------+
6 | | Version | Status | Released At | Message |
7 | +---------+-----------------+--------------+---------+
8 | | 3 | UPDATE_COMPLETE | 10 hours ago | Deploy |
9 | | 2 | UPDATE_COMPLETE | 18 hours ago | Deploy |
10 | | 1 | UPDATE_COMPLETE | 22 hours ago | Deploy |
11 | +---------+-----------------+--------------+---------+
12 |
13 | The shown releases are paginated. If you need to see more releases you can use the `--page` option.
14 |
15 | $ jets release:history --page 2
16 |
17 | ## Other Commands
18 |
19 | release:info View detailed information for a release
20 |
--------------------------------------------------------------------------------
/lib/jets/cli/help/rollback.md:
--------------------------------------------------------------------------------
1 | ## Examples
2 |
3 | jets rollback 8
4 |
5 | Use the [jets release:history](/reference/jets-release-history/) command to view release history.
6 |
--------------------------------------------------------------------------------
/lib/jets/cli/help/schedule/validate.md:
--------------------------------------------------------------------------------
1 | Validate config/jets/schedule.yml by creating and deleting live event rules.
--------------------------------------------------------------------------------
/lib/jets/cli/help/stacks.md:
--------------------------------------------------------------------------------
1 | ## Examples
2 |
3 | ❯ jets stacks
4 | Stacks for project: demo
5 | demo-dev
6 | demo-prod
7 |
--------------------------------------------------------------------------------
/lib/jets/cli/help/status.md:
--------------------------------------------------------------------------------
1 | The CloudFormation stack status info. Essentially the events of the CloudFormation stack since the last update. If the CloudFormation stack is currently updating, this will live tail the events logs.
2 |
3 | ## Example
4 |
5 | $ jets status
6 | The current status for the stack demo-dev is UPDATE_COMPLETE
7 | Stack events:
8 | 05:21:42AM UPDATE_IN_PROGRESS AWS::CloudFormation::Stack demo-dev User Initiated
9 | 05:21:45AM CREATE_IN_PROGRESS AWS::CloudFormation::Stack ApiGateway
10 | ...
11 | 05:23:22AM CREATE_COMPLETE AWS::CloudFormation::Stack JetsPrewarmEvent
12 | 05:23:25AM UPDATE_COMPLETE_CLEANUP_IN_PROGRESS AWS::CloudFormation::Stack demo-dev
13 | 05:23:25AM UPDATE_COMPLETE AWS::CloudFormation::Stack demo-dev
14 | $
--------------------------------------------------------------------------------
/lib/jets/cli/help/url.md:
--------------------------------------------------------------------------------
1 | If routes have been configured and an API Gateway was created for the app, this provides the application's url.
2 |
3 | ## Example
4 |
5 | $ jets url
6 | https://v9wa7vqcia.execute-api.us-west-2.amazonaws.com/dev
--------------------------------------------------------------------------------
/lib/jets/cli/init/templates/config/jets/bootstrap.rb.tt:
--------------------------------------------------------------------------------
1 | Jets.bootstrap.configure do
2 | # Docs: http://rubyonjets.com/docs/remote/codebuild/
3 | # config.codebuild.project.env.vars = {
4 | # BUNDLE_GITHUB__COM: "SSM:/#{ssm_env}/BUNDLE_GITHUB__COM",
5 | # DOCKER_PASS: "SSM:/#{ssm_env}/DOCKER_PASS",
6 | # DOCKER_USER: "SSM:/#{ssm_env}/DOCKER_USER",
7 | # # Tip: Use your own Docker host for faster deploys
8 | # DOCKER_HOST: "SSM:/#{ssm_env}/DOCKER_HOST",
9 | # JETS_SSH_KEY: "SSM:/#{ssm_env}/JETS_SSH_KEY",
10 | # JETS_SSH_KNOWN: "SSM:/#{ssm_env}/JETS_SSH_KNOWN"
11 | # }
12 |
13 | # Additional codebuild remote-lambda runner for commands that can leverage it
14 | # Docs: https://docs.rubyonjets.com/docs/remote/codebuild/compute-type/#lambda-compute-type
15 | # config.codebuild.lambda.enable = true
16 | end
17 |
--------------------------------------------------------------------------------
/lib/jets/cli/init/templates/config/jets/project.rb.tt:
--------------------------------------------------------------------------------
1 | Jets.project.configure do
2 | config.name = "<%= init_project_name %>"
3 | end
4 |
--------------------------------------------------------------------------------
/lib/jets/cli/init/templates/env/.env.tt:
--------------------------------------------------------------------------------
1 | # Docs: https://docs.rubyonjets.com/docs/env/files/
2 | # This file is optional.
3 | # It's recommended to use SSM parameters with conventional paths instead.
4 | # See: https://docs.rubyonjets.com/docs/env/ssm/conventions/
5 | <% if framework == "rails" -%>
6 | RAILS_ENV=production
7 | RAILS_LOG_TO_STDOUT=1
8 | # SECRET_KEY_BASE=SSM # => SSM:/<%= init_project_name %>/dev/SECRET_KEY_BASE remember to set a secret key base
9 | <% end -%>
10 |
11 | # Examples:
12 | # No Conventions:
13 | # KEY1=value1 # hard coded value
14 | # KEY2=SSM:/abs/path/to/KEY2
15 | # Conventions https://docs.rubyonjets/docs/env/ssm/conventions/
16 | # SSM path under /<%= init_project_name %>/dev/ are autoloaded.
17 | # For example, DATABASE_URL does not need to be declared here.
18 | # It can be conventionally autoloaded.
19 | # Below is an example of how to manually declare but it's not needed.
20 | # DATABASE_URL=SSM # => SSM:/<%= init_project_name %>/dev/DATABASE_URL
21 |
--------------------------------------------------------------------------------
/lib/jets/cli/lambda/checks.rb:
--------------------------------------------------------------------------------
1 | module Jets::CLI::Lambda
2 | module Checks
3 | extend Memoist
4 |
5 | def check_deployed!
6 | return if stack_exists?(Jets.project.namespace)
7 | warn "ERROR: Project has not been deployed".color(:red)
8 | exit 1
9 | end
10 |
11 | def check_workers!
12 | return if workers_deployed?
13 | warn "No worker functions deployed"
14 | exit 1
15 | end
16 |
17 | def workers_deployed?
18 | Jets::CLI::Maintenance::Worker::Saver.new(@options).lambda_functions.size > 0
19 | end
20 | memoize :workers_deployed?
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/lib/jets/cli/lambda/functions.rb:
--------------------------------------------------------------------------------
1 | module Jets::CLI::Lambda
2 | module Functions
3 | extend Memoist
4 | def lambda_functions
5 | names = Jets::CLI::Functions.new(full: true).all
6 | names.map { |name| Jets::CLI::Lambda::Function.new(name) }
7 | end
8 | memoize :lambda_functions
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/lib/jets/cli/login.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI
2 | class Login < Base
3 | def run
4 | Jets::Api::Config.instance.update_api_key(@options[:token])
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/lib/jets/cli/logout.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI
2 | class Logout < Base
3 | def run
4 | Jets::Api::Config.instance.clear_token
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/lib/jets/cli/logs.rb:
--------------------------------------------------------------------------------
1 | require "aws-logs"
2 |
3 | class Jets::CLI
4 | class Logs < Base
5 | include Jets::AwsServices::AwsHelpers
6 |
7 | def run
8 | options = @options.dup # so it can be modified
9 | options[:log_group_name] = log_group_name
10 | options[:since] ||= "10m" # by default, start search 10m in the past
11 | options[:wait_exists_retries] = 60 # 300 seconds = 300 / 5 = 60 retries
12 | options[:wait_exists_seconds] = 5
13 |
14 | verb = options[:follow] ? "Tailing" : "Showing"
15 | warn "#{verb} logs for #{options[:log_group_name]}"
16 |
17 | tail = AwsLogs::Tail.new(options)
18 | tail.run
19 | end
20 |
21 | def log_group_name
22 | log_group_name = @options[:log_group_name] # can be nil
23 | if log_group_name.nil?
24 | begin
25 | log_group_name = Jets::CLI::Lambda::Lookup.function("controller") # function_name
26 | rescue Jets::CLI::Call::Error => e
27 | puts "ERROR: #{e.message}"
28 | abort "Unable to determine log group name by looking it up. Can you double check it?"
29 | end
30 | end
31 | unless log_group_name.include?(Jets.project.namespace)
32 | log_group_name = "#{Jets.project.namespace}-#{log_group_name}"
33 | end
34 | unless log_group_name.include?("aws/lambda")
35 | log_group_name = "/aws/lambda/#{log_group_name}"
36 | end
37 | log_group_name
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/lib/jets/cli/maintenance.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI
2 | class Maintenance < Jets::Thor::Base
3 | class_option :role, aliases: [:r], default: "web", desc: "Role to apply the maintenance mode to. IE: web worker"
4 |
5 | desc "on", "Turn on maintenance mode"
6 | long_desc Help.text("maintenance/on")
7 | yes_option
8 | def on
9 | Mode.new(options).on
10 | end
11 |
12 | # Note: --yes or -y is not used for the off command.
13 | # User will not be prompted for confirmation.
14 | # The option is allowed in case users accidentally use it.
15 | # Example:
16 | # jets maintenance off -y
17 | # This is why the option is hidden. This makes the user experience better.
18 | desc "off", "Turn off maintenance mode"
19 | long_desc Help.text("maintenance/off")
20 | option :yes, aliases: [:y], type: :boolean, desc: "Skip are you sure prompt", hide: true
21 | def off
22 | Mode.new(options).off
23 | end
24 |
25 | desc "status", "Show maintenance mode status"
26 | long_desc Help.text("maintenance/status")
27 | option :all, aliases: [:a], type: :boolean, desc: "Show status for all roles. Takes precedence over --role option", default: false
28 | def status
29 | Mode.new(options).status
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/lib/jets/cli/maintenance/base.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI::Maintenance
2 | class Base < Jets::CLI::Base
3 | include Jets::CLI::Lambda::Checks
4 | include Jets::Util::Truthy
5 |
6 | def initialize(options = {})
7 | super
8 | check_deployed!
9 | end
10 |
11 | def status
12 | on? ? "on" : "off"
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/jets/cli/maintenance/mode.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI::Maintenance
2 | class Mode < Base
3 | def on
4 | are_you_sure?
5 | warn "Enabling #{role_info} maintenance mode #{for_info}"
6 | role.on
7 | end
8 |
9 | def off
10 | warn "Disabling #{role_info} maintenance mode #{for_info}"
11 | role.off
12 | end
13 |
14 | def status
15 | if @options[:all]
16 | warn "Maintenance status for #{Jets.project.namespace}"
17 | puts "web #{Web.new.status}"
18 | puts "worker #{Worker.new.status}"
19 | else
20 | warn "#{role_info.titleize} maintenance status #{for_info}"
21 | puts role.status
22 | end
23 | end
24 |
25 | def are_you_sure?
26 | sure?("Will enable #{role_info} maintenance mode #{for_info}")
27 | end
28 |
29 | def role_info
30 | @options[:role]
31 | end
32 |
33 | def for_info
34 | "for #{Jets.project.namespace}"
35 | end
36 |
37 | def role
38 | # IE: Web or Worker
39 | klass = Jets::CLI::Maintenance.const_get(@options[:role].camelize)
40 | klass.new(@options)
41 | end
42 | memoize :role
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/lib/jets/cli/maintenance/web.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI::Maintenance
2 | class Web < Base
3 | def initialize(options = {})
4 | super
5 | function_name = Jets::CLI::Lambda::Lookup.function("controller")
6 | @lambda_function = Jets::CLI::Lambda::Function.new(function_name)
7 | end
8 |
9 | def on
10 | if on?
11 | warn "Web maintenance is already on"
12 | else
13 | @lambda_function.environment_variables = {JETS_MAINTENANCE: "on"}
14 | warn "Web maintenance has been turned on"
15 | end
16 | end
17 |
18 | def off
19 | if on?
20 | @lambda_function.environment_variables = {JETS_MAINTENANCE: nil}
21 | warn "Web maintenance has been turned off"
22 | else
23 | warn "Web maintenance is already off"
24 | end
25 | end
26 |
27 | def on?
28 | truthy?(@lambda_function.environment_variables["JETS_MAINTENANCE"])
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/lib/jets/cli/maintenance/worker.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI::Maintenance
2 | class Worker < Base
3 | def on
4 | check_workers!
5 |
6 | if on?
7 | warn "Worker maintenance is already on"
8 | else
9 | Saver.new(@options).save_concurrency_settings
10 | Zeroer.new(@options).zero_all_concurrency
11 | warn "Worker maintenance has been turned on"
12 | end
13 | end
14 |
15 | def off
16 | check_workers!
17 |
18 | if on?
19 | Restorer.new(@options).restore_concurrency_settings
20 | warn "Worker maintenance has been turned off"
21 | else
22 | warn "Worker maintenance is already off"
23 | end
24 | end
25 |
26 | def on?
27 | check_workers!
28 | Zeroer.new(@options).all_zeroed?
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/lib/jets/cli/maintenance/worker/base.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI::Maintenance::Worker
2 | class Base < Jets::CLI::Base
3 | include Jets::CLI::Lambda::Functions
4 |
5 | attr_reader :s3_bucket
6 | def initialize(options = {})
7 | super
8 | @s3_bucket = Jets.aws.s3_bucket
9 | end
10 |
11 | def state_file
12 | "jets/state/maintenance/lambda_concurrency_settings.json"
13 | end
14 |
15 | def lambda_functions
16 | super.select do |lambda_function|
17 | # Accounts for both app/events and app/jobs (from jets geneneration)
18 | lambda_function.name.match(/_event-/)
19 | end
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/lib/jets/cli/maintenance/worker/restorer.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI::Maintenance::Worker
2 | class Restorer < Base
3 | def restore_concurrency_settings
4 | data = read_from_s3
5 | data.each do |function_name, settings|
6 | lambda_function = Jets::CLI::Lambda::Function.new(function_name)
7 | lambda_function.reserved_concurrency = settings["reserved_concurrency"] if settings["reserved_concurrency"]
8 | lambda_function.provisioned_concurrency = settings["provisioned_concurrency"] if settings["provisioned_concurrency"]
9 | end
10 | end
11 |
12 | private
13 |
14 | def read_from_s3
15 | response = s3.get_object(bucket: s3_bucket, key: state_file)
16 | JSON.parse(response.body.read)
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/jets/cli/maintenance/worker/saver.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI::Maintenance::Worker
2 | class Saver < Base
3 | def save_concurrency_settings
4 | concurrency_settings = Jets::CLI::Concurrency::Info.new(@options).concurrency_settings
5 | save_to_s3(concurrency_settings)
6 | end
7 |
8 | private
9 |
10 | def save_to_s3(data)
11 | s3.put_object(bucket: s3_bucket, key: state_file, body: data.to_json)
12 | log.debug "Saved concurrency settings to s3://#{s3_bucket}/#{state_file}"
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/jets/cli/maintenance/worker/zeroer.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI::Maintenance::Worker
2 | class Zeroer < Base
3 | def zero_all_concurrency
4 | lambda_functions.each do |lambda_function|
5 | # must zero provisioned before reserved
6 | lambda_function.provisioned_concurrency = nil
7 | lambda_function.reserved_concurrency = 0
8 | end
9 | end
10 |
11 | def all_zeroed?
12 | lambda_functions.all? do |lambda_function|
13 | # check both reserved and provisioned
14 | lambda_function.provisioned_concurrency_unset? &&
15 | lambda_function.reserved_concurrency_zero?
16 | end
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/jets/cli/package.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI
2 | class Package < Jets::Thor::Base
3 | desc "dockerfile", "Build dockerfile"
4 | def dockerfile
5 | Dockerfile.new(options).run
6 | end
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/lib/jets/cli/package/dockerfile.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI::Package
2 | class Dockerfile < Jets::CLI::Base
3 | def run
4 | sure?("Will build a Dockerfile for #{Jets.project.namespace.color(:green)}")
5 | Jets::Cfn::Bootstrap.new(@options).run
6 | Jets::Remote::Runner.new(@options.merge(command: "package:dockerfile")).run
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/jets/cli/ping.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI
2 | class Ping < Base
3 | rescue_api_error
4 |
5 | def run
6 | Jets::Api::Ping.create
7 | puts "Auth check successful"
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/lib/jets/cli/projects.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI
2 | class Projects < Base
3 | rescue_api_error
4 |
5 | def run
6 | resp = Jets::Api::Project.list(paging_params)
7 | present(resp[:data])
8 | paginate(resp)
9 | end
10 |
11 | private
12 |
13 | def present(items)
14 | presenter = CliFormat::Presenter.new(@options)
15 | presenter.empty_message = "No projects found"
16 | items.each do |item|
17 | presenter.rows << [item[:name]]
18 | end
19 | presenter.show
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/lib/jets/cli/release.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI
2 | class Release < Jets::Thor::Base
3 | desc "history", "Release history"
4 | paging_options(order: "desc", limit: 10)
5 | def history
6 | History.new(options).run
7 | end
8 | map list: :history
9 |
10 | desc "info", "Release detailed information"
11 | format_option(default: "info")
12 | def info(version = nil)
13 | Info.new(options.merge(version: version)).run
14 | end
15 | map show: :info
16 |
17 | desc "rollback VERSION", "Rollback to a previous release", hide: true
18 | option :yes, aliases: :y, type: :boolean, desc: "Skip are you sure prompt"
19 | def rollback(version)
20 | Rollback.new(options.merge(version: version)).run
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/lib/jets/cli/release/base.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI::Release
2 | class Base < Jets::CLI::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/lib/jets/cli/release/history.rb:
--------------------------------------------------------------------------------
1 | require "date"
2 | require "tzinfo"
3 |
4 | class Jets::CLI::Release
5 | class History < Base
6 | include Jets::Util::FormatTime
7 | rescue_api_error
8 |
9 | def run
10 | resp = Jets::Api::Stack.retrieve(:current)
11 |
12 | name = "#{resp[:name]} #{resp[:location]}"
13 | resp = Jets::Api::Release.list(@options)
14 |
15 | data = resp[:data]
16 | if data.empty?
17 | log.info "No releases found for stack: #{name}"
18 | else
19 | log.info "Releases for stack: #{name}"
20 | show_items(data)
21 | paginate(resp)
22 | end
23 | end
24 |
25 | def show_items(items)
26 | presenter = CliFormat::Presenter.new(@options)
27 | header = ["Version", "Status", "Released At", "Message"]
28 | header << "Git Sha" if @options[:sha]
29 | presenter.header = header
30 | items.each do |item|
31 | version = item[:version]
32 | status = item[:stack_status]
33 | released_at = item[:created_at]
34 | message = item[:message] || "Deployed"
35 | message = message[0..50]
36 |
37 | row = [version, status, pretty_time(released_at), message]
38 | if @options[:sha]
39 | sha = item[:git_sha].to_s[0..7] if item[:git_sha]
40 | row << sha
41 | end
42 | presenter.rows << row
43 | end
44 | presenter.show
45 | end
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/lib/jets/cli/release/info.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI::Release
2 | class Info < History
3 | rescue_api_error
4 |
5 | def run
6 | release = get(@options[:version])
7 |
8 | release_fields = %w[
9 | version
10 | status
11 | created_at
12 | message
13 | deploy_user
14 | jets_env
15 | jets_extra
16 | jets_version
17 | jets_remote
18 | ruby_version
19 | region
20 | docker_image
21 | zip_location
22 | git_branch
23 | git_sha
24 | git_url
25 | git_message
26 | ].map(&:to_sym)
27 | data = release_fields.map do |field|
28 | # special cases for values
29 | value = if field == :created_at
30 | pretty_time(release[:created_at])
31 | else
32 | release[field]
33 | end
34 |
35 | label = field.to_s.titleize
36 | [label, value]
37 | end
38 |
39 | release.endpoints.each do |endpoint|
40 | name = endpoint[:name].titleize
41 | data << [name, endpoint[:url]]
42 | end
43 |
44 | warn Jets.project.namespace.color(:green)
45 | presenter = CliFormat::Presenter.new(@options)
46 | presenter.empty_message = "Release not found for: #{Jets.project.namespace}"
47 | data.each do |row|
48 | presenter.rows << row
49 | end
50 | presenter.show
51 | end
52 |
53 | def get(version = nil)
54 | version ||= "latest"
55 | Jets::Api::Release.retrieve(version)
56 | end
57 | end
58 | end
59 |
--------------------------------------------------------------------------------
/lib/jets/cli/release/rollback.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI::Release
2 | class Rollback < Base
3 | rescue_api_error
4 |
5 | def run
6 | version = @options[:version]
7 | resp = Info.new(@options).get(version)
8 | unless resp[:version]
9 | puts "ERROR: version #{version} not found".color(:red)
10 | exit 1
11 | end
12 | Jets::Cfn::Rollback.new(@options).run
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/jets/cli/schedule.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI
2 | class Schedule < Jets::Thor::Base
3 | desc "translate", "Translate Sidekiq Schedule to Jets"
4 | yes_option
5 | def translate
6 | Translate.new(options).run
7 | end
8 |
9 | desc "validate", "Validate config/jets/schedule.yml"
10 | yes_option
11 | def validate
12 | Validate.new(options).run
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/jets/cli/schedule/base.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI::Schedule
2 | class Base < Jets::CLI::Base
3 | include Jets::Event::Dsl::RateExpression
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/lib/jets/cli/stacks.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI
2 | class Stacks < Base
3 | rescue_api_error
4 |
5 | def run
6 | params = paging_params.merge(options)
7 | resp = Jets::Api::Stack.list(params)
8 | log.info stacks_for_message
9 | present(resp[:data])
10 | paginate(resp)
11 | end
12 |
13 | private
14 |
15 | def stacks_for_message
16 | if options[:all_projects]
17 | "Stacks for all projects:"
18 | else
19 | "Stacks for project: #{Jets.project.name}"
20 | end
21 | end
22 |
23 | def present(items)
24 | presenter = CliFormat::Presenter.new(@options)
25 | presenter.empty_message = "No stacks found"
26 | items.each do |item|
27 | row = "#{item[:name]} #{item[:location]}"
28 | presenter.rows << [row]
29 | end
30 | presenter.show
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/lib/jets/cli/stop.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI
2 | class Stop < Jets::CLI::Ci::Base
3 | def run
4 | are_you_sure?
5 | check_build_id!
6 | run_with_exception_handling do
7 | stopped = stop_build
8 | if stopped
9 | log.info <<~EOL
10 | Deploy has been stopped: #{build_id}
11 | If the deploy has already started the CloudFormation update it will continue.
12 | Please check the logs.
13 |
14 | EOL
15 | show_console_log_url(build_id)
16 | end
17 | end
18 | end
19 |
20 | def stack_name
21 | "#{Jets.project.namespace}-remote"
22 | end
23 | alias_method :project_name, :stack_name
24 |
25 | def are_you_sure?
26 | unless @options[:yes]
27 | sure? "Will attempt to stop the deploy for project #{project_name.color(:green)} build_id #{build_id.color(:green)}"
28 | end
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/lib/jets/cli/teardown.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI
2 | class Teardown < Base
3 | def run
4 | sure?("Will teardown the stack #{Jets.project.namespace}")
5 | Jets::Cfn::Teardown.new(@options).run
6 | end
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/lib/jets/cli/url.rb:
--------------------------------------------------------------------------------
1 | require "cli-format"
2 |
3 | class Jets::CLI
4 | class Url < Base
5 | rescue_api_error
6 |
7 | def run
8 | if @options[:format] == "json"
9 | puts data.to_json # simpler json format allows for: jets url | jq
10 | else
11 | present(data)
12 | end
13 | end
14 |
15 | private
16 |
17 | def present(items)
18 | presenter = CliFormat::Presenter.new(@options)
19 | presenter.empty_message = "No url info found"
20 | presenter.header = ["Name", "Value"] if @options[:header] # default: false
21 | data.keys.sort.each do |name|
22 | next if name.to_s == "queue_url" # dont show Queue Url
23 | name_url = name.to_s.titleize
24 | value = data[name]
25 | row = [name_url, value]
26 | presenter.rows << row
27 | end
28 | presenter.show
29 | end
30 |
31 | def data
32 | release = Release::Info.new(@options).get
33 | data = release.endpoints.inject({}) do |acc, endpoint|
34 | acc.merge!(endpoint[:name] => endpoint[:url])
35 | end
36 | data.delete_if { |k, v| v.nil? } # remove nil values
37 | data.delete_if { |k| k.include?("queue_url") } unless @options[:all]
38 | data
39 | end
40 | memoize :data
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/lib/jets/cli/waf.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI
2 | class Waf < Jets::Thor::Base
3 | class << self
4 | # interface method
5 | def waf_name
6 | [Jets.env, Jets.extra].compact.join("-")
7 | end
8 | end
9 |
10 | Init.cli_options.each { |args| option(*args) }
11 | register(Init, "init", "init", "WAF init creates config/jets/waf.rb")
12 |
13 | desc "build", "WAF build"
14 | yes_option
15 | def build
16 | Build.new(options).run
17 | end
18 |
19 | desc "deploy", "WAF deploy"
20 | yes_option
21 | def deploy
22 | Deploy.new(options).run
23 | end
24 |
25 | desc "delete", "WAF delete"
26 | yes_option
27 | def delete
28 | Delete.new(options).run
29 | end
30 |
31 | desc "info", "WAF info"
32 | format_option(default: "info")
33 | option :name, aliases: :n, default: waf_name, desc: "Web ACL name"
34 | def info
35 | Info.new(options).run
36 | end
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/lib/jets/cli/waf/base.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI::Waf
2 | class Base < Jets::CLI::Base
3 | def stack_name
4 | [Jets::CLI::Waf.waf_name, "waf"].compact.join("-")
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/lib/jets/cli/waf/build.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI::Waf
2 | class Build < Base
3 | def run
4 | Jets::Cfn::Bootstrap.new(@options).run
5 | Jets::Remote::Runner.new(@options.merge(command: "waf:build")).run
6 | end
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/lib/jets/cli/waf/delete.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI::Waf
2 | class Delete < Base
3 | def run
4 | are_you_sure?
5 | Jets::Cfn::Bootstrap.new(@options).run
6 | Jets::Remote::Runner.new(@options.merge(command: "waf:delete")).run
7 | end
8 |
9 | private
10 |
11 | def are_you_sure?
12 | unless @options[:yes]
13 | sure?("Will delete #{stack_name.color(:green)} in us-east-1")
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/jets/cli/waf/deploy.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI::Waf
2 | class Deploy < Base
3 | def run
4 | are_you_sure?
5 | Jets::Cfn::Bootstrap.new(@options).run
6 | Jets::Remote::Runner.new(@options.merge(command: "waf:deploy")).run
7 | end
8 |
9 | def are_you_sure?
10 | sure? <<~EOL
11 | Will deploy stack #{stack_name.color(:green)} to us-east-1
12 |
13 | Uses #{Jets.project.namespace} remote runner to deploy a separate stack for WAF resources.
14 | EOL
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/jets/cli/waf/init.rb:
--------------------------------------------------------------------------------
1 | class Jets::CLI::Waf
2 | class Init < Jets::CLI::Group::Base
3 | include Jets::Util::Sure
4 |
5 | def self.cli_options
6 | [
7 | [:force, aliases: :f, type: :boolean, desc: "Bypass overwrite are you sure prompt for existing files"],
8 | [:yes, aliases: :y, type: :boolean, desc: "Skip are you sure prompt"]
9 | ]
10 | end
11 | cli_options.each { |args| class_option(*args) }
12 |
13 | source_root "#{__dir__}/templates"
14 |
15 | private
16 |
17 | def sure_message
18 | <<~EOL
19 | This will create a config/jets/waf.rb file with initial waf settings.
20 |
21 | The waf is designed to be a shared resource used by multiple projects.
22 | Having a separate project that manages the waf stack may make sense.
23 |
24 | Please make sure you have backed up and committed your changes first.
25 | EOL
26 | end
27 |
28 | public
29 |
30 | def are_you_sure?
31 | return if options[:yes]
32 | sure?(sure_message)
33 | end
34 |
35 | # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codebuild-project-source.html#cfn-codebuild-project-source-type
36 | def config_jets_ci
37 | template "waf.rb.tt", "config/jets/waf.rb"
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/lib/jets/cli/waf/templates/waf.rb.tt:
--------------------------------------------------------------------------------
1 | Jets.deploy.configure do
2 | # Logging and Monitoring mode
3 | # config.waf.logging.enable = true # default: false
4 | # config.waf.monitoring = true # default: false
5 |
6 | # Add custom rules
7 | # https://docs.rubyonjets.com/docs/routing/lambda/cloudfront/waf/custom-rules/
8 | # https://docs.rubyonjets.com/docs/routing/lambda/cloudfront/waf/default-rules/
9 | # config.waf.rules = []
10 |
11 | # Jets WAF Managed Rules
12 | # config.waf.custom_rules.blanket_rate_limiter.enable = true # default: true
13 | # config.waf.custom_rules.blanket_rate_limiter.limit = 1000
14 | # config.waf.custom_rules.blanket_rate_limiter.evaluation_window_sec = 300
15 | # config.waf.custom_rules.blanket_rate_limiter.aggregate_key_type = "IP"
16 |
17 | # config.waf.custom_rules.uri_rate_limiter.enable = true # default: false
18 | # config.waf.custom_rules.uri_rate_limiter.limit = 100
19 | # config.waf.custom_rules.uri_rate_limiter.logical_statement = "Or"
20 | # config.waf.custom_rules.uri_rate_limiter.paths = ["/login", "/signup"] # default: ["/"]
21 | # config.waf.custom_rules.uri_rate_limiter.evaluation_window_sec = 300
22 | # config.waf.custom_rules.uri_rate_limiter.aggregate_key_type = "IP"
23 | # config.waf.custom_rules.uri_rate_limiter.string_match_condition = "STARTS_WITH"
24 |
25 | # Docs: https://docs.rubyonjets.com/docs/routing/lambda/cloudfront/waf/
26 | end
27 |
--------------------------------------------------------------------------------
/lib/jets/code/copy/full.rb:
--------------------------------------------------------------------------------
1 | module Jets::Code::Copy
2 | # Inherits from Base for build_root and class run method
3 | class Full < Base
4 | # Completely override run since Full has complete different behavior
5 | def run
6 | FileUtils.cp_r(Jets.root, "#{build_root}/stage/code")
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/jets/code/copy/git_copy.rb:
--------------------------------------------------------------------------------
1 | module Jets::Code::Copy
2 | class GitCopy < Base
3 | # interface method
4 | def create_temp_zip
5 | copy_to_code_temp # interface method
6 |
7 | # We leverage git archive for gitignore settings.
8 | Dir.chdir("#{build_root}/stage/code-temp") do
9 | if git_info.params[:git_dirty]
10 | quiet_sh "git add ."
11 | gitconfig
12 | quiet_sh "git commit -m 'add working tree files' > /dev/null || true"
13 | end
14 | quiet_sh "git archive -o #{build_root}/stage/code-temp.zip HEAD"
15 | end
16 | end
17 |
18 | # Copy project to stage/code-temp to use git add and archive
19 | # without affecting up the current git repo.
20 | # interface method. overriden by GitRsync
21 | def copy_to_code_temp
22 | FileUtils.mkdir_p("#{build_root}/stage")
23 | FileUtils.cp_r(Jets.root, "#{build_root}/stage/code-temp")
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/jets/code/copy/git_inline.rb:
--------------------------------------------------------------------------------
1 | module Jets::Code::Copy
2 | class GitInline < Base
3 | def create_temp_zip
4 | # Git add and commit to current repo. Affects the working dir but faster.
5 | if git_info.params[:git_dirty]
6 | quiet_sh "git add ."
7 | gitconfig
8 | quiet_sh "git commit -m 'commit for deploy' > /dev/null || true"
9 | end
10 | quiet_sh "git archive -o #{build_root}/stage/code-temp.zip HEAD"
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/jets/code/copy/rsync.rb:
--------------------------------------------------------------------------------
1 | module Jets::Code::Copy
2 | class Rsync < Base
3 | # interface method
4 | def create_temp_zip
5 | FileUtils.mkdir_p("#{build_root}/stage/code-temp")
6 | excludes = " --exclude .git"
7 | excludes << " --exclude-from=#{Jets.root}/.gitignore" if File.exist?("#{Jets.root}/.gitignore")
8 | quiet_sh "rsync -aq#{excludes} #{Jets.root}/ #{build_root}/stage/code-temp"
9 | # Create zip file
10 | check_zip_installed!
11 | quiet_sh "cd #{build_root}/stage/code-temp && zip -q -r #{build_root}/stage/code-temp.zip ."
12 | end
13 |
14 | def check_zip_installed!
15 | return if system("type zip > /dev/null 2>&1")
16 | abort "ERROR: zip command is required. Please install zip command."
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/jets/code/dummy.rb:
--------------------------------------------------------------------------------
1 | class Jets::Code
2 | class Dummy < Stager
3 | def stage_code
4 | # copies jets config
5 | return unless File.exist?("#{Jets.root}/config/jets")
6 | FileUtils.rm_rf("#{build_root}/stage/code/config/jets")
7 | FileUtils.mkdir_p("#{build_root}/stage/code/config")
8 | FileUtils.cp_r("#{Jets.root}/config/jets", "#{build_root}/stage/code/config/jets")
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/lib/jets/code/user.rb:
--------------------------------------------------------------------------------
1 | require "aws-sdk-iam"
2 |
3 | class Jets::Code
4 | class User
5 | delegate :build_root, to: Jets
6 |
7 | def save
8 | user = iam_user || ENV["USER"] || ENV["JETS_DEPLOY_USER"]
9 | FileUtils.mkdir_p(File.dirname(user_file))
10 | IO.write(user_file, user)
11 | user
12 | end
13 |
14 | def user_file
15 | "#{build_root}/stage/code/.jets/deploy_user"
16 | end
17 |
18 | def iam_user
19 | @iam ||= Aws::IAM::Client.new
20 | @iam.get_user.user.user_name
21 | rescue Aws::IAM::Errors::ValidationError
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/lib/jets/core/booter.rb:
--------------------------------------------------------------------------------
1 | require "securerandom"
2 |
3 | module Jets::Core
4 | class Booter
5 | class << self
6 | extend Memoist
7 |
8 | attr_reader :boot_at, :gid
9 | def boot!
10 | return false if @boot_at
11 |
12 | Jets::Bundle.require # require all the gems in the Gemfile
13 | require_config(:project) # for config.dotenv.overwrite
14 |
15 | if require_bootstrap?
16 | Jets::Dotenv.load!
17 | require_config(:bootstrap)
18 | end
19 |
20 | initialize!
21 |
22 | @gid = SecureRandom.uuid[0..7]
23 | @boot_at = Time.now.utc
24 | end
25 |
26 | def initialize!
27 | main = Jets::Autoloaders.main
28 | main.configure(Jets.root)
29 | main.setup
30 | end
31 |
32 | # Essentially deployment commands require the bootstrap to be run
33 | def require_bootstrap?
34 | args = ARGV.reject { |arg| arg.start_with?("-") }
35 | %w[
36 | bootstrap
37 | build
38 | delete
39 | deploy
40 | dockerfile
41 | release:rollback
42 | ].include?(args.last)
43 | end
44 |
45 | def require_config(name)
46 | files = [
47 | "config/jets/#{name}.rb",
48 | "config/jets/#{name}/#{Jets.env}.rb"
49 | ]
50 | files.each do |file|
51 | next unless File.exist?(file)
52 | require "#{Jets.root}/#{file}"
53 | end
54 | end
55 | memoize :require_config
56 | end
57 | end
58 | end
59 |
--------------------------------------------------------------------------------
/lib/jets/core/config/base.rb:
--------------------------------------------------------------------------------
1 | require "singleton"
2 |
3 | module Jets::Core::Config
4 | class Base
5 | extend Memoist
6 | include Jets::Util::Camelize
7 | include Singleton
8 |
9 | def configure(&block)
10 | instance_eval(&block) if block
11 | end
12 |
13 | def config
14 | self
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/jets/core/config/bootstrap.rb:
--------------------------------------------------------------------------------
1 | module Jets::Core::Config
2 | class Bootstrap < Base
3 | include Helpers
4 | # config settings
5 | include Cfn
6 | include Code
7 | include Codebuild
8 | include S3Bucket
9 |
10 | attr_accessor :infra, :logger
11 | def initialize(*)
12 | super
13 | @infra = false
14 | @logger = default_logger
15 | end
16 |
17 | def default_logger
18 | logger = ActiveSupport::Logger.new($stderr)
19 | logger.formatter = ActiveSupport::Logger::SimpleFormatter.new # no timestamps
20 | logger.level = ENV["JETS_LOG_LEVEL"] || :info
21 | logger
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/lib/jets/core/config/bootstrap/cfn.rb:
--------------------------------------------------------------------------------
1 | class Jets::Core::Config::Bootstrap
2 | module Cfn
3 | attr_accessor :cfn
4 |
5 | def initialize(*)
6 | super
7 |
8 | @cfn = ActiveSupport::OrderedOptions.new
9 | @cfn.resource_tags = {} # tags to add to all resources
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/lib/jets/core/config/bootstrap/code.rb:
--------------------------------------------------------------------------------
1 | class Jets::Core::Config::Bootstrap
2 | module Code
3 | attr_accessor :code
4 |
5 | def initialize(*)
6 | super
7 |
8 | @code = ActiveSupport::OrderedOptions.new
9 | @code.copy = ActiveSupport::OrderedOptions.new
10 | @code.copy.always_keep = ["config/jets/env"]
11 | @code.copy.always_remove = ["tmp"]
12 | @code.copy.strategy = "auto"
13 | @code.copy.warn_large = true
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/lib/jets/core/config/bootstrap/s3_bucket.rb:
--------------------------------------------------------------------------------
1 | class Jets::Core::Config::Bootstrap
2 | module S3Bucket
3 | attr_accessor :s3_bucket
4 |
5 | def initialize(*)
6 | super
7 |
8 | @s3_bucket = ActiveSupport::OrderedOptions.new
9 | @s3_bucket.cors_configuration = {
10 | CorsRules: [{
11 | AllowedHeaders: ["*"],
12 | AllowedMethods: ["GET"],
13 | AllowedOrigins: ["*"],
14 | ExposedHeaders: []
15 | }]
16 | }
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/jets/core/config/helpers.rb:
--------------------------------------------------------------------------------
1 | module Jets::Core::Config
2 | module Helpers
3 | include Ssm
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/lib/jets/core/config/helpers/ssm.rb:
--------------------------------------------------------------------------------
1 | require "aws-sdk-ssm"
2 |
3 | module Jets::Core::Config::Helpers
4 | module Ssm
5 | def ssm_env
6 | Jets::Dotenv::Convention.ssm_env
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/jets/core/config/info.rb:
--------------------------------------------------------------------------------
1 | require "singleton"
2 |
3 | module Jets::Core::Config
4 | class Info
5 | extend Memoist
6 | include Singleton
7 |
8 | def data
9 | data = File.exist?(path) ? YAML.load_file(path) : {}
10 | ActiveSupport::HashWithIndifferentAccess.new(data)
11 | end
12 | memoize :data
13 |
14 | def method_missing(name, *args)
15 | data.key?(name.to_sym) ? data[name.to_sym] : super
16 | end
17 |
18 | def respond_to_missing?(name, include_private = false)
19 | data.key?(name.to_sym) || super
20 | end
21 |
22 | # Do not use absolute path. This is because the path is written to the stage/code area
23 | def path
24 | "config/jets/info.yml"
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/lib/jets/core_ext.rb:
--------------------------------------------------------------------------------
1 | require "jets/core_ext/bundler"
2 | require "jets/core_ext/file"
3 | require "jets/core_ext/symbol"
4 |
--------------------------------------------------------------------------------
/lib/jets/core_ext/bundler.rb:
--------------------------------------------------------------------------------
1 | # Bundler 2.0 does yet not have with_unbundled_env
2 | # Bundler 2.1 deprecates with_clean_env for with_unbundled_env
3 |
4 | require "bundler"
5 | unless Bundler.respond_to?(:with_unbundled_env)
6 | def Bundler.with_unbundled_env(&block)
7 | with_clean_env(&block)
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/jets/core_ext/file.rb:
--------------------------------------------------------------------------------
1 | class File
2 | class << self
3 | # Ruby 3.2 removed File.exists?
4 | # aws_config/store.rb uses it
5 | # https://github.com/a2ikm/aws_config/blob/ef9cdd0eda116577f7d358bc421afd8e2f1eb1d3/lib/aws_config/store.rb#L6
6 | # Probably a bunch of other libraries still use File.exists? also
7 | alias_method :exists?, :exist?
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/jets/core_ext/symbol.rb:
--------------------------------------------------------------------------------
1 | class Symbol
2 | def camelize
3 | to_s.camelize.to_sym
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/lib/jets/dotenv.rb:
--------------------------------------------------------------------------------
1 | require "dotenv"
2 |
3 | module Jets
4 | class Dotenv
5 | include Jets::AwsServices
6 | include Jets::Util::Logging
7 |
8 | def load!
9 | return unless load?
10 | variables = ::Dotenv.load(*dotenv_files)
11 | Ssm.new(variables).interpolate!
12 | end
13 |
14 | def parse
15 | return {} unless load?
16 | variables = ::Dotenv.parse(*dotenv_files)
17 | Ssm.new(variables).interpolate!
18 | end
19 |
20 | def load?
21 | enabled = ENV["JETS_DOTENV"] != "0" # allow to disable with JETS_DOTENV=0
22 | # Prevent ssm calls when on AWS Lambda but will call if on AWS CodeBuild
23 | on_aws = (ENV["ON_AWS"] || ENV["_HANDLER"]) && !ENV["CODEBUILD_CI"]
24 | enabled && !on_aws
25 | end
26 |
27 | # dotenv files with the following precedence:
28 | #
29 | # - config/jets/env/.env.dev.extra (highest)
30 | # - config/jets/env/.env.dev
31 | # - config/jets/env/.env - The original (lowest)
32 | #
33 | def dotenv_files
34 | files = []
35 |
36 | files << ".env.#{Jets.env}.#{Jets.extra}" if Jets.extra
37 | files << ".env.#{Jets.env}"
38 | files << ".env"
39 |
40 | files.map! { |f| Jets.root.join("config/jets/env", f) }.compact
41 | files.map(&:to_s)
42 | end
43 |
44 | class << self
45 | extend Memoist
46 | @@load = nil
47 | def load!
48 | @@load ||= new.load!
49 | end
50 | memoize :load!
51 |
52 | @@parse = nil
53 | def parse
54 | @@parse ||= new.parse
55 | end
56 | memoize :parse
57 | end
58 | end
59 | end
60 |
--------------------------------------------------------------------------------
/lib/jets/dotenv/var.rb:
--------------------------------------------------------------------------------
1 | class Jets::Dotenv
2 | class Var
3 | extend Memoist
4 | include Jets::AwsServices
5 | include Jets::Util::Logging
6 |
7 | attr_reader :raw_key, :raw_value
8 | def initialize(raw_key, raw_value)
9 | @raw_key, @raw_value = raw_key, raw_value
10 | end
11 |
12 | def name
13 | @raw_key
14 | end
15 |
16 | def value
17 | ssm? ? ssm_value : @raw_value
18 | end
19 | memoize :value
20 |
21 | SSM_VARIABLE_REGEXP = /^SSM:(.*)/i
22 | def ssm_name
23 | if @raw_value == "SSM"
24 | # "/#{Jets.project.name}/#{Jets.env}/#{@raw_key}"
25 | Convention.new.ssm_name(@raw_key)
26 | else
27 | value = @raw_value.sub(/SSM:/i, "")
28 | if value.start_with?("/")
29 | value
30 | else
31 | # "/#{Jets.project.name}/#{Jets.env}/#{value}"
32 | Convention.new.ssm_name(value)
33 | end
34 | end
35 | end
36 |
37 | def ssm_value
38 | return "fake-ssm-value" if ENV["JETS_NO_INTERNET"]
39 |
40 | name = ssm_name
41 | resp = ssm.get_parameter(name: name, with_decryption: true)
42 | resp.parameter.value
43 | rescue Aws::SSM::Errors::ParameterNotFound
44 | @ssm_missing = true
45 | nil
46 | rescue Aws::SSM::Errors::ValidationException
47 | puts "ERROR: Invalid SSM parameter name: #{name.inspect}".color(:red)
48 | raise
49 | end
50 |
51 | def ssm_missing?
52 | value # trigger memoization
53 | !!@ssm_missing
54 | end
55 |
56 | def ssm?
57 | @raw_value&.start_with?("SSM")
58 | end
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/lib/jets/event/base.rb:
--------------------------------------------------------------------------------
1 | require "json"
2 |
3 | # Event public methods get turned into Lambda functions.
4 | #
5 | # Jets::Event::Base < Jets::Lambda::Functions
6 | # Both Jets::Event::Base and Jets::Lambda::Functions have Dsl modules included.
7 | # So the Jets::Event::Dsl overrides some of the Jets::Lambda::Functions behavior.
8 | module Jets::Event
9 | class Base < Jets::Lambda::Functions
10 | class Error < StandardError; end
11 |
12 | include Dsl
13 |
14 | # non-DSL methods
15 | include Helpers::KinesisEvent
16 | include Helpers::LogEvent
17 | include Helpers::S3Event
18 | include Helpers::SnsEvent
19 | include Helpers::SqsEvent
20 | prepend Jets::ExceptionReporting::Process
21 |
22 | class << self
23 | include Jets::Util::Logging
24 |
25 | def handle(event, context, meth = :perform)
26 | runner = new(event, context, meth)
27 | runner.send(meth)
28 | end
29 |
30 | def handle_now(meth = :handle, event = {}, context = {})
31 | handle(event, context, meth)
32 | end
33 |
34 | def handle_later(meth = :handle, event = {}, context = {})
35 | function = "#{name.underscore}-#{meth}" # IE: "cool_event-handle"
36 | call = Jets::CLI::Call.new(
37 | function: function,
38 | event: JSON.dump(event),
39 | invocation_type: "Event"
40 | )
41 | resp = begin
42 | call.invoke
43 | rescue Jets::CLI::Call::Error => e
44 | puts "ERROR: #{e.message}".color(:red)
45 | puts "The stack may not be full deployed yet. Please check the stack and try again."
46 | return
47 | end
48 | unless resp.status_code == 202
49 | raise Error, "Error calling Lambda function #{function} with invocation_type Event. status code: #{resp.status_code}"
50 | end
51 | resp
52 | end
53 | end
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/lib/jets/event/dsl.rb:
--------------------------------------------------------------------------------
1 | require "active_support"
2 | require "active_support/core_ext/class"
3 |
4 | # Jets::Event::Base < Jets::Lambda::Functions
5 | # Both Jets::Event::Base and Jets::Lambda::Functions have Dsl modules included.
6 | # So the Jets::Event::Dsl overrides some of the Jets::Lambda::Functions behavior.
7 | #
8 | # Implements:
9 | #
10 | # default_associated_resource_definition
11 | #
12 | module Jets::Event::Dsl
13 | extend ActiveSupport::Concern
14 |
15 | included do
16 | class << self
17 | include Jets::AwsServices
18 |
19 | include DynamodbEvent
20 | include IotEvent
21 | include KinesisEvent
22 | include LogEvent
23 | include S3Event
24 | include ScheduledEvent
25 | include SnsEvent
26 | include SqsEvent
27 |
28 | # TODO: Get rid of default_associated_resource_definition concept.
29 | # Also gets rid of the need to keep track of running @associated_properties too.
30 | def default_associated_resource_definition(meth)
31 | events_rule_definition
32 | end
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/lib/jets/event/dsl/dynamodb_event.rb:
--------------------------------------------------------------------------------
1 | module Jets::Event::Dsl
2 | module DynamodbEvent
3 | # interface method
4 | def dynamodb_event(table_name_without_namespace, options = {})
5 | {}
6 | end
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/lib/jets/event/dsl/iot_event.rb:
--------------------------------------------------------------------------------
1 | module Jets::Event::Dsl
2 | module IotEvent
3 | # The user must at least pass in an SQL statement
4 | # Returns topic_props
5 | # interface method
6 | def iot_event(props = {})
7 | if props.is_a?(String) # SQL Statement
8 | props = {Sql: props}
9 | {TopicRulePayload: props}
10 | elsif props.key?(:TopicRulePayload) # full properties structure
11 | props
12 | else # just the TopicRulePayload
13 | {TopicRulePayload: props}
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/jets/event/dsl/kinesis_event.rb:
--------------------------------------------------------------------------------
1 | module Jets::Event::Dsl
2 | module KinesisEvent
3 | # interface method
4 | def kinesis_event(stream_name, options = {})
5 | {}
6 | end
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/lib/jets/event/dsl/log_event.rb:
--------------------------------------------------------------------------------
1 | module Jets::Event::Dsl
2 | module LogEvent
3 | # interface method
4 | def log_event(log_group_name, props = {})
5 | {}
6 | end
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/lib/jets/event/dsl/rate_expression.rb:
--------------------------------------------------------------------------------
1 | require "fugit"
2 |
3 | module Jets::Event::Dsl
4 | module RateExpression
5 | # normalizes the rate expression
6 | def rate_expression(expr)
7 | duration = Fugit::Duration.parse(expr)
8 | map = {
9 | sec: "second",
10 | min: "minute",
11 | hou: "hour",
12 | day: "day",
13 | wee: "week",
14 | mon: "month",
15 | yea: "year"
16 | }
17 | # duration.h has a hash like {:hou=>1}. unit is truncated to 3 characters
18 | h = duration.h # IE: {:hou=>1}
19 | value = h.values.first
20 | unit = h.keys.first
21 | unit = map[unit]
22 | # Fix the unit to be singular or plural for user
23 | unit = (value > 1) ? unit.pluralize : unit.singularize
24 | "#{value} #{unit}"
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/lib/jets/event/dsl/s3_event.rb:
--------------------------------------------------------------------------------
1 | module Jets::Event::Dsl
2 | module S3Event
3 | def s3_event(bucket_name, props = {})
4 | {}
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/lib/jets/event/dsl/scheduled_event.rb:
--------------------------------------------------------------------------------
1 | module Jets::Event::Dsl
2 | module ScheduledEvent
3 | include RateExpression
4 | # Public: Creates CloudWatch Event Rule
5 | #
6 | # expression - The rate expression.
7 | #
8 | # Examples
9 | #
10 | # rate("10 minutes")
11 | # rate("10 minutes", description: "Hard event")
12 | #
13 | def rate(expression, props = {})
14 | expression = rate_expression(expression) # normalize the rate expression
15 | md = expression.match(/\d+\s+\w+/)
16 | raise ArgumentError, "Invalid rate expression: #{expression}" unless md
17 |
18 | expression = "rate(#{expression})"
19 | scheduled_event(expression, props)
20 | end
21 |
22 | # Public: Creates CloudWatch Event Rule
23 | #
24 | # expression - The cron expression.
25 | #
26 | # Examples
27 | #
28 | # cron("0 */12 * * ? *")
29 | # cron("0 */12 * * ? *", description: "Hard event")
30 | #
31 | def cron(expression, props = {})
32 | expression = normalize_cron_expression(expression)
33 | scheduled_event("cron(#{expression})", props)
34 | end
35 |
36 | def normalize_cron_expression(expr)
37 | parts = expr.split(" ")
38 | # AWS Cron expressions require ? for the day of the week field
39 | parts[-2] = "?" if parts[-2] == "*"
40 | parts.join(" ")
41 | end
42 |
43 | def scheduled_event(expression, props = {})
44 | props = props.merge(ScheduleExpression: expression)
45 | rule_event(props)
46 | end
47 |
48 | # interface method
49 | def rule_event(props = {})
50 | {}
51 | end
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/lib/jets/event/dsl/sns_event.rb:
--------------------------------------------------------------------------------
1 | module Jets::Event::Dsl
2 | module SnsEvent
3 | # interface method
4 | def sns_event(topic_name, props = {})
5 | {}
6 | end
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/lib/jets/event/dsl/sqs_event.rb:
--------------------------------------------------------------------------------
1 | module Jets::Event::Dsl
2 | module SqsEvent
3 | # interface method
4 | def sqs_event(queue_name, options = {})
5 | {}
6 | end
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/lib/jets/event/helpers/kinesis_event.rb:
--------------------------------------------------------------------------------
1 | require "base64"
2 |
3 | module Jets::Event::Helpers
4 | module KinesisEvent
5 | def kinesis_data
6 | records = event["Records"]
7 | records.map do |record|
8 | encoded = record["kinesis"]["data"]
9 | Base64.decode64(encoded) # data
10 | end
11 | end
12 |
13 | def kinesis_data?
14 | event["Records"]&.any? { |r| r.dig("kinesis", "data") }
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/jets/event/helpers/log_event.rb:
--------------------------------------------------------------------------------
1 | require "base64"
2 | require "json"
3 | require "stringio"
4 | require "zlib"
5 |
6 | module Jets::Event::Helpers
7 | module LogEvent
8 | def log_event
9 | encoded = event["awslogs"]["data"]
10 | compressed_string = Base64.decode64(encoded)
11 | gz = Zlib::GzipReader.new(StringIO.new(compressed_string))
12 | uncompressed_string = gz.read
13 | data = JSON.load(uncompressed_string)
14 | ActiveSupport::HashWithIndifferentAccess.new(data)
15 | end
16 |
17 | def log_event?
18 | !!event.dig("awslogs", "data")
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/jets/event/helpers/sns_event.rb:
--------------------------------------------------------------------------------
1 | module Jets::Event::Helpers
2 | module SnsEvent
3 | def sns_events
4 | records = event["Records"]
5 | return [] unless records
6 | records.map do |record|
7 | message = record["Sns"]["Message"]
8 | ActiveSupport::HashWithIndifferentAccess.new(JSON.load(message))
9 | end
10 | end
11 |
12 | def sns_events?
13 | event["Records"]&.any? { |r| r.dig("Sns", "Message") }
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/lib/jets/event/helpers/sqs_event.rb:
--------------------------------------------------------------------------------
1 | module Jets::Event::Helpers
2 | module SqsEvent
3 | extend Memoist
4 |
5 | def sqs_records
6 | event[:Records].map { |record| record }
7 | end
8 | memoize :sqs_records
9 |
10 | def sqs_events
11 | records = sqs_records
12 | return [] unless records
13 | records.map do |record|
14 | JSON.parse(record[:body])
15 | end
16 | end
17 | memoize :sqs_events
18 |
19 | def sqs_events?
20 | sqs_records&.any? { |r| r.dig(:body) }
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/lib/jets/event/s3.rb:
--------------------------------------------------------------------------------
1 | module Jets::Event
2 | module S3
3 | extend self
4 |
5 | # The registry tracks bucket each time an s3_event is declared
6 | # Map of bucket_name => stack_name (nested part)
7 | cattr_accessor :registry
8 | @@registry = {}
9 |
10 | def any?
11 | !@@registry.empty?
12 | end
13 |
14 | def create_s3_event_buckets
15 | buckets = @@registry.keys
16 | buckets.each do |bucket|
17 | Jets::AwsServices::S3Bucket.ensure_exists(bucket)
18 | end
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/jets/exception_reporting.rb:
--------------------------------------------------------------------------------
1 | module Jets
2 | module ExceptionReporting
3 | extend ActiveSupport::Concern
4 |
5 | delegate :with_exception_reporting, to: :class
6 | module ClassMethods
7 | def with_exception_reporting
8 | yield
9 | rescue => exception
10 | Jets.report_exception(exception)
11 | decorate_with_exception_reported(exception)
12 | raise
13 | end
14 |
15 | # We decorate the exception with a with_exception_reported? method so we
16 | # can use it the MainProcessor rescue Exception handling to not
17 | # double report the exception and only reraise.
18 | #
19 | # If we have properly rescue all exceptions then this would not be needed.
20 | # However, we're being paranoid also by rescuing Exception in the MainProcessor.
21 | #
22 | # Also, in general, it's hard to follow Exception bubbling logic. This
23 | # approach allows us to not have to worry about bubbling and call
24 | # with_exception_reporting indiscriminately.
25 | def decorate_with_exception_reported(exception)
26 | unless exception.respond_to?(:with_exception_reported?)
27 | exception.define_singleton_method(:with_exception_reported?) { true }
28 | end
29 | end
30 | end
31 |
32 | module Process
33 | extend ActiveSupport::Concern
34 | module ClassMethods
35 | def process(event, context, meth)
36 | with_exception_reporting do
37 | super
38 | end
39 | end
40 | end
41 | end
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/lib/jets/framework.rb:
--------------------------------------------------------------------------------
1 | module Jets
2 | class Framework
3 | class << self
4 | extend Memoist
5 | def env_var
6 | "#{name.upcase}_ENV" if name # can be nil
7 | end
8 |
9 | def env
10 | return unless env_var
11 | ENV[env_var] || "production" # for Dockerfile default to production
12 | end
13 |
14 | def name
15 | gems.each do |gem|
16 | frameworks.each do |framework|
17 | if gem == framework
18 | # Special case for puma. If puma is detected, it means it is a rack app.
19 | if framework == "puma"
20 | return "rack"
21 | else
22 | return framework
23 | end
24 | end
25 | end
26 | end
27 | nil
28 | end
29 | memoize :name
30 |
31 | def frameworks
32 | %w[
33 | rails
34 | sinatra
35 | hanami
36 | rack
37 | puma
38 | ]
39 | end
40 |
41 | def gems
42 | return [] unless File.exist?("Gemfile")
43 | Bundler.with_unbundled_env do
44 | gemfile_content = File.read("Gemfile")
45 | dsl = Bundler::Dsl.evaluate(Bundler.default_gemfile, gemfile_content, {})
46 | dsl.dependencies.map(&:name)
47 | end
48 | end
49 | end
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/lib/jets/git.rb:
--------------------------------------------------------------------------------
1 | module Jets
2 | module Git
3 | class Error < StandardError; end
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/lib/jets/git/base.rb:
--------------------------------------------------------------------------------
1 | module Jets::Git
2 | class Base
3 | extend Memoist
4 |
5 | def params
6 | base.merge(info)
7 | end
8 | memoize :params
9 |
10 | # interface method
11 | def info
12 | {}
13 | end
14 |
15 | def base
16 | {
17 | git_user: user.name
18 | }
19 | end
20 |
21 | def user
22 | User.new
23 | end
24 | memoize :user
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/jets/git/bitbucket.rb:
--------------------------------------------------------------------------------
1 | module Jets::Git
2 | class Bitbucket < Base
3 | def info
4 | info = {
5 | git_system: "bitbucket",
6 | git_branch: git_branch,
7 | git_sha: git_sha,
8 | git_dirty: false
9 | # git_message: nil,
10 | # git_version: nil,
11 | }
12 | info[:git_url] = git_url if git_url
13 | info
14 | end
15 |
16 | def git_branch
17 | ENV["BITBUCKET_BRANCH"]
18 | end
19 |
20 | def git_sha
21 | ENV["BITBUCKET_COMMIT"]
22 | end
23 |
24 | def git_url
25 | host = ENV["BITBUCKET_HOST"] || "https://bitbucket.org"
26 | full_repo = ENV["BITBUCKET_REPO_FULL_NAME"]
27 | "#{host}/#{full_repo}"
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/lib/jets/git/circleci.rb:
--------------------------------------------------------------------------------
1 | module Jets::Git
2 | class Circleci < Base
3 | def info
4 | {
5 | git_system: "circleci",
6 | git_branch: git_branch,
7 | git_sha: git_sha,
8 | git_dirty: false
9 | # git_message: nil,
10 | # git_version: nil,
11 | }
12 | # info[:git_url] = git_url if git_url
13 | end
14 |
15 | def git_branch
16 | ENV["CIRCLE_BRANCH"]
17 | end
18 |
19 | def git_sha
20 | ENV["CIRCLE_SHA1"]
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/lib/jets/git/codebuild.rb:
--------------------------------------------------------------------------------
1 | module Jets::Git
2 | class Codebuild < Base
3 | def info
4 | info = {
5 | git_system: "codebuild",
6 | git_branch: git_branch,
7 | git_sha: git_sha,
8 | git_dirty: false,
9 | git_url: git_url
10 | # git_message: nil,
11 | # git_version: nil,
12 | }
13 | info.delete_if { |k, v| v.nil? }
14 | info
15 | end
16 |
17 | def git_branch
18 | ENV["CODEBUILD_SOURCE_VERSION"]
19 | end
20 |
21 | def git_sha
22 | ENV["CODEBUILD_RESOLVED_SOURCE_VERSION"]
23 | end
24 |
25 | def git_url
26 | "#{host}/#{full_repo}" if host && full_repo
27 | end
28 |
29 | def host
30 | return unless ENV["CODEBUILD_SOURCE_REPO_URL"]
31 | uri = URI(ENV["CODEBUILD_SOURCE_REPO_URL"]) # https://github.com/ORG/REPO
32 | "#{uri.scheme}://#{uri.host}"
33 | end
34 |
35 | # ORG/REPO
36 | def full_repo
37 | return unless repo_url
38 | uri = URI(repo_url)
39 | uri.path.sub(/^\//, "")
40 | end
41 |
42 | # https://github.com/ORG/REPO
43 | def repo_url
44 | return unless ENV["CODEBUILD_SOURCE_REPO_URL"]
45 | # https://github.com/ORG/REPO.git
46 | ENV["CODEBUILD_SOURCE_REPO_URL"].sub(".git", "")
47 | end
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/lib/jets/git/custom.rb:
--------------------------------------------------------------------------------
1 | module Jets::Git
2 | class Custom < Base
3 | def info
4 | info = {
5 | git_system: "custom",
6 | git_branch: git_branch,
7 | git_sha: git_sha,
8 | git_dirty: false,
9 | git_message: git_message
10 | # git_version: nil,
11 | }
12 | info[:git_url] = git_url if git_url
13 | info
14 | end
15 |
16 | def git_branch
17 | ENV["JETS_GIT_CUSTOM_BRANCH"]
18 | end
19 |
20 | def git_sha
21 | ENV["JETS_GIT_CUSTOM_SHA"]
22 | end
23 |
24 | def git_url
25 | ENV["JETS_GIT_CUSTOM_URL"]
26 | end
27 |
28 | def git_message
29 | ENV["JETS_GIT_CUSTOM_MESSAGE"]
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/lib/jets/git/git_cli.rb:
--------------------------------------------------------------------------------
1 | module Jets::Git
2 | module GitCli
3 | def git?
4 | git_folder? && git_installed? && git_commits?
5 | end
6 |
7 | def git_folder?
8 | File.exist?(".git")
9 | end
10 |
11 | def git_installed?
12 | system "type git > /dev/null 2>&1"
13 | end
14 |
15 | # Edge case: git init but no commits yet
16 | def git_commits?
17 | system "git rev-parse HEAD >/dev/null 2>&1"
18 | end
19 |
20 | def git(args, on_error: :nil)
21 | out = `git #{args}`.strip
22 | unless $?.success?
23 | case on_error
24 | when :raise
25 | raise Jets::Git::Error, "ERROR: git #{args} failed".color(:red)
26 | when :nil
27 | return
28 | end
29 | end
30 | out
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/lib/jets/git/github.rb:
--------------------------------------------------------------------------------
1 | module Jets::Git
2 | class Github < Base
3 | def info
4 | info = {
5 | git_system: "github",
6 | git_branch: git_branch,
7 | git_sha: git_sha,
8 | git_dirty: false
9 | # git_message: nil,
10 | # git_version: nil,
11 | }
12 | info[:git_url] = git_url if git_url
13 | info
14 | end
15 |
16 | def git_branch
17 | if build_type == "pull_request"
18 | pr.dig("pull_request", "head", "ref")
19 | else # push
20 | ENV["GITHUB_REF_NAME"]
21 | end
22 | end
23 |
24 | def git_sha
25 | if build_type == "pull_request"
26 | pr.dig("pull_request", "head", "sha")
27 | else # push
28 | ENV["GITHUB_SHA"]
29 | end
30 | end
31 |
32 | def git_url
33 | host = ENV["GITHUB_SERVER_URL"] || "https://github.com"
34 | full_repo = ENV["GITHUB_REPOSITORY"]
35 | "#{host}/#{full_repo}"
36 | end
37 |
38 | # GitHub webhook JSON payload in file and path is set in GITHUB_EVENT_PATH
39 | def pr
40 | return {} unless ENV["GITHUB_EVENT_PATH"]
41 | JSON.load(IO.read(ENV["GITHUB_EVENT_PATH"]))
42 | end
43 |
44 | def build_type
45 | ENV["GITHUB_EVENT_NAME"]
46 | end
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/lib/jets/git/gitlab.rb:
--------------------------------------------------------------------------------
1 | module Jets::Git
2 | class Gitlab < Base
3 | def info
4 | info = {
5 | git_system: "gitlab",
6 | git_branch: git_branch,
7 | git_sha: git_sha,
8 | git_dirty: false
9 | # git_message: nil,
10 | # git_version: nil,
11 | }
12 | info[:git_url] = git_url if git_url
13 | info
14 | end
15 |
16 | def git_branch
17 | ENV["CI_COMMIT_REF_NAME"]
18 | end
19 |
20 | def git_sha
21 | ENV["CI_COMMIT_SHA"]
22 | end
23 |
24 | def git_url
25 | host = ENV["CI_SERVER_URL"] || "https://gitlab.com"
26 | full_repo = ENV["CI_PROJECT_PATH"]
27 | "#{host}/#{full_repo}"
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/lib/jets/git/info.rb:
--------------------------------------------------------------------------------
1 | module Jets::Git
2 | class Info
3 | extend Memoist
4 | # Not using options but trying to future proof initialize
5 | def initialize(options = {})
6 | @options = options
7 | end
8 |
9 | def user
10 | User.new
11 | end
12 | memoize :user
13 |
14 | # Best effort to get git info
15 | def params
16 | return {} if ENV["JETS_GIT_DISABLED"]
17 | strategy_class.new.params
18 | end
19 |
20 | def strategy_class
21 | return Saved if File.exist?(".jets/gitinfo.yml")
22 |
23 | env_map = {
24 | BITBUCKET_COMMIT: Bitbucket,
25 | CIRCLECI: Circleci,
26 | CODEBUILD_CI: Codebuild,
27 | GITHUB_ACTIONS: Github,
28 | GITLAB_CI: Gitlab,
29 | JETS_GIT_CUSTOM: Custom,
30 | SYSTEM_TEAMFOUNDATIONSERVERURI: Azure
31 | }
32 | found = env_map.find do |env_key, strategy_class|
33 | ENV[env_key.to_s]
34 | end
35 | found ? found[1] : Local
36 | end
37 | memoize :strategy_class
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/lib/jets/git/saved.rb:
--------------------------------------------------------------------------------
1 | require "yaml"
2 | require "active_support"
3 | require "active_support/core_ext/hash"
4 |
5 | module Jets::Git
6 | class Saved
7 | extend Memoist
8 |
9 | # gitinfo.yml contains original git info from the project
10 | def params
11 | return {} unless File.exist?(".jets/gitinfo.yml")
12 | data = YAML.load_file(".jets/gitinfo.yml")
13 | ActiveSupport::HashWithIndifferentAccess.new(data)
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/lib/jets/git/user.rb:
--------------------------------------------------------------------------------
1 | module Jets::Git
2 | class User
3 | extend Memoist
4 | include GitCli
5 |
6 | def first_name
7 | name.split(" ").first if name # name can be nil
8 | end
9 |
10 | def name
11 | saved[:git_user] || git_config["user.name"]
12 | end
13 |
14 | def saved
15 | return {} unless File.exist?(".jets/gitinfo.yml")
16 | data = YAML.load_file(".jets/gitinfo.yml")
17 | ActiveSupport::HashWithIndifferentAccess.new(data)
18 | end
19 |
20 | def git_config
21 | return {} if ENV["JETS_GIT_DISABLED"]
22 |
23 | return {} unless git?
24 | list = git("config --list")
25 | lines = list.split("\n")
26 | # Other values in the git config are not needed.
27 | # And can cause .to_h to bomb and throw an error.
28 | lines.select! { |l| l =~ /^user\./ }
29 | lines.map { |l| l.split("=") }.to_h
30 | end
31 | memoize :git_config
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/lib/jets/lambda/function.rb:
--------------------------------------------------------------------------------
1 | module Jets::Lambda
2 | class Function < Functions
3 | # Override and change the signature so we do not have to provide info at
4 | # initialization. So:
5 | #
6 | # hello_function = HelloFunction.new
7 | # hello_function.lambda_handler(event, context)
8 | #
9 | # Normally controller and job functions initialize like this:
10 | #
11 | # controller = PostController.new(event, context, "handler_handler")
12 | def initialize
13 | end
14 |
15 | def self.handler
16 | handler_definition.meth
17 | end
18 |
19 | def self.handler_definition
20 | definitions.first
21 | end
22 |
23 | # Used by main_processor.rb. Same interface as controllers and jobs.
24 | def self.process(event, context, meth)
25 | function = new
26 | function.send(handler, event, context)
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/lib/jets/lambda/functions.rb:
--------------------------------------------------------------------------------
1 | require "json"
2 |
3 | # Jets::Lambda::Functions represents a collection of Lambda functions.
4 | #
5 | # Jets::Lambda::Functions is the superclass of:
6 | # Jets::Event::Base
7 | module Jets::Lambda
8 | class Functions
9 | include Jets::ExceptionReporting
10 | include Jets::Util::Logging
11 |
12 | attr_reader :event, :context, :meth
13 | def initialize(event, context, meth)
14 | @event = HashWithIndifferentAccess.new(event) # Hash, JSON.parse(event) ran BaseProcessor
15 | @context = context # Hash. JSON.parse(context) ran in BaseProcessor
16 | @meth = meth # useful to identify which template to use later.
17 | end
18 |
19 | include Dsl # At the end so methods like event, context and method
20 | # do not trigger method_added
21 |
22 | # Pretty hacky since action_view/rendering.rb _normalize_options calls super
23 | def _normalize_options(options) # :doc:
24 | options
25 | end
26 |
27 | class << self
28 | include Jets::Util::Logging
29 |
30 | attr_reader :abstract
31 | alias_method :abstract?, :abstract
32 | @abstract = true
33 |
34 | def inherited(base)
35 | super
36 | subclasses << base if base.name
37 | end
38 |
39 | # Define a controller as abstract. See internal_methods for more details.
40 | def abstract!
41 | @abstract = true
42 | end
43 |
44 | def _prefixes
45 | []
46 | end
47 |
48 | # Tracking subclasses because it helps with Lambda::Dsl#find_all_definitions
49 | def subclasses
50 | @subclasses ||= []
51 | end
52 |
53 | # Needed for depends_on. Got added due to stagger logic.
54 | def output_keys
55 | []
56 | end
57 | end
58 | end
59 | end
60 |
--------------------------------------------------------------------------------
/lib/jets/names.rb:
--------------------------------------------------------------------------------
1 | # This class groups the names in one place.
2 | # Some names are for CloudFormation
3 | # Some are for the Build process
4 | class Jets::Names
5 | # Mainly used by build.rb
6 | class << self
7 | extend Memoist
8 |
9 | def templates_folder
10 | "#{Jets.build_root}/templates"
11 | end
12 |
13 | def one_controller_template_path
14 | "#{templates_folder}/controller.yml"
15 | end
16 |
17 | def app_template_path(app_class)
18 | underscored = underscore(app_class)
19 | "#{templates_folder}/app-#{underscored}.yml"
20 | end
21 |
22 | def shared_template_path(shared_class)
23 | underscored = underscore(shared_class)
24 | "#{templates_folder}/shared-#{underscored}.yml"
25 | end
26 |
27 | # consider moving these methods into cfn/builder/helpers.rb or that area.
28 | def parent_template_path
29 | "#{templates_folder}/parent.yml"
30 | end
31 |
32 | # consider moving these methods into cfn/builder/helpers.rb or that area.
33 | def api_gateway_template_path
34 | "#{templates_folder}/api-gateway.yml"
35 | end
36 |
37 | def api_deployment_template_path
38 | "#{templates_folder}/api-deployment.yml"
39 | end
40 |
41 | def api_mapping_template_path
42 | "#{templates_folder}/api-mapping.yml"
43 | end
44 |
45 | def shared_resources_template_path
46 | "#{templates_folder}/shared-resources.yml"
47 | end
48 |
49 | def parent_stack_name
50 | Jets.project.namespace
51 | end
52 |
53 | def gateway_api_name
54 | Jets.project.namespace
55 | end
56 |
57 | def authorizer_template_path(path)
58 | underscored = underscore(path)
59 | underscored.sub!(/^app-/, "")
60 | "#{templates_folder}/#{underscored}.yml"
61 | end
62 |
63 | def underscore(s)
64 | s.to_s.underscore.sub(/\.rb$/, "").tr("/", "-")
65 | end
66 | end
67 | end
68 |
--------------------------------------------------------------------------------
/lib/jets/prewarm.rb:
--------------------------------------------------------------------------------
1 | module Jets
2 | class Prewarm
3 | class << self
4 | include Jets::Util::Logging
5 |
6 | # Can use to prewarm post deploy
7 | # Jets::Prewarm.handle
8 | # Jets::Prewarm.handle(verbose: true, invocation_type: "RequestResponse")
9 | # Note: verbose is only useful when invocation_type is "RequestResponse"
10 | def handle(options = {})
11 | defaults = {
12 | function_name: "controller",
13 | event: '{"_prewarm": 1}'
14 | }
15 | options = defaults.merge(options.symbolize_keys)
16 | # Always calls Lambda, not local
17 | # Use invoke so messages don't get printed
18 | Jets::CLI::Call.new(options).invoke
19 | rescue Jets::CLI::Call::Error => e
20 | puts "ERROR: #{e.message}".color(:red)
21 | puts "The stack may not be full deployed yet. Please check the stack and try again."
22 | end
23 |
24 | delegate :stats, to: Jets::Shim::Adapter::Prewarm
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/lib/jets/rdoc.rb:
--------------------------------------------------------------------------------
1 | module Jets
2 | module Rdoc
3 | # Use for both jets.gemspec and rake rdoc task
4 | def options
5 | exclude = %w[
6 | docs
7 | spec
8 | vendor
9 | core.rb
10 | .js
11 | templates
12 | commands
13 | internal
14 | support
15 | Dockerfile
16 | Dockerfile.base
17 | Gemfile
18 | Gemfile.lock
19 | Guardfile
20 | LICENSE
21 | Procfile
22 | Rakefile
23 | bin
24 | ]
25 | exclude = exclude.map { |word| ["-x", word] }.flatten
26 | ["-m", "README.md", "--markup", "tomdoc"] + exclude
27 | end
28 | extend self
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/lib/jets/remote/base.rb:
--------------------------------------------------------------------------------
1 | module Jets::Remote
2 | class Base
3 | extend Memoist
4 | include Jets::AwsServices
5 | include Jets::Util::Logging
6 |
7 | def initialize(options)
8 | @options = options
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/lib/jets/shim.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Jets
4 | module Shim
5 | extend Memoist
6 |
7 | def handler(event, context, route = nil)
8 | Handler.new(event, context, route).handle
9 | end
10 |
11 | def to_rack_env(event, context)
12 | Handler.new(event, context).to_rack_env
13 | end
14 |
15 | def boot
16 | # Don't boot Jets in maintenance mode. Makes cold start much faster.
17 | return if Maintenance.enabled?
18 |
19 | paths = %w[
20 | config/jets/shim.rb
21 | ]
22 | paths.map! do |path|
23 | path.starts_with?(".") ? path : "./#{path}"
24 | end
25 | found = paths.find { |p| File.exist?(p) }
26 | if found
27 | require found # calls Jets.shim.configure
28 | else
29 | config.rack_app # all settings are inferred
30 | end
31 |
32 | # Boot Jets to add additional features
33 | Jets.boot
34 | end
35 |
36 | def configure
37 | yield config
38 | end
39 |
40 | def config
41 | Config.instance
42 | end
43 |
44 | extend self
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/lib/jets/shim/adapter/alb.rb:
--------------------------------------------------------------------------------
1 | module Jets::Shim::Adapter
2 | class Alb < Apigw
3 | def env
4 | super.merge(
5 | "HTTP_PORT" => headers["x-forwarded-port"],
6 | "SERVER_PORT" => headers["x-forwarded-port"],
7 | "SERVER_PROTOCOL" => event.dig("requestContext", "protocol") || "HTTP/1.1"
8 | )
9 | end
10 |
11 | def handle?
12 | host =~ /elb\.amazonaws\.com/ ||
13 | event.dig("requestContext", "elb")
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/lib/jets/shim/adapter/apigw.rb:
--------------------------------------------------------------------------------
1 | module Jets::Shim::Adapter
2 | class Apigw < Web
3 | # See: https://github.com/rack/rack/blob/main/lib/rack/constants.rb
4 | def env
5 | {
6 | # Request env keys
7 | "HTTP_HOST" => host,
8 | "HTTP_PORT" => headers["X-Forwarded-Port"],
9 | "HTTPS" => https,
10 | "PATH_INFO" => path_info,
11 | "QUERY_STRING" => query_string,
12 | "REQUEST_METHOD" => event["httpMethod"] || "GET", # useful to default to GET when testing with Lambda console
13 | "REQUEST_PATH" => path_info,
14 | "SCRIPT_NAME" => "",
15 | "SERVER_NAME" => host,
16 | "SERVER_PORT" => headers["X-Forwarded-Port"],
17 | "SERVER_PROTOCOL" => event.dig("requestContext", "protocol") || "HTTP/1.1"
18 | }
19 | end
20 |
21 | def path_info
22 | event["path"] || "/" # always set by API Gateway, but setting to make shim testing easier
23 | end
24 |
25 | def handle?
26 | host =~ /execute-api/ ||
27 | event["resource"] && event.dig("requestContext", "stage")
28 | end
29 |
30 | private
31 |
32 | def query_string
33 | query = event["queryStringParameters"] || {} # always set with API Gateway but when testing shim might not be
34 | Rack::Utils.build_nested_query(query)
35 | end
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/lib/jets/shim/adapter/base.rb:
--------------------------------------------------------------------------------
1 | require "active_support/core_ext/hash"
2 | require "base64"
3 |
4 | module Jets::Shim::Adapter
5 | class Base
6 | extend Memoist
7 | include Jets::Util::Logging
8 |
9 | attr_reader :event, :context, :target
10 | def initialize(event, context = nil, target = nil)
11 | @event = ActiveSupport::HashWithIndifferentAccess.new(event)
12 | @context = context
13 | @target = target # IE: cool_event.party
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/lib/jets/shim/adapter/command.rb:
--------------------------------------------------------------------------------
1 | require "open3"
2 |
3 | module Jets::Shim::Adapter
4 | class Command < Base
5 | def handle
6 | cmd = event[:command]
7 | result = {stdout: "", stderr: ""}
8 | # splat works for both String and Array
9 | Open3.popen3(*cmd) do |stdin, stdout, stderr, wait_thread|
10 | result[:stdout] << stdout.read
11 | result[:stderr] << stderr.read
12 | result[:status] = wait_thread.value.exitstatus
13 | end
14 | result
15 | end
16 |
17 | def handle?
18 | event[:command]
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/jets/shim/adapter/event.rb:
--------------------------------------------------------------------------------
1 | module Jets::Shim::Adapter
2 | class Event < Base
3 | def handle
4 | target_class.handle(event, context, target_method)
5 | end
6 |
7 | def handle?
8 | target && target_class && target_method?
9 | end
10 |
11 | def target_class
12 | class_name, _ = target.split(".")
13 | class_name.camelize.constantize
14 | rescue NameError
15 | end
16 |
17 | def target_method
18 | _, method_name = target.split(".")
19 | method_name ||= "perform"
20 | method_name.to_sym
21 | end
22 |
23 | def target_method?
24 | target_class.public_instance_methods.include?(target_method)
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/lib/jets/shim/adapter/lambda.rb:
--------------------------------------------------------------------------------
1 | module Jets::Shim::Adapter
2 | class Lambda < Web
3 | def env
4 | {
5 | # Request env keys
6 | "HTTP_HOST" => host,
7 | "HTTP_PORT" => headers["x-forwarded-port"],
8 | "HTTPS" => https,
9 | "PATH_INFO" => path_info,
10 | "QUERY_STRING" => query_string,
11 | "REQUEST_METHOD" => event.dig("requestContext", "http", "method") || "GET", # useful to default to GET when testing with Lambda console
12 | "REQUEST_PATH" => path_info,
13 | "SCRIPT_NAME" => "",
14 | "SERVER_NAME" => host,
15 | "SERVER_PORT" => headers["x-forwarded-proto"],
16 | "SERVER_PROTOCOL" => event.dig("requestContext", "http", "protocol") || "HTTP/1.1"
17 | }
18 | end
19 |
20 | def handle?
21 | host =~ /lambda-url/ ||
22 | event["version"] && event["routeKey"]
23 | end
24 |
25 | private
26 |
27 | def path_info
28 | event["rawPath"] || "/"
29 | end
30 |
31 | def query_string
32 | event["rawQueryString"] || ""
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/lib/jets/shim/adapter/prewarm.rb:
--------------------------------------------------------------------------------
1 | module Jets::Shim::Adapter
2 | class Prewarm < Base
3 | @@prewarm_count = 0
4 | @@prewarm_at = nil
5 |
6 | def handle
7 | @@prewarm_count += 1
8 | @@prewarm_at = Time.now.utc
9 | result = self.class.stats
10 | log.info "Prewarm request: #{JSON.dump(result)}" if ENV["JETS_PREWARM_LOG"]
11 | result
12 | end
13 |
14 | def handle?
15 | event["_prewarm"]
16 | end
17 |
18 | def self.stats
19 | {
20 | boot_at: Jets::Core::Booter.boot_at,
21 | gid: Jets::Core::Booter.gid,
22 | prewarm_at: @@prewarm_at,
23 | prewarm_count: @@prewarm_count
24 | }
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/lib/jets/shim/maintenance.rb:
--------------------------------------------------------------------------------
1 | module Jets::Shim
2 | class Maintenance
3 | class << self
4 | extend Memoist
5 | include Jets::Util::Truthy
6 |
7 | def app
8 | self
9 | end
10 |
11 | def call(env)
12 | [503, {"Content-Type" => content_type}, [body]]
13 | end
14 |
15 | # IE: application/json; charset=utf-8
16 | # IE: text/html
17 | def content_type
18 | maintenance_file.end_with?("json") ? "application/json" : "text/html"
19 | end
20 |
21 | def body
22 | IO.read(maintenance_file)
23 | end
24 |
25 | def maintenance_file
26 | default_path = "#{__dir__}/maintenance/maintenance.html"
27 | paths = %w[public/maintenance.html public/maintenance.json]
28 | paths.find { |path| File.exist?(path) } || default_path
29 | end
30 | memoize :maintenance_file
31 |
32 | def enabled?
33 | truthy?(ENV["JETS_MAINTENANCE"])
34 | end
35 | end
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/lib/jets/shim/maintenance/maintenance.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Maintenance Page
8 |
36 |
37 |
38 |
39 |
40 |
We'll be back soon!
41 |
We're currently undergoing some maintenance. Thank you for your patience. Please check back later!
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/lib/jets/shim/response/alb.rb:
--------------------------------------------------------------------------------
1 | module Jets::Shim::Response
2 | class Alb < Apigw
3 | def translate
4 | hash = super
5 | desc = Rack::Utils::HTTP_STATUS_CODES[hash[:statusCode]]
6 | hash[:statusDescription] = "#{hash[:statusCode]} #{desc}"
7 | hash
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/lib/jets/shim/response/apigw.rb:
--------------------------------------------------------------------------------
1 | module Jets::Shim::Response
2 | class Apigw < Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/lib/jets/shim/response/lambda.rb:
--------------------------------------------------------------------------------
1 | module Jets::Shim::Response
2 | class Lambda < Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/lib/jets/shim/response/web.rb:
--------------------------------------------------------------------------------
1 | module Jets::Shim::Response
2 | class Web < Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/lib/jets/stack.rb:
--------------------------------------------------------------------------------
1 | module Jets
2 | class Stack
3 | include Dsl
4 |
5 | class << self
6 | extend Memoist
7 |
8 | # Track all command subclasses.
9 | def subclasses
10 | @subclasses ||= []
11 | end
12 |
13 | def inherited(base)
14 | super
15 | subclasses << base if base.name
16 | end
17 |
18 | # Do not name this output, it'll collide with the output DSL method
19 | def output_value(logical_id)
20 | puts "lookup logical_id: #{logical_id}"
21 | outputs.value(logical_id)
22 | end
23 | # Keep lookup for backwards compatibility
24 | alias_method :lookup, :output_value
25 |
26 | def outputs
27 | Outputs.new(self)
28 | end
29 | memoize :outputs
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/lib/jets/stack/dsl.rb:
--------------------------------------------------------------------------------
1 | class Jets::Stack
2 | module Dsl
3 | extend ActiveSupport::Concern
4 |
5 | include Main
6 | include Output
7 | include Parameter
8 | include Resource
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/lib/jets/stack/dsl/main.rb:
--------------------------------------------------------------------------------
1 | module Jets::Stack::Dsl
2 | module Main
3 | extend ActiveSupport::Concern
4 |
5 | class_methods do
6 | include Base
7 | include Cloudwatch
8 | include Iam
9 | include Lambda
10 | include S3
11 | include Sns
12 | include Sqs
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/jets/stack/dsl/main/base.rb:
--------------------------------------------------------------------------------
1 | module Jets::Stack::Dsl::Main
2 | module Base
3 | def ref(value)
4 | end
5 |
6 | # Examples:
7 | # get_attr("logical_id.attribute")
8 | # get_attr("logical_id", "attribute")
9 | # get_attr(["logical_id", "attribute"])
10 | def get_att(*item)
11 | end
12 |
13 | def logical_id(value)
14 | end
15 |
16 | def depends_on(*stacks)
17 | end
18 |
19 | # Due to `if Jets::Stack.has_resources?` check early on in the bootstraping process
20 | # The code has not been built at that point. So we use a placeholder and will replace
21 | # the placeholder as part of the cfn template build process after the code has been built
22 | # and the code_s3_key with md5 is available.
23 | def code_s3_key
24 | end
25 |
26 | # resource(:hello,
27 | # function_name: "hello",
28 | # code: {
29 | # s3_bucket: "!Ref S3Bucket",
30 | # s3_key: code_s3_key
31 | # },
32 | # description: "Hello world",
33 | # handler: handler_function("hello.lambda_handler"),
34 | # memory_size: 128,
35 | # role: "!Ref IamRole",
36 | # runtime: "python3.6",
37 | # timeout: 20,
38 | # )
39 | def handler(name)
40 | end
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/lib/jets/stack/dsl/main/cloudwatch.rb:
--------------------------------------------------------------------------------
1 | module Jets::Stack::Dsl::Main
2 | module Cloudwatch
3 | def cloudwatch_alarm(id, hash = {})
4 | end
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/lib/jets/stack/dsl/main/iam.rb:
--------------------------------------------------------------------------------
1 | module Jets::Stack::Dsl::Main
2 | module Iam
3 | def iam_role(id, props = {})
4 | end
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/lib/jets/stack/dsl/main/kinesis.rb:
--------------------------------------------------------------------------------
1 | module Jets::Stack::Dsl::Main
2 | module Kinesis
3 | def kinesis_stream(id, props = {})
4 | end
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/lib/jets/stack/dsl/main/lambda.rb:
--------------------------------------------------------------------------------
1 | module Jets::Stack::Dsl::Main
2 | module Lambda
3 | # Example:
4 | #
5 | # function(:hello,
6 | # handler: handler("hello.lambda_hander"),
7 | # runtime: "python3.6"
8 | # )
9 | #
10 | # Defaults to ruby. So:
11 | #
12 | # function(:hello)
13 | #
14 | # is the same as:
15 | #
16 | # function(:hello,
17 | # handler: handler("hello.hande"),
18 | # runtime: :ruby
19 | # )
20 | #
21 | def function(id, props = {})
22 | end
23 | alias_method :ruby_function, :function
24 | alias_method :lambda_function, :function
25 |
26 | def python_function(id, props = {})
27 | end
28 |
29 | def node_function(id, props = {})
30 | end
31 |
32 | # Usage:
33 | #
34 | # permission(:my_permission, principal: "events.amazonaws.com")
35 | #
36 | def permission(id, props = {})
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/lib/jets/stack/dsl/main/s3.rb:
--------------------------------------------------------------------------------
1 | module Jets::Stack::Dsl::Main
2 | module S3
3 | def s3_bucket(id, props = {})
4 | end
5 |
6 | def s3_bucket_configuration(id, props = {})
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/jets/stack/dsl/main/sns.rb:
--------------------------------------------------------------------------------
1 | module Jets::Stack::Dsl::Main
2 | module Sns
3 | def sns_topic(id, props = {})
4 | end
5 |
6 | def sns_topic_policy(id, props = {})
7 | end
8 |
9 | def sns_subscription(id, props = {})
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/lib/jets/stack/dsl/main/sqs.rb:
--------------------------------------------------------------------------------
1 | module Jets::Stack::Dsl::Main
2 | module Sqs
3 | def sqs_queue(id, props = {})
4 | end
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/lib/jets/stack/dsl/output.rb:
--------------------------------------------------------------------------------
1 | module Jets::Stack::Dsl
2 | module Output
3 | extend ActiveSupport::Concern
4 |
5 | class_methods do
6 | def output(*definition)
7 | {}
8 | end
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/lib/jets/stack/dsl/parameter.rb:
--------------------------------------------------------------------------------
1 | module Jets::Stack::Dsl
2 | module Parameter
3 | extend ActiveSupport::Concern
4 |
5 | class_methods do
6 | def parameter(*definition)
7 | {}
8 | end
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/lib/jets/stack/dsl/resource.rb:
--------------------------------------------------------------------------------
1 | module Jets::Stack::Dsl
2 | module Resource
3 | extend ActiveSupport::Concern
4 |
5 | class_methods do
6 | def resource(*definition)
7 | {}
8 | end
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/lib/jets/stack/outputs.rb:
--------------------------------------------------------------------------------
1 | class Jets::Stack
2 | class Outputs
3 | include Jets::AwsServices
4 |
5 | def initialize(stack_subclass)
6 | @stack_subclass = stack_subclass
7 | end
8 |
9 | @@cache = {}
10 | def value(logical_id)
11 | logical_id = logical_id.to_s.camelize
12 | cache_key = "#{@stack_subclass}:#{logical_id}"
13 | return @@cache[cache_key] if @@cache[cache_key]
14 |
15 | child_stack_id = @stack_subclass.to_s.camelize
16 |
17 | stack_arn = shared_stack_arn(child_stack_id)
18 | resp = cfn.describe_stacks(stack_name: stack_arn)
19 | child = resp.stacks.first
20 | return unless child
21 |
22 | @@cache[cache_key] = output_value(child, logical_id)
23 | end
24 |
25 | # Shared child stack arn
26 | def shared_stack_arn(logical_id)
27 | parent_stack = Jets.project.namespace
28 | resp = cfn.describe_stacks(stack_name: parent_stack)
29 | parent = resp.stacks.first
30 | output_value(parent, logical_id)
31 | end
32 |
33 | def output_value(stack, key)
34 | key = key.to_s.camelize
35 | output = stack.outputs.find do |o|
36 | o.output_key == key
37 | end
38 | output&.output_value
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/lib/jets/thor/project_check.rb:
--------------------------------------------------------------------------------
1 | module Jets::Thor
2 | class ProjectCheck
3 | class NotProjectError < StandardError; end
4 |
5 | def initialize(args)
6 | @args = args
7 | end
8 |
9 | def check!
10 | return if no_project_command? || project?
11 | raise NotProjectError, "Not a Jets project. Please run this command from a Jets project folder."
12 | end
13 |
14 | def project?
15 | File.exist?("config/jets")
16 | end
17 |
18 | # Tricky: Thor load the command and then the subcommand.
19 | # IE: jets generate:event
20 | # @args = ["generate", "event"] # first pass
21 | # @args = ["event"] # second pass
22 | # We only check first pass to see if it is a no_project_command.
23 | # And cache it so the second pass never occurs.
24 | @@no_project_command = nil
25 | def no_project_command?
26 | return @@no_project_command unless @@no_project_command.nil?
27 | @@no_project_command = (no_project_commands & @args).any? || @args.empty?
28 | end
29 |
30 | def no_project_commands
31 | # generate for generate:event
32 | # Allow generate in case `jets init` has not been called yet and user
33 | # can generate event classes before `jets init` is called.
34 | #
35 | # The delete command is a special case. It is allowed to run without a project
36 | # in an empty folder. This is because the delete command is used to clean up
37 | # Jets API deployment record.
38 | commands = %w[
39 | delete
40 | generate
41 | init
42 | login
43 | logout
44 | projects
45 | version
46 | ]
47 | commands + Jets::Thor::Base.help_flags + Jets::Thor::Base.version_flags
48 | end
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/lib/jets/thor/shared_options.rb:
--------------------------------------------------------------------------------
1 | module Jets::Thor
2 | # Not naming Options to avoid conflict with Thor::Options
3 | module SharedOptions
4 | extend ActiveSupport::Concern
5 | module ClassMethods
6 | def paging_options(defaults = {})
7 | option :limit, default: defaults[:limit] || 25, aliases: :l, type: :numeric, desc: "Per page limit"
8 | option :order, default: defaults[:order] || "asc", aliases: :o, desc: "Order: asc or desc"
9 | option :page, aliases: :p, type: :numeric, desc: "Page number"
10 | end
11 |
12 | def yes_option
13 | option :yes, aliases: :y, type: :boolean, desc: "Skip are you sure prompt"
14 | end
15 |
16 | def format_option(defaults = {})
17 | default = defaults[:default] || "table"
18 | option :format, default: default, desc: "Output format: #{CliFormat.formats.join(", ")}"
19 | end
20 |
21 | def verbose_option
22 | option :verbose, aliases: :v, default: false, type: :boolean, desc: "Show more verbose logging output. Useful for debugging what's under the hood"
23 | end
24 |
25 | def function_name_option(defaults = {})
26 | default = defaults[:default] || "controller"
27 | option :function, aliases: :n, default: default, desc: "Lambda Function name"
28 | end
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/lib/jets/thor/version_check.rb:
--------------------------------------------------------------------------------
1 | require "fileutils"
2 | require "gems"
3 |
4 | module Jets::Thor
5 | class VersionCheck
6 | def check!
7 | return unless check_needed?
8 |
9 | remote_version = Gems.info("jets")["version"]
10 | local_version = Gem.loaded_specs["jets"].version
11 |
12 | return if remote_version.nil?
13 |
14 | if Gem::Version.new(remote_version) > Gem::Version.new(local_version)
15 | puts <<~EOL
16 | jets has a newer version available.
17 |
18 | installed version: #{local_version}
19 | latest version: #{remote_version}
20 |
21 | Please update jets
22 | EOL
23 | end
24 |
25 | save_last_checked_time
26 | end
27 |
28 | def check_needed?
29 | check_interval = 24 * 60 * 60 # 24 hours in seconds
30 | Time.now - last_checked_time >= check_interval
31 | end
32 |
33 | def last_checked_time
34 | last_time = File.exist?(last_check_file) ? File.read(last_check_file) : "1970-01-01 00:00:00 UTC"
35 | Time.parse(last_time)
36 | end
37 |
38 | def save_last_checked_time
39 | FileUtils.mkdir_p(File.dirname(last_check_file))
40 | File.write(last_check_file, Time.now)
41 | end
42 |
43 | def last_check_file
44 | # Do not define last_check_file as a LAST_CHECK_FILE constant
45 | # On AWS lambda, Jets eager load errors since ENV["HOME"] is nil
46 | # Note: Added an extra safeguard in case ENV["HOME"] is nil
47 | home = ENV["HOME"] || "/root"
48 | File.join(home, ".jets/tmp/last-checked.txt")
49 | end
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/lib/jets/util/call_line.rb:
--------------------------------------------------------------------------------
1 | module Jets::Util
2 | module CallLine
3 | include Pretty
4 |
5 | def jets_call_line
6 | caller.find { |l| l.include?("#{Jets.root}/") }
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/jets/util/camelize.rb:
--------------------------------------------------------------------------------
1 | module Jets::Util
2 | module Camelize
3 | # Not named camelize! because it conflicts with zeitwerk's camelize!
4 | def camelize(object)
5 | result = case object
6 | when Array
7 | object.map { |o| camelize(o) }
8 | when Hash
9 | Jets::Camelizer.transform(object).deep_symbolize_keys
10 | else
11 | object
12 | end
13 |
14 | case object
15 | when Symbol
16 | object
17 | when NilClass
18 | nil
19 | else
20 | object.replace(result)
21 | end
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/lib/jets/util/format_time.rb:
--------------------------------------------------------------------------------
1 | module Jets::Util
2 | module FormatTime
3 | def pretty_time(time)
4 | datetime = case time
5 | when Time
6 | time.to_datetime
7 | when String
8 | DateTime.parse(time)
9 | else
10 | time
11 | end
12 |
13 | if datetime > 1.day.ago.utc
14 | time_ago_in_words(datetime) + " ago"
15 | else
16 | tz_override = ENV["JETS_TZ"] # IE: America/Los_Angeles
17 | local = if tz_override
18 | tz = TZInfo::Timezone.get(tz_override)
19 | tz.time_to_local(datetime)
20 | else
21 | datetime.new_offset(DateTime.now.offset) # local time
22 | end
23 |
24 | if tz_override
25 | local.strftime("%b %-d, %Y %-l:%M:%S%P")
26 | else
27 | local.strftime("%b %-d, %Y %H:%M:%S")
28 | end
29 | end
30 | end
31 |
32 | # Simple implementation of time_ago_in_words so we dont have to include ActionView::Helpers::DateHelper
33 | def time_ago_in_words(from_time, to_time = Time.now)
34 | distance_in_seconds = (to_time - from_time).to_i
35 | case distance_in_seconds
36 | when 0..59
37 | "#{distance_in_seconds} #{"second".pluralize(distance_in_seconds)}"
38 | when 60..3599
39 | minutes = distance_in_seconds / 60
40 | "#{minutes} #{"minute".pluralize(minutes)}"
41 | when 3600..86_399
42 | hours = distance_in_seconds / 3600
43 | "#{hours} #{"hour".pluralize(hours)}"
44 | when 86_400..604_799
45 | days = distance_in_seconds / 86_400
46 | "#{days} #{"day".pluralize(days)}"
47 | else
48 | from_time.strftime("%B %d, %Y")
49 | end
50 | end
51 | end
52 | end
53 |
--------------------------------------------------------------------------------
/lib/jets/util/git.rb:
--------------------------------------------------------------------------------
1 | module Jets::Util
2 | module Git
3 | def git?
4 | File.exist?("#{Jets.root}/.git") && git_installed?
5 | end
6 |
7 | def git_installed?
8 | system("type git > /dev/null 2>&1")
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/lib/jets/util/logging.rb:
--------------------------------------------------------------------------------
1 | module Jets::Util
2 | module Logging
3 | # Both work within the Jets source code.
4 | #
5 | # logger.info
6 | # log.info (encouraged)
7 | #
8 | # Jets.logger also points to this via jets/core.rb by default.
9 | # Hoewever, it can be overridden by other frameworks.
10 | #
11 | delegate :logger, to: "Jets.bootstrap.config"
12 | def log
13 | logger
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/lib/jets/util/pretty.rb:
--------------------------------------------------------------------------------
1 | module Jets::Util
2 | module Pretty
3 | def pretty_path(path)
4 | path.sub("#{Jets.root}/", "").sub(/^\.\//, "")
5 | end
6 |
7 | # Replace HOME with ~ - different from the main pretty_path
8 | def pretty_home(path)
9 | path.sub(ENV["HOME"], "~")
10 | end
11 |
12 | # http://stackoverflow.com/questions/4175733/convert-duration-to-hoursminutesseconds-or-similar-in-rails-3-or-ruby
13 | def pretty_time(total_seconds)
14 | minutes = (total_seconds / 60) % 60
15 | seconds = total_seconds % 60
16 | if total_seconds < 60
17 | "#{seconds.to_i}s"
18 | else
19 | "#{minutes.to_i}m #{seconds.to_i}s"
20 | end
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/lib/jets/util/sh.rb:
--------------------------------------------------------------------------------
1 | module Jets::Util
2 | module Sh
3 | def sh(command, options = {})
4 | quiet = options[:quiet]
5 | on_fail = options[:on_fail] || :raise
6 |
7 | puts "=> #{command}" unless quiet
8 | system(command)
9 | success = $?.success?
10 |
11 | case on_fail
12 | when :raise
13 | raise Jets::Error.new("Command failed: #{command}\n#{caller(1..1).first}") unless success
14 | when :exit
15 | unless success
16 | if quiet
17 | abort("Command failed: #{command}\n")
18 | else
19 | abort("Command failed: #{command}\n#{caller.join("\n")}")
20 | end
21 | end
22 | end
23 |
24 | success
25 | end
26 |
27 | def quiet_sh(command, options = {})
28 | options = options.merge(quiet: true) unless ENV["JETS_DEBUG"]
29 | sh(command, options)
30 | end
31 |
32 | def capture(command)
33 | out = `#{command}`.strip
34 | raise Jets::Error.new("Command failed: #{command}\n#{caller(1..1).first}") unless $?.success?
35 | out
36 | end
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/lib/jets/util/sure.rb:
--------------------------------------------------------------------------------
1 | module Jets::Util
2 | module Sure
3 | private
4 |
5 | def sure?(message = nil)
6 | confirm = "Are you sure?"
7 | if @options[:yes]
8 | yes = "y"
9 | else
10 | out = if message
11 | "#{message}\n#{confirm} (y/N) "
12 | else
13 | "#{confirm} (y/N) "
14 | end
15 | print out
16 | yes = $stdin.gets
17 | end
18 |
19 | unless /^y/.match?(yes)
20 | puts "Whew! Exiting."
21 | exit 0
22 | end
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/lib/jets/util/truthy.rb:
--------------------------------------------------------------------------------
1 | module Jets::Util
2 | module Truthy
3 | # Allows use non-truthy values like
4 | # n no false off null nil 0
5 | def truthy?(value)
6 | %w[y yes true on 1].include?(value.to_s.downcase)
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/jets/util/yamler.rb:
--------------------------------------------------------------------------------
1 | # Named Yamler to make it clear it's not the YAML class.
2 | module Jets::Util
3 | class Yamler
4 | class << self
5 | def load(text)
6 | options = RUBY_VERSION.match?(/^3/) ? {aliases: true} : {} # Ruby 3.0.0 deprecates aliases: true
7 | YAML.load(text, **options)
8 | end
9 |
10 | def load_file(path)
11 | options = RUBY_VERSION.match?(/^3/) ? {aliases: true} : {} # Ruby 3.0.0 deprecates aliases: true
12 | YAML.load_file(path, **options)
13 | end
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/lib/jets/version.rb:
--------------------------------------------------------------------------------
1 | module Jets
2 | VERSION = "6.0.5"
3 | end
4 |
--------------------------------------------------------------------------------
/spec/fixtures/shim/events/alb.json:
--------------------------------------------------------------------------------
1 | {
2 | "requestContext": {
3 | "elb": {
4 | "targetGroupArn": "arn:aws:elasticloadbalancing:us-east-2:123456789012:targetgroup/lambda-279XGJDqGZ5rsrHC2Fjr/49e9d65c45c6791a"
5 | }
6 | },
7 | "httpMethod": "POST",
8 | "path": "/path/to/resource",
9 | "queryStringParameters": {
10 | "query": "1234ABCD"
11 | },
12 | "headers": {
13 | "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
14 | "accept-encoding": "gzip",
15 | "accept-language": "en-US,en;q=0.9",
16 | "connection": "keep-alive",
17 | "host": "lambda-alb-123578498.us-east-2.elb.amazonaws.com",
18 | "upgrade-insecure-requests": "1",
19 | "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36",
20 | "x-amzn-trace-id": "Root=1-5c536348-3d683b8b04734faae651f476",
21 | "x-forwarded-for": "72.12.164.125",
22 | "x-forwarded-port": "80",
23 | "x-forwarded-proto": "http",
24 | "x-imforwards": "20"
25 | },
26 | "body": "eyJ0ZXN0IjoiYm9keSJ9",
27 | "isBase64Encoded": true
28 | }
29 |
--------------------------------------------------------------------------------
/spec/fixtures/shim/frameworks/rails/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require_relative "config/environment"
4 |
5 | run Rails.application
6 | Rails.application.load_server
7 |
--------------------------------------------------------------------------------
/spec/fixtures/shim/frameworks/sinatra/config.ru:
--------------------------------------------------------------------------------
1 | require File.expand_path("my_app", File.dirname(__FILE__))
2 |
3 | run MyApp
4 |
--------------------------------------------------------------------------------
/spec/fixtures/shim/frameworks/sinatra_modular/app.rb:
--------------------------------------------------------------------------------
1 | class App < Sinatra::Base
2 | get "/" do
3 | "hello world"
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/spec/fixtures/shim/frameworks/sinatra_modular/config.ru:
--------------------------------------------------------------------------------
1 | require "sinatra/base"
2 |
3 | class MyApp < Sinatra::Base
4 | get "/" do
5 | "hello world"
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/spec/fixtures/shim/handlers/controller.rb:
--------------------------------------------------------------------------------
1 | require "bundler/setup"
2 | require "jets/shim"
3 |
4 | Jets::Shim.boot
5 |
6 | def lambda_handler(event:, context:)
7 | Jets::Shim.handler(event, context)
8 | end
9 |
10 | if __FILE__ == $0
11 | event_path = ENV["EVENT"]
12 | event = if event_path && File.exist?(event_path)
13 | JSON.load(IO.read(event_path))
14 | else
15 | # APIGW
16 | {
17 | path: "/posts",
18 | httpMethod: "GET",
19 | headers: {
20 | Host: "foobar.execute-api.us-west-2.amazonaws.com"
21 | }
22 | }
23 | end
24 | resp = lambda_handler(event: event, context: {})
25 | puts "resp: "
26 | pp resp
27 | end
28 |
--------------------------------------------------------------------------------
/spec/lib/jets/cli/curl/request_spec.rb:
--------------------------------------------------------------------------------
1 | describe Jets::CLI::Curl::Request do
2 | let :request do
3 | described_class.new(options)
4 | end
5 |
6 | describe "jets url /" do
7 | let :options do
8 | # Example:
9 | # {
10 | # function: "controller",
11 | # verbose: true,
12 | # request: "GET",
13 | # headers: {},
14 | # trim: true,
15 | # data: "@data.json",
16 | # path: "/"
17 | # }
18 | {path: "/", trim: true}
19 | end
20 |
21 | it "convert payload" do
22 | hash = JSON.parse(request.payload) # payload is a JSON string
23 | expect(hash["rawPath"]).to eq "/"
24 | end
25 |
26 | # Sanity check
27 | it "invoke" do
28 | mocked_response = {statusCode: 200, body: "body"}
29 | allow(request).to receive(:invoke).and_return(mocked_response)
30 | # stub methods
31 | allow(request).to receive(:warn)
32 | allow(request).to receive(:function_name)
33 |
34 | response = request.run
35 | expect(response).to eq mocked_response
36 | end
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/spec/lib/jets/shim/adapter/alb_spec.rb:
--------------------------------------------------------------------------------
1 | describe Jets::Shim::Adapter::Alb do
2 | describe "alb" do
3 | let :adapter do
4 | described_class.new(event)
5 | end
6 | let :event do
7 | JSON.load(IO.read("spec/fixtures/shim/events/alb.json"))
8 | end
9 |
10 | it "transforms event to rack env" do
11 | env = adapter.to_rack_env
12 | expect(env["REQUEST_METHOD"]).to eq "POST"
13 | expect(env["PATH_INFO"]).to eq "/path/to/resource"
14 | expect(env["QUERY_STRING"]).to eq "query=1234ABCD"
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/spec/lib/jets/shim/adapter/apigw_spec.rb:
--------------------------------------------------------------------------------
1 | describe Jets::Shim::Adapter::Apigw do
2 | let :adapter do
3 | described_class.new(event)
4 | end
5 |
6 | describe "apigw" do
7 | let :event do
8 | JSON.load(IO.read("spec/fixtures/shim/events/apigw.json"))
9 | end
10 |
11 | it "transforms event to rack env" do
12 | env = adapter.to_rack_env
13 | expect(env["REQUEST_METHOD"]).to eq "POST"
14 | expect(env["PATH_INFO"]).to eq "/path/to/resource"
15 | expect(env["QUERY_STRING"]).to eq "foo=bar"
16 |
17 | expect(env["HTTP_HOST"]).to eq "1234567890.execute-api.us-east-1.amazonaws.com"
18 | end
19 | end
20 |
21 | # Note: I tested it and content-length is not available in apigw event
22 | # See: https://stackoverflow.com/questions/56693981/how-do-i-get-http-header-content-length-in-api-gateway-lambda-proxy-integratio
23 | describe "content-type" do
24 | let :event do
25 | # curl -H "Content-Type: application/json" => produces this:
26 | {
27 | "headers" => {
28 | "content-type" => "application/json"
29 | }
30 | }
31 | end
32 |
33 | it "should set CONTENT_TYPE" do
34 | env = adapter.to_rack_env
35 | expect(env["CONTENT_TYPE"]).to eq "application/json"
36 | expect(env["HTTP_CONTENT_TYPE"]).to be nil
37 | end
38 | end
39 |
40 | describe "request-uri" do
41 | let :event do
42 | {
43 | "path" => "/path/to/resource",
44 | "queryStringParameters" => {
45 | "foo" => "bar"
46 | }
47 | }
48 | end
49 |
50 | it "should set REQUEST_URI" do
51 | env = adapter.to_rack_env
52 | expect(env["REQUEST_URI"]).to eq "/path/to/resource?foo=bar"
53 | end
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/spec/lib/jets/shim/adapter/response/alb_spec.rb:
--------------------------------------------------------------------------------
1 | describe Jets::Shim::Response::Alb do
2 | let :response do
3 | described_class.new(triplet)
4 | end
5 |
6 | describe "apigw" do
7 | let :triplet do
8 | [
9 | 200,
10 | {"Content-Type" => "application/json"},
11 | ["body"]
12 | ]
13 | end
14 |
15 | it "has alb structure" do
16 | h = response.translate
17 | expect(h.keys.size).to eq 5
18 | expect(h.keys.sort).to eq [:body, :headers, :isBase64Encoded, :statusCode, :statusDescription]
19 | expect(h[:statusDescription]).to eq "200 OK"
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/spec/lib/jets/shim/config_spec.rb:
--------------------------------------------------------------------------------
1 | describe Jets::Shim::Config do
2 | let :config do
3 | described_class.instance
4 | end
5 | before(:each) do
6 | config.flush_cache # unmemoize :framework?
7 | end
8 |
9 | describe "config" do
10 | it "Rails" do
11 | Dir.chdir("spec/fixtures/shim/frameworks/rails") do
12 | expect(config.rails?).to be true
13 | end
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | ENV["JETS_ENV"] = "test"
2 | ENV["JETS_TEST"] = "1"
3 | ENV["AWS_MFA_SECURE_TEST"] = "1"
4 | # Ensures aws api never called. Fixture home folder does not contain ~/.aws/credentials
5 | ENV["HOME"] = File.join(Dir.pwd, "spec/fixtures/shim/home")
6 |
7 | require "aws-sdk-core"
8 | require "byebug"
9 | require "fileutils"
10 | require "memoist"
11 | require "pp"
12 |
13 | root = File.expand_path("..", __dir__)
14 | require "#{root}/lib/jets"
15 |
16 | module Helpers
17 | end
18 |
19 | RSpec.configure do |c|
20 | c.before(:suite) do
21 | Aws.config.update(stub_responses: true)
22 | end
23 |
24 | c.include Helpers
25 | end
26 |
--------------------------------------------------------------------------------