├── .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 | [![Gem Version](https://badge.fury.io/rb/jets.svg)](https://badge.fury.io/rb/jets) 6 | 7 | [![BoltOps Badge](https://img.boltops.com/boltops/badges/boltops-badge.png)](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 | --------------------------------------------------------------------------------