diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 84608ff93..000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,92 +0,0 @@ -# Ruby CircleCI 2.0 configuration file -# -# Check https://circleci.com/docs/2.0/language-ruby/ for more details -# -version: 2 -jobs: - build: - docker: - # specify the version you desire here - - image: circleci/ruby:2.5.0-node-browsers - - # Specify service dependencies here if necessary - # CircleCI maintains a library of pre-built images - # documented at https://circleci.com/docs/2.0/circleci-images/ - # - image: circleci/postgres:9.4 - - working_directory: ~/repo - - steps: - - checkout - - - run: - name: submodule sync - command: | - git submodule sync - git submodule update --init - - # Download and cache dependencies - - restore_cache: - keys: - - v2-dependencies-{{ checksum "jets.gemspec" }} - # fallback to using the latest cache if no exact match is found - - v2-dependencies- - - - run: - name: install dependencies - command: | - bundle install --jobs=4 --retry=3 --path vendor/bundle - - - save_cache: - paths: - - ./vendor/bundle - key: v2-dependencies-{{ checksum "jets.gemspec" }} - - # Database setup - # - run: bundle exec rake db:create - # - run: bundle exec rake db:schema:load - - - run: - name: install dynamodb-local - command: | - whoami > /tmp/whoami.txt - - mkdir ~/dynamodb_local_latest - cd ~/dynamodb_local_latest - wget https://s3-us-west-2.amazonaws.com/dynamodb-local/dynamodb_local_latest.tar.gz - tar zxf dynamodb_local_latest.tar.gz - cat >~/dynamodb-local <.rspec < - -# Install bundle of gems first in a layer -# so if the Gemfile doesnt chagne it wont have to install gems again -WORKDIR /tmp -COPY Gemfile* /tmp/ -RUN bundle install && rm -rf /root/.bundle/cache - -# Add the Rails app -ENV HOME /root -WORKDIR /app -COPY . /app -RUN bundle install - -CMD ["uptime"] diff --git a/Dockerfile.base b/Dockerfile.base deleted file mode 100644 index b0775278f..000000000 --- a/Dockerfile.base +++ /dev/null @@ -1,53 +0,0 @@ -FROM ruby:2.5.1 -MAINTAINER Tung Nguyen - -RUN apt-get update && \ - apt-get install -y net-tools netcat && \ - rm -rf /var/lib/apt/lists/* && apt-get clean && apt-get purge - -# Packages -# capybara-webkit: libqt4-dev libqtwebkit-dev -RUN apt-get update && \ - apt-get install -y software-properties-common && \ - add-apt-repository ppa:git-core/ppa -y && \ - apt-get update && \ - apt-get install -y \ - build-essential \ - libqt4-dev libqtwebkit-dev \ - nodejs \ - telnet \ - curl \ - vim \ - htop \ - mysql-client \ - lsof && \ - rm -rf /var/lib/apt/lists/* && apt-get clean && apt-get purge - -# ssh key for bundle to access gems that are in private repos -# COPY config/ssh /root/.ssh -# RUN chmod 600 /root/.ssh/id_rsa-boltops-docker - -# Install bundle of gems -RUN gem install bundler -WORKDIR /tmp -COPY lib/jets/version.rb /tmp/lib/jets/version.rb -COPY jets.gemspec /tmp/ -COPY Gemfile* /tmp/ -RUN bundle install --jobs=4 --retry=3 && rm -rf /root/.bundle/cache - -# Do not try to precompile assets here because it could resurrect files -# This happened with config/initializers/rollbar.rb. - -# Add development like customizations -# COPY config/home/irbrc /root/.irbrc -ENV TERM xterm - -COPY .codebuild/scripts /tmp/scripts -RUN bash -eux /tmp/scripts/install-docker.sh -RUN bash -exu /tmp/scripts/install-java.sh -RUN bash -exu /tmp/scripts/install-dynamodb-local.sh -RUN bash -exu /tmp/scripts/install-node.sh - -RUN apt-get install -y zip - -CMD ["/bin/bash"] diff --git a/Gemfile b/Gemfile index 174c51aca..2804878c2 100644 --- a/Gemfile +++ b/Gemfile @@ -8,3 +8,7 @@ group :development, :test do gem "mysql2", "~> 0.5.2" gem "dynomite", "~> 2.0.0" end + +group :test do + gem "actionpack", "~> 7.1.3" # jets shim specs +end diff --git a/Guardfile b/Guardfile deleted file mode 100644 index 4759a57c4..000000000 --- a/Guardfile +++ /dev/null @@ -1,22 +0,0 @@ -guard "bundler", cmd: "bundle" do - watch("Gemfile") - watch(/^.+\.gemspec/) -end - -guard :rspec, cmd: "bundle exec rspec" do - require "guard/rspec/dsl" - dsl = Guard::RSpec::Dsl.new(self) - - # RSpec files - rspec = dsl.rspec - watch(rspec.spec_helper) { rspec.spec_dir } - watch(rspec.spec_support) { rspec.spec_dir } - watch(rspec.spec_files) - - # Ruby files - ruby = dsl.ruby - puts "ruby.lib_files #{ruby.lib_files.inspect}" - dsl.watch_spec_files_for(ruby.lib_files) - - watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } -end diff --git a/Procfile b/Procfile deleted file mode 100644 index e634d02bf..000000000 --- a/Procfile +++ /dev/null @@ -1,2 +0,0 @@ -local: dynamodb-local # port 8000 -admin: env AWS_ACCESS_KEY_ID=$DYNAMODB_ADMIN_AWS_ACCESS_KEY_ID PORT=8001 dynamodb-admin # port 8001 diff --git a/README.md b/README.md index f229cdcd7..fb963108d 100644 --- a/README.md +++ b/README.md @@ -2,189 +2,43 @@ -Ruby and Lambda had a baby and that child's name is [Jets](http://rubyonjets.com/). - -![Build Status](https://codebuild.us-west-2.amazonaws.com/badges?uuid=eyJlbmNyeXB0ZWREYXRhIjoiZ08vK2hjOHczQUVoUDhSYnBNNUU4T0gxQWJuOTlLaXpwVGQ1NjJ3NnVDY1dSdFVXQ3d2VXVSQzRFcU1qd1JPMndFZlByRktIcTUrZm5GWlM5dHpjM1ZrPSIsIml2UGFyYW1ldGVyU3BlYyI6Imluc1Qrd25GanhUdHlidjUiLCJtYXRlcmlhbFNldFNlcmlhbCI6MX0%3D&branch=master) [![Gem Version](https://badge.fury.io/rb/jets.svg)](https://badge.fury.io/rb/jets) -[![Support](https://img.shields.io/badge/Support-Help-blue.svg)](http://rubyonjets.com/support/) [![BoltOps Badge](https://img.boltops.com/boltops/badges/boltops-badge.png)](https://www.boltops.com) Please **watch/star** this repo to help grow and support the project. -**Upgrading**: If you are upgrading Jets, please check on the [Upgrading Notes](http://rubyonjets.com/docs/extras/upgrading/). - -## What is Ruby on Jets? - -Jets is a Ruby Serverless Framework. Jets allows you to create serverless applications with a beautiful language: Ruby. It includes everything required to build and deploy an application to AWS Lambda. - -Understanding AWS Lambda and API Gateway is key to understanding Jets conceptually. Jets map your code to Lambda functions and other AWS Resources like API Gateway and Event Rules. - -* **AWS Lambda** is functions as a service. It allows you to upload and run functions without worrying about the underlying infrastructure. -* **API Gateway** is the routing layer for Lambda. It is used to route REST URL endpoints to Lambda functions. -* **EventBridge Rules** are events as a service. You can automatically run Lambda functions triggered from AWS services. You decide what events to catch and how to react to them. - -The official documentation is at [Ruby on Jets](http://rubyonjets.com). - -Refer to the official docs for more info, but here's a quick intro. - -### Jets Functions - -Jets supports writing AWS Lambda functions with Ruby. You define them in the `app/functions` folder. A function looks like this: - -app/functions/simple.rb: - -```ruby -def lambda_handler(event:, context:) - puts "hello world" - {hello: "world"} -end -``` - -Here's the function in the Lambda console: - -![Code Example in AWS Lambda console](https://img.boltops.com/tools/jets/readme/simple-lambda-function.png) - - -Though simple functions are supported by Jets, they do not add as much value as other ways to write Ruby code with Jets. Classes like [Controllers](http://rubyonjets.com/docs/controllers/) and [Jobs](http://rubyonjets.com/docs/jobs/) add many conveniences and are more powerful to use. We’ll cover them next. - -### Jets Controllers +## What is Jets? -A Jets controller handles a web request and renders a response. Here's an example: +[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. -app/controllers/posts_controller.rb: +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. -```ruby -class PostsController < ApplicationController - def index - # renders Lambda Proxy structure compatible with API Gateway - render json: {hello: "world", action: "index"} - end - - def show - id = params[:id] # params available - # puts goes to the lambda logs - puts event # raw lambda event available - render json: {action: "show", id: id} - end -end -``` - -Helper methods like `params` provide the parameters from the API Gateway event. The `render` method returns a Lambda Proxy structure that API Gateway understands. - -Jets creates single Lambda functions to handle your Jets Controller requests. The Lambda Function handler is a shim that routes to your controller action. - -### Jets Routing - -You connect Lambda functions to API Gateway URL endpoints with a routes file: - -config/routes.rb: - -```ruby -Jets.application.routes.draw do - resources :posts - any "posts/hot", to: "posts#hot" # GET, POST, PUT, etc request all work -end -``` - -The `routes.rb` gets translated to API Gateway resources: - -![API Gateway Resources generated from routes in AWS console](https://img.boltops.com/tools/jets/readme/apigw.png) - -Test your API Gateway endpoints with curl or postman. Note, replace the URL endpoint with the one that is created: - - $ curl -s "https://quabepiu80.execute-api.us-east-1.amazonaws.com/dev/posts" | jq . - { - "hello": "world", - "action": "index" - } +## Quick Start -### Jets Jobs +Add to your project. -A Jets job handles asynchronous background jobs outside the web request/response cycle. Here's an example: - -app/jobs/hard_job.rb: +Gemfile ```ruby -class HardJob < ApplicationJob - rate "10 hours" # every 10 hours - def dig - puts "done digging" - end - - cron "0 */12 * * ? *" # every 12 hours - def lift - puts "done lifting" - end -end +gem "jets-rails", ">= 1.0" +gem "jets", ">= 6.0" ``` -![Jets Jobs in AWS Lambda Console](https://img.boltops.com/tools/jets/readme/jets-jobs.png) - -`HardJob#dig` runs every 10 hours, and `HardJob#lift` runs every 12 hours. The `rate` and `cron` methods created CloudWatch Event Rules. Example: +And run -![CloudWatch Event Rules in AWS Console](https://img.boltops.com/tools/jets/readme/jets-jobs-event-rules.png) + jets init + jets deploy -This simple example uses Scheduled Events. There are many more possibilities, see the [Events Docs](https://docs.rubyonjets.com/docs/events/). +That's it. -### Jets Deployment +## Docs -You can test your application with a local server that mimics API Gateway: [Jets Local Server](http://rubyonjets.com/docs/local-server/). Once ready, deploying to AWS Lambda is a single command. +Official Docs: [docs.rubyonjets.com](https://docs.rubyonjets.com) - jets deploy +Getting Started Learn Guides: -After deployment, you can test the Lambda functions with the AWS Lambda console or the CLI. - -### Live Demos - -Here are some demos of Jets applications: - -* [Quintessential CRUD Jets app](https://demo.rubyonjets.com/) -* [API Demo](https://api.demo.rubyonjets.com/) -* [Image Upload with CarrierWave](https://upload.demo.rubyonjets.com/) - -Please feel free to add your examples to the [rubyonjets/examples](https://github.com/rubyonjets/examples) repo. - -### More Info - -For more documentation, check out the official docs: [Ruby on Jets](http://rubyonjets.com/). Here's a list of useful links: - -* [Quick Start](http://rubyonjets.com/quick-start/) -* [Local Jets Server](http://rubyonjets.com/docs/local-server/) -* [REPL Console](http://rubyonjets.com/docs/repl-console/) -* [Project Structure](http://rubyonjets.com/docs/structure/) -* [App Configuration](http://rubyonjets.com/docs/config/) -* [Database Support](http://rubyonjets.com/docs/database/) -* [Polymorphic Support](http://rubyonjets.com/docs/polymorphic/) -* [Rails Support](http://rubyonjets.com/docs/rails-support/) -* [Tutorials](http://rubyonjets.com/docs/tutorials/) -* [Prewarming](http://rubyonjets.com/docs/prewarming/) -* [Custom Resources](http://rubyonjets.com/docs/custom/) -* [Shared Resources](http://rubyonjets.com/docs/custom/shared-resources/) -* [Installation](http://rubyonjets.com/docs/install/) -* [CLI Reference](http://rubyonjets.com/reference/) -* [Contributing](http://rubyonjets.com/docs/more/contributing/) -* [Support Jets](http://rubyonjets.com/donate/) -* [Example Projects](https://github.com/tongueroo/jets-examples) - -## Learning Content - -* [Introducing Jets: A Ruby Serverless Framework](https://blog.boltops.com/2018/08/18/introducing-jets-a-ruby-serverless-framework) -* [Official AWS Ruby Support for Jets](https://blog.boltops.com/2018/12/12/official-aws-ruby-support-for-jets-serverless-framework) -* [Build an API with Jets](https://blog.boltops.com/2019/01/13/build-an-api-service-with-jets-ruby-serverless-framework) -* [Serverless Ruby Cron Jobs Tutorial: Route53 Backup](https://blog.boltops.com/2019/01/03/serverless-ruby-cron-jobs-with-jets-route53-backup) -* [Serverless Slack Commands: Fun with AWS Image Recognition](https://blog.boltops.com/2021/02/02/serverless-slack-commands-with-ruby) -* [Jets Afterburner: Rails Support](https://blog.boltops.com/2018/12/21/jets-afterburner-serverless-rails-on-aws-lambda-in-5-minutes) -* [Jets Mega Mode: Jets and Rails](https://blog.boltops.com/2018/11/03/jets-mega-mode-run-rails-on-aws-lambda) -* [Toronto Serverless Presentation](https://blog.boltops.com/2018/09/25/toronto-serverless-presentation-jets-framework-on-aws-lambda) -* [Jets Image Uploads Tutorial with CarrierWave](https://blog.boltops.com/2018/12/13/jets-image-upload-carrierwave-tutorial-binary-support) -* [Jets Tutorial An Introductory CRUD App Part 1](https://blog.boltops.com/2018/09/07/jets-tutorial-crud-app-introduction-part-1) -* [Jets Tutorial Deploy to AWS Lambda Part 2](https://blog.boltops.com/2018/09/08/jets-tutorial-deploy-to-aws-lambda-part-2) -* [Jets Tutorial Debugging Logs Part 3](https://blog.boltops.com/2018/09/09/jets-tutorial-debugging-logs-part-3) -* [Jets Tutorial Background Jobs Part 4](https://blog.boltops.com/2018/09/10/jets-tutorial-background-jobs-part-4) -* [Jets Tutorial IAM Policies Part 5](https://blog.boltops.com/2018/09/11/jets-tutorial-iam-policies-part-5) -* [Jets Tutorial Function Properties Part 6](https://blog.boltops.com/2018/09/12/jets-tutorial-function-properties-part-6) -* [Jets Tutorial Extra Environments Part 7](https://blog.boltops.com/2018/09/13/jets-tutorial-extra-environments-part-7) -* [Jets Tutorial Different Environments Part 8](https://blog.boltops.com/2018/09/26/jets-tutorial-different-environments-part-8) -* [Jets Tutorial Polymorphic Support Part 9](https://blog.boltops.com/2018/09/27/jets-tutorial-polymorphic-support-part-9) -* [Jets Delete Tutorial](https://blog.boltops.com/2018/11/12/jets-tutorial-jets-delete) +* [Rails](https://docs.rubyonjets.com/docs/learn/rails/) +* [Sinatra](https://docs.rubyonjets.com/docs/learn/sinatra/) +* [Rack](https://docs.rubyonjets.com/docs/learn/rack/) +* [Events](https://docs.rubyonjets.com/docs/learn/events/) diff --git a/Rakefile b/Rakefile index d312124c5..30b2c4884 100644 --- a/Rakefile +++ b/Rakefile @@ -3,7 +3,7 @@ Bundler.setup require "bundler/gem_tasks" require "rspec/core/rake_task" -task :default => :spec +task default: :spec RSpec::Core::RakeTask.new @@ -16,8 +16,8 @@ task :docs do end # Thanks: https://docs.ruby-lang.org/en/2.1.0/RDoc/Task.html -require 'rdoc/task' -require 'jets/rdoc' +require "rdoc/task" +require "jets/rdoc" RDoc::Task.new do |rdoc| rdoc.options += Jets::Rdoc.options diff --git a/engines/internal/app/controllers/jets/application_controller.rb b/engines/internal/app/controllers/jets/application_controller.rb deleted file mode 100644 index 2dc8ea868..000000000 --- a/engines/internal/app/controllers/jets/application_controller.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -class Jets::ApplicationController < Jets::Controller::Base # :nodoc: - prepend_view_path File.expand_path("../../../app/views", __dir__) - layout "application" - - before_action :disable_content_security_policy_nonce! - - content_security_policy do |policy| - policy.script_src :self, :unsafe_inline - policy.style_src :self, :unsafe_inline - end - - private - def require_local! - unless local_request? - render html: "

For security purposes, this information is only available to local requests.

".html_safe, status: :forbidden - end - end - - def local_request? - Jets.application.config.consider_all_requests_local || request.local? - end - - def disable_content_security_policy_nonce! - request.content_security_policy_nonce_generator = nil - end -end diff --git a/engines/internal/app/controllers/jets/bare_controller.rb b/engines/internal/app/controllers/jets/bare_controller.rb deleted file mode 100644 index a2e68ca37..000000000 --- a/engines/internal/app/controllers/jets/bare_controller.rb +++ /dev/null @@ -1,6 +0,0 @@ -# Parent class for MountController -class Jets::BareController < Jets::Controller::Base - layout false - abstract! - skip_forgery_protection -end \ No newline at end of file diff --git a/engines/internal/app/controllers/jets/health_controller.rb b/engines/internal/app/controllers/jets/health_controller.rb deleted file mode 100644 index d1441aadb..000000000 --- a/engines/internal/app/controllers/jets/health_controller.rb +++ /dev/null @@ -1,55 +0,0 @@ -# frozen_string_literal: true - -module Jets - # Built-in Health Check Endpoint - # - # \Jets also comes with a built-in health check endpoint that is reachable at - # the +/up+ path. This endpoint will return a 200 status code if the app has - # booted with no exceptions, and a 500 status code otherwise. - # - # In production, many applications are required to report their status upstream, - # whether it's to an uptime monitor that will page an engineer when things go - # wrong, or a load balancer or Kubernetes controller used to determine a pod's - # health. This health check is designed to be a one-size fits all that will work - # in many situations. - # - # While any newly generated \Jets applications will have the health check at - # +/up+, you can configure the path to be anything you'd like in your - # "config/routes.rb": - # - # Jets.application.routes.draw do - # get "healthz" => "jets/health#show", as: :jets_health_check - # end - # - # The health check will now be accessible via the +/healthz+ path. - # - # NOTE: This endpoint does not reflect the status of all of your application's - # dependencies, such as the database or redis cluster. Replace - # "jets/health#show" with your own controller action if you have - # application specific needs. - # - # Think carefully about what you want to check as it can lead to situations - # where your application is being restarted due to a third-party service going - # bad. Ideally, you should design your application to handle those outages - # gracefully. - class HealthController < Jets::Controller::Base - rescue_from(Exception) { render_down } - - def show - render_up - end - - private - def render_up - render html: html_status(color: "green") - end - - def render_down - render html: html_status(color: "red"), status: 500 - end - - def html_status(color:) - %().html_safe - end - end -end diff --git a/engines/internal/app/controllers/jets/info_controller.rb b/engines/internal/app/controllers/jets/info_controller.rb deleted file mode 100644 index dc800e9d9..000000000 --- a/engines/internal/app/controllers/jets/info_controller.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true - -class Jets::InfoController < Jets::ApplicationController # :nodoc: - prepend_view_path ActionDispatch::DebugView::RESCUES_TEMPLATE_PATH - layout -> { request.xhr? ? false : "application" } - - before_action :require_local! - - def index - redirect_to action: :routes - end - - def properties - @info = Jets::Info.to_html - @page_title = "Properties" - end - - def routes - if path = params[:path] - path = URI::DEFAULT_PARSER.escape path - normalized_path = with_leading_slash path - render json: { - exact: match_route { |it| it.match normalized_path }, - fuzzy: match_route { |it| it.spec.to_s.match path } - } - else - @routes_table = routes_table - @page_title = "Routes" - end - end - - private - def routes_table - text = Jets::Router::Help.new(format: "markdown").text - Kramdown::Document.new(text).to_html - end - - def match_route - _routes.routes.filter_map { |route| route.path.spec.to_s if yield route.path } - end - - def with_leading_slash(path) - ("/" + path).squeeze("/") - end -end diff --git a/engines/internal/app/controllers/jets/mailers_controller.rb b/engines/internal/app/controllers/jets/mailers_controller.rb deleted file mode 100644 index 7f7ed4559..000000000 --- a/engines/internal/app/controllers/jets/mailers_controller.rb +++ /dev/null @@ -1,95 +0,0 @@ -# frozen_string_literal: true - -class Jets::MailersController < Jets::Controller::Base # :nodoc: - around_action :set_locale, only: :preview - before_action :find_preview, only: [:preview] - before_action :require_local!, unless: :show_previews? - - helper_method :part_query, :locale_query - - content_security_policy(false) - - def index - @previews = ActionMailer::Preview.all - @page_title = "Mailer Previews" - end - - def preview - if params[:path] == @preview.preview_name - @page_title = "Mailer Previews for #{@preview.preview_name}" - render action: "mailer" - else - @email_action = File.basename(params[:path]) - - if @preview.email_exists?(@email_action) - @page_title = "Mailer Preview for #{@preview.preview_name}##{@email_action}" - @email = @preview.call(@email_action, params) - - if params[:part] - part_type = Mime::Type.lookup(params[:part]) - - if part = find_part(part_type) - response.content_type = part_type - render plain: part.respond_to?(:decoded) ? part.decoded : part - else - raise AbstractController::ActionNotFound, "Email part '#{part_type}' not found in #{@preview.name}##{@email_action}" - end - else - @part = find_preferred_part(request.format, Mime[:html], Mime[:text]) - render action: "email", layout: false, formats: [:html] - end - else - raise AbstractController::ActionNotFound, "Email '#{@email_action}' not found in #{@preview.name}" - end - end - end - - private - def show_previews? # :doc: - ActionMailer::Base.show_previews - end - - def find_preview # :doc: - candidates = [] - params[:path].to_s.scan(%r{/|$}) { candidates << $` } - preview = candidates.detect { |candidate| ActionMailer::Preview.exists?(candidate) } - - if preview - @preview = ActionMailer::Preview.find(preview) - else - raise AbstractController::ActionNotFound, "Mailer preview '#{params[:path]}' not found" - end - end - - def find_preferred_part(*formats) # :doc: - formats.each do |format| - if part = @email.find_first_mime_type(format) - return part - end - end - - if formats.any? { |f| @email.mime_type == f } - @email - end - end - - def find_part(format) # :doc: - if part = @email.find_first_mime_type(format) - part - elsif @email.mime_type == format - @email - end - end - - def part_query(mime_type) - request.query_parameters.merge(part: mime_type).to_query - end - - def locale_query(locale) - request.query_parameters.merge(locale: locale).to_query - end - - def set_locale(&block) - I18n.with_locale(params[:locale] || I18n.default_locale, &block) - end -end diff --git a/engines/internal/app/controllers/jets/mount_controller.rb b/engines/internal/app/controllers/jets/mount_controller.rb deleted file mode 100644 index 2a1a350a9..000000000 --- a/engines/internal/app/controllers/jets/mount_controller.rb +++ /dev/null @@ -1,59 +0,0 @@ -# routes mount support -class Jets::MountController < Jets::BareController - def call - route = find_route - # On Lambda, the route should always be found so this check on lambda is not needed. - # But this is useful when we're testing locally with the shim directly. - unless route - render json: {status: "route not found"}, status: 404 - return - end - - # The reason we look up the route is because it contains mounted class info - mount_class = route.mount_class # IE: RackApp - env = build_env(route.path) - - status, headers, io = mount_class.call(env) - body = read_body(io) - self.headers.merge!(headers) - render body: body, status: status - end - -private - # Rack response will return an IO object that responds to each. Sometimes this a Rack::BodyProxy - # Found this to be the case in Rails and Grape. - # Doing an IO#read may not work. So we'll always use the IO#each method - def read_body(io) - result = [] - io.each { |body| result << body } - result.join - end - - # Locally Jets::Router::Matcher gets called in Jets::Controller::Middleware::Mimic - # On Lambda, Jets::Router::Matcher in handlers. - def find_route - Jets::Router::Matcher.new.find_by_request(request, "ANY") - end - - def build_env(path) - env = Jets::Controller::RackAdapter::Env.new(event, context, adapter: true).convert - # remap path info - mount_at = mount_at(path) - path_info = env["PATH_INFO"] - env["SCRIPT_NAME"] = script_name(mount_at) - env["PATH_INFO"] = path_info.sub(mount_at,'') - env["ORIGINAL_PATH_INFO"] = path_info - env - end - - # Removes the wildcard: rack/*path => rack - def mount_at(path) - path.gsub(/\*.*/,'') - end - - # Adding forward slash to script name, to generate proper path. - # First remove all the occurance of forward slash at the begining and then add one. - def script_name(path) - '/' + path.gsub(/^\/*/,'') - end -end diff --git a/engines/internal/app/controllers/jets/public_controller.rb b/engines/internal/app/controllers/jets/public_controller.rb deleted file mode 100644 index c8f2975f1..000000000 --- a/engines/internal/app/controllers/jets/public_controller.rb +++ /dev/null @@ -1,35 +0,0 @@ -require "rack/mime" -require "mini_mime" - -class Jets::PublicController < Jets::Controller::Base - layout false - - def show - catchall = params[:catchall].blank? ? 'index.html' : params[:catchall] - public_path = Jets.root + "public" - catchall_path = "#{public_path}/#{catchall}" - - if File.exist?(catchall_path) - content_type = Rack::Mime.mime_type(File.extname(catchall_path)) - binary = !MiniMime.lookup_by_filename(catchall_path).content_type.include?("text") - - # For binary support to work, the client also has to send the right Accept header. - # And the media type has been to added to api gateway. - # Cavaet: adding * to as the media type breaks regular form submission. - # All form submission gets treated as binary. - if binary - encoded_content = Base64.encode64(IO.read(catchall_path)) - render plain: encoded_content, content_type: content_type, base64: true - else - render file: catchall_path, content_type: content_type - end - else - not_found_path = "#{public_path}/404.html" - if File.exist?(not_found_path) - render file: not_found_path, status: 404 - else - render plain: "404 Not Found", status: 404 - end - end - end -end diff --git a/engines/internal/app/controllers/jets/welcome_controller.rb b/engines/internal/app/controllers/jets/welcome_controller.rb deleted file mode 100644 index a88806eda..000000000 --- a/engines/internal/app/controllers/jets/welcome_controller.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -class Jets::WelcomeController < Jets::ApplicationController # :nodoc: - skip_forgery_protection - layout false - - def index - end -end diff --git a/engines/internal/app/functions/jets/base_path.rb b/engines/internal/app/functions/jets/base_path.rb deleted file mode 100644 index 9eca119d3..000000000 --- a/engines/internal/app/functions/jets/base_path.rb +++ /dev/null @@ -1,109 +0,0 @@ -begin - require 'bundler/setup' - # When require bundler/setup fails, AWS Lambda won't be able to load base_path_mapping - # So we'll require base_path_mapping within begin/rescue block so that it does not also - # fail the entire lambda function. - # require 'jets/internal/app/functions/jets/base_path_mapping' - require_relative "base_path_mapping" -rescue Exception => e - # Note: rescue LoadError is not enough in AWS Lambda environment - # Actual exceptions: - # require bundler/setup: Ruby exception "Bundler::GemNotFound" - # require base_path_mappnig: AWS Lambda reported error "errorType": "Init" - # Will use a generic rescue Exception though in case error changes in the future. - puts "WARN: #{e.class} #{e.message}" - puts <<~EOL - Could not require bundler/setup. - This can happen for weird timeout missing error. Example: - - Could not find timeout-0.3.2 in locally installed gems - - Happens when the gem command is out-of-date on old versions of ruby 2.7. - See: https://community.boltops.com/t/could-not-find-timeout-0-3-1-in-any-of-the-sources/996 - EOL -end -require 'cfn_response' - -STAGE_NAME = "<%= @stage_name %>" - -def lambda_handler(event:, context:) - cfn = CfnResponse.new(event, context) - cfn.response do - # Super edge case: mapping is nil when require bundler/setup fails - begin - mapping = BasePathMapping.new(event, STAGE_NAME) - rescue NameError => e - puts "ERROR: #{e.class} #{e.message}" - puts error_message - end - - # This is the "second pass" of CloudFormation when it tries to delete the BasePathMapping during rollback - if mapping.nil? && event['RequestType'] == "Delete" - cfn.success # so that CloudFormation can continue the delete process from a rollback - delay - return - end - - # Normal behavior when mapping is not nil when bundler/setup loads successfully - case event['RequestType'] - when "Create", "Update" - mapping.update - when "Delete" - mapping.delete(true) if mapping.should_delete? - end - end -end - -def delay - puts "Delaying 60 seconds to allow user some time to see lambda function logs." - 60.times do - puts Time.now - sleep 1 - end -end - -def error_message - <<~EOL - This is ultimately the result of require bundler/setup failing to load. - On the CloudFormation first pass, the BasePathMapping fails to CREATE. - CloudFormation does a rollback and tries to delete the BasePathMapping. - - Jets will send a success response to CloudFormation so it can continue and delete - BasePathMapping on the rollback. Otherwise, CloudFormation ends up in the terminal - UPDATE_FAILED state and the CloudFormation console provides 3 options: - - 1) retry 2) update 3) rollback. - - The only option that seems to work is rollback to get it out of UPDATE_FAILED to - UPDATE_ROLLBACK_COMPLETE. But then, if we `jets deploy` again without fixing the - require bundler/setup issue, we'll end back up in the UPDATE_FAILED state. - - Will handle this error so we can continue the stack because we do not want it to fail - and never be able to send the CfnResponse. Then we have to wait hours for a CloudFormation timeout. - Sometimes deleting the APP-dev-ApiDeployment20230518230443-EXAMPLEY8YQP0 stack - allows the cloudformation stacks to continue, but usually, it'll only just slightly speed up the rollback. - - Some examples of actual rollback times: - - When left alone, the rollback takes about 2.5 hours. - - 2023-05-19 05:39:25 User Initiated - 2023-05-19 08:01:48 UPDATE_ROLLBACK_COMPLETE - - When deleting the APP-dev-ApiDeployment20230518230443-EXAMPLEY8YQP0 stack, it takes about 1.5 hours. - - 2023-05-19 06:25:41 User Initiated - 2023-05-19 07:47:03 UPDATE_ROLLBACK_COMPLETE - - Rescuing and handling the error here allows the CloudFormation stack to continue and finish the rollback process. - It takes the rollback time down to about 3 minutes. Example: - - 2023-05-19 16:34:19 User Initiated - 2023-05-19 16:37:34 UPDATE_ROLLBACK_COMPLETE - - Note: The first cloudformation CREATE pass sends FAILED Status to CloudFormation, - and the second cloudformation DELETE pass sends SUCCESS Status to CloudFormation. - - Related: https://community.boltops.com/t/could-not-find-timeout-0-3-1-in-any-of-the-sources/996 - EOL -end diff --git a/engines/internal/app/functions/jets/base_path_mapping.rb b/engines/internal/app/functions/jets/base_path_mapping.rb deleted file mode 100644 index 0f55b3600..000000000 --- a/engines/internal/app/functions/jets/base_path_mapping.rb +++ /dev/null @@ -1,191 +0,0 @@ -require 'aws-sdk-apigateway' -require 'aws-sdk-cloudformation' - -class BasePathMapping - def initialize(event, stage_name) - @event, @stage_name = event, stage_name - aws_config_update! - end - - # Override the AWS retry settings. The aws-sdk-core has expondential backup with this formula: - # - # 2 ** c.retries * c.config.retry_base_delay - # - # So the max delay will be 2 ** 7 * 0.6 = 76.8s - # - # Useful links: - # - # https://github.com/aws/aws-sdk-ruby/blob/master/gems/aws-sdk-core/lib/aws-sdk-core/plugins/retry_errors.rb - # https://docs.aws.amazon.com/apigateway/latest/developerguide/limits.html - # - def aws_config_update! - Aws.config.update( - retry_limit: 7, # default: 3 - retry_base_delay: 0.6, # default: 0.3 - ) - end - - # Cannot use update_base_path_mapping to update the base_mapping because it doesnt - # allow us to change the rest_api_id. So we delete and create. - def update - puts "BasePathMapping update" - if rest_api_changed? - delete(true) - create - else - puts "BasePathMapping update: rest_api_id #{rest_api_id} did not change. Skipping." - end - - puts "BasePathMapping update complete" - end - - def rest_api_changed? - puts "BasePathMapping checking if rest_api_id changed" - mapping = current_base_path_mapping - return true unless mapping - mapping.rest_api_id != rest_api_id - end - - def current_base_path_mapping - resp = apigateway.get_base_path_mapping(base_path: "(none)", domain_name: domain_name) - rescue Aws::APIGateway::Errors::NotFoundException - return nil - end - - # Dont delete the newly created base path mapping unless this is an operation - # where we're fully deleting the stack - def should_delete? - deleting_parent? - end - - def delete(fail_silently=false) - puts "BasePathMapping delete" - options = { - domain_name: domain_name, # required - base_path: base_path.empty? ? '(none)' : base_path, - } - puts "BasePathMapping delete options #{options.inspect}" - apigateway.delete_base_path_mapping(options) - wait_for_delete - puts "BasePathMapping delete complete" - # https://github.com/tongueroo/jets/issues/255 - # Used to return: Aws::APIGateway::Errors::NotFoundException - # Now returns: Aws::APIGateway::Errors::InternalFailure - # So we'll use a more generic error - rescue Aws::APIGateway::Errors::ServiceError => e - raise(e) unless fail_silently - end - - # Wait for deletion to complete - # Otherwise, CloudFormation continues too fast during a `jets delete` - # and the initially fails deletion. CloudFormaton then retries until it's successfully. - # The stack ultimately finishes deleting successfully but the error messages - # are confusing to the user. - def wait_for_delete - loop do - resp = apigateway.get_base_path_mapping( - domain_name: domain_name, - base_path: base_path.empty? ? '(none)' : base_path, - ) - break if resp.nil? || resp.empty? - sleep(5) - end - rescue Aws::APIGateway::Errors::NotFoundException - nil - end - - def create - puts "BasePathMapping create" - options = { - domain_name: domain_name, # required - base_path: base_path, - rest_api_id: rest_api_id, # required - stage: @stage_name, - } - puts "BasePathMapping create options #{options.inspect}" - apigateway.create_base_path_mapping(options) - puts "BasePathMapping create complete" - rescue Aws::APIGateway::Errors::ServiceError => e - puts "ERROR: #{e.class}: #{e.message}" - puts "BasePathMapping create failed" - if e.message.include?("Invalid domain name identifier specified") - puts <<~EOL - This super edge case error seems to happen when the cloudformation stack does a rollback - because the BasePathMapping custom resource fails. This has happened with a strange combination of - ruby 2.7 and the timeout gem not being picked up in the AWS Lambda runtime environment - Specifically, when jets deploy was used with a rubygems install that is out-of-date. - See: https://community.boltops.com/t/could-not-find-timeout-0-3-1-in-any-of-the-sources/996 - - The new base path mapping is not created correctly and the old base path mapping is not properly deleted. - The old ghost base mapping interferes with the new base path mapping. - The workaround solution seems to require removing all the config.domain settings and deploying again. Example: - - config/application.rb - - config.domain.cert_arn = "arn:aws:acm:us-west-2:111111111111:certificate/EXAMPLE1-a3de-4fe7-b72e-4cc153c5303e" - config.domain.hosted_zone_name = "example.com" - - Comment out those settings, deploy, then uncomment and deploy again. - If there's a better workaround, please let us know. - EOL - end - raise(e) - end - - def mapping_stack - @mapping_stack ||= cfn.describe_stacks(stack_name: @event['StackId']).stacks.first - end - - def rest_api_id - @rest_api_id ||= parameter_value('RestApi') - end - - def domain_name - @domain_name ||= parameter_value('DomainName') - end - - def base_path - @base_path ||= parameter_value('BasePath') || '' - end - - def parameter_value(parameter_key) - puts "mapping_stack #{mapping_stack.inspect}" - param = mapping_stack[:parameters].find { |p| p.parameter_key == parameter_key } - param&.parameter_value # possible for this to be nil when removing the config: IE: config.domain.name = nil - end - - def deleting_parent? - stack = cfn.describe_stacks(stack_name: parent_stack_name).stacks.first - stack.stack_status == 'DELETE_IN_PROGRESS' - end - - def parent_stack_name - mapping_stack[:root_id] - end - - def apigateway - @apigateway ||= Aws::APIGateway::Client.new(aws_options) - end - - def cfn - @cfn ||= Aws::CloudFormation::Client.new(aws_options) - end - - def aws_options - options = { - retry_limit: 7, # default: 3 - retry_base_delay: 0.6, # default: 0.3 - } - options.merge!( - log_level: :debug, - logger: Logger.new($stdout), - ) if ENV['JETS_DEBUG_AWS_SDK'] - options - end -end - -if __FILE__ == $0 - event = JSON.load(File.read(ARGV[0])) - stage_name = 'dev' # change to test - BasePathMapping.new(event, stage_name).update -end diff --git a/engines/internal/app/jobs/jets/preheat_job.rb b/engines/internal/app/jobs/jets/preheat_job.rb deleted file mode 100644 index 754e59856..000000000 --- a/engines/internal/app/jobs/jets/preheat_job.rb +++ /dev/null @@ -1,64 +0,0 @@ -class Jets::PreheatJob < Jets::Job::Base - include Jets::AwsServices - - class_timeout 30 - class_memory 1024 - class_iam_policy([ - { - Sid: "Statement1", - Action: ["logs:*"], - Effect: "Allow", - Resource: [{ - "Fn::Sub": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${JetsPreheatJobWarmLambdaFunction}" - }] - }, - { - Sid: "Statement2", - Action: ["lambda:InvokeFunction", "lambda:InvokeAsync"], - Effect: "Allow", - Resource: [{ - "Fn::Sub": "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:#{Jets.project_namespace}-*" - }] - } - ]) - - rate(Jets.config.prewarm.rate) if Jets.config.prewarm.enable - def warm - options = call_options(event[:quiet]) - Jets::Preheat.warm_all(options) - "Finished prewarming your application." - end - - class << self - # Can use this to prewarm post deploy - def prewarm! - perform_now(:warm, quiet: true) - end - end - - private - - # Usually: jets-preheat_job-warm unless JETS_RESET=1, in that case need to lookup the function name - def warm_function_name - # Return early to avoid lookup call normally - return "jets-preheat_job-warm" unless ENV["JETS_RESET"] == "1" - - parent_stack = cfn.describe_stack_resources(stack_name: Jets::Names.parent_stack_name) - preheat_stack = parent_stack.stack_resources.find do |resource| - resource.logical_resource_id =~ /JetsPreheatJob/ - end - - resp = cfn.describe_stack_resources(stack_name: preheat_stack.physical_resource_id) - resources = resp.stack_resources - warm_function = resources.find do |resource| - resource.logical_resource_id =~ /WarmLambdaFunction/ - end - warm_function.physical_resource_id # IE: jets-preheat_job-warm - end - - def call_options(quiet) - options = {} - options.merge!(mute: true, mute_output: true) if quiet - options - end -end diff --git a/engines/internal/app/shared/functions/jets/s3_bucket_config.rb b/engines/internal/app/shared/functions/jets/s3_bucket_config.rb deleted file mode 100644 index 46e74de4b..000000000 --- a/engines/internal/app/shared/functions/jets/s3_bucket_config.rb +++ /dev/null @@ -1,33 +0,0 @@ -require 'bundler/setup' -require 'active_support/core_ext/hash' -require 'cfn_response' - -def lambda_handler(event:, context:) - cfn = CfnResponse.new(event, context) - cfn.response do - case event['RequestType'] - when "Create", "Update" - properties = event["ResourceProperties"].dup - # After deleting ServiceToken, the rest of the values are the bucket configuration properties. - properties.delete("ServiceToken") - configurator = BucketConfigurator.new - configurator.put(properties) - end - end -end - -######################################################## -require "aws-sdk-s3" - -class BucketConfigurator - def put(props={}) - # all props including bucket gets passed from the Custom::S3BucketConfiguration resource - props = props.deep_transform_keys { |k| k.to_s.underscore.to_sym } - puts "props: #{JSON.dump(props)}" - s3.put_bucket_notification_configuration(props) - end - - def s3 - @s3 ||= Aws::S3::Client.new - end -end diff --git a/engines/internal/app/views/jets/info/properties.html.erb b/engines/internal/app/views/jets/info/properties.html.erb deleted file mode 100644 index d47cbab20..000000000 --- a/engines/internal/app/views/jets/info/properties.html.erb +++ /dev/null @@ -1 +0,0 @@ -<%= @info.html_safe %> \ No newline at end of file diff --git a/engines/internal/app/views/jets/info/routes.html.erb b/engines/internal/app/views/jets/info/routes.html.erb deleted file mode 100644 index 2bb13e636..000000000 --- a/engines/internal/app/views/jets/info/routes.html.erb +++ /dev/null @@ -1,10 +0,0 @@ -

- Routes -

- -

- Routes match in priority from top to bottom -

- -<%# @routes_inspector.format(ActionDispatch::Routing::HtmlTableFormatter.new(self)) %> -<%= raw @routes_table %> \ No newline at end of file diff --git a/engines/internal/app/views/jets/mailers/email.html.erb b/engines/internal/app/views/jets/mailers/email.html.erb deleted file mode 100644 index 59c2f283d..000000000 --- a/engines/internal/app/views/jets/mailers/email.html.erb +++ /dev/null @@ -1,165 +0,0 @@ - - -<%= @page_title %> - - - - - -
-
- <% if @email.respond_to?(:smtp_envelope_from) && Array(@email.from) != Array(@email.smtp_envelope_from) %> -
SMTP-From:
-
<%= @email.smtp_envelope_from %>
- <% end %> - - <% if @email.respond_to?(:smtp_envelope_to) && @email.to != @email.smtp_envelope_to %> -
SMTP-To:
-
<%= @email.smtp_envelope_to %>
- <% end %> - -
From:
-
<%= @email.header['from'] %>
- - <% if @email.reply_to %> -
Reply-To:
-
<%= @email.header['reply-to'] %>
- <% end %> - -
To:
-
<%= @email.header['to'] %>
- - <% if @email.cc %> -
CC:
-
<%= @email.header['cc'] %>
- <% end %> - -
Date:
-
<%= Time.current.rfc2822 %>
- -
Subject:
-
<%= @email.subject %>
- - <% unless @email.attachments.nil? || @email.attachments.empty? %> -
Attachments:
-
- <% @email.attachments.each do |a| %> - <% filename = a.respond_to?(:original_filename) ? a.original_filename : a.filename %> - <%= link_to filename, "data:application/octet-stream;charset=utf-8;base64,#{Base64.encode64(a.body.to_s)}", download: filename %> - <% end %> -
- <% end %> - -
Format:
- <% if @email.html_part && @email.text_part %> -
- -
- <% elsif @part %> -
<%= @part.mime_type == 'text/html' ? 'HTML email' : 'plain-text email' %>
- <% else %> -
- <% end %> - - <% if I18n.available_locales.count > 1 %> -
Locale:
-
- -
- <% end %> -
-
- -<% if @part && @part.mime_type %> - -<% else %> -

- You are trying to preview an email that does not have any content. - This is probably because the mail method has not been called in <%= @preview.preview_name %>#<%= @email_action %>. -

-<% end %> - - - - - diff --git a/engines/internal/app/views/jets/mailers/index.html.erb b/engines/internal/app/views/jets/mailers/index.html.erb deleted file mode 100644 index d7e3eaa3d..000000000 --- a/engines/internal/app/views/jets/mailers/index.html.erb +++ /dev/null @@ -1,8 +0,0 @@ -<% @previews.each do |preview| %> -

<%= link_to preview.preview_name.titleize, url_for(controller: "jets/mailers", action: "preview", path: preview.preview_name) %>

-
    -<% preview.emails.each do |email| %> -
  • <%= link_to email, url_for(controller: "jets/mailers", action: "preview", path: "#{preview.preview_name}/#{email}") %>
  • -<% end %> -
-<% end %> diff --git a/engines/internal/app/views/jets/mailers/mailer.html.erb b/engines/internal/app/views/jets/mailers/mailer.html.erb deleted file mode 100644 index 40bb0e0a7..000000000 --- a/engines/internal/app/views/jets/mailers/mailer.html.erb +++ /dev/null @@ -1,6 +0,0 @@ -

<%= @preview.preview_name.titleize %>

-
    -<% @preview.emails.each do |email| %> -
  • <%= link_to email, url_for(controller: "jets/mailers", action: "preview", path: "#{@preview.preview_name}/#{email}") %>
  • -<% end %> -
diff --git a/engines/internal/app/views/jets/welcome/index.html.erb b/engines/internal/app/views/jets/welcome/index.html.erb deleted file mode 100644 index ce8cc9f86..000000000 --- a/engines/internal/app/views/jets/welcome/index.html.erb +++ /dev/null @@ -1,95 +0,0 @@ - - - - Ruby on Jets - - - - - - -
-
- -

Welcome and congrats!
Jets is running.

-
-
-

- To get started: -

-

-        $ jets generate scaffold post title:string
-        $ jets db:create db:migrate
-        $ jets server
-        $ open http://localhost:8888/posts
-        $ jets help
-      
-

Docs on: rubyonjets.com

-

CLI: Jets CLI reference

-

Jets Info: <%= link_to "Properties", controller: "jets/info", action: "properties" %>

-

Jets Info: <%= link_to "Routes", controller: "jets/info", action: "routes" %>

-
-

-

    -
  • Jets version: <%= Jets.version %>
  • -
  • Ruby version: <%= RUBY_DESCRIPTION %>
  • -
-

-
- - diff --git a/engines/internal/app/views/layouts/application.html.erb b/engines/internal/app/views/layouts/application.html.erb deleted file mode 100644 index 47a6bafa2..000000000 --- a/engines/internal/app/views/layouts/application.html.erb +++ /dev/null @@ -1,51 +0,0 @@ - - - - - <%= @page_title %> - - - - -<%= yield %> - - - diff --git a/engines/internal/lib/internal/actiondispatch.rb b/engines/internal/lib/internal/actiondispatch.rb deleted file mode 100644 index 4012a09ac..000000000 --- a/engines/internal/lib/internal/actiondispatch.rb +++ /dev/null @@ -1,39 +0,0 @@ -require "active_support/messages/rotation_configuration" - -module Jets::Internal - # Reference: https://github.com/rails/rails/blob/master/actiondispatch/lib/action_dispatch/railtie.rb - class Actiondispatch < ::Jets::Turbine - config.action_dispatch = ActiveSupport::OrderedOptions.new - config.action_dispatch.x_sendfile_header = nil - config.action_dispatch.ip_spoofing_check = true - config.action_dispatch.show_exceptions = true - config.action_dispatch.tld_length = 1 - config.action_dispatch.ignore_accept_header = false - config.action_dispatch.rescue_templates = {} - config.action_dispatch.rescue_responses = {} - config.action_dispatch.default_charset = nil - config.action_dispatch.rack_cache = false - config.action_dispatch.http_auth_salt = "http authentication" - config.action_dispatch.signed_cookie_salt = "signed cookie" - config.action_dispatch.encrypted_cookie_salt = "encrypted cookie" - config.action_dispatch.encrypted_signed_cookie_salt = "signed encrypted cookie" - config.action_dispatch.authenticated_encrypted_cookie_salt = "authenticated encrypted cookie" - config.action_dispatch.use_authenticated_cookie_encryption = false - config.action_dispatch.use_cookies_with_metadata = false - config.action_dispatch.perform_deep_munge = true - config.action_dispatch.request_id_header = "X-Request-Id" - config.action_dispatch.return_only_request_media_type_on_content_type = true - config.action_dispatch.log_rescued_responses = true - - config.action_dispatch.default_headers = { - "X-Frame-Options" => "SAMEORIGIN", - "X-XSS-Protection" => "1; mode=block", - "X-Content-Type-Options" => "nosniff", - "X-Download-Options" => "noopen", - "X-Permitted-Cross-Domain-Policies" => "none", - "Referrer-Policy" => "strict-origin-when-cross-origin" - } - - config.action_dispatch.cookies_rotations = ActiveSupport::Messages::RotationConfiguration.new - end -end diff --git a/engines/internal/lib/internal/actionmailer.rb b/engines/internal/lib/internal/actionmailer.rb deleted file mode 100644 index bd7ec7521..000000000 --- a/engines/internal/lib/internal/actionmailer.rb +++ /dev/null @@ -1,51 +0,0 @@ -require "action_mailer" - -module Jets::Internal - # Reference: https://github.com/rails/rails/blob/master/actionmailer/lib/action_mailer/railtie.rb - class Actionmailer < ::Jets::Turbine - config.action_mailer = ActiveSupport::OrderedOptions.new - config.action_mailer.show_previews = false - - initializer "action_mailer.logger" do - ActiveSupport.on_load(:action_mailer) { self.logger ||= Jets.logger } - end - - initializer "action_mailer.set_configs" do |app| - options = app.config.action_mailer - options.default_url_options ||= {} - options.preview_path ||= "#{Jets.root}/app/previews" if options.show_previews - - ActiveSupport.on_load(:action_mailer) do - include AbstractController::UrlFor - extend ::JetsTurbines::RoutesHelpers.with(app.routes) # named routes helpers - include app.routes.mounted_helpers # mounted routes helpers: main_app and blorgh - - register_interceptors(options.delete(:interceptors)) - register_preview_interceptors(options.delete(:preview_interceptors)) - register_observers(options.delete(:observers)) - - if options.smtp_settings - self.smtp_settings = options.smtp_settings - end - - smtp_timeout = options.delete(:smtp_timeout) - - if self.smtp_settings && smtp_timeout - self.smtp_settings[:open_timeout] ||= smtp_timeout - self.smtp_settings[:read_timeout] ||= smtp_timeout - end - - options.each { |k, v| send("#{k}=", v) } - end - end - - initializer "action_mailer.routes" do |app| - if app.config.action_mailer.show_previews - app.routes.append do - get "/jets/mailers", to: "jets/mailers#index", internal: true - get "/jets/mailers/*path", to: "jets/mailers#preview", internal: true - end - end - end - end -end diff --git a/engines/internal/lib/internal/actionview.rb b/engines/internal/lib/internal/actionview.rb deleted file mode 100644 index 671a03b30..000000000 --- a/engines/internal/lib/internal/actionview.rb +++ /dev/null @@ -1,152 +0,0 @@ -require_relative "turbines/asset_tag_helper" - -module Jets::Internal - class Actionview < ::Jets::Turbine - config.action_view = ActiveSupport::OrderedOptions.new - config.action_view.embed_authenticity_token_in_remote_forms = nil - config.action_view.debug_missing_translation = true - config.action_view.default_enforce_utf8 = nil - config.action_view.image_loading = nil - config.action_view.image_decoding = nil - config.action_view.apply_stylesheet_media_default = true - - config.after_initialize do |app| - ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = - app.config.action_view.delete(:embed_authenticity_token_in_remote_forms) - end - - config.after_initialize do |app| - form_with_generates_remote_forms = app.config.action_view.delete(:form_with_generates_remote_forms) - ActionView::Helpers::FormHelper.form_with_generates_remote_forms = form_with_generates_remote_forms - end - - config.after_initialize do |app| - form_with_generates_ids = app.config.action_view.delete(:form_with_generates_ids) - unless form_with_generates_ids.nil? - ActionView::Helpers::FormHelper.form_with_generates_ids = form_with_generates_ids - end - end - - config.after_initialize do |app| - default_enforce_utf8 = app.config.action_view.delete(:default_enforce_utf8) - unless default_enforce_utf8.nil? - ActionView::Helpers::FormTagHelper.default_enforce_utf8 = default_enforce_utf8 - end - end - - config.after_initialize do |app| - button_to_generates_button_tag = app.config.action_view.delete(:button_to_generates_button_tag) - unless button_to_generates_button_tag.nil? - ActionView::Helpers::UrlHelper.button_to_generates_button_tag = button_to_generates_button_tag - end - end - - config.after_initialize do |app| - frozen_string_literal = app.config.action_view.delete(:frozen_string_literal) - ActionView::Template.frozen_string_literal = frozen_string_literal - end - - config.after_initialize do |app| - ActionView::Helpers::AssetTagHelper.image_loading = app.config.action_view.delete(:image_loading) - ActionView::Helpers::AssetTagHelper.image_decoding = app.config.action_view.delete(:image_decoding) - ActionView::Helpers::AssetTagHelper.preload_links_header = app.config.action_view.delete(:preload_links_header) - ActionView::Helpers::AssetTagHelper.apply_stylesheet_media_default = app.config.action_view.delete(:apply_stylesheet_media_default) - end - - config.after_initialize do |app| - ActiveSupport.on_load(:action_view) do - app.config.action_view.each do |k, v| - send "#{k}=", v - end - end - end - - initializer "action_view.logger" do - # Override log subscriber rails_root to use Jets.root - require "action_view/log_subscriber" - ActionView::LogSubscriber.class_eval do - def rails_root - @root ||= "#{Jets.root}/" - end - end - ActiveSupport.on_load(:action_view) { self.logger ||= Jets.logger } - end - - initializer "action_view.caching" do |app| - ActiveSupport.on_load(:action_view) do - if app.config.action_view.cache_template_loading.nil? - ActionView::Resolver.caching = app.config.cache_classes - end - end - end - - initializer "action_view.setup_action_pack" do |app| - # Changed to :jets_controller - ActiveSupport.on_load(:jets_controller) do - ActionView::RoutingUrlFor.include(ActionDispatch::Routing::UrlFor) - end - end - - initializer "action_view.collection_caching", after: "jets_controller.set_configs" do |app| - ActiveSupport.on_load(:action_view) do - ActionView::PartialRenderer.collection_cache = app.config.jets_controller.cache_store - end - end - - config.after_initialize do |app| - enable_caching = if app.config.action_view.cache_template_loading.nil? - app.config.cache_classes - else - app.config.action_view.cache_template_loading - end - - unless enable_caching - app.executor.register_hook ActionView::CacheExpiry::Executor.new(watcher: app.config.file_watcher) - end - end - - rake_tasks do |app| - unless app.config.api_only - load "action_view/tasks/cache_digests.rake" - end - end - - # Jets additions - initializer "action_view.event" do |app| - ActiveSupport.on_load :action_view do - class_eval do - def event - @event - end - end - end - end - - initializer "action_view.asset_tag_helper" do |app| - ActiveSupport.on_load :action_view do - ActionView::Helpers.send(:include, JetsTurbines::AssetTagHelper) - end - end - - initializer "action_view.override_debug_exceptions" do |app| - ActiveSupport.on_load :jets_controller do - require_relative "overrides/debug_exceptions" - end - end - - # Deprecated: Will remove support for webpacker in the future. - initializer "action_view.webpacker" do |app| - if Jets.webpacker? - require 'webpacker' - require 'webpacker/helper' - ActiveSupport.on_load :action_controller do - ActionController::Base.helper Webpacker::Helper - end - - ActiveSupport.on_load :action_view do - include Webpacker::Helper - end - end - end - end -end diff --git a/engines/internal/lib/internal/activerecord.rb b/engines/internal/lib/internal/activerecord.rb deleted file mode 100644 index e07f4758c..000000000 --- a/engines/internal/lib/internal/activerecord.rb +++ /dev/null @@ -1,37 +0,0 @@ -require "active_record" - -module Jets::Internal - # Reference: https://github.com/rails/rails/blob/master/actionmailer/lib/action_mailer/railtie.rb - class Activerecord < ::Jets::Turbine - config.active_record = ActiveSupport::OrderedOptions.new - config.active_record.log_queries = false - - rake_tasks do - namespace :db do - task :load_config do - if defined?(ENGINE_ROOT) && engine = Jets::Engine.find(ENGINE_ROOT) - if engine.paths["db/migrate"].existent - ActiveRecord::Tasks::DatabaseTasks.migrations_paths += engine.paths["db/migrate"].to_a - end - end - end - end - - load "active_record/railties/databases.rake" - end - - initializer "active_record.initialize_database" do - ActiveSupport.on_load(:active_record) do - self.configurations = Jets.application.config.database_configuration - establish_connection - end - end - - initializer "active_record.logger" do - # use STDOUT instead of Jets.logger so we don't have to set Jets.logger.level = :debug - if config.active_record.log_queries || ENV['JETS_AR_LOG'] || ENV['AR_LOG'] - ActiveSupport.on_load(:active_record) { self.logger = Logger.new(STDOUT) } - end - end - end -end diff --git a/engines/internal/lib/internal/activesupport.rb b/engines/internal/lib/internal/activesupport.rb deleted file mode 100644 index 13bdcd265..000000000 --- a/engines/internal/lib/internal/activesupport.rb +++ /dev/null @@ -1,47 +0,0 @@ -require "active_support/ordered_options" - -module Jets::Internal - # Reference: https://github.com/rails/rails/blob/master/activesupport/lib/active_support/railtie.rb - # Naming Activesupport to avoid having to use ::ActiveSupport::OrderedOptions.new - class Activesupport < ::Jets::Turbine - config.active_support = ActiveSupport::OrderedOptions.new - config.active_support.disable_to_s_conversion = false - - # Currently, config.active_support.cache_format_version is used in application/bootstrap.rb - - # Note this is how Rails does it. Jets uses Jets.report_exception - # Consider using the Rails approach. Unsure if it's worth it. - initializer "active_support.set_error_reporter" do |app| - ActiveSupport.error_reporter = app.executor.error_reporter - end - - initializer "active_support.set_configs" do |app| - app.config.active_support.each do |k, v| - k = "#{k}=" - ActiveSupport.public_send(k, v) if ActiveSupport.respond_to? k - end - end - - # Sets the default value for Time.zone - # If assigned value cannot be matched to a TimeZone, an exception will be raised. - initializer "active_support.initialize_time_zone" do |app| - begin - TZInfo::DataSource.get - rescue TZInfo::DataSourceNotFound => e - raise e.exception "tzinfo-data is not present. Please add gem 'tzinfo-data' to your Gemfile and run bundle install" - end - require "active_support/core_ext/time/zones" - Time.zone_default = Time.find_zone!(app.config.time_zone) - end - - # Sets the default week start - # If assigned value is not a valid day symbol (e.g. :sunday, :monday, ...), an exception will be raised. - initializer "active_support.initialize_beginning_of_week" do |app| - require "active_support/core_ext/date/calculations" - beginning_of_week_default = Date.find_beginning_of_week!(app.config.beginning_of_week) - - Date.beginning_of_week_default = beginning_of_week_default - end - - end -end diff --git a/engines/internal/lib/internal/engine.rb b/engines/internal/lib/internal/engine.rb deleted file mode 100644 index 506cbdf74..000000000 --- a/engines/internal/lib/internal/engine.rb +++ /dev/null @@ -1,13 +0,0 @@ -module Jets::Internal - # Reference: https://github.com/rails/rails/blob/master/actionmailer/lib/action_mailer/railtie.rb - class Engine < ::Jets::Engine - end -end - -require_relative "actiondispatch" -require_relative "actionmailer" -require_relative "actionview" -require_relative "activerecord" -require_relative "activesupport" -require_relative "i18n_engine" -require_relative "jets_controller" diff --git a/engines/internal/lib/internal/i18n_engine.rb b/engines/internal/lib/internal/i18n_engine.rb deleted file mode 100644 index 9bb510fd3..000000000 --- a/engines/internal/lib/internal/i18n_engine.rb +++ /dev/null @@ -1,19 +0,0 @@ -module Jets::Internal - # Reference: https://github.com/rails/rails/blob/master/activesupport/lib/active_support/i18n_railtie.rb - class I18nEngine < ::Jets::Turbine - config.i18n = ActiveSupport::OrderedOptions.new - config.i18n.turbines_load_path = [] - config.i18n.load_path = [] - config.i18n.fallbacks = ActiveSupport::OrderedOptions.new - - # Set the i18n configuration after initialization since a lot of - # configuration is still usually done in application initializers. - config.after_initialize do |app| - I18n.load_path |= app.config.i18n.turbines_load_path.flat_map(&:existent) - I18n.load_path |= app.config.i18n.load_path.flat_map(&:existent) - I18n.load_path |= Dir["#{app.root}/config/locales/*.yml"] - I18n.load_path.uniq! - I18n.backend.load_translations - end - end -end diff --git a/engines/internal/lib/internal/jets_controller.rb b/engines/internal/lib/internal/jets_controller.rb deleted file mode 100644 index 9ddb5233c..000000000 --- a/engines/internal/lib/internal/jets_controller.rb +++ /dev/null @@ -1,46 +0,0 @@ -require_relative "turbines/helpers" -require_relative "turbines/routes_helpers" - -module Jets::Internal - class JetsController < ::Jets::Turbine - config.cache_store = :memory_store - config.jets_controller = ActiveSupport::OrderedOptions.new - config.jets_controller.default_protect_from_forgery = nil - config.jets_controller.perform_caching = false - config.jets_controller.wrap_parameters_by_default = true - config.jets_controller.cache_store = nil - - initializer "action_controller.set_helpers_path" do |app| - require "action_controller/metal/helpers" - ActionController::Helpers.helpers_path = app.helpers_paths - end - - initializer "jets_controller.set_configs" do |app| - paths = app.config.paths - options = app.config.jets_controller - - options.logger ||= Jets.logger - options.cache_store ||= Jets.cache - - ActiveSupport.on_load(:jets_controller) do - wrap_parameters format: [:json] if app.config.jets_controller.wrap_parameters_by_default && respond_to?(:wrap_parameters) - end - end - - initializer "jets_controller.set_caching" do |app| - ActiveSupport.on_load(:jets_controller) do - self.perform_caching = app.config.jets_controller.perform_caching - self.cache_store = app.config.cache_store # IE: default is :memory_store - end - end - - initializer "jets_controller.request_forgery_protection" do |app| - ActiveSupport.on_load(:jets_controller) do - default_protect_from_forgery = app.config.jets_controller.default_protect_from_forgery - if default_protect_from_forgery.nil? && app.config.mode == "html" || default_protect_from_forgery - protect_from_forgery with: :exception - end - end - end - end -end diff --git a/engines/internal/lib/internal/overrides/debug_exceptions.rb b/engines/internal/lib/internal/overrides/debug_exceptions.rb deleted file mode 100644 index 06f5495bd..000000000 --- a/engines/internal/lib/internal/overrides/debug_exceptions.rb +++ /dev/null @@ -1,40 +0,0 @@ -# These overrides allows DebugExceptions middleware to work with Jets. - -# Straight override the initialize method to customize the template paths and -# include additiional jets rescue templates when needed. -# Note: With the way the ActionDispatch::DebugView class is written, we cannot -# use super to call the original initialize method. Got to override the whole -# method. -ActionDispatch::DebugView.class_eval do - def initialize(assigns) - jets_templates = File.expand_path("../templates", __dir__) - paths = [jets_templates, ActionDispatch::DebugView::RESCUES_TEMPLATE_PATH] - lookup_context = ActionView::LookupContext.new(paths) - super(lookup_context, assigns, nil) - end -end - -# Override source_fragment to use Jets.root instead of Rails.root -ActionDispatch::ExceptionWrapper.class_eval do -private - def source_fragment(path, line) - # Jets.root was Rails.root - return unless Jets.respond_to?(:root) && Jets.root - full_path = Jets.root.join(path) - if File.exist?(full_path) - File.open(full_path, "r") do |file| - start = [line - 3, 0].max - lines = file.each_line.drop(start).take(6) - Hash[*(start + 1..(lines.count + start)).zip(lines).flatten] - end - end - end -end - -load "action_dispatch/middleware/debug_exceptions.rb" - -ActionView::Helpers::SanitizeHelper::ClassMethods.module_eval do - def sanitizer_vendor - Jets::Html::Sanitizer # was Rails::Html::Sanitizer - end -end diff --git a/engines/internal/lib/internal/templates/rescues/_trace.html.erb b/engines/internal/lib/internal/templates/rescues/_trace.html.erb deleted file mode 100644 index 733809ede..000000000 --- a/engines/internal/lib/internal/templates/rescues/_trace.html.erb +++ /dev/null @@ -1,62 +0,0 @@ -<% names = traces.keys %> -<% error_index = local_assigns[:error_index] || 0 %> - -

Jets.root: <%= defined?(Jets) && Jets.respond_to?(:root) ? Jets.root : "unset" %>

- -
- <% names.each do |name| %> - <% - show = "show('#{name.gsub(/\s/, '-')}-#{error_index}');" - hide = (names - [name]).collect {|hide_name| "hide('#{hide_name.gsub(/\s/, '-')}-#{error_index}');"} - %> - <%= name %> <%= '|' unless names.last == name %> - <% end %> - - <% traces.each do |name, trace| %> -
" style="display: <%= (name == trace_to_show) ? 'block' : 'none' %>;"> - - <% trace.each do |frame| %> - - <%= frame[:trace] %> - -
- <% end %> -
-
- <% end %> - - -
diff --git a/engines/internal/lib/internal/templates/rescues/_trace.text.erb b/engines/internal/lib/internal/templates/rescues/_trace.text.erb deleted file mode 100644 index 90750e2e9..000000000 --- a/engines/internal/lib/internal/templates/rescues/_trace.text.erb +++ /dev/null @@ -1,9 +0,0 @@ -Jets.root: <%= defined?(Jets) && Jets.respond_to?(:root) ? Jets.root : "unset" %> - -<% @traces.each do |name, trace| %> -<% if trace.any? %> -<%= name %> -<%= trace.map { |t| t[:trace] }.join("\n") %> - -<% end %> -<% end %> diff --git a/engines/internal/lib/internal/turbines/asset_tag_helper.rb b/engines/internal/lib/internal/turbines/asset_tag_helper.rb deleted file mode 100644 index 793ccbb67..000000000 --- a/engines/internal/lib/internal/turbines/asset_tag_helper.rb +++ /dev/null @@ -1,70 +0,0 @@ -# Override to prepend stage name when on AWS. -module JetsTurbines - module AssetTagHelper - extend Memoist - include Jets::Controller::Decorate::ApigwStage - include Jets::AwsServices - - # All paths lead to here: path_to_asset / asset_path - # Examples: - # - # javascript_include_tag => path_to_javascript => path_to_asset - # stylesheet_link_tag => path_to_stylesheet => path_to_asset - # image_tag => path_to_image => path_to_asset - # - # Also note: Tried using compute_path but that does not always get reached. - # IE: jetpacker will copute sources for manifest before calling javascript_include_tag - # - # javascript_include_tag(*sources_from_manifest_entries...) - # On the other hand, path_to_asset is always called. - def asset_path(source, options = {}) - path = super - # Decorate path and prepend with s3 url when on AWS Lambda - # This serves assets out of s3 when on AWS Lambda. - path = prepend_s3_jets_public(path) - path - end - alias_method :path_to_asset, :asset_path - # Note: Must alias path_to_asset again because asset_path conflicts with an asset_path named route - # Otherwise, method is not called. - # Rails does this internally also. - - # Serves favicon out of s3 when on API gateway. - # - # Useful helper for API Gateway since serving binary data like images without - # an Accept header doesnt work well. You can changed Media Types to '*/*' - # but then that messes up form data. - # - # This is Jets specific and not part of Rails. It was in the orignal Jets v3 codebase. - # Example usage: - # - # public/favicon.ico - # Since Jets v5, you can also use asset_path helper and put the asset in app/assets/images - # Example: - # - # app/assets/images/favicon.ico - # - def favicon_path(path='favicon.ico') - add_s3_public? ? "#{s3_public}/#{path}" : "/#{path}" - end - - private - def prepend_s3_jets_public(asset_path) - if add_s3_public? && !asset_path.starts_with?('http') - asset_path = "#{s3_public}#{asset_path}" - end - asset_path - end - - def add_s3_public? - !!ENV['_HANDLER'] # only add s3 public when on AWS Lambda - end - - def s3_public - # s3_base_url.txt is created as part of the build and deploy process - s3_base_url = IO.read("#{Jets.root}/config/s3_base_url.txt").strip - "#{s3_base_url}/public" - end - memoize :s3_public - end -end diff --git a/engines/internal/lib/internal/turbines/helpers.rb b/engines/internal/lib/internal/turbines/helpers.rb deleted file mode 100644 index be74b8186..000000000 --- a/engines/internal/lib/internal/turbines/helpers.rb +++ /dev/null @@ -1,18 +0,0 @@ -# Implemented this way to remind us how Rails implemented it. -# Otherwise, would have just added another module to the include chain in -# Jets::Controller::Base. -module JetsTurbines - module Helpers - def inherited(klass) - super - return unless klass.respond_to?(:helpers_path=) # IE: ActionMailer does not respond to helpers_path= - - paths = ActionController::Helpers.helpers_path - klass.helpers_path = paths - - if klass.superclass == Jets::Controller::Base - klass.helper :all - end - end - end -end diff --git a/engines/internal/lib/internal/turbines/routes_helpers.rb b/engines/internal/lib/internal/turbines/routes_helpers.rb deleted file mode 100644 index dcab707de..000000000 --- a/engines/internal/lib/internal/turbines/routes_helpers.rb +++ /dev/null @@ -1,42 +0,0 @@ -# Rails implements with abstract_controller/turbines/routes_helpers.rb -# -# It's implement it this way by Rails so that the controller class _routes -# definition points at the namespace engine routes correctly. -# -# This _routes controller class method used in ActionView::Rendering to create a -# view_context_class that includes the routes url_helpers and mounted_helpers. -# -require "active_support/core_ext/module/introspection" - -module JetsTurbines - module RoutesHelpers - def self.with(routes, include_path_helpers = true) - Module.new do - define_method(:inherited) do |klass| - super(klass) - - namespace = klass.module_parents.detect { |m| m.respond_to?(:turbine_routes_url_helpers) } - actual_routes = namespace ? namespace.turbine_routes_url_helpers._routes : routes - - if namespace - klass.include(namespace.turbine_routes_url_helpers(include_path_helpers)) - else - klass.include(routes.url_helpers(include_path_helpers)) - end - - # In the case that we have ex. - # class A::Foo < ApplicationController - # class Bar < A::Foo - # We will need to redefine _routes because it will not be correct - # via inheritance. - unless klass._routes.equal?(actual_routes) - klass.redefine_singleton_method(:_routes) { actual_routes } - klass.include(Module.new do - define_method(:_routes) { @_routes || actual_routes } - end) - end - end - end - end - end -end diff --git a/exe/jets b/exe/jets index 500c1e379..cba1e6020 100755 --- a/exe/jets +++ b/exe/jets @@ -1,5 +1,6 @@ #!/usr/bin/env ruby -$:.unshift(File.expand_path("../../lib", __FILE__)) +$:.unshift(File.expand_path("../lib", __dir__)) require "jets" require "jets/cli" +Jets::CLI.start(ARGV) diff --git a/jets.gemspec b/jets.gemspec index be931944f..f3df18fde 100644 --- a/jets.gemspec +++ b/jets.gemspec @@ -7,9 +7,8 @@ Gem::Specification.new do |spec| spec.name = "jets" spec.version = Jets::VERSION spec.author = "Tung Nguyen" - spec.email = "tongueroo@gmail.com" - spec.summary = "Ruby Serverless Framework" - spec.description = "Jets is a framework that allows you to create serverless applications with a beautiful language: Ruby. It includes everything required to build and deploy an application. Jets leverages the power of Ruby to make serverless joyful for everyone." + spec.summary = "Serverless Deployment Service" + spec.description = "Jets is a Serverless Deployment Service. Jets makes it easy to deploy and run your app on Serverless. It packages up your code and runs it on AWS Lambda. Jets can deploy Rails, Sinatra, and any Rack app." spec.homepage = "https://rubyonjets.com" spec.license = "MIT" @@ -25,44 +24,41 @@ Gem::Specification.new do |spec| spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] - spec.add_dependency "actionmailer", "~> 7.0.8" - spec.add_dependency "actionpack", "~> 7.0.8" - spec.add_dependency "actionview", "~> 7.0.8" - spec.add_dependency "activerecord", "~> 7.0.8" - spec.add_dependency "activesupport", "~> 7.0.8" - spec.add_dependency "railties", "~> 7.0.8" # for ActiveRecord database_tasks.rb - - spec.add_dependency "aws-logs" - spec.add_dependency "aws-mfa-secure", "~> 0.4.0" + spec.add_dependency "aws-logs", ">= 1.0" + spec.add_dependency "aws-mfa-secure", ">= 0.4.0" spec.add_dependency "aws-sdk-apigateway" spec.add_dependency "aws-sdk-cloudformation" + spec.add_dependency "aws-sdk-cloudwatchevents" spec.add_dependency "aws-sdk-cloudwatchlogs" + spec.add_dependency "aws-sdk-codebuild" spec.add_dependency "aws-sdk-dynamodb" + spec.add_dependency "aws-sdk-iam" spec.add_dependency "aws-sdk-kinesis" spec.add_dependency "aws-sdk-lambda" spec.add_dependency "aws-sdk-s3" spec.add_dependency "aws-sdk-sns" spec.add_dependency "aws-sdk-sqs" spec.add_dependency "aws-sdk-ssm" - spec.add_dependency "cfn_camelizer", ">= 0.4.9" + spec.add_dependency "aws-sdk-wafv2" + spec.add_dependency "cfn_camelizer", ">= 0.6.0" spec.add_dependency "cfn_response" - spec.add_dependency "cfn-status", ">= 0.5.0" - spec.add_dependency "cli-format", ">= 0.4.0" + spec.add_dependency "cfn-status", ">= 0.6.1" + spec.add_dependency "cli-format", ">= 0.6.1" spec.add_dependency "dotenv", ">= 3.1" spec.add_dependency "dsl_evaluator", ">= 0.3.0" # for DslEvaluator.print_code + spec.add_dependency "fugit" spec.add_dependency "gems" spec.add_dependency "hashie" - spec.add_dependency "jets-api", ">= 0.1.4" - spec.add_dependency "jets-git" - spec.add_dependency "jets-html-sanitizer" spec.add_dependency "kramdown" spec.add_dependency "memoist" - spec.add_dependency "mini_mime" + spec.add_dependency "mime-types" spec.add_dependency "rack" spec.add_dependency "rainbow" spec.add_dependency "recursive-open-struct" + spec.add_dependency "shellwords" spec.add_dependency "text-table" spec.add_dependency "thor" + spec.add_dependency "tty-screen" spec.add_dependency "zeitwerk", ">= 2.6.0" spec.add_development_dependency "bundler" diff --git a/lib/jets.rb b/lib/jets.rb index 59b10269c..913084a04 100644 --- a/lib/jets.rb +++ b/lib/jets.rb @@ -9,21 +9,19 @@ require "active_support/ordered_options" require "cfn_camelizer" require "cfn_status" +require "cli-format" require "fileutils" require "json" require "memoist" require "rainbow/ext/string" -require "jets-api" -require "jets-git" + +CliFormat.default_format = "table" require "jets/core_ext" require "jets/autoloaders" -loader = Jets::Autoloaders.for_gem -loader.setup +Jets::Autoloaders.gem.setup module Jets class Error < StandardError; end extend Core # root, logger, etc end - -loader.eager_load if Jets.eager_load_gem? diff --git a/lib/jets/api.rb b/lib/jets/api.rb new file mode 100644 index 000000000..c78007ebc --- /dev/null +++ b/lib/jets/api.rb @@ -0,0 +1,17 @@ +module Jets + module Api + extend Memoist + extend self + + def api + Jets::Api::Client.new + end + memoize :api + + def api_key + Jets::Api::Config.instance.api_key + end + end +end + +require "jets/api/error" # load all error classes diff --git a/lib/jets/api/agree.rb b/lib/jets/api/agree.rb new file mode 100644 index 000000000..30844da33 --- /dev/null +++ b/lib/jets/api/agree.rb @@ -0,0 +1,62 @@ +module Jets::Api + class Agree + def initialize + @agree_file = "#{ENV["HOME"]}/.jets/agree" + end + + # Only prompts if hasnt prompted before and saved a ~/.jets/agree file + def prompt + return if bypass_prompt + return if File.exist?(@agree_file) && File.mtime(@agree_file) > Time.parse("2021-04-12") + + puts <<~EOL + To use jets you must agree to the terms of service. + + Jets Terms: https://www.rubyonjets.com/terms + + Is it okay to send your gem data to Jets Api? (Y/n)? + EOL + + answer = $stdin.gets.strip + value = /y/i.match?(answer) ? "yes" : "no" + + write_file(value) + end + + # Allow user to bypass prompt with JETS_AGREE=1 JETS_AGREE=yes etc + # Useful for CI/CD pipelines. + def bypass_prompt + agree = ENV["JETS_AGREE"] + return false unless agree + + if %w[1 yes true].include?(agree.downcase) + write_file("yes") + else + write_file("no") + end + + true + end + + def yes? + File.exist?(@agree_file) && IO.read(@agree_file).strip == "yes" + end + + def no? + File.exist?(@agree_file) && IO.read(@agree_file).strip == "no" + end + + def yes! + write_file("yes") + end + + def no! + write_file("no") + end + + def write_file(content) + FileUtils.mkdir_p(File.dirname(@agree_file)) + IO.write(@agree_file, content) + end + end +end diff --git a/lib/jets/api/base.rb b/lib/jets/api/base.rb new file mode 100644 index 000000000..80bfd9131 --- /dev/null +++ b/lib/jets/api/base.rb @@ -0,0 +1,8 @@ +module Jets::Api + class Base + include Jets::Api + class << self + include Jets::Api + end + end +end diff --git a/lib/jets/api/client.rb b/lib/jets/api/client.rb new file mode 100644 index 000000000..2933e2694 --- /dev/null +++ b/lib/jets/api/client.rb @@ -0,0 +1,246 @@ +require "aws-sdk-core" +require "open-uri" + +module Jets::Api + class Client + extend Memoist + include Jets::Api::Error::Handlers + include Jets::Util::Logging + + @@max_retries = 3 # 4 attempts total + + def execute_request(klass, path, data = {}, headers = {}) + data = global_params(path).merge(data) + if klass == Net::HTTP::Get + path = path_with_query(path, data) + data = {} + end + + url = url(path) + req = build_request(klass, url, data, headers) + http_resp = http_request(req) + + resp = Jets::Api::Response.new(http_resp) + puts_debug_resp(url, resp) + + if handle_as_error?(resp.http_status) + handle_error_response!(resp) + end + + # Always translate Json Response to Ruby Hash + resp.data # JSON.parse(@http_resp.body) => Ruby hash + end + + def build_request(klass, url, data = {}, headers = {}) + req = klass.new(url) # url includes query string and uri.path does not, must used url + set_headers!(req) + if [Net::HTTP::Delete, Net::HTTP::Patch, Net::HTTP::Post, Net::HTTP::Put].include?(klass) + text = JSON.dump(data) + puts_debug_request(data) + req.body = text + req.content_length = text.bytesize + req.content_type = "application/json" + end + req + end + + NETWORK_ERRORS = [ + Errno::ECONNREFUSED, + Errno::ECONNRESET, + Errno::EHOSTUNREACH, + Errno::ETIMEDOUT, + Jets::Api::Error::Maintenance, + Jets::Api::Error::ServiceUnavailable, # mimic 503 Net::HTTPServiceUnavailable + Net::HTTPServiceUnavailable, # cannot rescue. Unsure why + Net::OpenTimeout, + Net::ReadTimeout, + OpenSSL::SSL::SSLError, + OpenURI::HTTPError, + SocketError + ] + + def http_request(req, retries = 0) + resp = http.request(req) # send request. returns raw response + reraise_error_from_503!(req, resp) + resp + rescue *NETWORK_ERRORS => error + if retries < @@max_retries + delay = 2**retries + log.debug "Error: #{error.class} #{error.message} retrying after #{delay} seconds..." + sleep delay + retries += 1 + retry + elsif error.is_a?(Jets::Api::Error::Maintenance) + # The Jets API is under maintenance + log.info error.message # message provides context already + exit 1 + else + message = "Unexpected error #{error.class.name} communicating with the Jets API. " + message += " Request was attempted #{retries + 1} times." + raise Jets::Api::Error::Connection, message + "\nNetwork error: #{error.message}" + end + end + + # For some reason, rescue Net::HTTPServiceUnavailable is not being caught. + # So mimic it. + # Can reproduce by using local Jets API service and not starting it up + def reraise_error_from_503!(req, resp) + return unless resp.code == "503" # Service Unavailable + + if maintenance_mode?(resp.body) + payload = JSON.parse(resp.body) + raise Jets::Api::Error::Maintenance, payload["message"] + else + raise Jets::Api::Error::ServiceUnavailable, "Request #{req.path}" + end + end + + def maintenance_mode?(body) + payload = JSON.parse(body) + payload["status"] == "maintenance" + rescue JSON::ParserError + false + end + + def http + uri = URI(endpoint) + http = Net::HTTP.new(uri.host, uri.port) + http.open_timeout = http.read_timeout = 30 + http.use_ssl = true if uri.scheme == "https" + http + end + memoize :http + + def global_params(path) + args = ARGV.reject { |arg| arg.include?("-") } + params = { + account: Jets.aws.account, + command: command, + jets_env: Jets.env.to_s, + jets_extra: Jets.extra, + jets_go_version: ENV["JETS_GO_VERSION"], + jets_remote_version: ENV["JETS_REMOTE_VERSION"], + jets_version: Jets::VERSION, + region: Jets.aws.region, + ruby_version: RUBY_VERSION + } + if Jets::Thor::ProjectCheck.new(args).project? || command == "delete" + params[:project_namespace] = Jets.project.namespace + params[:project_name] = Jets.project.name + end + + params.delete_if { |k, v| v.nil? } + params + end + + def command + args = ARGV.reject { |arg| arg.include?("-") } + if args.first == "rollback" # IE: jets rollback 8 + args.first + else + args.join(":") + end + end + + # API does not include the /. IE: https://app.terraform.io/api/v2 + def url(path) + path = "/#{path}" unless path.starts_with?("/") + "#{endpoint}#{path}" + end + + def path_with_query(path, query = {}) + return path if query.empty? + separator = path.include?("?") ? "&" : "?" + "#{path}#{separator}#{query.to_query}" + end + + def set_headers!(req, headers = {}) + headers.each { |k, v| req[k] = v } + req["Authorization"] = api_key if api_key + req["x-account"] = account if account + req["x-session"] = session if session + req + end + + # 422 Unprocessable Entity: Server understands the content type of the request entity, and + # the syntax of the request entity is correct, but it was unable to process the contained + # instructions. + # TODO: remove? or rename to ha + def processable?(http_code) + http_code =~ /^2/ || http_code =~ /^4/ + end + + def session + session_path = "#{ENV["HOME"]}/.jets/session.yml" + if File.exist?(session_path) + data = YAML.load_file(session_path) + data["secret_token"] + end + end + + def api_key + Jets::Api.api_key + end + + def get(path, data = {}) + execute_request(Net::HTTP::Get, path, data) + end + + def post(path, data = {}) + execute_request(Net::HTTP::Post, path, data) + end + + def put(path, data = {}) + execute_request(Net::HTTP::Put, path, data) + end + + def patch(path, data = {}) + execute_request(Net::HTTP::Patch, path, data) + end + + def delete(path, data = {}) + execute_request(Net::HTTP::Delete, path, data) + end + + def account + sts.get_caller_identity.account + rescue + nil + end + memoize :account + + def sts + Aws::STS::Client.new + end + memoize :sts + + def endpoint + return ENV["JETS_API"] if ENV["JETS_API"] + + major = Jets::VERSION.split(".").first.to_i + if major >= 6 + "https://api.rubyonjets.com/v2" + else + "https://api.rubyonjets.com/v1" + end + end + memoize :endpoint + + def puts_debug_resp(url, resp) + return unless ENV["JETS_DEBUG_API"] + puts "API Response for url #{url}" + begin + puts JSON.pretty_generate(resp.data) + rescue + puts "Cannot JSON pretty_generate resp #{resp.inspect}" + nil + end + end + + def puts_debug_request(data) + return unless ENV["JETS_DEBUG_API"] + log.info "POST data:" + log.info JSON.pretty_generate(data) + end + end +end diff --git a/lib/jets/api/config.rb b/lib/jets/api/config.rb new file mode 100644 index 000000000..4ae807ca7 --- /dev/null +++ b/lib/jets/api/config.rb @@ -0,0 +1,73 @@ +require "singleton" + +module Jets::Api + class Config + include Singleton + extend Memoist + + def initialize(options = {}) + @options = options + @config_path = "#{ENV["HOME"]}/.jets/config.yml" + end + + def api_key + ENV["JETS_API_KEY"] || data["api_key"] || data["key"] # keep key for backwards compatibility + end + + def api_key? + !!api_key + end + + def clear_api_key + FileUtils.rm_f(@config_path) + puts "Removed #{@config_path.sub(ENV["HOME"], "~")}" + end + + def data + @data ||= load + end + + # Ensure a Hash is returned + def load + return {} unless File.exist?(@config_path) + + data = YAML.load_file(@config_path) + if data.is_a?(Hash) + data + else + puts "WARN: #{@config_path} is not in the correct format. Loading an empty hash.".color(:yellow) + {} + end + end + + def prompt + puts <<~EOL + You are about to configure your #{pretty_path(@config_path)} + You can get an api key from www.rubyonjets.com + EOL + print "Please provide your api key: " + $stdin.gets.strip + end + + # interface method: do not remove + def update_api_key(api_key = nil) + api_key ||= prompt + write( + key: api_key, # legacy jets 5 and below + api_key: api_key # legacy jets 6 and above + ) # specify keys to allow + end + + def write(values = {}) + data = load + data.merge!(values.deep_stringify_keys) + FileUtils.mkdir_p(File.dirname(@config_path)) + IO.write(@config_path, YAML.dump(data)) + puts "Updated #{pretty_path(@config_path)}" + end + + def pretty_path(path) + path.sub(ENV["HOME"], "~") + end + end +end diff --git a/lib/jets/api/error.rb b/lib/jets/api/error.rb new file mode 100644 index 000000000..cabe6413e --- /dev/null +++ b/lib/jets/api/error.rb @@ -0,0 +1,70 @@ +module Jets::Api + class Error < StandardError + def initialize(message = nil, http_status: nil, http_body: nil, + json_body: nil, http_headers: nil, code: nil) + @message = message + @http_status = http_status + @http_body = http_body + @http_headers = http_headers || {} + @json_body = json_body + @code = code + @request_id = @http_headers["request-id"] + super(message) + end + + module Handlers + def handle_as_error?(http_status) + http_status >= 400 + end + + def handle_error_response!(resp) + error = if resp.data[:error].nil? + general_api_error("Indeterminate error", resp.http_status) + elsif resp.data[:error].is_a?(String) # Internal Server Error + general_api_error(resp.data[:error], resp.http_status) + else + specific_api_error(resp) + end + + raise error + end + + def general_api_error(message, http_status) + Error.new(message, http_status: http_status) + end + + def specific_api_error(resp) + message = resp.data[:error][:message] + http_status = resp.http_status + + error_class = Jets::Api::Error.http_errors[http_status] + if error_class + Jets::Api::Error.const_get(error_class).new(message, http_status: http_status) + else + general_api_error(message, http_status) + end + end + end + + cattr_reader :http_errors + @@http_errors = { + 400 => "BadRequest", + 401 => "Unauthorized", + 403 => "Forbidden", + 404 => "NotFound", + 422 => "UnprocessableEntity", + 429 => "TooManyRequests", + 500 => "InternalServerError" + } + # Do not correspond to http status codes + @@other_errors = %w[ + Connection + Maintenance + ServiceUnavailable + ] + @@error_classes = @@http_errors.values + @@other_errors + @@error_classes.each do |error_class| + Jets::Api::Error.const_set(error_class, Class.new(Error)) + end + end +end diff --git a/lib/jets/api/ping.rb b/lib/jets/api/ping.rb new file mode 100644 index 000000000..f698d9836 --- /dev/null +++ b/lib/jets/api/ping.rb @@ -0,0 +1,9 @@ +module Jets::Api + class Ping < Base + class << self + def create(params = {}) + api.post("/pings", params) + end + end + end +end diff --git a/lib/jets/api/project.rb b/lib/jets/api/project.rb new file mode 100644 index 000000000..c18f19687 --- /dev/null +++ b/lib/jets/api/project.rb @@ -0,0 +1,9 @@ +module Jets::Api + class Project < Base + class << self + def list(params = {}) + api.get("/projects", params) + end + end + end +end diff --git a/lib/jets/api/release.rb b/lib/jets/api/release.rb new file mode 100644 index 000000000..eb2eef903 --- /dev/null +++ b/lib/jets/api/release.rb @@ -0,0 +1,17 @@ +module Jets::Api + class Release < Base + class << self + def list(params = {}) + api.get("/releases", params) + end + + def retrieve(id, params = {}) + api.get("/releases/#{id}", params) + end + + def create(params = {}) + api.post("/releases", params) + end + end + end +end diff --git a/lib/jets/api/response.rb b/lib/jets/api/response.rb new file mode 100644 index 000000000..91e817b19 --- /dev/null +++ b/lib/jets/api/response.rb @@ -0,0 +1,27 @@ +require "hashie" + +module Jets::Api + class Response + attr_reader( + :http_resp, + :http_body, + :http_headers, + :http_status, + :request_id + ) + def initialize(http_resp) + @http_resp = http_resp + @http_body = http_resp.body + @http_headers = http_resp.to_hash + @http_status = http_resp.code.to_i + @request_id = http_resp["request-id"] + end + + def data + data = JSON.parse(@http_resp.body, symbolize_names: true) + Hashie::Mash.new(data) + rescue JSON::ParserError + raise Jets::Api::Error, http_resp + end + end +end diff --git a/lib/jets/api/sig.rb b/lib/jets/api/sig.rb new file mode 100644 index 000000000..10d3b2146 --- /dev/null +++ b/lib/jets/api/sig.rb @@ -0,0 +1,13 @@ +module Jets::Api + class Sig < Base + class << self + def create(params = {}) + api.post("/sigs", params) + end + + def update(id, params = {}) + api.put("sigs/#{id}", params) + end + end + end +end diff --git a/lib/jets/api/stack.rb b/lib/jets/api/stack.rb new file mode 100644 index 000000000..a14e1388a --- /dev/null +++ b/lib/jets/api/stack.rb @@ -0,0 +1,14 @@ +module Jets::Api + class Stack < Base + class << self + def list(params = {}) + api.get("/stacks", params) + end + + def retrieve(id, params = {}) + id = Jets.project.namespace if id == :current + api.get("/stacks/#{id}", params) + end + end + end +end diff --git a/lib/jets/app_loader.rb b/lib/jets/app_loader.rb deleted file mode 100644 index 7c351060f..000000000 --- a/lib/jets/app_loader.rb +++ /dev/null @@ -1,77 +0,0 @@ -# frozen_string_literal: true - -require "pathname" -require "jets/version" - -module Jets - module AppLoader # :nodoc: - extend self - - RUBY = Gem.ruby - EXECUTABLES = ["bin/jets", "script/jets"] - BUNDLER_WARNING = < 'my sensible data' - # - # See the ActiveSupport::MessageVerifier documentation for more information. - def message_verifier(verifier_name) - @message_verifiers[verifier_name] ||= begin - secret = key_generator.generate_key(verifier_name.to_s) - ActiveSupport::MessageVerifier.new(secret) - end - end - - # Convenience for loading config/foo.yml for the current Jets env. - # - # Examples: - # - # # config/exception_notification.yml: - # production: - # url: http://127.0.0.1:8080 - # namespace: my_app_production - # - # development: - # url: http://localhost:3001 - # namespace: my_app_development - # - # # config/environments/production.rb - # Jets.application.configure do - # config.middleware.use ExceptionNotifier, config_for(:exception_notification) - # end - # - # # You can also store configurations in a shared section which will be - # # merged with the environment configuration - # - # # config/example.yml - # shared: - # foo: - # bar: - # baz: 1 - # - # development: - # foo: - # bar: - # qux: 2 - # - # # development environment - # Jets.application.config_for(:example)[:foo][:bar] - # # => { baz: 1, qux: 2 } - def config_for(name, env: Jets.env) - yaml = name.is_a?(Pathname) ? name : Pathname.new("#{paths["config"].existent.first}/#{name}.yml") - - if yaml.exist? - require "erb" - all_configs = ActiveSupport::ConfigurationFile.parse(yaml).deep_symbolize_keys - config, shared = all_configs[env.to_sym], all_configs[:shared] - - if shared - config = {} if config.nil? && shared.is_a?(Hash) - if config.is_a?(Hash) && shared.is_a?(Hash) - config = shared.deep_merge(config) - elsif config.nil? - config = shared - end - end - - if config.is_a?(Hash) - config = ActiveSupport::OrderedOptions.new.update(config) - end - - config - else - raise "Could not load configuration. No such file - #{yaml}" - end - end - - # Stores some of the Jets initial environment parameters which - # will be used by middlewares and engines to configure themselves. - def env_config - @app_env_config ||= super.merge( - "action_dispatch.parameter_filter" => config.filter_parameters, - "action_dispatch.redirect_filter" => config.filter_redirect, - "action_dispatch.secret_key_base" => secret_key_base, - "action_dispatch.show_exceptions" => config.action_dispatch.show_exceptions, - "action_dispatch.show_detailed_exceptions" => config.consider_all_requests_local, - "action_dispatch.log_rescued_responses" => config.action_dispatch.log_rescued_responses, - "action_dispatch.logger" => Jets.logger, - "action_dispatch.backtrace_cleaner" => Jets.backtrace_cleaner, - "action_dispatch.key_generator" => key_generator, - "action_dispatch.http_auth_salt" => config.action_dispatch.http_auth_salt, - "action_dispatch.signed_cookie_salt" => config.action_dispatch.signed_cookie_salt, - "action_dispatch.encrypted_cookie_salt" => config.action_dispatch.encrypted_cookie_salt, - "action_dispatch.encrypted_signed_cookie_salt" => config.action_dispatch.encrypted_signed_cookie_salt, - "action_dispatch.authenticated_encrypted_cookie_salt" => config.action_dispatch.authenticated_encrypted_cookie_salt, - "action_dispatch.use_authenticated_cookie_encryption" => config.action_dispatch.use_authenticated_cookie_encryption, - "action_dispatch.encrypted_cookie_cipher" => config.action_dispatch.encrypted_cookie_cipher, - "action_dispatch.signed_cookie_digest" => config.action_dispatch.signed_cookie_digest, - "action_dispatch.cookies_serializer" => config.action_dispatch.cookies_serializer, - "action_dispatch.cookies_digest" => config.action_dispatch.cookies_digest, - "action_dispatch.cookies_rotations" => config.action_dispatch.cookies_rotations, - "action_dispatch.cookies_same_site_protection" => coerce_same_site_protection(config.action_dispatch.cookies_same_site_protection), - "action_dispatch.use_cookies_with_metadata" => config.action_dispatch.use_cookies_with_metadata, - "action_dispatch.content_security_policy" => config.content_security_policy, - "action_dispatch.content_security_policy_report_only" => config.content_security_policy_report_only, - "action_dispatch.content_security_policy_nonce_generator" => config.content_security_policy_nonce_generator, - "action_dispatch.content_security_policy_nonce_directives" => config.content_security_policy_nonce_directives, - "action_dispatch.permissions_policy" => config.permissions_policy, - ) - end - - # If you try to define a set of Rake tasks on the instance, these will get - # passed up to the Rake tasks defined on the application's class. - def rake_tasks(&block) - self.class.rake_tasks(&block) - end - - # Sends the initializers to the +initializer+ method defined in the - # Jets::Initializable module. Each Jets::Application class has its own - # set of initializers, as defined by the Initializable module. - def initializer(name, opts = {}, &block) - self.class.initializer(name, opts, &block) - end - - # Sends any runner called in the instance of a new application up - # to the +runner+ method defined in Jets::Turbine. - def runner(&blk) - self.class.runner(&blk) - end - - # Sends any console called in the instance of a new application up - # to the +console+ method defined in Jets::Turbine. - def console(&blk) - self.class.console(&blk) - end - - # Sends any generators called in the instance of a new application up - # to the +generators+ method defined in Jets::Turbine. - def generators(&blk) - self.class.generators(&blk) - end - - # Sends any server called in the instance of a new application up - # to the +server+ method defined in Jets::Turbine. - def server(&blk) - self.class.server(&blk) - end - - # Sends the +isolate_namespace+ method up to the class method. - def isolate_namespace(mod) - self.class.isolate_namespace(mod) - end - - ## Jets internal API - - # This method is called just after an application inherits from Jets::Application, - # allowing the developer to load classes in lib and use them during application - # configuration. - # - # class MyApplication < Jets::Application - # require "my_backend" # in lib/my_backend - # config.i18n.backend = MyBackend - # end - # - # Notice this method takes into consideration the default root path. So if you - # are changing config.root inside your application definition or having a custom - # Jets application, you will need to add lib to $LOAD_PATH on your own in case - # you need to load files in lib/ during the application configuration as well. - def self.add_lib_to_load_path!(root) # :nodoc: - path = File.join root, "lib" - if File.exist?(path) && !$LOAD_PATH.include?(path) - $LOAD_PATH.unshift(path) - end - end - - def require_environment! # :nodoc: - environment = paths["config/environment"].existent.first - require environment if environment - end - - def routes_reloader # :nodoc: - @routes_reloader ||= RoutesReloader.new - end - - # Returns an array of file paths appended with a hash of - # directories-extensions suitable for ActiveSupport::FileUpdateChecker - # API. - def watchable_args # :nodoc: - files, dirs = config.watchable_files.dup, config.watchable_dirs.dup - - ActiveSupport::Dependencies.autoload_paths.each do |path| - File.file?(path) ? files << path.to_s : dirs[path.to_s] = [:rb] - end - - [files, dirs] - end - - # Initialize the application passing the given group. By default, the - # group is :default - def initialize!(group = :default) # :nodoc: - raise "Application has been already initialized." if @initialized - run_initializers(group, self) - @initialized = true - self - end - - def initializers # :nodoc: - Bootstrap.initializers_for(self) + - turbines_initializers(super) + - Finisher.initializers_for(self) - end - - def config # :nodoc: - @config ||= Application::Configuration.new(self.class.find_root(self.class.called_from)) - end - - attr_writer :config - - def secrets - @secrets ||= begin - secrets = ActiveSupport::OrderedOptions.new - files = config.paths["config/secrets"].existent - files = files.reject { |path| path.end_with?(".enc") } unless config.read_encrypted_secrets - secrets.merge! Jets::Secrets.parse(files, env: Jets.env) - - # Fallback to config.secret_key_base if secrets.secret_key_base isn't set - secrets.secret_key_base ||= config.secret_key_base - - secrets - end - end - - attr_writer :secrets, :credentials - - # The secret_key_base is used as the input secret to the application's key generator, which in turn - # is used to create all ActiveSupport::MessageVerifier and ActiveSupport::MessageEncryptor instances, - # including the ones that sign and encrypt cookies. - # - # In development and test, this is randomly generated and stored in a - # temporary file in tmp/development_secret.txt. - # - # In all other environments, we look for it first in ENV["SECRET_KEY_BASE"], - # then +credentials.secret_key_base+, and finally +secrets.secret_key_base+. For most applications, - # the correct place to store it is in the encrypted credentials file. - def secret_key_base - if Jets.env.development? || Jets.env.test? - secrets.secret_key_base ||= generate_development_secret - else - validate_secret_key_base( - ENV["SECRET_KEY_BASE"] || credentials.secret_key_base || secrets.secret_key_base - ) - end - end - - # Returns an ActiveSupport::EncryptedConfiguration instance for the - # credentials file specified by +config.credentials.content_path+. - # - # By default, +config.credentials.content_path+ will point to either - # config/credentials/#{environment}.yml.enc for the current - # environment (for example, +config/credentials/production.yml.enc+ for the - # +production+ environment), or +config/credentials.yml.enc+ if that file - # does not exist. - # - # The encryption key is taken from either ENV["JETS_MASTER_KEY"], - # or from the file specified by +config.credentials.key_path+. By default, - # +config.credentials.key_path+ will point to either - # config/credentials/#{environment}.key for the current - # environment, or +config/master.key+ if that file does not exist. - def credentials - @credentials ||= encrypted(config.credentials.content_path, key_path: config.credentials.key_path) - end - - # Returns an ActiveSupport::EncryptedConfiguration instance for an encrypted - # file. By default, the encryption key is taken from either - # ENV["JETS_MASTER_KEY"], or from the +config/master.key+ file. - # - # my_config = Jets.application.encrypted("config/my_config.enc") - # - # my_config.read - # # => "foo:\n bar: 123\n" - # - # my_config.foo.bar - # # => 123 - # - # Encrypted files can be edited with the bin/jets encrypted:edit - # command. (See the output of bin/jets encrypted:edit --help for - # more information.) - def encrypted(path, key_path: "config/master.key", env_key: "JETS_MASTER_KEY") - ActiveSupport::EncryptedConfiguration.new( - config_path: Jets.root.join(path), - key_path: Jets.root.join(key_path), - env_key: env_key, - raise_if_missing_key: config.require_master_key - ) - end - - def to_app # :nodoc: - self - end - - def helpers_paths # :nodoc: - config.helpers_paths - end - - console do - unless ::Kernel.private_method_defined?(:y) - require "psych/y" - end - end - - # Return an array of turbines respecting the order they're loaded - # and the order specified by the +turbines_order+ config. - # - # While running initializers we need engines in reverse order here when - # copying migrations from turbines ; we need them in the order given by - # +turbines_order+. - def migration_turbines # :nodoc: - ordered_turbines.flatten - [self] - end - - # Eager loads the application code. - def eager_load! - Jets.autoloaders.each(&:eager_load) - end - - # Added for Application::Configuration::Defaults - def aws - @aws ||= Jets::AwsInfo.new - end - - protected - alias :build_middleware_stack :app - - def run_tasks_blocks(app) # :nodoc: - turbines.each { |r| r.run_tasks_blocks(app) } - super - load "jets/tasks.rb" - task :environment do - ActiveSupport.on_load(:before_initialize) { config.eager_load = config.rake_eager_load } - - require_environment! - end - end - - def run_generators_blocks(app) # :nodoc: - turbines.each { |r| r.run_generators_blocks(app) } - super - end - - def run_runner_blocks(app) # :nodoc: - turbines.each { |r| r.run_runner_blocks(app) } - super - end - - def run_console_blocks(app) # :nodoc: - turbines.each { |r| r.run_console_blocks(app) } - super - end - - def run_server_blocks(app) # :nodoc: - turbines.each { |r| r.run_server_blocks(app) } - super - end - - # Returns the ordered turbines for this application considering turbines_order. - def ordered_turbines # :nodoc: - @ordered_turbines ||= begin - order = config.turbines_order.map do |turbine| - if turbine == :main_app - self - elsif turbine.respond_to?(:instance) - turbine.instance - else - turbine - end - end - - all = (turbines - order) - all.push(self) unless (all + order).include?(self) - order.push(:all) unless order.include?(:all) - - index = order.index(:all) - order[index] = all - order - end - end - - def turbines_initializers(current) # :nodoc: - initializers = [] - ordered_turbines.reverse.flatten.each do |t| - if t == self - initializers += current - else - initializers += t.initializers - end - end - initializers - end - - def default_middleware_stack # :nodoc: - default_stack = DefaultMiddlewareStack.new(self, config, paths) - default_stack.build_stack - end - - def validate_secret_key_base(secret_key_base) - if secret_key_base.is_a?(String) && secret_key_base.present? - secret_key_base - elsif secret_key_base - raise ArgumentError, "`secret_key_base` for #{Jets.env} environment must be a type of String`" - else - raise ArgumentError, "Missing `secret_key_base` for '#{Jets.env}' environment, set this string with `bin/jets credentials:edit`" - end - end - - private - def generate_development_secret - if secrets.secret_key_base.nil? - # key_file = Jets.root.join("tmp/development_secret.txt") - key_file = Pathname.new("/tmp/jets/#{Jets.project_name}/development_secret.txt") - - if !File.exist?(key_file) - random_key = SecureRandom.hex(64) - FileUtils.mkdir_p(key_file.dirname) - File.binwrite(key_file, random_key) - end - - secrets.secret_key_base = File.binread(key_file) - end - - secrets.secret_key_base - end - - def build_middleware - config.app_middleware + super - end - - def coerce_same_site_protection(protection) - protection.respond_to?(:call) ? protection : proc { protection } - end - end -end diff --git a/lib/jets/application/bootstrap.rb b/lib/jets/application/bootstrap.rb deleted file mode 100644 index dd1295dc7..000000000 --- a/lib/jets/application/bootstrap.rb +++ /dev/null @@ -1,100 +0,0 @@ -# frozen_string_literal: true - -require "fileutils" -require "active_support/notifications" -require "active_support/dependencies" -require "active_support/descendants_tracker" -require "jets/secrets" - -module Jets - class Application - module Bootstrap - include Initializable - - initializer :load_environment_hook, group: :all do end - - initializer :load_active_support, group: :all do - ENV["JETS_DISABLE_DEPRECATED_TO_S_CONVERSION"] = "true" if config.active_support.disable_to_s_conversion - require "active_support/all" unless config.active_support.bare - end - - initializer :set_eager_load, group: :all do - if config.eager_load.nil? - warn <<~INFO - config.eager_load is set to nil. Please update your config/environments/*.rb files accordingly: - - * development - set it to false - * test - set it to false (unless you use a tool that preloads your test environment) - * production - set it to true - - INFO - config.eager_load = config.cache_classes - end - end - - # Initialize the logger early in the stack in case we need to log some deprecation. - initializer :initialize_logger, group: :all do - Jets.logger ||= config.logger || begin - logger = ActiveSupport::Logger.new(config.default_log_file) - logger.formatter = config.log_formatter - logger = ActiveSupport::TaggedLogging.new(logger) - logger - rescue StandardError - path = config.paths["log"].first - logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDERR)) - logger.level = ActiveSupport::Logger::WARN - logger.warn( - "Jets Error: Unable to access log file. Please ensure that #{path} exists and is writable " \ - "(i.e. make it writable for user and group: chmod 0664 #{path}). " \ - "The log level has been raised to WARN and the output directed to STDERR until the problem is fixed." - ) - logger - end - Jets.logger.level = ActiveSupport::Logger.const_get(config.log_level.to_s.upcase) - - unless config.consider_all_requests_local - Jets.error.logger = Jets.logger - end - end - - # Initialize cache early in the stack so turbines can make use of it. - initializer :initialize_cache, group: :all do - cache_format_version = config.active_support.delete(:cache_format_version) - ActiveSupport.cache_format_version = cache_format_version if cache_format_version - - unless Jets.cache - Jets.cache = ActiveSupport::Cache.lookup_store(*config.cache_store) - - if Jets.cache.respond_to?(:middleware) - config.middleware.insert_before(::Rack::Runtime, Jets.cache.middleware) - end - end - end - - # We setup the once autoloader this early so that engines and applications - # are able to autoload from these paths during initialization. - initializer :setup_once_autoloader, after: :set_eager_load_paths, before: :bootstrap_hook do - autoloader = Jets.autoloaders.once - - ActiveSupport::Dependencies.autoload_once_paths.freeze - ActiveSupport::Dependencies.autoload_once_paths.uniq.each do |path| - # Zeitwerk only accepts existing directories in `push_dir`. - next unless File.directory?(path) - - autoloader.push_dir(path) - autoloader.do_not_eager_load(path) unless ActiveSupport::Dependencies.eager_load?(path) - end - - autoloader.setup - end - - initializer :bootstrap_hook, group: :all do |app| - ActiveSupport.run_load_hooks(:before_initialize, app) - end - - initializer :set_secrets_root, group: :all do - Jets::Secrets.root = root - end - end - end -end diff --git a/lib/jets/application/configuration.rb b/lib/jets/application/configuration.rb deleted file mode 100644 index ebce99e86..000000000 --- a/lib/jets/application/configuration.rb +++ /dev/null @@ -1,494 +0,0 @@ -# frozen_string_literal: true - -require "ipaddr" -require "active_support/core_ext/kernel/reporting" -require "active_support/file_update_checker" -require "active_support/configuration_file" -require "jets/engine/configuration" - -require "action_dispatch/middleware/host_authorization" - -module Jets - class Application - class Configuration < ::Jets::Engine::Configuration - include Defaults - - attr_accessor :allow_concurrency, :asset_host, :autoflush_log, - :cache_classes, :cache_store, :consider_all_requests_local, :console, - :eager_load, :exceptions_app, :file_watcher, :filter_parameters, - :force_ssl, :helpers_paths, :hosts, :host_authorization, :logger, :log_formatter, - :log_tags, :turbines_order, :relative_url_root, :secret_key_base, - :ssl_options, :public_file_server, - :session_options, :time_zone, :reload_classes_only_on_change, - :beginning_of_week, :filter_redirect, :x, :enable_dependency_loading, - :read_encrypted_secrets, :log_level, :content_security_policy_report_only, - :content_security_policy_nonce_generator, :content_security_policy_nonce_directives, - :require_master_key, :credentials, :disable_sandbox, :add_autoload_paths_to_load_path, - :rake_eager_load, :server_timing - - attr_reader :encoding, :api_only, :loaded_config_version - - def initialize(*) - super - self.encoding = Encoding::UTF_8 - @allow_concurrency = nil - @consider_all_requests_local = false - @filter_parameters = [] - @filter_redirect = [] - @helpers_paths = [] - if Jets.env.development? - @hosts = ActionDispatch::HostAuthorization::ALLOWED_HOSTS_IN_DEVELOPMENT + - ENV["JETS_DEVELOPMENT_HOSTS"].to_s.split(",").map(&:strip) - else - @hosts = [] - end - @host_authorization = {} - @public_file_server = ActiveSupport::OrderedOptions.new - @public_file_server.enabled = true - @public_file_server.index_name = "index" - @force_ssl = false - @ssl_options = {} - @session_store = nil - @time_zone = "UTC" - @beginning_of_week = :monday - @log_level = :debug - @generators = app_generators - @cache_store = [ :file_store, "#{root}/tmp/cache/" ] - @turbines_order = [:all] - @relative_url_root = ENV["JETS_RELATIVE_URL_ROOT"] - @reload_classes_only_on_change = true - @file_watcher = ActiveSupport::FileUpdateChecker - @exceptions_app = nil - @autoflush_log = true - @log_formatter = ActiveSupport::Logger::SimpleFormatter.new - @eager_load = nil - @secret_key_base = nil - @api_only = false - @debug_exception_response_format = nil - @x = Custom.new - @enable_dependency_loading = false - @read_encrypted_secrets = false - @content_security_policy = nil - @content_security_policy_report_only = false - @content_security_policy_nonce_generator = nil - @content_security_policy_nonce_directives = nil - @require_master_key = false - @loaded_config_version = nil - @credentials = ActiveSupport::OrderedOptions.new - @credentials.content_path = default_credentials_content_path - @credentials.key_path = default_credentials_key_path - @disable_sandbox = false - @add_autoload_paths_to_load_path = true - @permissions_policy = nil - @rake_eager_load = false - @server_timing = false - end - - # Loads default configuration values for a target version. This includes - # defaults for versions prior to the target version. See the - # {configuration guide}[https://guides.rubyonjets.org/configuring.html] - # for the default values associated with a particular version. - def load_rails_defaults(target_version) - case target_version.to_s - when "5.0" - if respond_to?(:action_controller) - action_controller.per_form_csrf_tokens = true - action_controller.forgery_protection_origin_check = true - action_controller.urlsafe_csrf_tokens = false - end - - ActiveSupport.to_time_preserves_timezone = true - - if respond_to?(:active_record) - active_record.belongs_to_required_by_default = true - end - - self.ssl_options = { hsts: { subdomains: true } } - when "5.1" - load_rails_defaults "5.0" - - if respond_to?(:assets) - assets.unknown_asset_fallback = false - end - - if respond_to?(:action_view) - action_view.form_with_generates_remote_forms = true - end - when "5.2" - load_rails_defaults "5.1" - - if respond_to?(:active_record) - active_record.cache_versioning = true - end - - if respond_to?(:action_dispatch) - action_dispatch.use_authenticated_cookie_encryption = true - end - - if respond_to?(:active_support) - active_support.use_authenticated_message_encryption = true - active_support.hash_digest_class = OpenSSL::Digest::SHA1 - end - - if respond_to?(:action_controller) - action_controller.default_protect_from_forgery = true - end - - if respond_to?(:action_view) - action_view.form_with_generates_ids = true - end - when "6.0" - load_rails_defaults "5.2" - - if respond_to?(:action_view) - action_view.default_enforce_utf8 = false - end - - if respond_to?(:action_dispatch) - action_dispatch.use_cookies_with_metadata = true - end - - if respond_to?(:action_mailer) - action_mailer.delivery_job = "ActionMailer::MailDeliveryJob" - end - - if respond_to?(:active_storage) - active_storage.queues.analysis = :active_storage_analysis - active_storage.queues.purge = :active_storage_purge - - active_storage.replace_on_assign_to_many = true - end - - if respond_to?(:active_record) - active_record.collection_cache_versioning = true - end - when "6.1" - load_rails_defaults "6.0" - - if respond_to?(:active_record) - active_record.has_many_inversing = true - active_record.legacy_connection_handling = false - end - - if respond_to?(:active_job) - active_job.retry_jitter = 0.15 - end - - if respond_to?(:action_dispatch) - action_dispatch.cookies_same_site_protection = :lax - action_dispatch.ssl_default_redirect_status = 308 - end - - if respond_to?(:action_controller) - action_controller.delete(:urlsafe_csrf_tokens) - end - - if respond_to?(:action_view) - action_view.form_with_generates_remote_forms = false - action_view.preload_links_header = true - end - - if respond_to?(:active_storage) - active_storage.track_variants = true - - active_storage.queues.analysis = nil - active_storage.queues.purge = nil - end - - if respond_to?(:action_mailbox) - action_mailbox.queues.incineration = nil - action_mailbox.queues.routing = nil - end - - if respond_to?(:action_mailer) - action_mailer.deliver_later_queue_name = nil - end - - ActiveSupport.utc_to_local_returns_utc_offset_times = true - when "7.0" - load_rails_defaults "6.1" - - if respond_to?(:action_dispatch) - action_dispatch.default_headers = { - "X-Frame-Options" => "SAMEORIGIN", - "X-XSS-Protection" => "0", - "X-Content-Type-Options" => "nosniff", - "X-Download-Options" => "noopen", - "X-Permitted-Cross-Domain-Policies" => "none", - "Referrer-Policy" => "strict-origin-when-cross-origin" - } - action_dispatch.return_only_request_media_type_on_content_type = false - action_dispatch.cookies_serializer = :json - end - - if respond_to?(:action_view) - action_view.button_to_generates_button_tag = true - action_view.apply_stylesheet_media_default = false - end - - if respond_to?(:active_support) - active_support.hash_digest_class = OpenSSL::Digest::SHA256 - active_support.key_generator_hash_digest_class = OpenSSL::Digest::SHA256 - active_support.remove_deprecated_time_with_zone_name = true - active_support.cache_format_version = 7.0 - active_support.use_rfc4122_namespaced_uuids = true - active_support.executor_around_test_case = true - active_support.disable_to_s_conversion = true - end - - if respond_to?(:action_mailer) - action_mailer.smtp_timeout = 5 - end - - if respond_to?(:active_storage) - active_storage.video_preview_arguments = - "-vf 'select=eq(n\\,0)+eq(key\\,1)+gt(scene\\,0.015),loop=loop=-1:size=2,trim=start_frame=1'" \ - " -frames:v 1 -f image2" - - active_storage.variant_processor = :vips - active_storage.multiple_file_field_include_hidden = true - end - - if respond_to?(:active_record) - active_record.verify_foreign_keys_for_fixtures = true - active_record.partial_inserts = false - active_record.automatic_scope_inversing = true - end - - if respond_to?(:action_controller) - action_controller.raise_on_open_redirects = true - - action_controller.wrap_parameters_by_default = true - end - else - raise "Unknown version #{target_version.to_s.inspect}" - end - - @loaded_config_version = target_version - end - - def encoding=(value) - @encoding = value - silence_warnings do - Encoding.default_external = value - Encoding.default_internal = value - end - end - - def api_only=(value) - @api_only = value - generators.api_only = value - - @debug_exception_response_format ||= :api - end - - def debug_exception_response_format - @debug_exception_response_format || :default - end - - attr_writer :debug_exception_response_format - - def paths - @paths ||= begin - paths = super - paths.add "config/database", with: "config/database.yml" - paths.add "config/secrets", with: "config", glob: "secrets.yml{,.enc}" - paths.add "config/environment", with: "config/environment.rb" - paths.add "lib/templates" - paths.add "log", with: "log/#{Jets.env}.log" - paths.add "public" - paths.add "public/javascripts" - paths.add "public/stylesheets" - paths.add "tmp" - paths - end - end - - # Load the database YAML without evaluating ERB. This allows us to - # create the rake tasks for multiple databases without filling in the - # configuration values or loading the environment. Do not use this - # method. - # - # This uses a DummyERB custom compiler so YAML can ignore the ERB - # tags and load the database.yml for the rake tasks. - def load_database_yaml # :nodoc: - if path = paths["config/database"].existent.first - require "jets/application/dummy_erb_compiler" - - yaml = DummyERB.new(Pathname.new(path).read).result - - if YAML.respond_to?(:unsafe_load) - YAML.unsafe_load(yaml) || {} - else - YAML.load(yaml) || {} - end - else - {} - end - end - - # Loads and returns the entire raw configuration of database from - # values stored in config/database.yml. - def database_configuration - path = paths["config/database"].existent.first - yaml = Pathname.new(path) if path - - config = if yaml&.exist? - loaded_yaml = ActiveSupport::ConfigurationFile.parse(yaml) - if (shared = loaded_yaml.delete("shared")) - loaded_yaml.each do |env, config| - if config.is_a?(Hash) && config.values.all?(Hash) - config.map do |name, sub_config| - sub_config.reverse_merge!(shared) - end - else - config.reverse_merge!(shared) - end - end - end - Hash.new(shared).merge(loaded_yaml) - elsif ENV["DATABASE_URL"] - # Value from ENV['DATABASE_URL'] is set to default database connection - # by Active Record. - {} - else - raise "Could not load database configuration. No such file - #{paths["config/database"].instance_variable_get(:@paths)}" - end - - config - rescue => e - raise e, "Cannot load database configuration:\n#{e.message}", e.backtrace - end - - def colorize_logging - ActiveSupport::LogSubscriber.colorize_logging - end - - def colorize_logging=(val) - ActiveSupport::LogSubscriber.colorize_logging = val - generators.colorize_logging = val - end - - # Specifies what class to use to store the session. Possible values - # are +:cookie_store+, +:mem_cache_store+, a custom store, or - # +:disabled+. +:disabled+ tells Jets not to deal with sessions. - # - # Additional options will be set as +session_options+: - # - # config.session_store :cookie_store, key: "_your_app_session" - # config.session_options # => {key: "_your_app_session"} - # - # If a custom store is specified as a symbol, it will be resolved to - # the +ActionDispatch::Session+ namespace: - # - # # use ActionDispatch::Session::MyCustomStore as the session store - # config.session_store :my_custom_store - def session_store(new_session_store = nil, **options) - if new_session_store - if new_session_store == :active_record_store - begin - ActionDispatch::Session::ActiveRecordStore - rescue NameError - raise "`ActiveRecord::SessionStore` is extracted out of Jets into a gem. " \ - "Please add `activerecord-session_store` to your Gemfile to use it." - end - end - - @session_store = new_session_store - @session_options = options || {} - else - case @session_store - when :disabled - nil - when :active_record_store - ActionDispatch::Session::ActiveRecordStore - when Symbol - ActionDispatch::Session.const_get(@session_store.to_s.camelize) - else - @session_store - end - end - end - - def session_store? # :nodoc: - @session_store - end - - def annotations - Jets::SourceAnnotationExtractor::Annotation - end - - # Configures the ActionDispatch::ContentSecurityPolicy. - def content_security_policy(&block) - if block_given? - @content_security_policy = ActionDispatch::ContentSecurityPolicy.new(&block) - else - @content_security_policy - end - end - - # Configures the ActionDispatch::PermissionsPolicy. - def permissions_policy(&block) - if block_given? - @permissions_policy = ActionDispatch::PermissionsPolicy.new(&block) - else - @permissions_policy - end - end - - def default_log_file - $stdout - # path = paths["log"].first - # unless File.exist? File.dirname path - # FileUtils.mkdir_p File.dirname path - # end - - # f = File.open path, "a" - # f.binmode - # f.sync = autoflush_log # if true make sure every write flushes - # f - end - - class Custom # :nodoc: - def initialize - @configurations = Hash.new - end - - def method_missing(method, *args) - if method.end_with?("=") - @configurations[:"#{method[0..-2]}"] = args.first - else - @configurations.fetch(method) { - @configurations[method] = ActiveSupport::OrderedOptions.new - } - end - end - - def respond_to_missing?(symbol, *) - true - end - end - - private - def default_credentials_content_path - if credentials_available_for_current_env? - root.join("config", "credentials", "#{Jets.env}.yml.enc") - else - root.join("config", "credentials.yml.enc") - end - end - - def default_credentials_key_path - if credentials_available_for_current_env? - root.join("config", "credentials", "#{Jets.env}.key") - else - root.join("config", "master.key") - end - end - - def credentials_available_for_current_env? - File.exist?(root.join("config", "credentials", "#{Jets.env}.yml.enc")) - end - end - end -end diff --git a/lib/jets/application/configuration/defaults.rb b/lib/jets/application/configuration/defaults.rb deleted file mode 100644 index 3b9e78efe..000000000 --- a/lib/jets/application/configuration/defaults.rb +++ /dev/null @@ -1,256 +0,0 @@ -class Jets::Application::Configuration < ::Jets::Engine::Configuration - # These are Jets specific defaults. The other defaults in Configuration come from using Rails components. - module Defaults - extend ActiveSupport::Concern - - attr_accessor :api, - :api_mode, - :app, - :build, - :cfn, - :controllers, - :default_iam_policy, - :deploy, - :domain, - :environment, - :events, - :filter_parameters, - :function, - :gems, # deprecated - :helpers_paths, - :helpers, - :iam_policy, - :ignore_paths, - :inflections, - :lambda, - :logger, - :logging, - :managed_iam_policy, - :managed_policy_definitions, - :mode, - :prewarm, - :pro, - :routes, - :s3_event, - :webpacker # deprecated - - def initialize(*) - Jets::Dotenv.load! - super - @api = ActiveSupport::OrderedOptions.new - @api.api_key_required = false # Turn off API key required - @api.authorization_type = "NONE" - @api.authorizers = ActiveSupport::OrderedOptions.new - @api.authorizers.default_token_source = "Auth" # method.request.header.Auth - @api.auto_replace = nil # https://github.com/boltops-tools/jets/issues/391 - @api.binary_media_types = ['multipart/form-data'] - - @api.cors = default_cors - @api.endpoint_policy = nil # required when endpoint_type is EDGE - @api.endpoint_type = 'EDGE' # PRIVATE, EDGE, REGIONAL - @api.vpc_endpoint_ids = nil - - @api_mode = nil - - @app = ActiveSupport::OrderedOptions.new - @app.domain = nil - - @build = ActiveSupport::OrderedOptions.new - @build.prebundle_copy = [] - - @cfn = ActiveSupport::OrderedOptions.new - @cfn.build = ActiveSupport::OrderedOptions.new - @cfn.build.controllers = "one_lambda_for_all_controllers" # also: one_lambda_per_controller one_lambda_for_all_controllers - @cfn.build.resource_tags = {} # tags to add to all resources - @cfn.build.routes = "one_apigw_method_for_all_routes" # also: one_apigw_method_per_route - - @controllers = ActiveSupport::OrderedOptions.new - @controllers.default_protect_from_forgery = nil - - @deploy = ActiveSupport::OrderedOptions.new - @deploy.stagger = ActiveSupport::OrderedOptions.new - @deploy.stagger.enabled = false - @deploy.stagger.batch_size = 10 - - # @domain.name = "#{Jets.project_namespace}.coolapp.com" # Default is nil - # @domain.cert_arn = "..." - # @domain.route53 controls whether or not to create the managed route53 record. - # Useful to disable this when user wants to manage the route themself like pointing - # it to CloudFront for blue-green deployments instead. - @domain = ActiveSupport::OrderedOptions.new - @domain.base_path = '' # empty path represents root - @domain.endpoint_type = "REGIONAL" # EDGE or REGIONAL. Default to EDGE because CloudFormation update is faster - @domain.route53 = true - - @environment = ActiveSupport::OrderedOptions.new - - @events = ActiveSupport::OrderedOptions.new - - @events.dynamodb = ActiveSupport::OrderedOptions.new - @events.dynamodb.table_namespace = true # true will use Jets.table_name - @events.dynamodb.table_namespace_separator = '_' - - @events.s3 = ActiveSupport::OrderedOptions.new - @events.s3.configure_bucket = true - # These notification_configuration properties correspond to the ruby aws-sdk - # s3.put_bucket_notification_configuration - # in jets/s3_bucket.rb, not the CloudFormation Bucket properties. The CloudFormation - # bucket properties have a similiar structure but is slightly different so it can be confusing. - # - # Ruby aws-sdk S3 Docs: https://amzn.to/2N7m5Lr - @events.s3.notification_configuration = { - topic_configurations: [ - { - events: ["s3:ObjectCreated:*"], - topic_arn: "!Ref SnsTopic", # must use this logical id - }, - ], - } - - # Deprecated: Use @events.s3.configure_bucket instead - # Leaving in as a comment for now. Will remove comment in the future - # Already print a deprecation warning in jets/stack/s3_event.rb - @s3_event = ActiveSupport::OrderedOptions.new - # @s3_event.configure_bucket = true - # @s3_event.notification_configuration = { - # topic_configurations: [ - # { - # events: ["s3:ObjectCreated:*"], - # topic_arn: "!Ref SnsTopic", # must use this logical id - # }, - # ], - # } - - @filter_parameters = [] - - # default function.memory_size memory setting based on: - # https://medium.com/epsagon/how-to-make-lambda-faster-memory-performance-benchmark-be6ebc41f0fc - @function = ActiveSupport::OrderedOptions.new - @function.ephemeral_storage = { size: 512 } # megabytes - @function.memory_size = 1536 - @function.timeout = 30 - - # Old deprecated - @gems = ActiveSupport::OrderedOptions.new - @gems.clean = false - @gems.disable = false - @gems.source = "https://api.serverlessgems.com/api/v1" - # New - @pro = ActiveSupport::OrderedOptions.new - @pro.disable = false - - @helpers = ActiveSupport::OrderedOptions.new - @helpers.host = nil # nil by default. Other examples: https://myurl.com:8888 - @helpers_paths = [] - - @inflections = ActiveSupport::OrderedOptions.new - @inflections.irregular = {} - - # Custom user lambda layers - @lambda = ActiveSupport::OrderedOptions.new - @lambda.layers = [] - - @logger = ActiveSupport::Logger.new($stderr) - @logger.formatter = ActiveSupport::Logger::SimpleFormatter.new # jets v5 default: no timestamps - @logger.level = :debug # Logger::DEBUG - default to debug for development mode and to see ActionMailer logs - - @logging = ActiveSupport::OrderedOptions.new - @logging.event = false # jets v5 behavior - - @mode = nil # job api html - - @prewarm = ActiveSupport::OrderedOptions.new - @prewarm.enable = true - @prewarm.public_ratio = 3 - @prewarm.rate = '30 minutes' - - @routes = ActiveSupport::OrderedOptions.new - @routes.allow_sibling_conflicts = true # users/:id and users/:user_id/articles - - @default_iam_policy = self.class.default_iam_policy - @managed_policy_definitions = [] - @managed_iam_policy = [] - end - - # IAM policies must run lazily because they depend on @function.vpc_config - # Need the if Jets.application check because commands like - # jets generate kingsman:controllers -h - # will load Rails for generators and that will directly use a - # Jets config that does not call Jest.boot - # See: Jets::Generator - def iam_policy - return [] unless Jets.application - - if @default_iam_policy.nil? && @iam_policy.nil? - return self.class.default_iam_policy - end - - policy = [] - policy << @default_iam_policy - if @function.vpc_config - policy << vpc_iam_policy_statement - end - policy << @iam_policy - policy.flatten.compact - end - - def vpc_iam_policy_statement - { - Action: %w[ - ec2:CreateNetworkInterface - ec2:DeleteNetworkInterface - ec2:DescribeNetworkInterfaces - ec2:DescribeVpcs - ec2:DescribeSubnets - ec2:DescribeSecurityGroups - ], - Effect: "Allow", - Resource: "*", - } - end - - def default_cors - !!Gem.loaded_specs.detect do |gem_name, spec| - gem_name == "rack-cors" - end - end - - class_methods do - def default_iam_policy - project_namespace = Jets.project_namespace - logs = { - Action: ["logs:*"], - Effect: "Allow", - Resource: "arn:aws:logs:#{Jets.aws.region}:#{Jets.aws.account}:log-group:/aws/lambda/#{project_namespace}-*", - } - s3_readonly = { - Action: ["s3:Get*", "s3:List*", "s3:HeadBucket"], - Effect: "Allow", - Resource: "arn:aws:s3:::#{Jets.aws.s3_bucket}*", - } - policies = [logs, s3_readonly] - - cloudformation = { - Action: ["cloudformation:DescribeStacks", "cloudformation:DescribeStackResources"], - Effect: "Allow", - Resource: "arn:aws:cloudformation:#{Jets.aws.region}:#{Jets.aws.account}:stack/#{project_namespace}*", - } - policies << cloudformation - - policies - end - end - - def load_defaults(target_version) - load_rails_defaults "7.0" - - case target_version.to_s - when "5.0" - @host_authorization = { exclude: ->(request) do - request.host =~ /localhost/ || request.domain == "amazonaws.com" - end - } - end - end - end -end diff --git a/lib/jets/application/default_middleware_stack.rb b/lib/jets/application/default_middleware_stack.rb deleted file mode 100644 index 6f2e600f9..000000000 --- a/lib/jets/application/default_middleware_stack.rb +++ /dev/null @@ -1,140 +0,0 @@ -# frozen_string_literal: true - -require "rack" - -module Jets - class Application - class DefaultMiddlewareStack - attr_reader :config, :paths, :app - - def initialize(app, config, paths) - @app = app - @config = config - @paths = paths - end - - def build_stack - ActionDispatch::MiddlewareStack.new do |middleware| - # Slightly different than Rails. User needs to enable it explicitly. - if ENV['JETS_HOST_AUTHORIZATION'] - middleware.use ::ActionDispatch::HostAuthorization, config.hosts, **config.host_authorization - end - - if config.force_ssl - middleware.use ::ActionDispatch::SSL, **config.ssl_options, - ssl_default_redirect_status: config.action_dispatch.ssl_default_redirect_status - end - - if config.public_file_server.enabled - headers = config.public_file_server.headers || {} - - middleware.use ::ActionDispatch::Static, paths["public"].first, index: config.public_file_server.index_name, headers: headers - end - - if rack_cache = load_rack_cache - require "action_dispatch/http/rack_cache" - middleware.use ::Rack::Cache, rack_cache - end - - middleware.use ::Rack::Sendfile, config.action_dispatch.x_sendfile_header - - if config.allow_concurrency == false - # User has explicitly opted out of concurrent request - # handling: presumably their code is not threadsafe - - middleware.use ::Rack::Lock - end - - middleware.use ::ActionDispatch::Executor, app.executor - - middleware.use ::ActionDispatch::ServerTiming if config.server_timing - middleware.use ::Rack::Runtime - - middleware.use ::Rack::MethodOverride unless ENV['JETS_RACK_METHOD_OVERRIDE'] == '0' # must come before Middleware::Mimic for multipart post forms to work - - middleware.use ::ActionDispatch::RequestId, header: config.action_dispatch.request_id_header - middleware.use ::ActionDispatch::RemoteIp, config.action_dispatch.ip_spoofing_check, config.action_dispatch.trusted_proxies - middleware.use ::Jets::Rack::Logger, config.log_tags - - middleware.use ::ActionDispatch::ShowExceptions, show_exceptions_app - middleware.use ::ActionDispatch::DebugExceptions, app, config.debug_exception_response_format - - # Not supported by Jets. ActionableExceptions is slightly too coupled to Rails. - # if config.consider_all_requests_local - # middleware.use ::ActionDispatch::ActionableExceptions - # end - - unless config.cache_classes - middleware.use ::ActionDispatch::Reloader, app.reloader - end - - middleware.use Jets::Controller::Middleware::Mimic # mimics AWS Lambda for local server only - - middleware.use ActionDispatch::Callbacks - middleware.use ActionDispatch::Cookies unless config.api_only - - if !config.api_only && config.session_store - if config.force_ssl && config.ssl_options.fetch(:secure_cookies, true) && !config.session_options.key?(:secure) - config.session_options[:secure] = true - end - middleware.use config.session_store, config.session_options - end - - unless config.api_only - middleware.use ActionDispatch::Flash - middleware.use ActionDispatch::ContentSecurityPolicy::Middleware - middleware.use ActionDispatch::PermissionsPolicy::Middleware - end - - middleware.use ::Rack::Head - middleware.use ::Rack::ConditionalGet - middleware.use ::Rack::ETag, "no-cache" - - middleware.use ::Rack::TempfileReaper unless config.api_only - - if config.respond_to?(:active_record) - if selector_options = config.active_record.database_selector - resolver = config.active_record.database_resolver - context = config.active_record.database_resolver_context - - middleware.use ::ActiveRecord::Middleware::DatabaseSelector, resolver, context, selector_options - end - - if shard_resolver = config.active_record.shard_resolver - options = config.active_record.shard_selector || {} - - middleware.use ::ActiveRecord::Middleware::ShardSelector, shard_resolver, options - end - end - end - end - - private - def load_rack_cache - rack_cache = config.action_dispatch.rack_cache - return unless rack_cache - - begin - require "rack/cache" - rescue LoadError => error - error.message << " Be sure to add rack-cache to your Gemfile" - raise - end - - if rack_cache == true - { - metastore: "jets:/", - entitystore: "jets:/", - verbose: false - } - else - rack_cache - end - end - - def show_exceptions_app - config.exceptions_app || ActionDispatch::PublicExceptions.new(Jets.public_path) - end - end - end -end diff --git a/lib/jets/application/dummy_erb_compiler.rb b/lib/jets/application/dummy_erb_compiler.rb deleted file mode 100644 index 028e79029..000000000 --- a/lib/jets/application/dummy_erb_compiler.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -# These classes are used to strip out the ERB configuration -# values so we can evaluate the database.yml without evaluating -# the ERB values. -class DummyERB < ERB # :nodoc: - def make_compiler(trim_mode) - DummyCompiler.new trim_mode - end -end - -class DummyCompiler < ERB::Compiler # :nodoc: - def compile_content(stag, out) - if stag == "<%=" - out.push "_erbout << ''" - end - end -end diff --git a/lib/jets/application/finisher.rb b/lib/jets/application/finisher.rb deleted file mode 100644 index 43f33f341..000000000 --- a/lib/jets/application/finisher.rb +++ /dev/null @@ -1,266 +0,0 @@ -# frozen_string_literal: true - -require "active_support/core_ext/string/inflections" -require "active_support/core_ext/array/conversions" -require "active_support/descendants_tracker" -require "active_support/dependencies" - -module Jets - class Application - module Finisher - include Initializable - - initializer :add_generator_templates do - config.generators.templates.unshift(*paths["lib/templates"].existent) - end - - initializer :setup_main_autoloader do - autoloader = Jets.autoloaders.main - - ActiveSupport::Dependencies.autoload_paths.freeze - ActiveSupport::Dependencies.autoload_paths.uniq.each do |path| - # Zeitwerk only accepts existing directories in `push_dir`. - next unless File.directory?(path) - - autoloader.push_dir(path) - autoloader.do_not_eager_load(path) unless ActiveSupport::Dependencies.eager_load?(path) - end - - unless config.cache_classes - autoloader.enable_reloading - ActiveSupport::Dependencies.autoloader = autoloader - - autoloader.on_load do |_cpath, value, _abspath| - if value.is_a?(Class) && value.singleton_class < ActiveSupport::DescendantsTracker - ActiveSupport::Dependencies._autoloaded_tracked_classes << value - end - end - end - - autoloader.setup - end - - # Setup default session store if not already set in config/application.rb - initializer :setup_default_session_store, before: :build_middleware_stack do |app| - unless app.config.session_store? - app_name = app.class.name ? app.turbine_name.chomp("_application") : "" - app.config.session_store :cookie_store, key: "_#{app_name}_session" - end - end - - initializer :build_middleware_stack do - build_middleware_stack - end - - initializer :define_main_app_helper do |app| - app.routes.define_mounted_helper(:main_app) - end - - initializer :add_to_prepare_blocks do |app| - config.to_prepare_blocks.each do |block| - app.reloader.to_prepare(&block) - end - end - - # This needs to happen before eager load so it happens - # in exactly the same point regardless of config.eager_load - initializer :run_prepare_callbacks do |app| - app.reloader.prepare! - end - - # Shared extensions are added near the end because they require the Jets app load paths to first. - # We eager load the extensions and then use the loaded modules to extend Jets::Stack directly. - # Originally used an included hook but thats too early before app/shared/extensions is in the load_path. - initializer :preload_app_shared_extensions do |app| - base_path = "#{root}/app/shared/extensions" - Dir.glob("#{base_path}/**/*.rb").each do |path| - next unless File.file?(path) - require path - - class_name = path.sub("#{base_path}/", '').sub(/\.rb/,'').camelize - mod = class_name.constantize # autoload - Jets::Stack.extend(mod) - end - end - - initializer :preload_app_extensions do |app| - base_path = "#{root}/app/extensions" - Dir.glob("#{base_path}/**/*.rb").each do |path| - next unless File.file?(path) - require path - - class_name = path.sub("#{base_path}/", '').sub(/\.rb/,'').camelize - klass = class_name.constantize # autoload - Jets::Lambda::Functions.extend(klass) - end - end - - initializer :preload_app_shared_resources do |app| - base_path = "#{root}/app/shared/resources" - Dir.glob("#{base_path}/**/*.rb").each do |path| - next unless File.file?(path) - require path - end - end - - # Since using on_load(:jets_controller) and need to be setup before eager_load! runs. - initializer :mount_helpers do |app| - ActiveSupport.on_load(:jets_controller) do - include app.routes.mounted_helpers # mounted routes helpers: main_app and blorgh - extend ::JetsTurbines::RoutesHelpers.with(app.routes) # named routes helpers - extend ::JetsTurbines::Helpers # project helpers - end - end - - initializer :eager_load! do |app| - if config.eager_load - ActiveSupport.run_load_hooks(:before_eager_load, self) - autoloaders.log! if ENV['JETS_DEBUG_AUTOLOADERS'] - Zeitwerk::Loader.eager_load_all - config.eager_load_namespaces.each(&:eager_load!) - - unless config.cache_classes - app.reloader.after_class_unload do - Jets.autoloaders.main.eager_load - end - end - end - end - - # All initialization is done, including eager loading in production - initializer :finisher_hook do - ActiveSupport.run_load_hooks(:after_initialize, self) - end - - class MonitorHook # :nodoc: - def initialize(monitor = Monitor.new) - @monitor = monitor - end - - def run - @monitor.enter - end - - def complete(_state) - @monitor.exit - end - end - - module InterlockHook # :nodoc: - def self.run - ActiveSupport::Dependencies.interlock.start_running - end - - def self.complete(_state) - ActiveSupport::Dependencies.interlock.done_running - end - end - - initializer :configure_executor_for_concurrency do |app| - if config.allow_concurrency == false - # User has explicitly opted out of concurrent request - # handling: presumably their code is not threadsafe - - app.executor.register_hook(MonitorHook.new, outer: true) - - elsif config.allow_concurrency == :unsafe - # Do nothing, even if we know this is dangerous. This is the - # historical behavior for true. - - else - # Default concurrency setting: enabled, but safe - - unless config.cache_classes && config.eager_load - # Without cache_classes + eager_load, the load interlock - # is required for proper operation - - app.executor.register_hook(InterlockHook, outer: true) - end - end - end - - initializer :add_internal_routes do |app| - if Jets.env.development? - app.routes.prepend do - get "/jets/info/properties" => "jets/info#properties", internal: true - get "/jets/info/routes" => "jets/info#routes", internal: true - get "/jets/info" => "jets/info#index", internal: true - end - - routes_reloader.run_after_load_paths = -> do - app.routes.append do - get "/" => "jets/welcome#index", internal: true - end - end - end - end - - # Set routes reload after the finisher hook to ensure routes added in - # the hook are taken into account. - initializer :set_routes_reloader_hook do |app| - reloader = routes_reloader - reloader.eager_load = app.config.eager_load - reloader.execute - reloaders << reloader - app.reloader.to_run do - # We configure #execute rather than #execute_if_updated because if - # autoloaded constants are cleared we need to reload routes also in - # case any was used there, as in - # - # mount MailPreview => 'mail_view' - # - # This means routes are also reloaded if i18n is updated, which - # might not be necessary, but in order to be more precise we need - # some sort of reloaders dependency support, to be added. - require_unload_lock! - reloader.execute - end - end - - # Set clearing dependencies after the finisher hook to ensure paths - # added in the hook are taken into account. - initializer :set_clear_dependencies_hook, group: :all do |app| - callback = lambda do - # Order matters. - ActiveSupport::DescendantsTracker.clear(ActiveSupport::Dependencies._autoloaded_tracked_classes) - ActiveSupport::Dependencies.clear - end - - if config.cache_classes - app.reloader.check = lambda { false } - # elsif config.reload_classes_only_on_change - # app.reloader.check = lambda do - # app.reloaders.map(&:updated?).any? - # end - else - app.reloader.check = lambda { true } - end - - if config.cache_classes - # No reloader - ActiveSupport::DescendantsTracker.disable_clear! - elsif config.reload_classes_only_on_change - reloader = config.file_watcher.new(*watchable_args, &callback) - reloaders << reloader - - # Prepend this callback to have autoloaded constants cleared before - # any other possible reloading, in case they need to autoload fresh - # constants. - app.reloader.to_run(prepend: true) do - # In addition to changes detected by the file watcher, if routes - # or i18n have been updated we also need to clear constants, - # that's why we run #execute rather than #execute_if_updated, this - # callback has to clear autoloaded constants after any update. - class_unload! do - reloader.execute - end - end - else - app.reloader.to_complete do - class_unload!(&callback) - end - end - end - end - end -end diff --git a/lib/jets/application/routes_reloader.rb b/lib/jets/application/routes_reloader.rb deleted file mode 100644 index 22907a10d..000000000 --- a/lib/jets/application/routes_reloader.rb +++ /dev/null @@ -1,80 +0,0 @@ -# frozen_string_literal: true - -require "active_support/core_ext/module/delegation" - -module Jets - class Application - class RoutesReloader - include ActiveSupport::Callbacks - - attr_reader :route_sets, :paths, :external_routes - attr_accessor :eager_load - attr_writer :run_after_load_paths # :nodoc: - delegate :execute_if_updated, :execute, :updated?, to: :updater - - def initialize - @paths = [] - @route_sets = [] - @external_routes = [] - @eager_load = false - end - - def reload! - return if @@disabled - clear! - load_paths - finalize! - route_sets.each(&:eager_load!) if eager_load - ensure - revert - end - - # Added to help with specs - @@disabled = false - def self.disable! - @@disabled = true - end - - def self.enable! - @@disabled = false - end - - private - def updater - @updater ||= begin - dirs = @external_routes.each_with_object({}) do |dir, hash| - hash[dir.to_s] = %w(rb) - end - - ActiveSupport::FileUpdateChecker.new(paths, dirs) { reload! } - end - end - - def clear! - route_sets.each do |routes| - routes.disable_clear_and_finalize = true - routes.clear! - end - end - - def load_paths - paths.each { |path| load(path) } - run_after_load_paths.call - end - - def run_after_load_paths - @run_after_load_paths ||= -> { } - end - - def finalize! - route_sets.each(&:finalize!) - end - - def revert - route_sets.each do |routes| - routes.disable_clear_and_finalize = false - end - end - end - end -end diff --git a/lib/jets/authorizer/base.rb b/lib/jets/authorizer/base.rb deleted file mode 100644 index 554f3e971..000000000 --- a/lib/jets/authorizer/base.rb +++ /dev/null @@ -1,37 +0,0 @@ -# Authorizer public methods get turned into Lambda functions. -# -# Jets::Authorizer::Base < Jets::Lambda::Functions -# Both Jets::Authorizer::Base and Jets::Lambda::Functions have Dsl modules included. -# So the Jets::Authorizer::Dsl overrides some of the Jets::Lambda::Functions behavior. -module Jets::Authorizer - class Base < Jets::Lambda::Functions - include Dsl - include Helpers::IamHelper - prepend Jets::ExceptionReporting::Process - - class << self - def process(event, context, meth) - authorizer = new(event, context, meth) - authorizer.send(meth) - end - - # Parameter: authorizer: Example: "main#protect" - # - # Determines authorization_type based on whether the authorizer is a Lambda or Cognito authorizer. - # - # Returns custom or cognito_user_pools - def authorization_type(authorizer) - class_name, meth = authorizer.split('#') - klass = "#{class_name}_authorizer".camelize.constantize - meth = meth.to_sym - - # If there's a lambda function associated with the authorizer then it's a custom authorizer - # Otherwise it's a cognito authorizer. - # - # Returns: Valid values: none, aws_iam, custom, cognito_user_pools, aws_cross_account_iam - methods = klass.public_instance_methods(false) - methods.include?(meth) ? :custom : :cognito_user_pools - end - end - end -end diff --git a/lib/jets/authorizer/dsl.rb b/lib/jets/authorizer/dsl.rb deleted file mode 100644 index cc4edbb73..000000000 --- a/lib/jets/authorizer/dsl.rb +++ /dev/null @@ -1,73 +0,0 @@ -require 'active_support' -require 'active_support/core_ext/class' - -# Jets::Authorizer::Base < Jets::Lambda::Functions -# Both Jets::Authorizer::Base and Jets::Lambda::Functions have Dsl modules included. -# So the Jets::Authorizer::Dsl overrides some of the Jets::Lambda::Functions behavior. -# -# Implements: -# -# default_associated_resource_definition -# -module Jets::Authorizer - module Dsl - extend ActiveSupport::Concern - - class_methods do - include Jets::Util::Camelize - - def authorizer(props={}) - camelize(props) - if props[:Type].to_s.upcase == "COGNITO_USER_POOLS" - cognito_authorizer(props) - else - lambda_authorizer(props) - end - end - - def lambda_authorizer(props={}) - with_fresh_properties(multiple_resources: false) do - r = Jets::Cfn::Resource::ApiGateway::Authorizer.new(props) - resource(r.definition) # add associated resource immediately - end - end - - # Creates a definition but registers it to cognito_authorizers instead of all_tasks because there is no Lambda - # function associated with the cognito authorizer. - def cognito_authorizer(props={}) - camelize(props) - resources = [props] - # Authorizer name can have dashes, but "method" name should be underscored for correct logical id. - meth = props[:Name].gsub('-','_') - resource = Jets::Cfn::Resource::ApiGateway::Authorizer.new(props) - - # Mimic definition to grab base_replacements, namely namespace. - # Do not actually use the definition to create a Lambda function for cognito authorizer. - # Only using the definition for base_replacements. - definition = Jets::Lambda::Definition.new(self.name, meth, - resources: resources, - replacements: {}) # No need for need additional replacements. Baseline replacements suffice - all_cognito_authorizers[name] = { definition: resource.definition, replacements: definition.replacements } - clear_properties - end - - def all_cognito_authorizers - @all_cognito_authorizers ||= ActiveSupport::OrderedHash.new - end - - def cognito_authorizers - all_cognito_authorizers.values - end - - def build? - !definitions.empty? || !all_cognito_authorizers.empty? - end - end - - included do - class << self - include Jets::AwsServices - end - end - end -end \ No newline at end of file diff --git a/lib/jets/authorizer/helpers/iam_helper.rb b/lib/jets/authorizer/helpers/iam_helper.rb deleted file mode 100644 index a15d5e7cf..000000000 --- a/lib/jets/authorizer/helpers/iam_helper.rb +++ /dev/null @@ -1,49 +0,0 @@ -module Jets::Authorizer::Helpers - module IamHelper - include Jets::Util::Camelize - - private - # Structure: https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-lambda-authorizer-output.html - # Example: - # - # { - # "principalId" => "yyyyyyyy", // The principal user identification associated with the token sent by the client. - # "policyDocument" => {}, - # "context" => {}, - # "usageIdentifierKey" => "{api-key}" - # } - # - def build_policy(*args) - if args.first.is_a?(Hash) # generalized form - props = args.first - else # build_policy(resource, principal, context, usage_identifier_key) form - resource, principal_id, context, usage_identifier_key = args - props = { - principalId: principal_id || "default_user", - policyDocument: { - Version: "2012-10-17", - Statement: [ - Action: "execute-api:Invoke", - Effect: "Allow", - Resource: resource || "*", - ], - }, - } - props[:context] = context if context - props[:usageIdentifierKey] = usage_identifier_key if usage_identifier_key - end - - props.transform_keys! { |k| pascalize(k) } # Only top-level keys are pascalized - # policyDocument is camelized, everything else is left alone - props[:policyDocument] = camelize(props[:policyDocument]) - props - end - - def pascalize(value) - new_value = value.to_s.camelize - first_char = new_value[0..0].downcase - new_value[0] = first_char - new_value.to_sym - end - end -end diff --git a/lib/jets/autoloaders.rb b/lib/jets/autoloaders.rb index a35e23144..5bbd06a7f 100644 --- a/lib/jets/autoloaders.rb +++ b/lib/jets/autoloaders.rb @@ -1,52 +1,23 @@ require "jets/bundle" Jets::Bundle.setup require "zeitwerk" +require_relative "autoloaders/gem" +require_relative "autoloaders/main" module Jets class Autoloaders - require_relative "autoloaders/inflector" - - include Enumerable - - attr_reader :main, :once - - def initialize - # This `require` delays loading the library on purpose. - # - # In Rails 7.0.0, railties/lib/rails.rb loaded Zeitwerk as a side-effect, - # but a couple of edge cases related to Bundler and Bootsnap showed up. - # They had to do with order of decoration of `Kernel#require`, something - # the three of them do. - # - # Delaying this `require` up to this point is a convenient trade-off. - require "zeitwerk" - - @main = Zeitwerk::Loader.new - @main.tag = "jets.main" - @main.inflector = Inflector - - @once = Zeitwerk::Loader.new - @once.tag = "jets.once" - @once.inflector = Inflector - end - - def each - yield main - yield once - end - - def logger=(logger) - each { |loader| loader.logger = logger } - end - - def log! - each(&:log!) - end - - def self.for_gem - Gem.new.autoloader + class << self + extend Memoist + + def main + Main + end + memoize :main + + def gem + Gem + end + memoize :gem end end end - -require_relative "autoloaders/gem" diff --git a/lib/jets/autoloaders/gem.rb b/lib/jets/autoloaders/gem.rb index 2e1579d18..b3aa753e5 100644 --- a/lib/jets/autoloaders/gem.rb +++ b/lib/jets/autoloaders/gem.rb @@ -1,69 +1,58 @@ -require "zeitwerk" - module Jets class Autoloaders - class GemInflector < Zeitwerk::Inflector - def camelize(basename, _abspath) - map = { - cli: "CLI", - io: "IO", - version: "VERSION" - } - map[basename.to_sym] || super - end - end - # for jets gem itself class Gem - def autoloader - loader = Zeitwerk::Loader.new - loader.tag = "jets.gem" - loader.inflector = GemInflector.new - loader.push_dir(lib) - loader.do_not_eager_load(do_not_eager_load) - loader.ignore(ignore_paths) # loader.ignore requires full dir or path - # loader.log! - loader - end + class << self + extend Memoist - def lib - File.expand_path("#{__dir__}/../..") # jets/lib - end + def loader + loader = Zeitwerk::Loader.new + loader.tag = "jets.gem" + loader.inflector = Inflector.new + loader.push_dir(lib) + loader.do_not_eager_load(do_not_eager_load) + loader.ignore(ignore_paths) # loader.ignore requires full dir or path + # loader.log! + loader + end + memoize :loader + + def setup + loader.setup + end + + def lib + File.expand_path("#{__dir__}/../..") # jets/lib + end + + def do_not_eager_load + paths = %w[] + paths.map { |path| "#{lib}/#{path}" } # do_not_eager_load requires full dir or path + end - # For jets/info.rb see Jets::Info property Middleware for why we do not eager load. - def do_not_eager_load - paths = %w[ - jets/info.rb - jets/spec_helpers - jets/spec_helpers.rb - jets/commands - jets/commands.rb - jets/generators - jets/generators.rb - ] - paths.map { |path| "#{lib}/#{path}" } # do_not_eager_load requires full dir or path + def ignore_paths + # commands + paths = %w[ + jets/cli/generate/templates + jets/cli/init/templates + jets/core_ext + jets/core_ext.rb + jets/overrides + jets/shim/template + ] + paths.map { |path| "#{lib}/#{path}" } + end end - def ignore_paths - # commands - paths = %w[ - jets/application/dummy_config.rb - jets/application/dummy_erb_compiler.rb - jets/builders/rackup_wrappers - jets/builders/templates - jets/commands/templates - jets/controller/middleware/webpacker_setup.rb - jets/core_ext - jets/core_ext.rb - jets/generator - jets/overrides - jets/ruby_version_check.rb - jets/cli.rb - jets/commands.rb - jets/generators/jets/app/ignore - jets/tasks.rb - ] - paths.map { |path| "#{lib}/#{path}" } + class Inflector < Zeitwerk::Inflector + def camelize(basename, _abspath) + map = { + cli: "CLI", + io: "IO", + version: "VERSION" + } + map[basename.to_sym] || super + end end end end diff --git a/lib/jets/autoloaders/inflector.rb b/lib/jets/autoloaders/inflector.rb deleted file mode 100644 index f6944b4c5..000000000 --- a/lib/jets/autoloaders/inflector.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -require "active_support/inflector" - -module Jets - class Autoloaders - module Inflector # :nodoc: - # Concurrent::Map is not needed. This is a private class, and overrides - # must be defined while the application boots. - @overrides = {} - - def self.camelize(basename, _abspath) - @overrides[basename] || basename.camelize - end - - def self.inflect(overrides) - @overrides.merge!(overrides) - end - end - end -end diff --git a/lib/jets/autoloaders/main.rb b/lib/jets/autoloaders/main.rb new file mode 100644 index 000000000..ebfcb0de2 --- /dev/null +++ b/lib/jets/autoloaders/main.rb @@ -0,0 +1,64 @@ +module Jets + class Autoloaders + # For app code files. IE: app/events + class Main + class << self + extend Memoist + delegate :config, to: "Jets.project" + + def loader + loader = Zeitwerk::Loader.new + loader.tag = "jets.main" + loader + end + memoize :loader + + def configure(root = Jets.root) + all_loaders = Zeitwerk::Registry.loaders + already_configured_dirs = all_loaders.map(&:dirs).flatten + + full_path = proc { |path| "#{root}/#{path}" } + autoload_paths = config.autoload_paths.map(&full_path) + extension_paths = config.extension_paths.map(&full_path) + load_paths = autoload_paths + extension_paths + + load_paths.each do |path| + next if already_configured_dirs.include?(path) + next unless File.directory?(path) + loader.push_dir(path) + end + + ignore_paths = config.ignore_paths.map(&full_path) + ignore_paths.each do |path| + loader.ignore(path) + end + end + + # Call at end + def setup + loader.on_load do |cpath, value, abspath| + next unless abspath.include?("/extensions/") + if abspath.include?("/app") + Jets::Lambda::Functions.extend(value) + elsif abspath.include?("/shared") + Jets::Stack.extend(value) + end + end + + loader.setup + + # Eager load extensions first + full_path = proc { |path| "#{Jets.root}/#{path}" } + extension_paths = config.extension_paths.map(&full_path) + extension_paths.each do |path| + loader.eager_load_dir(path) if File.directory?(path) + end + + # Eager load rest of jets managed code. + # Needed for CloudFormation templates. + loader.eager_load + end + end + end + end +end diff --git a/lib/jets/aws_services.rb b/lib/jets/aws_services.rb index c1bd8679c..3e77afc24 100644 --- a/lib/jets/aws_services.rb +++ b/lib/jets/aws_services.rb @@ -1,13 +1,15 @@ require "aws-sdk-apigateway" require "aws-sdk-cloudformation" require "aws-sdk-cloudwatchlogs" +require "aws-sdk-codebuild" require "aws-sdk-dynamodb" require "aws-sdk-lambda" require "aws-sdk-s3" -require "aws-sdk-sts" -# Not used in Jets internally but convenient for shared resources require "aws-sdk-sns" require "aws-sdk-sqs" +require "aws-sdk-ssm" +require "aws-sdk-sts" +require "aws-sdk-wafv2" require "aws_mfa_secure/ext/aws" # add MFA support @@ -20,21 +22,27 @@ def apigateway end global_memoize :apigateway + def lambda_client + Aws::Lambda::Client.new(aws_options) + end + global_memoize :lambda_client + alias_method :aws_lambda, :lambda_client + def cfn Aws::CloudFormation::Client.new(aws_options) end global_memoize :cfn + def codebuild + Aws::CodeBuild::Client.new(aws_options) + end + global_memoize :codebuild + def dynamodb Aws::DynamoDB::Client.new(aws_options) end global_memoize :dynamodb - def aws_lambda - Aws::Lambda::Client.new(aws_options) - end - global_memoize :aws_lambda - def logs Aws::CloudWatchLogs::Client.new(aws_options) end @@ -55,16 +63,26 @@ def sns end global_memoize :sns - def sqs - Aws::SQS::Client.new(aws_options) + def ssm + Aws::SSM::Client.new(aws_options) end - global_memoize :sqs + global_memoize :ssm def sts Aws::STS::Client.new(aws_options) end global_memoize :sts + def sqs + Aws::SQS::Client.new(aws_options) + end + global_memoize :sqs + + def wafv2 + Aws::WAFV2::Client.new(aws_options) + end + global_memoize :wafv2 + # Override the AWS retry settings with Jets AWS clients. # # The aws-sdk-core has exponential backup with this formula: @@ -85,19 +103,20 @@ def aws_options options = { retry_limit: 7, # default: 3 retry_base_delay: 0.6, # default: 0.3 + log_level: :info } # See debug logger. Noisy. # Example: # D, [2022-12-02T13:18:55.298788 #26182] DEBUG -- : [Aws::APIGateway::Client 200 0.030837 0 retries] get_method(rest_api_id:"mke40eh6l0",resource_id:"zf8w2m",http_method:"GET") - options.merge!( - log_level: :debug, - logger: Logger.new($stdout), - ) if ENV['JETS_DEBUG_AWS_SDK'] + if ENV["JETS_DEBUG_AWS_SDK"] + options[:log_level] = :debug + options[:logger] = Logger.new($stdout) + end # https://docs.aws.amazon.com/sdk-for-ruby/v3/developer-guide/debugging.html to enable http_wire_trace # See the HTTP headers and JSON responses. Super noisy. - options.merge!( - http_wire_trace: true, - ) if ENV['JETS_DEBUG_AWS_SDK_HTTP_WIRE_TRACE'] + if ENV["JETS_DEBUG_AWS_SDK_HTTP_WIRE_TRACE"] + options[:http_wire_trace] = true + end options end end diff --git a/lib/jets/aws_services/aws_helpers.rb b/lib/jets/aws_services/aws_helpers.rb new file mode 100644 index 000000000..d5fc0016e --- /dev/null +++ b/lib/jets/aws_services/aws_helpers.rb @@ -0,0 +1,17 @@ +module Jets::AwsServices + module AwsHelpers # :nodoc: + include Jets::AwsServices + + def find_stack(stack_name) + resp = cfn.describe_stacks(stack_name: stack_name) + resp.stacks.first + rescue Aws::CloudFormation::Errors::ValidationError => e + # example: Stack with id demo-dev does not exist + if e.message =~ /Stack with/ && e.message =~ /does not exist/ + nil + else + raise + end + end + end +end diff --git a/lib/jets/aws_info.rb b/lib/jets/aws_services/aws_info.rb similarity index 82% rename from lib/jets/aws_info.rb rename to lib/jets/aws_services/aws_info.rb index 3a3485bef..54b437e6d 100644 --- a/lib/jets/aws_info.rb +++ b/lib/jets/aws_services/aws_info.rb @@ -1,21 +1,24 @@ - -module Jets +module Jets::AwsServices class AwsInfo extend Memoist - include AwsServices + include Jets::AwsServices def region - return 'us-east-1' if Jets.env.test? + return "us-east-1" if Jets.env.test? - return ENV['JETS_AWS_REGION'] if ENV['JETS_AWS_REGION'] # highest precedence - return ENV['AWS_REGION'] if ENV['AWS_REGION'] + return ENV["JETS_AWS_REGION"] if ENV["JETS_AWS_REGION"] # highest precedence + return ENV["AWS_REGION"] if ENV["AWS_REGION"] region = nil # First if aws binary is available # try to get it from the ~/.aws/config - if which('aws') - region = `aws configure get region 2>&1`.strip rescue nil + if which("aws") + region = begin + `aws configure get region 2>&1`.strip + rescue + nil + end exit_code = $?.exitstatus if exit_code != 0 exception_message = region.split("\n").grep(/botocore\.exceptions/).first @@ -28,7 +31,7 @@ def region puts "You can also get rid of this message by setting AWS_REGION or configuring ~/.aws/config with the region" region = nil end - region = nil if region == '' + region = nil if region == "" return region if region end @@ -37,22 +40,26 @@ def region begin az = `curl -s --max-time 3 --connect-timeout 5 http://169.254.169.254/latest/meta-data/placement/availability-zone` region = az.strip.chop # remove last char - region = nil if region == '' + region = nil if region == "" rescue end return region if region - 'us-east-1' # default if all else fails + "us-east-1" # default if all else fails end memoize :region + def jets_info + Jets::Core::Config::Info.instance + end + # aws sts get-caller-identity def account - return '123456789' if Jets.env.test? - return ENV['JETS_AWS_ACCOUNT'] if ENV['JETS_AWS_ACCOUNT'] + return "123456789" if Jets.env.test? + return jets_info.aws_account if jets_info.respond_to?(:aws_account) # ensure region set, required for sts.get_caller_identity.account to work - ENV['AWS_REGION'] ||= region + ENV["AWS_REGION"] ||= region begin sts.get_caller_identity.account rescue Aws::Errors::MissingCredentialsError, Aws::Errors::NoSuchEndpointError, Aws::STS::Errors::InvalidClientTokenId @@ -98,14 +105,14 @@ def s3_bucket # # source: https://stackoverflow.com/questions/2108727/which-in-ruby-checking-if-program-exists-in-path-from-ruby def which(cmd) - exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : [''] - ENV['PATH'].split(File::PATH_SEPARATOR).each do |path| + exts = ENV["PATHEXT"] ? ENV["PATHEXT"].split(";") : [""] + ENV["PATH"].split(File::PATH_SEPARATOR).each do |path| exts.each { |ext| exe = File.join(path, "#{cmd}#{ext}") return exe if File.executable?(exe) && !File.directory?(exe) } end - return nil + nil end end end diff --git a/lib/jets/aws_services/global_memoist.rb b/lib/jets/aws_services/global_memoist.rb index c47a6775e..727309b3b 100644 --- a/lib/jets/aws_services/global_memoist.rb +++ b/lib/jets/aws_services/global_memoist.rb @@ -21,15 +21,20 @@ # module Jets::AwsServices module GlobalMemoist + def reset_cache! + $__memo_methods = {} + end + extend self + def self.included(klass) klass.extend(Macros) end module Macros def global_memoize(*methods) - const_defined?(:"GlobalMemoist#{self.object_id}") ? - const_get(:"GlobalMemoist#{self.object_id}") : const_set(:"GlobalMemoist#{self.object_id}", Module.new) - mod = const_get(:"GlobalMemoist#{self.object_id}") + const_defined?(:"GlobalMemoist#{object_id}") ? + const_get(:"GlobalMemoist#{object_id}") : const_set(:"GlobalMemoist#{object_id}", Module.new) + mod = const_get(:"GlobalMemoist#{object_id}") mod.class_eval do methods.each do |method| @@ -54,4 +59,4 @@ def global_memoize_class_method(*methods) end end end -end \ No newline at end of file +end diff --git a/lib/jets/aws_services/s3_bucket.rb b/lib/jets/aws_services/s3_bucket.rb index c8577b771..3c6a079d6 100644 --- a/lib/jets/aws_services/s3_bucket.rb +++ b/lib/jets/aws_services/s3_bucket.rb @@ -19,16 +19,14 @@ def ensure_exists end def exists? - begin - s3.head_bucket(bucket: @name) - true - rescue Aws::S3::Errors::BucketAlreadyOwnedByYou, Aws::S3::Errors::Http301Error => e - # These exceptions indicate bucket already exists - # Aws::S3::Errors::Http301Error could be inaccurate but compromising for simplicity - true - rescue - false - end + s3.head_bucket(bucket: @name) + true + rescue Aws::S3::Errors::BucketAlreadyOwnedByYou, Aws::S3::Errors::Http301Error => e + # These exceptions indicate bucket already exists + # Aws::S3::Errors::Http301Error could be inaccurate but compromising for simplicity + true + rescue + false end end end diff --git a/lib/jets/aws_services/stack_status.rb b/lib/jets/aws_services/stack_status.rb index adf1e6042..2e5faf7ec 100644 --- a/lib/jets/aws_services/stack_status.rb +++ b/lib/jets/aws_services/stack_status.rb @@ -4,7 +4,7 @@ module StackStatus @@stack_exists_cache = [] # helps with CloudFormation rate limit def stack_exists?(stack_name) return false if Jets.env.test? - return true if ENV['JETS_NO_INTERNET'] + return true if ENV["JETS_NO_INTERNET"] return true if @@stack_exists_cache.include?(stack_name) exist = nil @@ -15,13 +15,13 @@ def stack_exists?(stack_name) @@stack_exists_cache << stack_name exist = true rescue Aws::CloudFormation::Errors::ValidationError => e - if e.message =~ /does not exist/ + if /does not exist/.match?(e.message) exist = false elsif e.message.include?("'stackName' failed to satisfy constraint") # Example of e.message when describe_stack with invalid stack name # "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:/._+]*" - puts "Invalid stack name: #{stack_name}" - puts "Full error message: #{e.message}" + log.info "Invalid stack name: #{stack_name}" + log.info "Full error message: #{e.message}" exit 1 else raise # re-raise exception because unsure what other errors can happen @@ -30,33 +30,7 @@ def stack_exists?(stack_name) exist end - # All CloudFormation states listed here: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-describing-stacks.html - # - # Returns resp so we can use it to grab data about the stack without calling api again. - def stack_in_progress?(stack_name) - return true if !stack_exists?(stack_name) - - # Assumes stack exists - resp = cfn.describe_stacks(stack_name: stack_name) - status = resp.stacks[0].stack_status - if status =~ /_IN_PROGRESS$/ - puts "The '#{stack_name}' stack status is #{status}. " \ - "Please wait until the stack is ready and try again.".color(:red) - exit 0 - elsif resp.stacks[0].outputs.empty? && status != 'ROLLBACK_COMPLETE' - # This Happens when the miminal stack fails at the very beginning. - # There is no s3 bucket at all. User should delete the stack. - puts "The minimal stack failed to create. Please delete the stack first and try again. " \ - "You can delete the CloudFormation stack or use the `jets delete` command" - exit 0 - else - true - end - end - - # Lookup output value. - # Used in Jets::Cfn::Resource::ApiGateway::RestApi::* andJets::Commands::Url - def lookup(outputs, key) + def output_value(outputs, key) out = outputs.find { |o| o.output_key == key } out&.output_value end diff --git a/lib/jets/backtrace_cleaner.rb b/lib/jets/backtrace_cleaner.rb deleted file mode 100644 index 9e223decb..000000000 --- a/lib/jets/backtrace_cleaner.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -require "active_support/backtrace_cleaner" -require "active_support/core_ext/string/access" - -module Jets - class BacktraceCleaner < ActiveSupport::BacktraceCleaner - APP_DIRS_PATTERN = /\A(?:\.\/)?(?:app|config|lib|test|\(\w*\))/ - RENDER_TEMPLATE_PATTERN = /:in `.*_\w+_{2,3}\d+_\d+'/ - - def initialize - super - @root = "#{Jets.root}/" - add_filter do |line| - line.start_with?(@root) ? line.from(@root.size) : line - end - add_filter do |line| - if RENDER_TEMPLATE_PATTERN.match?(line) - line.sub(RENDER_TEMPLATE_PATTERN, "") - else - line - end - end - add_silencer { |line| !APP_DIRS_PATTERN.match?(line) } - end - end -end diff --git a/lib/jets/booter.rb b/lib/jets/booter.rb deleted file mode 100644 index a1ca92690..000000000 --- a/lib/jets/booter.rb +++ /dev/null @@ -1,78 +0,0 @@ -require "jets/core_ext/kernel" # Hack prevents Jets const from being defined - -class Jets::Booter - class << self - @booted = false - def boot! - return if @booted - confirm_jets_project! - - # Registration phase - Jets::Bundle.require # engine config and register initializers - load_internal_engine # internal engine initializers - require_application! # override with user customizations - - # Run phase - Jets.application.initialize! - @booted = true - end - - def require_application! - require ENGINE_PATH if defined?(ENGINE_PATH) - - if defined?(APP_PATH) - check_old_jets_code! - require APP_PATH - else - require "#{Jets.root}/config/application" - end - end - - def load_internal_engine - require File.expand_path("../../engines/internal/lib/internal/engine.rb", __dir__) - end - - # All Turbines - def app_initializers - Dir.glob("#{Jets.root}/config/initializers/**/*").sort.each do |path| - load path - end - end - - # Cannot call this for the jets new - # We check that within a jets project again as Jets.boot - # Also checked in the jets/cli.rb more generally - def confirm_jets_project! - return if defined?(ENGINE_PATH) - unless File.exist?("#{Jets.root}/config/application.rb") - puts "It does not look like you are running this command within a jets project. Please confirm that you are in a jets project and try again.".color(:red) - exit 1 - end - end - - def check_old_jets_code! - application_rb = "#{Jets.root}/config/application.rb" - return unless File.exist?(application_rb) - return if File.read(application_rb).include?(' < Jets::Application') - puts "ERROR: Based on config/application.rb, it looks like your app code was written with an older version of Jets.".color(:red) - puts "Please install and run the jets-upgrade command to upgrade your project." - puts " jets-upgrade go".color(:green) - exit 1 - end - - def message - "Jets booting up in #{Jets.env.color(:green)} mode!" - end - - def check_config_ru! - config_ru = File.read("#{Jets.root}/config.ru") - unless config_ru.include?("Jets.boot") - puts 'The config.ru file is missing Jets.boot. Please add Jets.boot after require "jets"'.color(:red) - puts "This was changed as made in Jets v1.1.0." - puts "To have Jets update the config.fu file for you, you can run:\n\n" - puts " jets-upgrade" - exit 1 - end - end - end -end diff --git a/lib/jets/builders/code_builder.rb b/lib/jets/builders/code_builder.rb deleted file mode 100644 index 5b58c964d..000000000 --- a/lib/jets/builders/code_builder.rb +++ /dev/null @@ -1,352 +0,0 @@ -require "action_view" -require "fileutils" -require "net/http" -require "open-uri" -require "socket" - -# Some important folders to help understand how jets builds a project: -# -# /tmp/jets: build root where different jets projects get built. -# /tmp/jets/project: each jets project gets built in a different subdirectory. -# -# The rest of the folders are subfolders under /tmp/jets/project. -# -module Jets::Builders - class CodeBuilder - include Jets::AwsServices - include Util - extend Memoist - - attr_reader :full_project_path - def initialize - # Expanding to the full path and capture now. - # Dir.chdir gets called later and we'll lose this info. - @full_project_path = File.expand_path(Jets.root) + "/" - @version_purger = Purger.new - end - - def build - @version_purger.purge - cache_check_message - - clean_start - assets_precompile - run_webpack # easier to do before we copy the project because node and yarn has been likely setup in the that dir - copy_project - copy_ruby_version_file - Dir.chdir("#{stage_area}/code") do - # These commands run from project root - code_setup - package_ruby - code_finish - end - end - - # Resolves the chicken-and-egg problem with md5 checksums. The handlers need - # to reference files with the md5 checksum. The files are the: - # - # jets/code/rack-checksum.zip - # jets/code/opt-checksum.zip - # - # We compute the checksums before we generate the node shim handlers. - def calculate_md5s - Md5.compute! # populates Md5.checksums hash - end - - def generate_shims - headline "Generating shims in the handlers folder." - # Crucial that the Dir.pwd is in the tmp_code because for - # Jets::Builders::app_files because Jets.boot set ups - # autoload_paths and this is how project classes are loaded. - Jets::Builders::HandlerGenerator.build! - end - - def create_zip_files - folders = Md5.stage_folders - # Md5.stage_folders ["stage/bundled", "stage/code"] - folders.each do |folder| - zip = Md5Zip.new(folder) - if exist_on_s3?(zip.md5_name) - puts "Already exists: s3://#{s3_bucket}/jets/code/#{zip.md5_name}" - else - zip = Md5Zip.new(folder) - zip.create - end - end - end - - def exist_on_s3?(filename) - return false if ENV['JETS_NO_INTERNET'] - s3_key = "jets/code/#{filename}" - begin - Jets.logger.debug "Checking s3://#{s3_bucket}/#{s3_key}" - s3.head_object(bucket: s3_bucket, key: s3_key) - true - rescue Aws::S3::Errors::NotFound, Aws::S3::Errors::Forbidden - false - end - end - - def code_setup - reconfigure_development_webpacker - end - - def code_finish - # Reconfigure code - store_s3_base_url - disable_webpacker_middleware - - # Code prep and zipping - check_code_size! - calculate_md5s # must be called before create_zip_files and generate_shims because checksums need to be populated - # generate_shims and create_zip_files use checksums - # - # Notes: - # - # Had moved calculate_md5s to fix a what thought was a subtle issue https://github.com/tongueroo/jets/pull/424 - # But am unsure about that the fix now. This essentially reverts that issue. - # - # Fix in https://github.com/tongueroo/jets/pull/459 - # - generate_shims # the generated handlers/data.yml has rack_zip key - create_zip_files - end - - def check_code_size! - CodeSize.check! - end - - # Thanks https://stackoverflow.com/questions/9354595/recursively-getting-the-size-of-a-directory - # Seems to overestimate a little bit but close enough. - def dir_size(folder) - Dir.glob(File.join(folder, '**', '*')) - .select { |f| File.file?(f) } - .map{ |f| File.size(f) } - .inject(:+) - end - - # Store s3 base url is needed for asset serving from s3 later. Need to package this - # as part of the code so we have a reference to it. - # At this point the minimal stack exists, so we can grab it with the AWS API. - # We do not want to grab this as part of the live request because it is slow. - def store_s3_base_url - return if Jets.config.mode == "job" - return unless gemfile_include?("sprockets-jets") - write_s3_base_url("#{stage_area}/code/config/s3_base_url.txt") - end - - def write_s3_base_url(full_path) - FileUtils.mkdir_p(File.dirname(full_path)) - IO.write(full_path, s3_base_url) - end - - def s3_base_url - # Allow user to set assets.base_url - # - # Jets.application.configure do - # config.assets.base_url = "https://cloudfront.com/my/base/path" - # end - # - return Jets.config.assets.base_url if Jets.config.assets.base_url - - # Note: subdomain form works with CORs but the subfolder form does not. Using subfolder form. - region = Jets.aws.region - asset_base_url = region == 'us-east-1' ? - "https://#{s3_bucket}.s3.amazonaws.com/jets" : - "https://#{s3_bucket}.s3-#{region}.amazonaws.com/jets" - end - - def s3_bucket - Jets.aws.s3_bucket - end - - def disable_webpacker_middleware - full_path = "#{"#{stage_area}/code"}/config/disable-webpacker-middleware.txt" - FileUtils.mkdir_p(File.dirname(full_path)) - FileUtils.touch(full_path) - end - - def assets_precompile - return if skip_assets? - return unless gemfile_include?("sprockets-jets") - sh "jets assets:precompile" - end - - # This happens in the current app directory not the tmp code for simplicity. - # This is because the node and yarn has likely been set up correctly there. - def run_webpack - return if skip_assets? - return unless gemfile_include?("jetpacker") - - headline "Compling assets in current project directory" - sh("yarn install") - - ENV['WEBPACKER_ASSET_HOST'] = asset_host if Jets.config.assets.base_url - webpack_command = File.exist?("#{Jets.root}/bin/webpack") ? - "bin/webpack" : - `which webpack`.strip - sh "JETS_ENV=#{Jets.env} #{webpack_command}" - end - - def skip_assets? - if ENV['JETS_SKIP_ASSETS'] - puts "Skip compiling assets".color(:yellow) # useful for debugging - return true - end - Jets.config.mode == "job" - end - - # Different url for these. Examples: - # - # asset_host https://demo-dev-s3bucket-lw5vq7ht8ip4.s3.us-west-2.amazonaws.com/jets/public/packs/media/images/boltops-0dd1c6bd.png - # s3_base_url https://s3-us-west-2.amazonaws.com/demo-dev-s3bucket-lw5vq7ht8ip4/jets/packs/media/images/boltops-0dd1c6bd.png - # - # Interesting: asset_host works but s3_base_url does not for CORs. IE: reactjs or vuejs requests - # Thinking AWS configures the non-subdomain url endpoint to be more restrictive. - # - # Allow user to set assets.asset_host - # - # Jets.application.configure do - # config.assets.asset_host = "https://cloudfront.com/my/base/path" - # end - # - def asset_host - assets = Jets.config.assets - return assets.base_url if assets.base_url && assets.base_url != "s3_endpoint" - - # By default, will use the s3 url endpoint directly by convention - region = Jets.aws.region - asset_base_url = region == 'us-east-1' ? - "https://#{s3_bucket}.s3.amazonaws.com" : - "https://#{s3_bucket}.s3.#{region}.amazonaws.com" - - "#{asset_base_url}/jets/public" # s3_base_url - end - - def gemfile_include?(name) - # Old code, leaving around for now: - # Thanks: https://stackoverflow.com/questions/4195735/get-list-of-gems-being-used-by-a-bundler-project - # webpacker_loaded = Gem.loaded_specs.keys.include?("webpacker") - # return unless webpacker_loaded - - # Checking this way because when using jets standalone for Afterburner mode we don't want to run into - # bundler gem collisions. TODO: figure out the a better way to handle the collisions. - lines = IO.readlines("#{Jets.root}/Gemfile") - lines.detect { |l| l =~ /gem ['"]#{name}/ && l !~ /^\s*?#/ } - end - - # Cleans out non-cached files like code-*.zip in Jets.build_root - # for a clean start. Also ensure that the /tmp/jets/project build root exists. - # - # Most files are kept around after the build process for inspection and - # debugging. So we have to clean out the files. But we only want to clean out - # some of the files. - def clean_start - Dir.glob("#{Jets.build_root}/code/code-*.zip").each { |f| FileUtils.rm_f(f) } - FileUtils.mkdir_p(Jets.build_root) # /tmp/jets/demo - end - - # Copy project into temporary directory. Do this so we can keep the project - # directory untouched and we can also remove a bunch of unnecessary files like - # logs before zipping it up. - def copy_project - headline "Copying current project directory to temporary build area: #{"#{stage_area}/code"}" - FileUtils.rm_rf("#{build_area}/stage") # clear out from previous build's stage area - FileUtils.mkdir_p("#{build_area}/stage") - FileUtils.rm_rf("#{stage_area}/code") # remove current code folder - move_node_modules(Jets.root, Jets.build_root) - begin - Jets::Util.cp_r(@full_project_path, "#{stage_area}/code") - ensure - move_node_modules(Jets.build_root, Jets.root) # move node_modules directory back - end - end - - # Move the node modules to the tmp build folder to speed up project copying. - # A little bit risky because a ctrl-c in the middle of the project copying - # results in a missing node_modules but user can easily rebuild that. - # - # Tesing shows 6.623413 vs 0.027754 speed improvement. - def move_node_modules(source_folder, dest_folder) - source = "#{source_folder}/node_modules" - dest = "#{dest_folder}/node_modules" - if File.exist?(source) - FileUtils.mv(source, dest) - end - end - - # Bit hacky but this saves the user from accidentally forgetting to change this - # when they deploy a jets project in development mode - def reconfigure_development_webpacker - return unless Jets.env.development? - return unless gemfile_include?("jetpacker") - headline "Reconfiguring webpacker development settings for AWS Lambda." - - webpacker_yml = "#{"#{stage_area}/code"}/config/webpacker.yml" - return unless File.exist?(webpacker_yml) - - config = Jets::Util::Yamler.load_file(webpacker_yml) - config["development"]["compile"] = false # force this to be false for deployment - new_yaml = YAML.dump(config) - IO.write(webpacker_yml, new_yaml) - end - - def ruby_packager - RubyPackager.new(tmp_code) - end - memoize :ruby_packager - - def rack_packager - RackPackager.new("#{tmp_code}/rack") - end - memoize :rack_packager - - def package_ruby - if ENV['JETS_SKIP_PACKAGE'] - puts "Skip packaging ruby".color(:yellow) # useful for developing handlers - return - end - return unless Jets.gem_layer? - - check_agree - ruby_packager.install - rack_packager.install - ruby_packager.finish # by this time we have a /tmp/jets/demo/stage/code/vendor/gems - rack_packager.finish - - build_lambda_layer - end - - def check_agree - agree = Jets::Api::Agree.new - agree.prompt - end - - def build_lambda_layer - return unless Jets.gem_layer? - lambda_layer = LambdaLayer.new - lambda_layer.build - end - - def cache_check_message - if File.exist?("#{Jets.build_root}/cache") - puts "The #{Jets.build_root}/cache folder exists. Incrementally re-building the jets using the cache. To clear the cache: rm -rf #{Jets.build_root}/cache" - end - end - - def copy_ruby_version_file - ruby_version_path = Jets.root.join(".ruby-version") - return unless File.exists?(ruby_version_path) - FileUtils.cp_r(ruby_version_path, build_area) - end - - # Group all the path settings together here - def self.tmp_code - Jets::Cfn::Builder.tmp_code - end - - def tmp_code - self.class.tmp_code - end - end -end diff --git a/lib/jets/builders/code_size.rb b/lib/jets/builders/code_size.rb deleted file mode 100644 index fd976c963..000000000 --- a/lib/jets/builders/code_size.rb +++ /dev/null @@ -1,58 +0,0 @@ -module Jets::Builders - class CodeSize - LAMBDA_SIZE_LIMIT = 250 # Total lambda limit is 250MB - include Util - - def self.check! - new.check - end - - def check - return if within_lambda_limit? - say "Over the Lambda size limit of #{LAMBDA_SIZE_LIMIT}MB".color(:red) - say "Please reduce the size of your code." - display_sizes - exit 1 - end - - def within_lambda_limit? - total_size < LAMBDA_SIZE_LIMIT * 1024 # 120MB - end - - def total_size - code_size = compute_size("#{stage_area}/code") - opt_size = compute_size("#{stage_area}/opt") - opt_size + code_size # total_size - end - - def display_sizes - code_size = compute_size("#{stage_area}/code") - opt_size = compute_size("#{stage_area}/opt") - total_size = opt_size + code_size - overlimit = (LAMBDA_SIZE_LIMIT * 1024 - total_size) * -1 - say "Sizes:" - say "Code: #{megabytes(code_size)} - #{stage_area}/code" - say "Gem Layer: #{megabytes(opt_size)} - #{stage_area}/opt" - say "Total Package: #{megabytes(total_size)}" - say "Over limit by: #{megabytes(overlimit)}" - say "Sometimes blowing away the /tmp/jets cache will reduce the size: rm -rf /tmp/jets" - # sh "du -kcsh #{stage_area}/*" unless Jets.env.test? # uncomment to debug - end - - def compute_size(path) - return 0 unless File.exist?(path) - # -k option is required for macosx but not for linux - out = `du -ks #{path}` - out.split(' ').first.to_i # bytes - end - - def megabytes(bytes) - n = bytes / 1024.0 - sprintf('%.1f', n) + 'MB' - end - - def say(message) - puts message unless Jets.env.test? - end - end -end diff --git a/lib/jets/builders/gem_replacer.rb b/lib/jets/builders/gem_replacer.rb deleted file mode 100644 index d28402ecd..000000000 --- a/lib/jets/builders/gem_replacer.rb +++ /dev/null @@ -1,83 +0,0 @@ -module Jets::Builders - class GemReplacer - extend Memoist - include Util - - def initialize(options) - @options = options - end - - def run - # use_gemspec to resolve http-parser gem issue - check = Jets::Api::Gems::Check.new(use_gemspec: true) - if Jets.config.lambda.layers.empty? - found_gems = check.run! # exits early if missing gems found - else - # assumes missing gems are in the provided custom layer by the user - found_gems = check.run # does not exist early - end - - # found gems will only have gems that were found - found_gems.each do |gem_name| - gem_extractor = Jets::Api::Gems::Extract::Gem.new(gem_name, @options) - gem_extractor.run - rename_gem(gem_name) - end - - tidy - end - - def rename_gem(gem_name) - ruby_folder = "#{Jets.build_root}/stage/opt/ruby/gems/#{Jets::Api::Gems.ruby_folder}" - gems_folder = "#{ruby_folder}/gems" - expr = "#{gems_folder}/#{gem_name}-x*-{darwin,linux}" - src = Dir.glob(expr).first - return unless src - - dest = src.sub("-darwin", "-linux") - FileUtils.mv(src, dest) unless File.exist?(dest) # looks like rename_gem actually runs twice - end - - def ruby_folder - Jets::Api::Gems.ruby_folder - end - - # remove unnecessary files to reduce package size - def tidy - # project_root: /tmp/jets/demo/stage/code/ - # /tmp/jets/demo/stage/code/bundled - tidy_gems("#{@options[:project_root]}/ruby/gems/#{ruby_folder}/*/gems/*") - tidy_gems("#{@options[:project_root]}/ruby/gems/#{ruby_folder}/*/bundler/gems/*") - end - - def tidy_gems(gems_path) - Dir.glob(gems_path).each do |gem_path| - tidy_gem(gem_path) - end - end - - # Clean up some unneeded files to try to keep the package size down - # In a generated jets app this made a decent 9% difference: - # 175M test2 - # 191M test3 - def tidy_gem(path) - # remove top level tests and cache folders - Dir.glob("#{path}/*").each do |path| - next unless File.directory?(path) - folder = File.basename(path) - if %w[test tests spec features benchmark cache doc].include?(folder) - FileUtils.rm_rf(path) - end - end - - Dir.glob("#{path}/**/*").each do |path| - next unless File.file?(path) - ext = File.extname(path) - if %w[.rdoc .md .markdown].include?(ext) or - path =~ /LICENSE|CHANGELOG|README/ - FileUtils.rm_f(path) - end - end - end - end -end diff --git a/lib/jets/builders/handler_generator.rb b/lib/jets/builders/handler_generator.rb deleted file mode 100644 index f3a2d4b60..000000000 --- a/lib/jets/builders/handler_generator.rb +++ /dev/null @@ -1,214 +0,0 @@ -require "fileutils" -require "erb" - -# Example: -# -# Jets::Builders::HandlerGenerator.new( -# "PostsController", -# :create, :update -# ) -module Jets::Builders - class HandlerGenerator - def self.build! - new.build - end - - def build - generate_data_yaml - app_ruby_shims - poly_shims - shared_shims - internal_shims - end - - # The handlers/data.yml is used by the shims - def generate_data_yaml - vars = Jets::Builders::ShimVars::Base.new - data = { - "s3_bucket" => vars.s3_bucket - } - data["rack_zip"] = vars.rack_zip if vars.rack_zip - - content = YAML.dump(data) - path = "#{tmp_code}/handlers/data.yml" - FileUtils.mkdir_p(File.dirname(path)) - IO.write(path, content) - end - - def app_ruby_shims - app_files.each do |path| - # Generates one shim for each app class: controller, job, etc - vars = Jets::Builders::ShimVars::App.new(path) - if path.include?('app/functions') - copy_simple_function(path) - else - generate_handler(vars) - end - end - - if app_files.empty? && Jets.gem_layer? - # When there are no app files, still need to generate a shim for JetsController - lib = File.expand_path('../../..', __dir__) - path = "#{lib}/engines/internal/app/controllers/jets/public_controller.rb" - vars = Jets::Builders::ShimVars::App.new(path) - generate_handler(vars) - end - end - - def generate_handler(vars) - template = handler_template(vars) - result = evaluate_template(template, vars) - dest = "#{tmp_code}/#{vars.dest_path}" - - # Dont generate handles for all controllers if one_lambda_for_all_controllers - # Instead generate the same handler and use it. Writing to the same file - # multiple times but this is a simple way to implement this. - # It also keeps the logical in one spot. - if Jets.config.cfn.build.controllers == 'one_lambda_for_all_controllers' && - dest.include?('handlers/controllers') - dest = "handlers/controller.rb" - end - - FileUtils.mkdir_p(File.dirname(dest)) - IO.write(dest, result) - end - - def handler_template(vars) - if vars.process_type == "controller" - Jets.config.cfn.build.controllers + ".rb" # IE: templates/handlers/one_lambda_for_all_controllers.rb - else # job, rule, etc - "one_lambda_per_method.rb" - end - end - - # source_path: app/functions/simple.rb - def copy_simple_function(source_path) - # Handler: handlers/controllers/posts_controller.handle - dest_path = source_path.sub('app/functions', 'handlers/functions') - FileUtils.mkdir_p(File.dirname(dest_path)) - FileUtils.cp(source_path, dest_path) - end - - def app_files - Jets::Cfn::Builder.app_files - end - - def poly_shims - missing = [] - - app_files.each do |path| - vars = Jets::Builders::ShimVars::App.new(path) - poly_tasks = vars.klass.definitions.select { |d| d.lang != :ruby } - poly_tasks.each do |task| - source_path = get_source_path(path, task) - if File.exist?(source_path) - native_function(path, task) - else - missing << source_path - end - end - - unless missing.empty? - puts "ERROR: Missing source files. Please make sure these source files exist or remove their declarations".color(:red) - puts missing - exit 1 - end - end - end - - def shared_shims - Jets::Stack.subclasses.each do |subclass| - subclass.functions.each do |fun| - if fun.lang.to_s == "ruby" - shared_ruby_shim(fun) - else - copy_source_as_handler(fun) - end - end - end - end - - def internal_shims - jets_base_path if Jets.custom_domain? - s3_bucket_config if Jets.s3_events? - end - - def jets_base_path - copy_function_template("functions/jets/base_path.rb", stage_name: Jets::Cfn::Resource::ApiGateway::Deployment.stage_name) - copy_function_template("functions/jets/base_path_mapping.rb") - end - - def s3_bucket_config - copy_function_template("shared/functions/jets/s3_bucket_config.rb") - end - - # Copy code from internal folder to materialized app code - def copy_function_template(path, vars={}) - engines_internal_path = File.expand_path("../../../engines/internal", __dir__) - src = "#{engines_internal_path}/app/#{path}" - result = Jets::Erb.result(src, vars) - dest = "#{tmp_code}/handlers/#{path}" - FileUtils.mkdir_p(File.dirname(dest)) - IO.write(dest, result) - end - - # app/shared/functions/kevin.py => /tmp/jets/demo/app_root/handlers/shared/functions/kevin.py - def copy_source_as_handler(fun) - return if fun.internal? - - source_path = fun.source_file - unless source_path - attributes = fun.template.values.first - function_name = attributes[:Properties][:FunctionName] - puts "WARN: missing source file for: '#{function_name}' function".color(:yellow) - return - end - - dest_path = "#{tmp_code}/#{fun.handler_dest}" - FileUtils.mkdir_p(File.dirname(dest_path)) - FileUtils.cp(source_path, dest_path) - end - - def get_source_path(original_path, task) - folder = original_path.sub(/\.rb$/,'') - lang_folder = "#{folder}/#{task.lang}" - root = Jets.root unless original_path.include?("lib/jets/internal") - "#{root}/#{lang_folder}/#{task.meth}#{task.lang_ext}" - end - - # Builds and copies over the native source code: python or node - def native_function(original_path, task) - source_path = get_source_path(original_path, task) - # Handler: handlers/controllers/posts_controller.handle - dest_path = "#{tmp_code}/#{task.handler_path}" - FileUtils.mkdir_p(File.dirname(dest_path)) - FileUtils.cp(source_path, dest_path) - end - - def shared_ruby_shim(fun) - # Cant use native_function because that requires task. Just re-implement - dest_path = fun.handler_dest - source_path = dest_path.sub(/^handlers/,'app') - FileUtils.mkdir_p(File.dirname(dest_path)) - FileUtils.cp(source_path, dest_path) - end - - def common_base_shim - vars = Jets::Builders::ShimVars::Base.new - result = evaluate_template("shim.js", vars) - dest = "#{tmp_code}/handlers/shim.js" - FileUtils.mkdir_p(File.dirname(dest)) - IO.write(dest, result) - end - - def evaluate_template(template_file, vars) - template_path = File.expand_path("../templates/handlers/#{template_file}", __FILE__) - Jets::Erb.result(template_path, vars: vars) - end - - # TODO: move CodeBuilder.tmp_code to a common level for HandlerGenerator and CodeBuilder - def tmp_code - "#{Jets.build_root}/#{CodeBuilder.tmp_code}" - end - end -end diff --git a/lib/jets/builders/lambda_layer.rb b/lib/jets/builders/lambda_layer.rb deleted file mode 100644 index 78c72fead..000000000 --- a/lib/jets/builders/lambda_layer.rb +++ /dev/null @@ -1,63 +0,0 @@ -module Jets::Builders - class LambdaLayer - include Util - - # At this point gems are in the stage/code and stage/rack folders still. - # We consolidate all gems to stage/opt. - # Then replace the binary gems. - def build - consolidate_gems_to_opt - replace_compiled_gems unless Jets.config.pro.disable - end - - # Also restructure the folder from: - # vendor/gems/ruby/2.5.0 - # To: - # ruby/gems/2.5.0 - # - # For Lambda Layer structure - def consolidate_gems_to_opt - src = "#{stage_area}/code/vendor/gems/ruby/#{Jets.ruby_folder}" - dest = "#{stage_area}/opt/ruby/gems/#{Jets.ruby_folder}" - rsync_and_link(src, dest) - end - - def rsync_and_link(src, dest) - FileUtils.mkdir_p(dest) - # Trailing slashes are required - sh "rsync -a --links #{src}/ #{dest}/" - - FileUtils.rm_rf(src) # blow away original 2.5.0 folder - - # Create symlink that will point to the gems in the Lambda Layer: - # stage/opt/ruby/gems/2.5.0 -> /opt/ruby/gems/2.5.0 - FileUtils.ln_sf("/opt/ruby/gems/#{Jets::Api::Gems.ruby_folder}", src) - end - - # replace_compiled_gems: - # remove binary gems in vendor/gems/ruby/2.5.0 - # extract binary gems in opt/ruby/gems/2.5.0 - # move binary gems from opt/ruby/gems/2.5.0 to vendor/gems/ruby/2.5.0 - # - # After this point, gems have been replace in stage/code/vendor/gems with their - # binary extensions: a good state. This method moves these gems to the Lambda - # Layers structure and creates a symlinks to it. First: - # - # from stage/code/vendor/gems/ruby/2.5.0 - # to stage/opt/ruby/gems/2.5.0 - # - # Then: - # - # stage/code/vendor/gems/ruby/2.5.0 -> /opt/ruby/gems/2.5.0 - # - def replace_compiled_gems - project_root = "#{stage_area}/opt" - headline "Replacing compiled gems with AWS Lambda Linux compiled versions: #{project_root}" - options = { - build_root: cache_area, # used in jets-gems - project_root: project_root, # used in gem_replacer and jets-gems - } - GemReplacer.new(options).run - end - end -end diff --git a/lib/jets/builders/md5.rb b/lib/jets/builders/md5.rb deleted file mode 100644 index 18b7a129f..000000000 --- a/lib/jets/builders/md5.rb +++ /dev/null @@ -1,60 +0,0 @@ -require 'digest' - -# Resolves the chicken-and-egg problem with md5 checksums. The handlers need -# to reference files with the md5 checksum. The files are the: -# -# jets/code/rack-checksum.zip -# jets/code/opt-checksum.zip -# -# We compute the checksums before we generate the node shim handlers. -module Jets::Builders - class Md5 - class << self - @@checksums = {} - def checksums - @@checksums - end - - def stage_folders - paths = [] - paths << "stage/opt" if folder_exist?("opt") - paths << "stage/rack" if folder_exist?("rack") - # Important to have stage/code at the end, since its md5 checksum depends - # on the previous folders. - paths << "stage/code" - paths - end - - def folder_exist?(folder) - path = "#{Jets.build_root}/stage/#{folder}" - File.directory?(path) - end - - def compute! - stage_folders.each do |path| - @@checksums[path] = dir(path) - end - @@checksums - end - - def dir(short_path) - path = "#{Jets.build_root}/#{short_path}" - files = Dir["#{path}/**/*"] - files = files.reject { |f| File.directory?(f) } - .reject { |f| File.symlink?(f) } - content = files.map do |f| - Digest::MD5.file(f).to_s[0..7] - end.join - - # The stage/code md5 sha depends on the other 'symlinked' folders. - if short_path == "stage/code" - content += @@checksums.values.join - end - - md5 = Digest::MD5.new - md5.update(content) - md5.hexdigest.to_s[0..7] - end - end - end -end diff --git a/lib/jets/builders/md5_zip.rb b/lib/jets/builders/md5_zip.rb deleted file mode 100644 index bc4658d71..000000000 --- a/lib/jets/builders/md5_zip.rb +++ /dev/null @@ -1,61 +0,0 @@ -require "active_support/number_helper" - -# Examples: -# -# zip = Jets::Builders::Md5Zip.new("/tmp/jets/demo/stage/code") -# zip.create -# -# zip = Jets::Builders::Md5Zip.new("/tmp/jets/demo/stage/bundled") -# zip.create -# -module Jets::Builders - class Md5Zip - include ActiveSupport::NumberHelper # number_to_human_size - include Util - - def initialize(folder) - @path = "#{Jets.build_root}/#{folder}" - @checksum = Md5.checksums[folder] - end - - def create - headline "Creating zip file for #{@path}" - # => Creating zip file for /tmp/jets/demo/stage/bundled - - # https://serverfault.com/questions/265675/how-can-i-zip-compress-a-symlink - command = "cd #{@path} && chmod -R 755 . && zip --symlinks -rq #{zip_file} ." - sh(command) - # move out of the lower folder to the stage folder - # mv /tmp/jets/demo/stage/code/code.zip /tmp/jets/demo/stage/code.zip - FileUtils.mkdir_p(File.dirname(zip_dest)) - FileUtils.mv("#{@path}/#{zip_file}", zip_dest) - - # mv /tmp/jets/demo/stage/zips/code.zip /tmp/jets/demo/stage/zips/code-a8a604aa.zip - FileUtils.mv(zip_dest, md5_dest) - - file_size = number_to_human_size(File.size(md5_dest)) - puts "Zip file created at: #{md5_dest.color(:green)} (#{file_size})" - end - - # /tmp/jets/demo/stage/zips/code.zip - def zip_dest - stage_area, filename = File.dirname(@path), File.basename(@path) - zip_area = stage_area + '/zips' # /tmp/jets/demo/stage/zips - zip_file = filename + '.zip' # code.zip - "#{zip_area}/#{zip_file}" # /tmp/jets/demo/stage/zips/code.zip - end - - def zip_file - File.basename(zip_dest) - end - - # /tmp/jets/demo/stage/zips/code-SHA.zip - def md5_dest - zip_dest.sub(".zip", "-#{@checksum}.zip") - end - - def md5_name - File.basename(md5_dest) - end - end -end \ No newline at end of file diff --git a/lib/jets/builders/purger.rb b/lib/jets/builders/purger.rb deleted file mode 100644 index 677cde145..000000000 --- a/lib/jets/builders/purger.rb +++ /dev/null @@ -1,35 +0,0 @@ -# When upgrading jets, automatically rm -rf /tmp/jets/project in case the structure has changed. -module Jets::Builders - class Purger - def initialize - @project_name = Jets.project_name - @version_file = "/tmp/jets/#{@project_name}/jets_version.txt" - end - - def purge - if version_changed? - last_version = @last_version || "unknown" - puts "The jets version has changed enough since the last build to merit refreshing the build cache." - puts "Current jets version: #{Jets::VERSION} Last built jets version: #{last_version}" - puts "Removing /tmp/jets/#{@project_name} to start fresh." - FileUtils.rm_rf("/tmp/jets/#{@project_name}") - end - write_version - end - - # When jets changes versions major or minor version consider it a big enough can to purge the cache - def version_changed? - return true unless File.exist?(@version_file) - - @last_version = IO.read(@version_file).strip - last_major, last_minor, _ = @last_version.split('.') - current_major, current_minor, _ = Jets::VERSION.split('.') - last_major != current_major || last_minor != current_minor - end - - def write_version - FileUtils.mkdir_p(File.dirname(@version_file)) - IO.write(@version_file, Jets::VERSION) - end - end -end \ No newline at end of file diff --git a/lib/jets/builders/rack_packager.rb b/lib/jets/builders/rack_packager.rb deleted file mode 100644 index 736db0c4b..000000000 --- a/lib/jets/builders/rack_packager.rb +++ /dev/null @@ -1,51 +0,0 @@ -module Jets::Builders - class RackPackager < RubyPackager - def finish - return unless gemfile_exist? - - super - - symlink_gems - copy_rackup_wrappers - rack_symlink - end - - def symlink_gems - ruby_folder = Jets::Api::Gems.ruby_folder - # IE: @full_app_root: /tmp/jets/demo/stage/code/rack - dest = "#{@full_app_root}/vendor/gems/ruby/#{ruby_folder}" - FileUtils.mkdir_p(File.dirname(dest)) - FileUtils.ln_sf("/opt/ruby/gems/#{ruby_folder}", dest) - end - - def copy_rackup_wrappers - # IE: @full_app_root: /tmp/jets/demo/stage/code/rack - rack_bin = "#{@full_app_root}/bin" - %w[rackup rackup.rb].each do |file| - src = File.expand_path("./rackup_wrappers/#{file}", File.dirname(__FILE__)) - dest = "#{rack_bin}/#{file}" - FileUtils.mkdir_p(rack_bin) unless File.exist?(rack_bin) - FileUtils.cp(src, dest) - FileUtils.chmod 0755, dest - end - end - - # Moves folder to a stage folder and create a symlink its place - # that links from /var/task to /tmp. Example: - # - # stage/code/rack => /tmp/rack - # - def rack_symlink - src = @full_app_root - return unless File.exist?(src) - - dest = "#{stage_area}/rack" - dir = File.dirname(dest) - FileUtils.mkdir_p(dir) unless File.exist?(dir) - FileUtils.mv(src, dest) - - # Create symlink - FileUtils.ln_sf("/tmp/rack", src) - end - end -end \ No newline at end of file diff --git a/lib/jets/builders/rackup_wrappers/rackup b/lib/jets/builders/rackup_wrappers/rackup deleted file mode 100755 index 39e8a963d..000000000 --- a/lib/jets/builders/rackup_wrappers/rackup +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash - -set -e - -export JETS_MEGA=1 - -# Figure out where this script is located. -PROJECTDIR="`dirname \"$0\"`" -PROJECTDIR="`cd \"$PROJECTDIR/..\" && pwd`" - -unset BUNDLE_IGNORE_CONFIG -# When jets packages the app code it creates .bundle/config in the project root. -# We want that to be used. - -# Drop a pidfile -# echo $$ > /tmp/jets-rackup.pid - -# Run the actual app using the bundled Ruby interpreter, with Bundler activated. -exec ruby -rbundler/setup bin/rackup.rb "$@" diff --git a/lib/jets/builders/rackup_wrappers/rackup.rb b/lib/jets/builders/rackup_wrappers/rackup.rb deleted file mode 100644 index 86179c9c6..000000000 --- a/lib/jets/builders/rackup_wrappers/rackup.rb +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -# Copied from: https://github.com/rack/rack/blob/master/bin/rackup - -require "rack" -Rack::Server.start \ No newline at end of file diff --git a/lib/jets/builders/ruby_packager.rb b/lib/jets/builders/ruby_packager.rb deleted file mode 100644 index 02228efd0..000000000 --- a/lib/jets/builders/ruby_packager.rb +++ /dev/null @@ -1,288 +0,0 @@ -require "bundler" # for clean_old_submodules only - -module Jets::Builders - class RubyPackager - include Util - - GEM_REGEXP = /-(arm|x)\d+.*-(darwin|linux)/ - - attr_reader :full_app_root - def initialize(relative_app_root) - @full_app_root = "#{build_area}/#{relative_app_root}" - end - - def install - return unless gemfile_exist? - - clean_old_submodules - bundle_install - bundle_check - copy_bundle_config - copy_cache_gems - end - - # build gems in vendor/gems/ruby/2.5.0 (done in install phase) - def finish - return unless gemfile_exist? - tidy - end - - def gemfile_exist? - gemfile_path = "#{@full_app_root}/Gemfile" - File.exist?(gemfile_path) - end - - # Installs gems on the current target system: both compiled and non-compiled. - # If user is on a macosx machine, macosx gems will be installed. - # If user is on a linux machine, linux gems will be installed. - # - # Copies Gemfile* to /tmp/jets/demo/cache folder and installs - # gems with bundle install from there. - # - # We take the time to copy Gemfile and bundle into a separate directory - # because it gets left around to act as a 'cache'. So, when the builds the - # project gets built again not all the gems from get installed from the - # beginning. - def bundle_install - full_project_path = @full_app_root - headline "Bundling: running bundle install in cache area: #{cache_area}." - - copy_gemfiles(full_project_path) - copy_bundled_gems(full_project_path) - run_prebundle_copy - - # Uncomment out to always remove the cache/vendor/gems to debug - # FileUtils.rm_rf("#{cache_area}/vendor/gems") - - # Remove .bundle folder so .bundle/config doesnt affect how Jets packages gems. - # Not using BUNDLE_IGNORE_CONFIG=1 to allow home ~/.bundle/config to affect bundling though. - # This is useful if you have private gems sources that require authentication. Example: - # - # bundle config gems.myprivatesource.com user:pass - # - - create_bundle_config - require "bundler" # dynamically require bundler so user can use any bundler - Bundler.with_unbundled_env do - sh( - "cd #{cache_area} && " \ - "env bundle install" - ) - end - create_bundle_config(frozen: true) - - rewrite_gemfile_lock("#{cache_area}/Gemfile.lock") - - # Copy the Gemfile.lock back to the project in case it was updated. - # For example we add the jets-rails to the Gemfile. - copy_back_gemfile_lock - - puts 'Bundle install completed' - end - - # Example `bundle check` error: - # - # The following gems are missing - # * date (3.3.3) - # * timeout (0.3.2) - # Install missing gems with `bundle install` - # - # Example success: - # - # The Gemfile's dependencies are satisfied - # - def bundle_check - out = '' - Bundler.with_unbundled_env do - out = `cd #{cache_area} && bundle check 2>&1` - end - if out.include?("missing") - puts "Failed: bundle check".color(:red) - puts <<~EOL - This means something went wrong with the bundle install. - Jets will prevent the deployment to AWS Lambda. - It's better to error now instead of finding out on AWS Lambda. - The bundle install can fail for different system-specific reasons. - It could be an outdated or incompatible version of RubyGems and Ruby. - - Related: https://community.boltops.com/t/could-not-find-timeout-0-3-1-in-any-of-the-sources/996 - - EOL - exit 1 - end - end - - def copy_back_gemfile_lock - src = "#{cache_area}/Gemfile.lock" - dest = "#{@full_app_root}/Gemfile.lock" - FileUtils.cp(src, dest) - end - - # Clean up extra unneeded files to reduce package size - # Because we're removing files (something dangerous) use full paths. - def tidy - puts "Tidying project: removing ignored files to reduce package size." - tidy_project(@full_app_root) - # The rack sub project has it's own gitignore. - tidy_project(@full_app_root+"/rack") - end - - def tidy_project(path) - Tidy.new(path).cleanup! - end - - # When using submodules, bundler leaves old submodules behind. Over time this inflates - # the size of the the cache gems. So we'll clean it up. - def clean_old_submodules - # https://stackoverflow.com/questions/38800129/parsing-a-gemfile-lock-with-bundler - lockfile = "#{cache_area}/Gemfile.lock" - return unless File.exist?(lockfile) - - return if Bundler.bundler_major_version <= 1 # LockfileParser only works for Bundler version 2+ - - parser = Bundler::LockfileParser.new(Bundler.read_file(lockfile)) - specs = parser.specs - - # specs = Bundler.load.specs - # IE: spec.source.to_s: "https://github.com/tongueroo/webpacker.git (at jets@a8c4661)" - submoduled_specs = specs.select do |spec| - spec.source.to_s =~ /@\w+\)/ - end - - # find git shas to keep - # IE: ["a8c4661", "abc4661"] - git_shas = submoduled_specs.map do |spec| - md = spec.source.to_s.match(/@(\w+)\)/) - md[1] # git_sha - end - - # IE: /tmp/jets/demo/cache/vendor/gems/ruby/2.5.0/bundler/gems/webpacker-a8c46614c675 - Dir.glob("#{cache_area}/vendor/gems/ruby/2.5.0/bundler/gems/*").each do |path| - sha = path.split('-').last[0..6] # only first 7 chars of the git sha - unless git_shas.include?(sha) - FileUtils.rm_rf(path) # REMOVE old submodule directory - end - end - end - - - def copy_gemfiles(full_project_path) - FileUtils.mkdir_p(cache_area) - FileUtils.cp("#{full_project_path}/Gemfile", "#{cache_area}/Gemfile") - - gemfile_lock = "#{full_project_path}/Gemfile.lock" - dest = "#{cache_area}/Gemfile.lock" - return unless File.exist?(gemfile_lock) - - FileUtils.cp(gemfile_lock, dest) - end - - def copy_bundled_gems(full_project_path) - src = "#{full_project_path}/bundled_gems" - return unless File.exist?(src) - Jets::Util.cp_r(src, "#{cache_area}/bundled_gems") - end - - def run_prebundle_copy - paths = Jets.config.build.prebundle_copy - paths = Array(paths) - paths.each do |path| - src = "#{@full_app_root}/#{path}" - return unless File.exist?(src) - Jets::Util.cp_r(src, "#{cache_area}/#{path}") - end - end - - # Remove the BUNDLED WITH line since we don't control the bundler gem version on AWS Lambda - # And this can cause issues with require 'bundler/setup' - def rewrite_gemfile_lock(gemfile_lock) - lines = IO.readlines(gemfile_lock) - - # Remove BUNDLED WITH - # amount is the number of lines to remove - new_lines, capture, count, amount = [], true, 0, 2 - lines.each do |l| - capture = false if l.include?('BUNDLED WITH') - if capture - new_lines << l - end - if capture == false - count += 1 - capture = count > amount # renable capture - end - end - - # Replace things like nokogiri (1.11.1-x86_64-darwin) => nokogiri (1.11.1) - lines, new_lines = new_lines, [] - lines.each do |l| - l.sub!(GEM_REGEXP, '') if l =~ GEM_REGEXP - new_lines << l - end - - # Make sure platform is ruby - lines, new_lines, in_platforms_section, platforms_rewritten = new_lines, [], false, false - lines.each do |l| - if in_platforms_section && platforms_rewritten # once PLATFORMS has been found, skip all lines until the next section - if l.present? - next - else - in_platforms_section = false - end - end - - if in_platforms_section && !platforms_rewritten # specify ruby as the only platform - new_lines << " ruby\n" - platforms_rewritten = true - next - end - - in_platforms_section = l.include?('PLATFORMS') - new_lines << l - end - - content = new_lines.join('') - IO.write(gemfile_lock, content) - end - - def copy_bundle_config - # Override project's .bundle/config and ensure that .bundle/config matches - # at these 2 spots: - # app_root/.bundle/config - # vendor/gems/.bundle/config - cache_bundle_config = "#{cache_area}/.bundle/config" - app_bundle_config = "#{@full_app_root}/.bundle/config" - FileUtils.mkdir_p(File.dirname(app_bundle_config)) - FileUtils.cp(cache_bundle_config, app_bundle_config) - end - - # On circleci the "#{Jets.build_root}/.bundle/config" doesnt exist - # this only happens with ssh debugging, not when the ci.sh script gets ran. - # But on macosx it exists. - # Dont know why this is the case. - def create_bundle_config(frozen: false) - FileUtils.rm_rf("#{cache_area}/.bundle") - frozen_line = %Q|BUNDLE_FROZEN: "true"\n| if frozen - text =<<-EOL ---- -#{frozen_line}BUNDLE_PATH: "vendor/gems" -BUNDLE_WITHOUT: "development:test" -EOL - bundle_config = "#{cache_area}/.bundle/config" - FileUtils.mkdir_p(File.dirname(bundle_config)) - IO.write(bundle_config, text) - end - - def copy_cache_gems - vendor_gems = "#{@full_app_root}/vendor/gems" - if File.exist?(vendor_gems) - puts "Removing current vendor_gems from project" - FileUtils.rm_rf(vendor_gems) - end - # Leave #{Jets.build_root}/vendor_gems behind to act as cache - if File.exist?("#{cache_area}/vendor/gems") - FileUtils.mkdir_p(File.dirname(vendor_gems)) - Jets::Util.cp_r("#{cache_area}/vendor/gems", vendor_gems) - end - end - end -end diff --git a/lib/jets/builders/shim_vars/app.rb b/lib/jets/builders/shim_vars/app.rb deleted file mode 100644 index 557d5fba6..000000000 --- a/lib/jets/builders/shim_vars/app.rb +++ /dev/null @@ -1,82 +0,0 @@ -# Jets::Builders::ShimVars::Shared.new(path) -# -# @vars.functions.each do |function_name| -# @vars.handler_for(function_name) -# end -# -# Implements: -# -# functions: IE [:index, :show] -# handler_for(function_name): IE handlers/controllers/posts_controller.index -# dest_path: IE: handlers/controllers/posts_controller.js -# -module Jets::Builders::ShimVars - class App < Base - # Allow user to specify relative or full path. The right path gets used - # internally. Example paths: - # app/controllers/posts_controller.rb - # app/jobs/hard_job.rb - # /tmp/jets/build/app_root/app/jobs/hard_job.rb - # /tmp/jets/build/app_root/app/functions/hello.rb - def initialize(path) - @full_path = full(path) - @relative_path = relative(path) - end - - def full(path) - path = "#{Jets.root}/#{path}" unless path.starts_with?("/") - path - end - - def relative(path) - full_path = full(path) - full_path.sub("#{Jets.root}/", "") - .sub(/.*internal\/app/, "app") - - end - - # process_type is key, it will either "controller" or "job". - # It is used to deduce klass, etc. - # We get the process_type from the path. - # Example paths: - # app/controllers/posts_controller.rb - # app/jobs/hard_job.rb - def process_type - @relative_path.split('/')[1].singularize # controller or job - end - - # Returns the public methods of the child_class. - # Example: [:create, :update] - def functions - klass.lambda_functions - end - - # Examples: PostsController, HardJob, Hello, HelloFunction - def klass - @klass ||= Jets::Klass.from_path(@relative_path) - end - - def lang(meth) - klass.definitions.find - end - - # This gets called in the node shim js template - # Example: handlers/controllers/posts_controller.index - def handler_for(meth) - "#{handler_base}.#{meth}" - end - - # Base portion of handler path. - # Example: handlers/controllers/posts_controller - def handler_base - # possibly not include _function - underscored_name = @relative_path.sub(%r{app/(\w+)/},'').sub('.rb','') - "handlers/#{process_type.pluralize}/#{underscored_name}" - end - - # Example return: "handlers/controllers/posts.js" - def dest_path - @relative_path.sub("app", "handlers") - end - end -end diff --git a/lib/jets/builders/shim_vars/base.rb b/lib/jets/builders/shim_vars/base.rb deleted file mode 100644 index 0ed84fa5b..000000000 --- a/lib/jets/builders/shim_vars/base.rb +++ /dev/null @@ -1,25 +0,0 @@ -module Jets::Builders::ShimVars - class Base - include Jets::AwsServices - extend Memoist - - def s3_bucket - Jets.aws.s3_bucket - end - - def rack_zip - checksum_zip(:rack) - end - - def bundled_zip - checksum_zip(:bundled) - end - - private - def checksum_zip(name) - checksum = Jets::Builders::Md5.checksums["stage/#{name}"] - return unless checksum - "#{name}-#{checksum}.zip" - end - end -end diff --git a/lib/jets/builders/shim_vars/shared.rb b/lib/jets/builders/shim_vars/shared.rb deleted file mode 100644 index ceb0ec690..000000000 --- a/lib/jets/builders/shim_vars/shared.rb +++ /dev/null @@ -1,37 +0,0 @@ -# Jets::Builders::ShimVars::Shared.new(fun) -# -# @deducer.functions.each do |function_name| -# @deducer.handler_for(function_name) -# end -# -# Implements: -# -# functions: IE [:index, :show] -# handler_for(function_name): IE handlers/controllers/posts_controller.index -# dest_path: IE: handlers/controllers/posts_controller.js -# -module Jets::Builders::ShimVars - class Shared < Base - # fun is a Jets::Stack::Function - def initialize(fun) - @fun = fun - end - - # Always only one element for shared functions - # functions: IE [:handle] - def functions - [@fun.meth] # function_names - end - - # Dont need function_name arg but keeping the same interface as parent class - # IE handlers/shared/functions/bob.handle - def handler_for(function_name) - @fun.handler_dest - end - - # IE handlers/shared/functions/bob.js - def dest_path - @fun.handler_dest - end - end -end diff --git a/lib/jets/builders/templates/handlers/one_lambda_for_all_controllers.rb b/lib/jets/builders/templates/handlers/one_lambda_for_all_controllers.rb deleted file mode 100644 index 8249597b6..000000000 --- a/lib/jets/builders/templates/handlers/one_lambda_for_all_controllers.rb +++ /dev/null @@ -1,33 +0,0 @@ -require "bundler/setup" -require "jets" -Jets.once # runs once in lambda execution context - -def lambda_handler(event:, context:) - if event['_prewarm'] - Jets.process(event, context, nil) - return - end - -<% if @vars.process_type == "controller" -%> - route = Jets::Router.find_route_by_event(event) - controller = route.controller_name - action = route.action_name - if Jets.config.cfn.build.routes == "one_apigw_method_for_all_routes" - event["pathParameters"] = route.rebuild_path_parameters(event) # override - end -<% end -%> - Jets.process(event, context, "handlers/controllers/#{controller}.#{action}") -end -<% if ENV['JETS_DEBUG_HANDLER'] %> -<%# JETS_DEBUG_HANDLER=1 jets build %> -if __FILE__ == $0 - event = { - "path" => "/posts", - "httpMethod" => "GET", - "headers" => { - "Host" => "localhost:8888", - } - } - lambda_handler(event: event, context: {}) -end -<% end %> diff --git a/lib/jets/builders/templates/handlers/one_lambda_per_controller.rb b/lib/jets/builders/templates/handlers/one_lambda_per_controller.rb deleted file mode 100644 index 140b9589c..000000000 --- a/lib/jets/builders/templates/handlers/one_lambda_per_controller.rb +++ /dev/null @@ -1,20 +0,0 @@ -require "bundler/setup" -require "jets" -Jets.once # runs once in lambda execution context - -<%# # IE: handlers/controllers/up_controller.index -%> -<% handler = @vars.handler_base + '.#{action}' -%> -def lambda_handler(event:, context:) - if event['_prewarm'] - Jets.process(event, context, nil) - return - end - -<% if @vars.process_type == "controller" -%> - if Jets.config.cfn.build.routes == "one_apigw_method_for_all_routes" - route = Jets::Router.find_route_by_event(event) - event["pathParameters"] = route.rebuild_path_parameters(event) # override - end -<% end -%> - Jets.process(event, context, "<%= handler -%>") -end diff --git a/lib/jets/builders/templates/handlers/one_lambda_per_method.rb b/lib/jets/builders/templates/handlers/one_lambda_per_method.rb deleted file mode 100644 index 4b00030a9..000000000 --- a/lib/jets/builders/templates/handlers/one_lambda_per_method.rb +++ /dev/null @@ -1,18 +0,0 @@ -require "bundler/setup" -require "jets" -Jets.once # runs once in lambda execution context - -<% @vars.functions.each do |function_name| - handler = @vars.handler_for(function_name) - meth = handler.split('.').last --%> -def <%= meth -%>(event:, context:) -<% if @vars.process_type == "controller" -%> - if Jets.config.cfn.build.routes == "one_apigw_method_for_all_routes" - route = Jets::Router.find_route_by_event(event) - event["pathParameters"] = route.rebuild_path_parameters(event) # override - end -<% end -%> - Jets.process(event, context, "<%= handler -%>") -end -<% end %> \ No newline at end of file diff --git a/lib/jets/builders/tidy.rb b/lib/jets/builders/tidy.rb deleted file mode 100644 index 2178fadda..000000000 --- a/lib/jets/builders/tidy.rb +++ /dev/null @@ -1,107 +0,0 @@ -module Jets::Builders - class Tidy - include Util - - def initialize(project_root, noop: false) - @project_root = project_root - @noop = noop - end - - def cleanup! - removals.each do |removal| - removal = removal.sub(%r{^/},'') # remove leading slash - path = "#{@project_root}/#{removal}" - rm_rf(path) - end - - clean_vendor_gems - clean_webpack_assets - end - - # Clean out unnecessary src and compiled packs because Jets serves them out of s3. - # This keeps the code size down to help keep it in size limit so we can use the - # live Lambda console editor. - def clean_webpack_assets - FileUtils.rm_rf("#{@project_root}/app/javascript/src") - - return unless File.exist?("#{@project_root}/public/packs") # this class works for rack subfolder too - FileUtils.mv("#{@project_root}/public/packs/manifest.json", "#{stage_area}/manifest.json") - FileUtils.rm_rf("#{@project_root}/public/packs") - FileUtils.mkdir_p("#{@project_root}/public/packs") - FileUtils.mv("#{stage_area}/manifest.json", "#{@project_root}/public/packs/manifest.json") - end - - def removals - removals = always_removals - removals += get_removals("#{@project_root}/.gitignore") - removals += get_removals("#{@project_root}/.dockerignore") - removals += get_removals("#{@project_root}/.jetsignore") - removals = removals.reject do |p| - jetskeep.find do |keep| - p == keep - end - end - removals.uniq - end - - def get_removals(file) - path = file - return [] unless File.exist?(path) - - removal = File.read(path).split("\n") - removal.map {|i| i.strip}.reject {|i| i =~ /^#/ || i.empty?} - # IE: ["/handlers", "/bundled*", "/vendor/jets] - end - - # We clean out ignored files pretty aggressively. So provide - # a way for users to keep files from being cleaned out. - def jetskeep - always = %w[.bundle /public/packs /public/packs-test vendor] - path = "#{@project_root}/.jetskeep" - return always unless File.exist?(path) - - keep = IO.readlines(path) - keep = keep.map {|i| i.strip}.reject { |i| i =~ /^#/ || i.empty? } - (always + keep).uniq - end - - # folders to remove in the vendor/gems folder regardless of the level of the folder - def clean_vendor_gems - # Thanks: https://stackoverflow.com/questions/11385795/ruby-list-directory-with-dir-including-dotfiles-but-not-and - Dir.glob("#{@project_root}/vendor/gems/**/*", File::FNM_DOTMATCH).each do |path| - next unless File.directory?(path) - dir = File.basename(path) - next unless always_removals.include?(dir) - - rm_rf(path) - end - - remove_gem_cache - end - - # Reason do not remove the cache folder generally is because some gems have - # actual cache folders that they used. - def remove_gem_cache - cache_path = "#{@project_root}/vendor/gems/ruby/#{Jets.ruby_folder}/cache" - FileUtils.rm_rf(cache_path) - end - - def rm_rf(path) - exists = File.exist?("#{path}/.gitkeep") || File.exist?("#{path}/.keep") - return if exists - - # say " rm -rf #{path}".color(:yellow) # uncomment to debug - system("rm -rf #{path}") unless @noop - end - - # These directories will be removed regardless of dir level - def always_removals - %w[.git spec tmp] - end - - def say(message) - message = "NOOP #{message}" if @noop - puts message - end - end -end \ No newline at end of file diff --git a/lib/jets/builders/util.rb b/lib/jets/builders/util.rb deleted file mode 100644 index e0e6f717b..000000000 --- a/lib/jets/builders/util.rb +++ /dev/null @@ -1,31 +0,0 @@ -module Jets::Builders - module Util - private - def sh(command) - puts "=> #{command}".color(:green) - success = system(command) - unless success - puts "#{command} failed to run.".color(:red) - puts caller[0] - exit 1 - end - success - end - - def headline(message) - puts "=> #{message}".color(:cyan) - end - - def build_area - Jets.build_root - end - - def stage_area - "#{build_area}/stage" - end - - def cache_area - "#{build_area}/cache" # cleaner to use full path for this setting - end - end -end \ No newline at end of file diff --git a/lib/jets/bundle.rb b/lib/jets/bundle.rb index 840e5b739..4ffa4d575 100644 --- a/lib/jets/bundle.rb +++ b/lib/jets/bundle.rb @@ -10,7 +10,7 @@ module Bundle # # Note, this is called super early right before require "zeitwerk" # The initially Bundler.setup does not include the Jets.env group. - # Later in Jets::Booter, Bundle.require is called and includes the Jets.env group. + # Later in Jets::Core::Booter, Bundle.require is called and includes the Jets.env group. # def setup return unless jets_project? @@ -24,7 +24,7 @@ def setup # Bundler.require called when environment boots up via Jets.boot. This will eagerly require all gems in the # Gemfile. This means the user will not have to explictly require dependencies. # - # It also useful for when to loading Rake tasks in Jets::Commands::RakeTasks.load! For example, some gems like + # It also useful for when to loading Rake tasks in Jets::Thor::RakeTasks.load! For example, some gems like # webpacker that load rake tasks are specified with a git based source: # # gem "webpacker", git: "https://github.com/tongueroo/webpacker.git" @@ -34,7 +34,7 @@ def setup # user. So the rake tasks show up when calling jets help. # # When the user calls jets help from outside the project folder, bundler is not used and the load errors get - # rescued gracefully. This is done in Jets::Commands::RakeTasks.load! In the case when user is in another + # rescued gracefully. This is done in Jets::Thor::RakeTasks.load! In the case when user is in another # project with another Gemfile, the load errors will also be rescued. def require return unless jets_project? @@ -52,20 +52,22 @@ def handle_error(e) There may be something funny with your ruby and bundler setup. You can try upgrading bundler and rubygems: + gem install --default bundler gem update --system - gem install bundler + bundle update --bundler Here are some links that may be helpful: * https://bundler.io/blog/2019/01/03/announcing-bundler-2.html - * https://community.rubyonjets.com/t/jets-1-9-8-install-issue-bundler-setup-missing/185/2 + * https://community.boltops.com/t/jets-1-9-8-issue-cannot-load-such-file-bundler-setup-loaderror/185/2 + * https://community.boltops.com/t/could-not-find-timeout-0-3-1-in-any-of-the-sources/996 Also, running bundle exec in front of your command may remove this message. EOL end def gemfile? - ENV['BUNDLE_GEMFILE'] || File.exist?("Gemfile") + ENV["BUNDLE_GEMFILE"] || File.exist?("Gemfile") end def bundler_groups @@ -73,7 +75,7 @@ def bundler_groups end def jets_project? - File.exist?("config/application.rb") + File.exist?("config/jets") end extend self diff --git a/lib/jets/cfn/base.rb b/lib/jets/cfn/base.rb index 76807613a..5f9e0360b 100644 --- a/lib/jets/cfn/base.rb +++ b/lib/jets/cfn/base.rb @@ -6,11 +6,12 @@ # attributes # # These are computed methods that derive their values from the resource definition itself. -# Overriding these methods will remove the computed logical which handles things +# Overriding these methods will remove the computed logic which handles things # like camelizing and replacements. module Jets::Cfn class Base extend Memoist + include Jets::Util::Logging include Jets::Util::Camelize # interface method @@ -61,13 +62,23 @@ def outputs {} end - def permission - Resource::Lambda::Permission.new(replacements, self) + def normalize_tags(tags) + if tags.is_a?(Hash) + tags.map do |k, v| + {Key: k.to_s, Value: v.to_s} + end + else # Array passthrough + tags + end + end + + # interface method + def config + Jets.bootstrap.config end - memoize :permission class << self - def truncate_id(id, postfix = '') + def truncate_id(id, postfix = "") # Api Gateway resource name has a limit of 64 characters. # Yet it throws not found when ID is longer than 62 characters and I don't know why. # To keep it safe, let's stick to the 62 characters limit. diff --git a/lib/jets/cfn/bootstrap.rb b/lib/jets/cfn/bootstrap.rb new file mode 100644 index 000000000..878aab311 --- /dev/null +++ b/lib/jets/cfn/bootstrap.rb @@ -0,0 +1,13 @@ +module Jets::Cfn + class Bootstrap + def initialize(options = {}) + @options = options.merge(bootstrap: true) + end + + def run + Builder::Parent::Genesis.new(@options).build + success = Deploy.new(@options).sync # returns true if success + abort("Bootstrap deploy failed") unless success + end + end +end diff --git a/lib/jets/cfn/builder.rb b/lib/jets/cfn/builder.rb deleted file mode 100644 index bf2e98ccd..000000000 --- a/lib/jets/cfn/builder.rb +++ /dev/null @@ -1,230 +0,0 @@ -module Jets::Cfn - class Builder - extend Memoist - include Jets::AwsServices - - def initialize(options) - @options = options - end - - def build - puts "Building CloudFormation templates" - clean_templates - build_minimal_parent_template - if @options[:stack_type] == :full - build_all_templates - build_full_parent_template # must be called at the end - end - Jets::Router::State.save_apigw_state if ENV['JETS_API_STATE_DEBUG'] - puts "Built CloudFormation templates at #{Jets.build_root}/templates" - end - - def build_minimal_parent_template - Parent.new(@options.merge(stack_type: :minimal)).build - end - - def build_all_templates - # CloudFormation templates - # 1. Shared and authorizer templates - child templates needs them - build_api_gateway_templates unless Jets::Router.no_routes? - build_authorizer_templates # controllers can use these - # 2. Child templates - parent template needs them - build_one_lambda_for_all_controllers - build_app_child_templates - # 3. Child templates - parent template needs them - build_shared_resources_templates - end - - def build_full_parent_template - Parent.new(@options.merge(stack_type: :full)).build - end - - def build_api_gateway_templates - Api::Gateway.new(@options).build - Api::Resources.build_pages(@options) - Api::Methods.build_pages(@options) - Api::Deployment.new(@options).build - Api::Mapping.new(@options).build - end - - def build_authorizer_templates - authorizer_files.each do |path| - Authorizer.new(path).build - end - end - - def build_one_lambda_for_all_controllers - return if Jets.config.mode == "job" - return unless Jets.one_lambda_for_all_controllers? - OneController.new(@options).build - end - - def build_app_child_templates - app_files.each do |path| - build_child_template(path) - end - end - - def build_shared_resources_templates - Jets::Stack.subclasses.each do |subclass| - Shared.new(subclass).build - end - end - - # path: app/controllers/comments_controller.rb - # path: app/jobs/easy_job.rb - def build_child_template(path) - return if authorizer?(path) # AuthorizerBuilder is built earlier - - md = path.match(%r{app/(.*?)/}) # extract: controller, job or function - app_class = md[1].classify - - if app_class == "Controller" && Jets.one_lambda_for_all_controllers? # app/controllers - return - end - - builder_class = "Jets::Cfn::Builder::#{app_class}".constantize - - app_class = Jets::Klass.from_path(path) - if !Jets.gem_layer? && app_class == Jets::PreheatJob - return # No prewarm when there's only poly functions and no gem layer - end - - # Builder class fully qualified name: - # Controller => Jets::Cfn::Builder::Controller - # Examples: - # Controller.new(PostsController).build - # Job.new(EasyJob).build - # Rule.new(CheckRule).build - # Function.new(Hello).build - # Function.new(HelloFunction).build - builder_class.new(app_class).build - end - - def authorizer?(path) - path.include?("app/authorizers") - end - - def clean_templates - FileUtils.rm_rf("#{Jets.build_root}/templates") - end - - def app_files - self.class.app_files - end - - def shared_files - self.class.shared_files - end - - def authorizer_files - self.class.authorizer_files - end - - class << self - # Crucial that the Dir.pwd is in the tmp_code because for because Jets.boot set ups autoload_paths and this is - # how project classes are loaded. - # TODO: rework code so that Dir.pwd does not have to be in tmp_code for build to work. - # - # app_files used to determine what CloudFormation templates to build. - # app_files also used to determine what handlers to build. - def app_files - paths = [] - expression = "#{Jets.root}/app/**/**/*.rb" - Dir.glob(expression).each do |path| - next unless app_file?(path) - relative_path = path.sub("#{Jets.root}/", '') # rid of the Jets.root at beginning - paths << relative_path - end - - if Jets.config.prewarm.enable - internal = File.expand_path("../internal", __dir__) - paths << "#{internal}/app/jobs/jets/preheat_job.rb" - end - - paths - end - - APP_FOLDERS = %w[authorizers controllers functions jobs rules] - def app_file?(path) - return false unless File.extname(path) == ".rb" - return false unless File.file?(path) unless Jets.env.test? - return false if application_abstract_classes.detect { |p| path.include?(p) } - return false if concerns?(path) - return true if APP_FOLDERS.detect { |p| path.include?("app/#{p}") } - false - end - - # Do not define lamda functions for abstract application parent classes. Examples: - # - # application_controller.rb - # application_job.rb - # application_authorizer.rb - def application_abstract_classes - APP_FOLDERS.map { |a| "application_#{a.singularize}.rb" } - end - - def concerns?(path) - path =~ %r{app/\w+/concerns/} - end - - def authorizer_files - app_files.select { |p| p.include?("app/authorizers") } - end - - def shared_files - find_app_paths("shared/resources") - end - - def find_app_paths(app_path) - paths = [] - expression = "#{Jets.root}/app/#{app_path}/**/*.rb" - Dir.glob(expression).each do |path| - return false unless File.file?(path) - - relative_path = path.sub("#{Jets.root}/", '') - # Rids of the Jets.root at beginning - paths << relative_path - end - paths - end - - # Finds out of the app has polymorphic functions only and zero ruby functions. - # In this case, we can skip a lot of the ruby related building and speed up the - # deploy process. - def poly_only? - !app_has_ruby? && !shared_has_ruby? - end - - def app_has_ruby? - has_ruby = app_files.detect do |path| - app_class = Jets::Klass.from_path(path) # IE: PostsController, Jets::PublicController - langs = app_class.definitions.map(&:lang) - langs.include?(:ruby) && app_class != Jets::PreheatJob - end - !!has_ruby - end - - def shared_has_ruby? - has_ruby = false - Jets::Stack.subclasses.each do |klass| - klass.functions.each do |fun| - if fun.lang == :ruby - has_ruby = true - break - end - end - end - has_ruby - end - - def router_has?(controller) - Jets::Router.has_controller?(controller) - end - - def tmp_code(full_build_path=false) - full_build_path ? "#{Jets.build_root}/stage/code" : "stage/code" - end - end - end -end diff --git a/lib/jets/cfn/builder/api/base.rb b/lib/jets/cfn/builder/api/base.rb deleted file mode 100644 index 83e73e010..000000000 --- a/lib/jets/cfn/builder/api/base.rb +++ /dev/null @@ -1,12 +0,0 @@ -module Jets::Cfn::Builder::Api - class Base - extend Memoist - include Jets::Cfn::Builder::Interface - include Jets::AwsServices - - def initialize(options={}) - @options = options - @template = ActiveSupport::HashWithIndifferentAccess.new(Resources: {}) - end - end -end diff --git a/lib/jets/cfn/builder/api/deployment.rb b/lib/jets/cfn/builder/api/deployment.rb deleted file mode 100644 index d37df5b9f..000000000 --- a/lib/jets/cfn/builder/api/deployment.rb +++ /dev/null @@ -1,46 +0,0 @@ -module Jets::Cfn::Builder::Api - class Deployment < Base - # interface method - def compose - deployment = Jets::Cfn::Resource::ApiGateway::Deployment.new - add_resource(deployment) - add_parameters(deployment.parameters) - add_outputs(deployment.outputs) - end - - # Because Jets generates a new timestamped logical id for the API Deployment - # resource it also creates a new root base path mapping and fails. Additionally, - # the base path mapping depends on the API Deploy for the stage name. - # - # We resolve this by using a custom resource that does an in-place update. - # - # Note, also tried to change the domain name of to something like demo-dev-[random].mydomain.com - # but that does not work because the domain name has to match the route53 record exactly. - # - def add_base_path_mapping - return unless Jets.custom_domain? - - function = Jets::Cfn::Resource::ApiGateway::BasePath::Function.new - add_resource(function) - add_outputs(function.outputs) - - mapping = Jets::Cfn::Resource::ApiGateway::BasePath::Mapping.new - add_resource(mapping) - add_outputs(mapping.outputs) - - iam_role = Jets::Cfn::Resource::ApiGateway::BasePath::Role.new - add_resource(iam_role) - add_outputs(iam_role.outputs) - end - - # interface method - def template_path - Jets::Names.api_deployment_template_path - end - - # do not write a template if routes are empty - def write - super unless Jets::Router.no_routes? - end - end -end diff --git a/lib/jets/cfn/builder/api/gateway.rb b/lib/jets/cfn/builder/api/gateway.rb deleted file mode 100644 index 3fb2ff3d3..000000000 --- a/lib/jets/cfn/builder/api/gateway.rb +++ /dev/null @@ -1,104 +0,0 @@ -module Jets::Cfn::Builder::Api - class Gateway < Base - # interface method - def compose - add_gateway_rest_api # changes parent template - add_custom_domain # changes parent template - end - - # interface method - def template_path - Jets::Names.api_gateway_template_path - end - - # do write a template if routes are empty - def write - super unless Jets::Router.no_routes? - end - - # If the are routes in config/routes.rb add Gateway API in parent stack - def add_gateway_rest_api - rest_api = Jets::Cfn::Resource::ApiGateway::RestApi.new - add_resource(rest_api) - add_outputs(rest_api.outputs) - - deployment = Jets::Cfn::Resource::ApiGateway::Deployment.new - outputs = deployment.outputs(true) - add_output(:RestApiUrl, Value: outputs[:RestApiUrl]) - end - - def add_custom_domain - return unless Jets.custom_domain? - add_domain_name - add_route53_dns if Jets.config.domain.route53 - end - - def add_domain_name - add_outputs(create_domain_name) - end - - def add_route53_dns - dns = Jets::Cfn::Resource::Route53::RecordSet.new - if !existing_domain_name?(dns.domain_name) or existing_dns_record_on_stack? - add_resource(dns) - add_outputs(dns.outputs) - end - end - - def create_domain_name - resource = Jets::Cfn::Resource::ApiGateway::DomainName.new - - return { - DomainName: resource.domain_name - } if (existing_domain_name?(resource) and !existing_domain_name_on_stack?) - - add_resource(resource) - return resource.outputs - end - - def existing_domain_name?(resource) - apigateway.get_domain_name( - domain_name: resource.domain_name - ) - true - # IE: Aws::APIGateway::Errors::NotFoundException Invalid domain name identifier specified - rescue Aws::APIGateway::Errors::NotFoundException - false - end - memoize :existing_domain_name? - - def existing_domain_name_on_stack? - cfn.describe_stack_resource( - stack_name: api_gateway_physical_resource_id, - logical_resource_id: "DomainName" - ) - true - # IE: Aws::CloudFormation::Errors::ValidationError (Resource DomainName does not exist for stack demo-dev) - rescue Aws::CloudFormation::Errors::ValidationError - false - end - - def existing_dns_record_on_stack? - cfn.describe_stack_resource( - stack_name: api_gateway_physical_resource_id, - logical_resource_id: "DnsRecord" - ) - true - # IE: Aws::CloudFormation::Errors::ValidationError (Resource DnsRecord does not exist for stack demo-dev) - rescue Aws::CloudFormation::Errors::ValidationError - false - end - - def api_gateway_physical_resource_id - resp = cfn.describe_stack_resource( - stack_name: Jets::Names.parent_stack_name, - logical_resource_id: "ApiGateway" - ) - resp&.stack_resource_detail&.physical_resource_id - # IE: Aws::CloudFormation::Errors::ValidationError (Resource ApiGateway does not exist for stack demo-dev) - rescue Aws::CloudFormation::Errors::ValidationError - nil - end - memoize :api_gateway_physical_resource_id - end -end diff --git a/lib/jets/cfn/builder/api/mapping.rb b/lib/jets/cfn/builder/api/mapping.rb deleted file mode 100644 index 673448d91..000000000 --- a/lib/jets/cfn/builder/api/mapping.rb +++ /dev/null @@ -1,54 +0,0 @@ -module Jets::Cfn::Builder::Api - class Mapping < Base - # interface method - def compose - add_parameters(parameters) - add_base_path_mapping - end - - def parameters - p = { - GemLayer: "GemLayer", - IamRole: "IamRole", - RestApi: "RestApi", - S3Bucket: "S3Bucket", - } - p[:DomainName] = "DomainName" if Jets.custom_domain? - p[:BasePath] = "BasePath" - p - end - - # Because Jets generates a new timestamped logical id for the API Deployment - # resource it also creates a new root base path mapping and fails. Additionally, - # the base path mapping depends on the API Deploy for the stage name. - # - # We resolve this by using a custom resource that does an in-place update. - # - # Note, also tried to change the domain name of to something like demo-dev-[random].mydomain.com - # but that does not work because the domain name has to match the route53 record exactly. - # - def add_base_path_mapping - function = Jets::Cfn::Resource::ApiGateway::BasePath::Function.new - add_resource(function) - add_outputs(function.outputs) - - mapping = Jets::Cfn::Resource::ApiGateway::BasePath::Mapping.new - add_resource(mapping) - add_outputs(mapping.outputs) - - iam_role = Jets::Cfn::Resource::ApiGateway::BasePath::Role.new - add_resource(iam_role) - add_outputs(iam_role.outputs) - end - - # interface method - def template_path - Jets::Names.api_mapping_template_path - end - - # do not write a template unless custom domain is used - def write - super if Jets.custom_domain? - end - end -end diff --git a/lib/jets/cfn/builder/api/methods.rb b/lib/jets/cfn/builder/api/methods.rb deleted file mode 100644 index 303e52fa2..000000000 --- a/lib/jets/cfn/builder/api/methods.rb +++ /dev/null @@ -1,72 +0,0 @@ -module Jets::Cfn::Builder::Api - class Methods < Paged - # interface method - def compose - return if routes.empty? - add_api_methods - add_api_gateway_parameters - end - - # Resources Example (only showing keys we care about): - # - # UpIndexGetApiMethod: - # Type: AWS::ApiGateway::Method - # Properties: - # ResourceId: !Ref UpApiResource - # RestApiId: !Ref RestApi - # Integration: - # Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${UpControllerIndexLambdaFunction}/invocations - # - def add_api_methods - routes.each_with_index do |route, i| - method = Jets::Cfn::Resource::ApiGateway::Method.new(route) - add_resource(method) - end - end - - def add_api_gateway_parameters - api_methods = Jets::Cfn::Params::Api::Methods.new(template: @template) - api_methods.params.each do |key, _| - add_parameter(key) - end - end - - def routes - ensure_one_apigw_method_proxy_routes! - @items.map do |uid| - Jets::Router.routes.find do |route| - route.path == "/" && route.path == uid.split('|').last || # root route. any http method works - "#{route.http_method}|#{route.path}" == uid # Match the method and path. IE: GET|posts/:id - end - end.compact - end - memoize :routes - - # Only add missing root route with one_apigw_method_for_all_routes setting. - # Add the root route because that's how it works locally. - # With one_apigw_method_for_all_routes, it all goes to one lambda function - # and routing is determined by config/routes.rb - def ensure_one_apigw_method_proxy_routes! - return unless Jets.config.cfn.build.routes == "one_apigw_method_for_all_routes" - - # find before modifications - catchall_route = Jets::Router.routes.find { |route| route.path =~ /^\/\*/ } - root_route = Jets::Router.routes.find { |route| route.http_method == "GET" && route.path == "/" } - - # modifications - unless catchall_route - # Note: catchall to route does not matter. In one_apigw_method_for_all_routes mode it all goes to one lambda function - # and then gets routed by config/routes.rb - Jets::Router.routes << Jets::Router::Route.new(path: '/*catchall', http_method: 'ANY', to: 'jets/public#show') - end - if !root_route && catchall_route - Jets::Router.routes << Jets::Router::Route.new(path: '/', http_method: 'GET', to: catchall_route.to) - end - end - - # template_path is an interface method - def template_path - Jets::Names.api_methods_template_path(@page_number) - end - end -end diff --git a/lib/jets/cfn/builder/api/paged.rb b/lib/jets/cfn/builder/api/paged.rb deleted file mode 100644 index d240b4424..000000000 --- a/lib/jets/cfn/builder/api/paged.rb +++ /dev/null @@ -1,30 +0,0 @@ -module Jets::Cfn::Builder::Api - class Paged < Base - class << self - def build_pages(options={}) - # IE: Pages::Methods.pages Pages::Resources.pages - pages_class.pages.each do |page| - # Key builder here: - # Jets::Cfn::Builder::Api::Methods - # Jets::Cfn::Builder::Api::Resources - new(options.merge(page: page)).build - end - end - - # Examples: - # Pages::Methods.new(options) - # Pages::Resources.new(options) - def pages_class - class_name = self.to_s.gsub(/.*::Api::/, '') # IE: Methods or Resources - "Jets::Cfn::Builder::Api::Pages::#{class_name}".constantize - end - end - - def initialize(options={}) - super - @page = options[:page] - @items = @page.items # interface method: Cors: paths, Resources: paths, Methods: routes - @page_number = @page.number # interface method - end - end -end diff --git a/lib/jets/cfn/builder/api/pages/base.rb b/lib/jets/cfn/builder/api/pages/base.rb deleted file mode 100644 index aee057334..000000000 --- a/lib/jets/cfn/builder/api/pages/base.rb +++ /dev/null @@ -1,105 +0,0 @@ -module Jets::Cfn::Builder::Api::Pages - class Base - class_attribute :pages - @@pages = {} - - class << self - extend Memoist - - # Returns: Array - def pages - return @@pages[self.name] if @@pages[self.name] - - pages = [] - uids_map = build_uids_map - uids_map.each do |uid, existing_page| - if existing_page - pages[existing_page] ||= [] - pages[existing_page] << uid - end - end - pages.compact! # not using page 0 so need to compact to remove the first nil element - - # Remove existing uids from uids_map. Leave behind new uids - pages.each do |page| - page.each do |i| - uids_map.delete(i) - end - end - - # Fill up available space in each page so all existing pages are full - keys = uids_map.keys - pages.each do |page| - break if keys.empty? - while page.size < page_limit - uid = keys.shift - break if uid.nil? - page << uid - end - end - - # Add remaining slices to new additional pages - pages += keys.each_slice(page_limit).to_a - - @@pages[self.name] = [] - pages.each_with_index do |uids, i| - # Note: page number starts at 1 - # Because of this we need to do a pages.compact! above to remove the first nil element. Feel this is easier for follow for us humans. - @@pages[self.name] << Page.new(items: uids, number: i+1) - end - - @@pages[self.name] - end - - # Build map that has uids as keys and page number as value - # For Resources, the uid is the path - # Example: {"a1"=>0, "a2"=>0, "b1"=>1, "b2"=>1, "c1"=>2, "c2"=>2} - # For Methods, the uid is the method|path - # Example: {"GET|a1"=>0, "GET|a2"=>0, "GET|b1"=>1, "GET|b2"=>1, "GET|c1"=>2, "GET|c2"=>2} - def build_uids_map - map = {} - # uids is interface method - uids.each do |uid| - map[uid] = find_page_index(uid) - end - map - end - - def find_page_index(new_uid) - slices = previously_deployed || [] - slices.each_with_index do |slice, page_number| - items = slice["items"] - items.find do |old_uid| - return page_number if old_uid == new_uid # found - end - end - nil # not found - end - - def previously_deployed - state = Jets::Router::State.new - name = self.to_s.split('::').last.downcase - deployed = state.load(name) # IE: state.load("resources") or state.load("methods") - deployed - end - memoize :previously_deployed - - # Relevant CloudFormation limits: - # Resources 500 - # Parameters 200 - # Outputs 200 - # For API Gateway Methods template, - # Lambda Functions and APIGW Resources are passed in as parameters. - # Each APIGW Method can use 2 parameters. - # So use page limit of 100 to provide a buffer. - # Technically, can use a different limit for Resources and Cors templates, - # but keeping all at 100 for consistency. - # - # JETS_API_PAGE_LIMIT is based on that - # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cloudformation-limits.html - def page_limit - Integer(ENV['JETS_API_PAGE_LIMIT'] || 100) - end - end - end -end diff --git a/lib/jets/cfn/builder/api/pages/methods.rb b/lib/jets/cfn/builder/api/pages/methods.rb deleted file mode 100644 index 5cb815355..000000000 --- a/lib/jets/cfn/builder/api/pages/methods.rb +++ /dev/null @@ -1,17 +0,0 @@ -module Jets::Cfn::Builder::Api::Pages - class Methods < Base - class << self - # interface method - def uids - if Jets.config.cfn.build.routes == "one_apigw_method_for_all_routes" - return ['ANY|/', 'ANY|/*catchall'] - end - - routes = Jets::Router.routes - routes.map do |route| - "#{route.http_method}|#{route.path}" # IE: GET|posts/:id - end - end - end - end -end diff --git a/lib/jets/cfn/builder/api/pages/page.rb b/lib/jets/cfn/builder/api/pages/page.rb deleted file mode 100644 index 071cc4bdc..000000000 --- a/lib/jets/cfn/builder/api/pages/page.rb +++ /dev/null @@ -1,19 +0,0 @@ -module Jets::Cfn::Builder::Api::Pages - # Note: Do not use, it behaves differently in Ruby 2 v Ruby 3 - # Page = Struct.new(:items, :number) - # - # In Ruby 2, assigning an Array to items creates an extra :items key in the structure - # - # #["*catchall", "posts"], - # - # In Ruby 3, assigning an Array to items. - # - # # - # - class Page - attr_accessor :items, :number - def initialize(items:, number:) - @items, @number = items, number - end - end -end diff --git a/lib/jets/cfn/builder/api/pages/resources.rb b/lib/jets/cfn/builder/api/pages/resources.rb deleted file mode 100644 index 75abfa953..000000000 --- a/lib/jets/cfn/builder/api/pages/resources.rb +++ /dev/null @@ -1,21 +0,0 @@ -module Jets::Cfn::Builder::Api::Pages - class Resources < Base - class << self - # interface method - def uids - if Jets.config.cfn.build.routes == "one_apigw_method_for_all_routes" - return ['/', '/*catchall'] - end - - # Note: Do not use - # Jets::Router.routes.map(&:path) - # It does not include all the top of the leaves. Example: - # admin is not included - # admin/new is included - # Also, do not include the root/homepage path. It's already created - # in api-gateway.yml - Jets::Router.all_paths.reject { |p| p.blank? } - end - end - end -end diff --git a/lib/jets/cfn/builder/api/resources.rb b/lib/jets/cfn/builder/api/resources.rb deleted file mode 100644 index 7051b25de..000000000 --- a/lib/jets/cfn/builder/api/resources.rb +++ /dev/null @@ -1,40 +0,0 @@ -module Jets::Cfn::Builder::Api - class Resources < Paged - # interface method - def compose - add_gateway_resources - add_rest_api_parameter - end - - # interface method - def template_path - Jets::Names.api_resources_template_path(@page_number) - end - - def add_rest_api_parameter - add_parameter(:RestApi) - end - - def add_gateway_resources - @items.each do |path| - # IE: resource = Jets::Cfn::Resource::ApiGateway::Resource.new(path) - next if path == '/' || path == '' # skip root path - resource = api_gateway_resource_class.new(path) - add_resource(resource) - add_outputs(resource.outputs) - - parent_path = resource.parent_path_parameter - add_parameter(parent_path) unless part_of_template?(parent_path) - end - end - - # interface method - def api_gateway_resource_class - Jets::Cfn::Resource::ApiGateway::Resource - end - - def part_of_template?(parent_path) - @template[:Resources].key?(parent_path) - end - end -end diff --git a/lib/jets/cfn/builder/authorizer.rb b/lib/jets/cfn/builder/authorizer.rb deleted file mode 100644 index a59283f99..000000000 --- a/lib/jets/cfn/builder/authorizer.rb +++ /dev/null @@ -1,64 +0,0 @@ -class Jets::Cfn::Builder - class Authorizer < Nested - include Interface - include Jets::AwsServices - - def initialize(path) - @path = path # IE: app/authorizers/main_authorizer.rb - @app_class = Jets::Klass.from_path(path) - @template = ActiveSupport::HashWithIndifferentAccess.new(Resources: {}) - end - - # interface method - def compose - add_common_parameters - add_api_gateway_parameters - add_functions - add_resources - add_outputs - # These dont have lambda functions associated with them - add_cognito_authorizers - add_cognito_outputs - end - - # interface method - def template_path - Jets::Names.authorizer_template_path(@path) - end - - def add_cognito_authorizers - @app_class.cognito_authorizers.each do |authorizer| - resource = Jets::Cfn::Resource.new(authorizer[:definition], authorizer[:replacements]) - add_resource(resource) - end - end - - def add_outputs - # IE: @app_class = MainAuthorizer - # The associated resource is the Jets::Cfn::Resource::ApiGateway::Authorizer definition built during evaluation - # of the user defined Authorizer class. We'll use it to compute the logical id. - @app_class.definitions.each do |definition| - authorizer = definition.associated_resources.first - next unless authorizer - - resource = Jets::Cfn::Resource.new(authorizer.definition, definition.replacements) - logical_id = resource.logical_id - add_output(logical_id, value: "!Ref #{logical_id}") - end - end - - def add_cognito_outputs - @app_class.cognito_authorizers.each do |authorizer| - resource = Jets::Cfn::Resource.new(authorizer[:definition], authorizer[:replacements]) - logical_id = resource.logical_id - add_output(logical_id, value: "!Ref #{logical_id}") - end - end - - def add_api_gateway_parameters - return if Jets::Router.no_routes? - - add_parameter(:RestApi) - end - end -end diff --git a/lib/jets/cfn/builder/controller.rb b/lib/jets/cfn/builder/controller.rb deleted file mode 100644 index 166f47f87..000000000 --- a/lib/jets/cfn/builder/controller.rb +++ /dev/null @@ -1,28 +0,0 @@ -class Jets::Cfn::Builder - class Controller < Nested - # interface method - def compose - add_common_parameters - add_api_gateway_parameters - add_functions - add_resources - add_outputs - end - - def add_outputs - outputs = {} - @template[:Resources].each do |logical_id, resource| - next unless resource[:Type] == "AWS::Lambda::Function" - outputs.merge!(logical_id => { - Value: "!GetAtt #{logical_id}.Arn" - }) - end - @template[:Outputs] = outputs - end - - def add_api_gateway_parameters - return if Jets::Router.no_routes? - add_parameter(:RestApi) - end - end -end diff --git a/lib/jets/cfn/builder/function.rb b/lib/jets/cfn/builder/function.rb deleted file mode 100644 index d7eb853bc..000000000 --- a/lib/jets/cfn/builder/function.rb +++ /dev/null @@ -1,9 +0,0 @@ -class Jets::Cfn::Builder - class Function < Nested - # interface method - def compose - add_common_parameters - add_functions - end - end -end diff --git a/lib/jets/cfn/builder/interface.rb b/lib/jets/cfn/builder/interface.rb index 36a6cf05b..6c3a3e930 100644 --- a/lib/jets/cfn/builder/interface.rb +++ b/lib/jets/cfn/builder/interface.rb @@ -3,7 +3,7 @@ # * compose # * template_path # -class Jets::Cfn::Builder +module Jets::Cfn::Builder module Interface extend Memoist include Jets::Util::Camelize @@ -46,39 +46,29 @@ def add_description(desc) end def add_parameters(attributes) - attributes.each do |name,value| + attributes.each do |name, value| add_parameter(name.camelize.to_sym, Description: value) end end - def add_parameter(name, options={}) - defaults = { Type: "String" } + def add_parameter(name, options = {}) + defaults = {Type: "String"} options = defaults.merge(options) @template[:Parameters] ||= {} @template[:Parameters][name.camelize.to_sym] = camelize(options) end def add_outputs(attributes) - attributes.each do |name,value| + attributes.each do |name, value| add_output(name.camelize.to_sym, Value: value) end end - def add_output(name, options={}) + def add_output(name, options = {}) @template[:Outputs] ||= {} @template[:Outputs][name.camelize.to_sym] = camelize(options) end - def add_resources - @app_class.definitions.each do |definition| - definition.associated_resources.each do |associated| - resource = Jets::Cfn::Resource.new(associated.definition, definition.replacements) - add_resource(resource) - add_resource(resource.permission) - end - end - end - # Note: Jets::Cfn::Resource::Iam classes are special treated. # They are only a few resources that result in creating 2 CloudFormation resources. # Cfn::Builder::Api::Methods also creates a method, permission, and possible cors resource. @@ -94,15 +84,11 @@ def add_resources # # iam_policy("s3", "sns") # - # and be none-the-wiser about the special behavior. + # and are none-the-wiser about the special behavior. + # def add_resource(resource) add_template_resource(resource.logical_id, resource.type, resource.attributes) - - if resource.class.to_s.include?("Jets::Cfn::Resource::Iam") - role = resource # for clarity: resource is a Iam::*Role class - iam_policy = Jets::Cfn::Resource::Iam::Policy.new(role) - add_template_resource(iam_policy.logical_id, iam_policy.type, iam_policy.attributes) - end + add_outputs(resource.outputs) end # The add_resource method can take an options Hash with both with either @@ -131,16 +117,21 @@ def add_template_resource(logical_id, type, options) options = camelize(options) attributes = if options.include?(:Type) - base = { Type: type } - base.merge(options) # options are top-level attributes - else - { - Type: type, - Properties: options # options are properties - } - end + base = {Type: type} + base.merge(options) # options are top-level attributes + else + { + Type: type, + Properties: options # options are properties + } + end @template[:Resources][logical_id] = attributes end + + # interface method + def config + Jets.bootstrap.config + end end end diff --git a/lib/jets/cfn/builder/job.rb b/lib/jets/cfn/builder/job.rb deleted file mode 100644 index 79bf2f99f..000000000 --- a/lib/jets/cfn/builder/job.rb +++ /dev/null @@ -1,10 +0,0 @@ -class Jets::Cfn::Builder - class Job < Nested - # interface method - def compose - add_common_parameters - add_functions - add_resources - end - end -end diff --git a/lib/jets/cfn/builder/nested.rb b/lib/jets/cfn/builder/nested.rb deleted file mode 100644 index aea969813..000000000 --- a/lib/jets/cfn/builder/nested.rb +++ /dev/null @@ -1,130 +0,0 @@ -# Note: compose implemented by the classes that include this -class Jets::Cfn::Builder - class Nested - include Interface - - # The app_class is can be a controller, job or anonymous function class. - # IE: PostsController, HardJob - def initialize(app_class) - @app_class = app_class - @template = ActiveSupport::HashWithIndifferentAccess.new(Resources: {}) - end - - # interface method - def template_path - Jets::Names.app_template_path(@app_class) - end - - def add_common_parameters - common_parameters = Jets::Cfn::Params::Common.parameters - common_parameters.each do |k,_| - add_parameter(k) - end - - depends_on_params.each do |output_key, _| - add_parameter(output_key) - end - end - - def depends_on_params - return {} if Jets.one_lambda_for_all_controllers? && @app_class.to_s.ends_with?("Controller") - return {} if @app_class.is_a?(Hash) - return {} unless @app_class.depends_on - depends = Jets::Stack::Depends.new(@app_class.depends_on) - depends.params - end - - def add_functions - validate_function_names! - if Jets.one_lambda_per_controller? && @app_class.to_s.ends_with?("Controller") - one_lambda_per_controller - else - one_lambda_per_method - end - end - - def one_lambda_per_method - add_class_iam_policy - @app_class.definitions.each do |definition| - add_function(definition) - add_function_iam_policy(definition) - end - end - - def one_lambda_per_controller - add_class_iam_policy - definition = Jets::Lambda::Definition.new(@app_class, "lambda_handler", - lang: :ruby, - ) - add_function(definition) - unless Jets::Router.no_routes? - controller = Jets::Cfn::Resource::Lambda::Function::Controller.new(definition) - add_resource(controller.permission) - end - end - - def add_function(definition) - resource = Jets::Cfn::Resource::Lambda::Function.new(definition) - add_resource(resource) - # apigw lambda permission is also added to the controller template next to the function - route = Jets::Router.find_by_definition(definition) - if route - # Creates a permission more directly to set principal apigateway.amazonaws.com - method = Jets::Cfn::Resource::ApiGateway::Method.new(route) - add_resource(method.permission) - end - end - - # routes scoped to this controller template. - def scoped_routes - @routes ||= Jets::Router.routes.select do |route| - route.controller_name == @app_class.to_s - end - end - - def add_class_iam_policy - return unless build_class_iam_policy? - - resource = Jets::Cfn::Resource::Iam::ClassRole.new(@app_class) - add_resource(resource) - end - - def build_class_iam_policy? - should_build = false - klass = @app_class - while klass && klass != Object - if klass&.build_class_iam? - should_build = true - break - end - klass = klass.superclass - end - should_build - end - - def add_function_iam_policy(definition) - return unless definition.build_function_iam? - - resource = Jets::Cfn::Resource::Iam::FunctionRole.new(definition) - add_resource(resource) - end - - def validate_function_names! - invalids = @app_class.definitions.reject do |definition| - definition.meth.to_s =~ /^[a-zA-Z][a-zA-Z0-9_]/ - end - return if invalids.empty? - list = invalids.map do |definition| - " #{definition.class_name}##{definition.meth}" # IE: PostsController#index - end.join("\n") - puts "ERROR: Detected invalid AWS Lambda function names".color(:red) - puts <<~EOL - Lambda function names must start with a letter and can only contain letters, numbers, and underscores. - Invalid function names: - - #{list} - EOL - exit 1 - end - end -end diff --git a/lib/jets/cfn/builder/one_controller.rb b/lib/jets/cfn/builder/one_controller.rb deleted file mode 100644 index 722322ad1..000000000 --- a/lib/jets/cfn/builder/one_controller.rb +++ /dev/null @@ -1,53 +0,0 @@ -class Jets::Cfn::Builder - class OneController < Nested - def build? - true - end - - # interface method - def compose - add_one_function - add_common_parameters - add_api_gateway_parameters - add_outputs - end - - # interface method - def template_path - Jets::Names.one_controller_template_path - end - - def add_one_function - resource = Jets::Cfn::Resource::One::Function.new - add_resource(resource) - add_application_controller_iam_policy - unless Jets::Router.no_routes? - permission = Jets::Cfn::Resource::One::Permission.new - add_resource(permission) - end - end - - def add_application_controller_iam_policy - klass = ApplicationController - return unless klass.build_class_iam? - resource = Jets::Cfn::Resource::Iam::ClassRole.new(klass) - add_resource(resource) - end - - def add_outputs - outputs = {} - @template[:Resources].each do |logical_id, resource| - next unless resource[:Type] == "AWS::Lambda::Function" - outputs.merge!(logical_id => { - Value: "!GetAtt #{logical_id}.Arn" - }) - end - @template[:Outputs] = outputs - end - - def add_api_gateway_parameters - return if Jets::Router.no_routes? - add_parameter(:RestApi) - end - end -end diff --git a/lib/jets/cfn/builder/parent.rb b/lib/jets/cfn/builder/parent.rb deleted file mode 100644 index f3f3a64ea..000000000 --- a/lib/jets/cfn/builder/parent.rb +++ /dev/null @@ -1,149 +0,0 @@ -class Jets::Cfn::Builder - class Parent - include Interface - include Jets::AwsServices - include Stagger - - def initialize(options={}) - @options = options - @template = ActiveSupport::HashWithIndifferentAccess.new(Resources: {}) - end - - # interface method - def compose - build_minimal_resources - build_child_resources - end - - # interface method - def template_path - Jets::Names.parent_template_path - end - - def build_minimal_resources - add_description("Jets: #{Jets.version} Code: #{Util::Source.version}") - - # Initial s3 bucket, used to store code zipfile and templates Jets generates - # - # AWS changed the default behavior of s3 buckets to block public access - # https://aws.amazon.com/blogs/aws/amazon-s3-block-public-access-another-layer-of-protection-for-your-accounts-and-buckets/ - # https://github.com/aws-amplify/amplify-cli/issues/12503 - # - # Jets uploads assets to s3 bucket with acl: "public-read" here - # https://github.com/boltops-tools/jets/blob/c5858ec2706a606665a92c3ada3f16ae4c753372/lib/jets/cfn/upload.rb#L97 - # - # Use minimal s3 bucket policy to allow public read access to assets. - # Leave the other options as comments to help document the default behavior. - resource = Jets::Cfn::Resource::S3::JetsBucket.new - add_resource(resource) - add_outputs(resource.outputs) - - return unless full? - # Add application-wide IAM policy from Jets.config.iam_role - resource = Jets::Cfn::Resource::Iam::ApplicationRole.new - add_resource(resource) - add_outputs(resource.outputs) - - return unless Jets.gem_layer? - resource = Jets::Cfn::Resource::Lambda::GemLayer.new - add_resource(resource) - add_outputs(resource.outputs) - end - - def build_child_resources - return unless full? - - add_one_lambda_controller if Jets.one_lambda_for_all_controllers? && Jets.config.mode != "job" - for_each_path(:app) do |path| - add_app_class_stack(path) - end - for_each_path(:shared) do |path| - add_shared_resources(path) - end - - return if Jets::Router.no_routes? - for_each_path(:authorizers) do |path| - add_authorizer_resources(path) - end - add_api_gateway - add_api_resources - add_api_methods - add_api_deployment - add_api_mapping - end - - def full? - @options[:stack_type] == :full - end - - def add_one_lambda_controller - resource = Jets::Cfn::Resource::Nested::OneController.new(@options) - add_child_resources(resource) - end - - # Example paths: - # #{Jets.build_root}/templates/shared-resources.yml - # #{Jets.build_root}/templates/app-comments_controller.yml - # #{Jets.build_root}/templates/authorizers-main_authorizer.yml - def for_each_path(type) - expression = "#{Jets::Names.templates_folder}/#{type}-*" - Dir.glob(expression).each do |path| - next unless File.file?(path) - yield(path) - end - end - - def add_app_class_stack(path) - resource = Jets::Cfn::Resource::Nested::AppClass.new(@options.merge(path: path)) - add_stagger(resource) - add_child_resources(resource) - end - - def add_authorizer_resources(path) - resource = Jets::Cfn::Resource::Nested::Authorizer.new(@options.merge(path: path)) - add_child_resources(resource) - end - - def add_shared_resources(path) - resource = Jets::Cfn::Resource::Nested::Shared.new(@options.merge(path: path)) - add_child_resources(resource) if resource.resources? - end - - def add_api_gateway - resource = Jets::Cfn::Resource::Nested::Api::Gateway.new(@options) - add_child_resources(resource) - end - - def add_api_resources - pages = Api::Pages::Resources.pages - pages.each do |page| - resource = Jets::Cfn::Resource::Nested::Api::Resources.new(@options.merge(page_number: page.number)) - add_child_resources(resource) - end - end - - def add_api_methods - pages = Api::Pages::Methods.pages - pages.each do |page| - resource = Jets::Cfn::Resource::Nested::Api::Methods.new(@options.merge(page_number: page.number)) - add_child_resources(resource) - end - end - - def add_api_deployment - resource = Jets::Cfn::Resource::Nested::Api::Deployment.new(@options) - add_child_resources(resource) - end - - def add_api_mapping - return unless Jets.custom_domain? - resource = Jets::Cfn::Resource::Nested::Api::Mapping.new(@options) - add_child_resources(resource) - end - - def add_child_resources(resource) - add_resource(resource) - add_outputs(resource.outputs) - end - end -end diff --git a/lib/jets/cfn/builder/parent/genesis.rb b/lib/jets/cfn/builder/parent/genesis.rb new file mode 100644 index 000000000..1791f4dc5 --- /dev/null +++ b/lib/jets/cfn/builder/parent/genesis.rb @@ -0,0 +1,84 @@ +module Jets::Cfn::Builder::Parent + class Genesis + extend Memoist + include Jets::AwsServices + include Jets::Cfn::Builder::Interface + include Jets::Util::Logging + + def initialize(options) + @options = options + @template = ActiveSupport::HashWithIndifferentAccess.new(Resources: {}) + end + + # interface method + def compose + clean + add_description("Jets: #{Jets::VERSION}") + add_resource(Jets::Cfn::Resource::S3::JetsBucket.new) + + # codebuild resources + config = Jets.bootstrap.config # want bootstrap not deploy config here + add_resource(Jets::Cfn::Resource::Codebuild::IamRole.new) + unless config.infra + add_resource(Jets::Cfn::Resource::Codebuild::Project::Ec2.new) + end + if config.infra || config.codebuild.lambda.enable + add_resource(Jets::Cfn::Resource::Codebuild::Project::Lambda.new) + end + if config.codebuild.fleet.enable + add_resource(Jets::Cfn::Resource::Codebuild::Fleet.new) + end + + merge_existing_template! if @options[:bootstrap] + end + + # interface method: Finale overrides + def clean? + @options[:bootstrap] + end + + def clean + return unless clean? + templates_path = "#{Jets.build_root}/templates" + logger.debug "Parent Genesis clean #{templates_path}" + FileUtils.rm_rf(templates_path) + end + + # interface method + def template_path + Jets::Names.parent_template_path + end + + # Note: Tried concept of marking resources as a genesis resource as part of + # add_resource but that approach cannot handle deletion of resources. + # So we need a pre-defined list of genesis resources. + class_attribute :genesis_resources + self.genesis_resources = %w[S3Bucket Codebuild CodebuildRole CodebuildFleet] + delegate :genesis_resources, to: :class + + # This is how cfn delta updates are achieved. + def merge_existing_template! + existing = existing_template + return unless existing + + # Delete resources managed by the bootstrap genesis stack. + existing["Resources"].delete_if { |k, v| genesis_resources.include?(k) } + # Note: In case Outputs are all deleted by something else in the future + # Finale stack does not delete outputs, it filters them. + # Genesis resource outputs names should match the genesis resource logic id. + outputs = existing["Outputs"] + outputs&.delete_if { |k, v| genesis_resources.include?(k) } + + @template.deep_merge!(existing) + end + + def existing_template + stack_name = Jets::Names.parent_stack_name + template_body = cfn.get_template(stack_name: stack_name).template_body + # TODO: vs Jets::Util::Yamler + Jets::Cfn::Stack::Yamler.load(template_body) + rescue Aws::CloudFormation::Errors::ValidationError => e + raise unless /does not exist/.match?(e.message) + end + end +end diff --git a/lib/jets/cfn/builder/parent/stagger.rb b/lib/jets/cfn/builder/parent/stagger.rb deleted file mode 100644 index 31498a2ab..000000000 --- a/lib/jets/cfn/builder/parent/stagger.rb +++ /dev/null @@ -1,34 +0,0 @@ -class Jets::Cfn::Builder::Parent - module Stagger - def add_stagger(resource) - batch_size = stagger_batch_size # shorter convenience variable - return if !stagger_enabled || batch_size.nil? || batch_size == 0 - - # initialize all here to keep logic together - @previous_stacks ||= [] - @added_count ||= 0 - - if @previous_stacks.size >= batch_size - at_boundary = @added_count % batch_size == 0 - if at_boundary - @left = @added_count - batch_size - @right = @left + batch_size - 1 - end - previous_stack_batch = @previous_stacks[@left..@right] - resource.add_stagger_depends_on(previous_stack_batch) - end - - @added_count += 1 - @previous_stacks << resource - end - - def stagger_batch_size - Jets.config.deploy.stagger.batch_size - end - - # for spec-ing - def stagger_enabled - Jets.config.deploy.stagger.enabled - end - end -end diff --git a/lib/jets/cfn/builder/post_process.rb b/lib/jets/cfn/builder/post_process.rb index 9cae76dc5..3a0ae3c34 100644 --- a/lib/jets/cfn/builder/post_process.rb +++ b/lib/jets/cfn/builder/post_process.rb @@ -1,7 +1,7 @@ # post process the text so that # "!Ref IamRole" => !Ref IamRole # We strip the surrounding quotes -class Jets::Cfn::Builder +module Jets::Cfn::Builder class PostProcess def initialize(text) @text = text @@ -10,10 +10,10 @@ def initialize(text) def process results = @text.split("\n").map do |line| if line.include?(': "!') # IE: IamRole: "!Ref IamRole", - # IamRole: "!Ref IamRole" => IamRole: !Ref IamRole + # IamRole: "!Ref IamRole" => IamRole: !Ref IamRole line.sub(/: "(.*)"/, ': \1') elsif line.include?('- "!') # IE: - "!GetAtt Foo.Arn" - # IamRole: - "!GetAtt Foo.Arn" => - !GetAtt Foo.Arn + # IamRole: - "!GetAtt Foo.Arn" => - !GetAtt Foo.Arn line.sub(/- "(.*)"/, '- \1') else line diff --git a/lib/jets/cfn/builder/rule.rb b/lib/jets/cfn/builder/rule.rb deleted file mode 100644 index 1d95c1855..000000000 --- a/lib/jets/cfn/builder/rule.rb +++ /dev/null @@ -1,20 +0,0 @@ -class Jets::Cfn::Builder - class Rule < Nested - # interface method - def compose - add_common_parameters - add_functions - add_resources - add_managed_rules - end - - # Handle config_rules associated with aws managed rules. - # List of AWS Config Managed Rules: https://amzn.to/2BOt9KN - def add_managed_rules - @app_class.managed_rules.each do |rule| - resource = Jets::Cfn::Resource.new(rule[:definition], rule[:replacements]) - add_resource(resource) - end - end - end -end diff --git a/lib/jets/cfn/builder/shared.rb b/lib/jets/cfn/builder/shared.rb deleted file mode 100644 index 1a099edcb..000000000 --- a/lib/jets/cfn/builder/shared.rb +++ /dev/null @@ -1,15 +0,0 @@ -class Jets::Cfn::Builder - class Shared < Nested - # interface method - def compose - stack = @app_class.new # @app_class is subclass. IE: Alarm < Jets::Stack - builder = Jets::Stack::Builder.new(stack) - @template = builder.template # overwrite entire @template - end - - # interface method - def template_path - Jets::Names.shared_template_path(@app_class) - end - end -end diff --git a/lib/jets/cfn/builder/util/source.rb b/lib/jets/cfn/builder/util/source.rb deleted file mode 100644 index 9376b3b98..000000000 --- a/lib/jets/cfn/builder/util/source.rb +++ /dev/null @@ -1,21 +0,0 @@ -module Jets::Cfn::Builder::Util - class Source - class << self - def version - return '' unless git_installed? - sha = sh "git rev-parse HEAD 2>/dev/null" - return '' if sha == '' # if its not a git repo, it'll be an empty string - sha[0..7] - end - - private - def git_installed? - system("type git > /dev/null 2>&1") - end - - def sh(command) - `#{command}`.strip - end - end - end -end diff --git a/lib/jets/cfn/delete.rb b/lib/jets/cfn/delete.rb new file mode 100644 index 000000000..1d4da64ca --- /dev/null +++ b/lib/jets/cfn/delete.rb @@ -0,0 +1,17 @@ +module Jets::Cfn + class Delete < Stack + def run + bootstrap_if_needed + Jets::Remote::Runner.new(@options.merge(dummy: true, command: "delete")).run + Teardown.new(@options).run + end + + # In case user has deleted stack already and needs to delete the Jets API deployment record. + def bootstrap_if_needed + stack_name = Jets.project.namespace + return if stack_exists?(stack_name) + log.info "Creating dummy stack for deletion: #{stack_name}" + Jets::Cfn::Bootstrap.new(@options).run + end + end +end diff --git a/lib/jets/cfn/deploy.rb b/lib/jets/cfn/deploy.rb new file mode 100644 index 000000000..0cb4ddc6d --- /dev/null +++ b/lib/jets/cfn/deploy.rb @@ -0,0 +1,151 @@ +module Jets::Cfn + class Deploy < Stack + def sync + delete_rollback_complete! + continue_update_rollback! + check_deployable! + + log.debug "Parent template changed: #{changed?.inspect}" + # return true when no changes so deploy will continue and start remote runner + return true unless changed? + + # bootstrap can be "delete" or true + # Set quiet before stack exists check + quiet_sync = @options[:bootstrap] == true && stack_exists?(stack_name) + deploy_message + set_resource_tags + begin + sync_stack + rescue Aws::CloudFormation::Errors::InsufficientCapabilitiesException => e + capabilities = e.message.match(/\[(.*)\]/)[1] + confirm = prompt_for_iam(capabilities) + if /^y/.match?(confirm) + @options.merge!(capabilities: [capabilities]) + log.info "Re-running: #{command_with_iam(capabilities).color(:green)}" + retry + else + log.error "ERROR: Unable to deploy #{e.message}" + exit 1 + end + end + + # waits for /(_COMPLETE|_FAILED)$/ status to see if successfully deployed + success = cfn_status.wait(quiet: quiet_sync) + if success + log.info "Bootstrap synced" if @options[:bootstrap] == true # dont use quiet_sync + else + if quiet_sync # show full cfn stack status if quiet_sync + cfn_status.run + end + cfn_status.failure_message + end + success + end + + # note: required method: rollback.rb module uses + def cfn_status + Jets::Cfn::Status.new + end + memoize :cfn_status + + # CloudFormation always performs an update there are nested templates, even when the + # parent template has not changed at all. + # Note: Jets ressurects the template body with a slight difference + # !Ref S3Bucket vs {Ref: S3Bucket} + # However, tested with the exact template body without this difference + # and cloudformation still performs an update. + # So we have to check if the template has changed ourselves. + # This is useful for the bootstrap genesis update. + def changed? + return true if @options[:force_changed] + return true unless stack_exists?(stack_name) + + template_body = cfn.get_template(stack_name: stack_name).template_body + existing = Yamler.load(template_body) + fresh = Yamler.load(template.body) + + FileUtils.mkdir_p("#{Jets.build_root}/cfn/diff") + IO.write("#{Jets.build_root}/cfn/diff/existing.yml", YAML.dump(existing)) + IO.write("#{Jets.build_root}/cfn/diff/fresh.yml", YAML.dump(fresh)) + + existing != fresh + end + memoize :changed? + + def deploy_message + if @options[:bootstrap_message] + log.info "#{@options[:bootstrap_message]}: #{stack_name}" + elsif @options[:bootstrap] + log.info "Syncing bootstrap: #{stack_name}" + else + log.info "Deploying app: #{stack_name}" + end + end + + def set_resource_tags + @tags = Jets.bootstrap.config.cfn.resource_tags.map { |k, v| {key: k, value: v} } + end + + def sync_stack + if stack_exists?(stack_name) + update_stack + else + create_stack + end + end + + def create_stack + # initial create stack template is on filesystem + cfn.create_stack(stack_options) + end + + def update_stack + cfn.update_stack(stack_options) + rescue Aws::CloudFormation::Errors::ValidationError => e + log.debug "DEBUG bootstrap/deploy.rb update_stack #{e.message}" # ERROR: No updates are to be performed. + true + end + + # options common to both create_stack and update_stack + def stack_options + { + stack_name: stack_name, + capabilities: capabilities, + tags: @tags + }.merge(template.template_option) + end + + def template + Template.new(@options) + end + memoize :template + + # All CloudFormation states listed here: + # http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-describing-stacks.html + def stack_status + resp = cfn.describe_stacks(stack_name: stack_name) + status = resp.stacks[0].stack_status + [resp, status] + end + + def prompt_for_iam(capabilities) + log.info "This stack will create IAM resources. Please approve to run the command again with #{capabilities} capabilities." + log.info " #{command_with_iam(capabilities)}" + + log.info "Please confirm (y/n)" + $stdin.gets # confirm + end + + def command_with_iam(capabilities) + "#{File.basename($0)} #{ARGV.join(" ")} --capabilities #{capabilities}" + end + + def capabilities + ["CAPABILITY_IAM", "CAPABILITY_NAMED_IAM"] + end + + def stack_name + Jets::Names.parent_stack_name + end + end +end diff --git a/lib/jets/cfn/deployment.rb b/lib/jets/cfn/deployment.rb deleted file mode 100644 index 91da14f74..000000000 --- a/lib/jets/cfn/deployment.rb +++ /dev/null @@ -1,85 +0,0 @@ -module Jets::Cfn - class Deployment - extend Memoist - include Jets::AwsServices - include Jets::Command::AwsHelpers - include Jets::Command::ApiHelpers - - def initialize(options={}) - @options = options - @stack_name = options[:stack_name] # stack name or stack id (deleted) - @rollback_version = options[:rollback_version] - end - - def create - Jets.boot # needed since Jets is lazy loaded - return if disabled? - @stack = find_stack(@stack_name) - record_deployment if @stack - end - - def delete - Jets.boot # needed since Jets is lazy loaded - return if disabled? - @stack = find_stack(@stack_name) - delete_deployment if @stack - end - - def delete_deployment - resp = Jets::Api::Stack.retrieve("current") - return if resp["error"] == "not_found" - return unless resp["id"] - resp = Jets::Api::Stack.delete(resp["id"]) - puts resp["message"] # IE: Stack demo-dev deleted - resp - rescue Jets::Api::RequestError => e - puts "WARNING: Unable to delete release and stack. #{e.class}: #{e.message}" - end - - def record_deployment - params = stack_params.merge(git_info.params) - params["message"] = create_message - resp = Jets::Api::Release.create(params) - # Instead of check_for_error_message!(resp) we want to customize it a bit - if resp && resp["error"] - $stderr.puts "WARN: There was an error creating the release." - $stderr.puts "WARN: #{resp["error"]}" - exit 1 - end - puts "Release version: #{resp["version"]}" if resp["version"] - resp - rescue Jets::Api::RequestError => e - puts "WARNING: Unable to create release. #{e.class}: #{e.message}" - end - - def stack_params - { - stack_arn: @stack.stack_id, - stack_status: @stack.stack_status, - message: create_message, - deploy_user: deploy_user, - } - end - - def deploy_user - ENV['JETS_DEPLOY_USER'] || git_info.user.first_name || ENV['USER'] - end - - def git_info - Jets::Git::Info.new(@options) - end - memoize :git_info - - def create_message - if @options[:message] - @options[:message][0..255] - else - @rollback_version ? "Rollback to #{@rollback_version}" : "Deploy" - end - end - - def disabled? - Jets.config.pro.disable || !Jets::Api.token - end - end -end diff --git a/lib/jets/cfn/download.rb b/lib/jets/cfn/download.rb deleted file mode 100644 index d1ebec36c..000000000 --- a/lib/jets/cfn/download.rb +++ /dev/null @@ -1,39 +0,0 @@ -module Jets::Cfn - class Download - include Jets::AwsServices - - def download_templates(version) - bucket = s3_resource.bucket(bucket_name) - unless bucket.exists? - puts "ERROR: The bucket #{bucket_name} does not exist.".color(:red) - exit 1 - end - - # Cleanup templates folder - FileUtils.rm_rf(Jets::Names.templates_folder) - FileUtils.mkdir_p(Jets::Names.templates_folder) - - key_path = "jets/cfn-templates/versions/#{version}" - objects = bucket.objects(prefix: key_path) - if objects.count > 0 - objects.each do |object| - file_name = "#{Jets::Names.templates_folder}/#{object.key.split('/').last}" - object.get(response_target: file_name) - puts "Downloaded #{file_name} from s3://#{bucket_name}/#{object.key}" if ENV['JETS_DEBUG'] - end - else - puts "ERROR: Cannot rollback to this version because the CloudFormation templates are not available.".color(:red) - puts <<~EOL - This can happen the app was deployed before Jets Pro features were enabled, - or if the stack was delete and redeployed. Deleted stacks history are not - rollbackable because their original s3 bucket is deleted. - EOL - exit 1 - end - end - - def bucket_name - Jets.s3_bucket - end - end -end diff --git a/lib/jets/cfn/iam/managed_policy.rb b/lib/jets/cfn/iam/managed_policy.rb new file mode 100644 index 000000000..fbb725572 --- /dev/null +++ b/lib/jets/cfn/iam/managed_policy.rb @@ -0,0 +1,21 @@ +module Jets::Cfn::Iam + # Examples: + # config.codebuild.iam.managed_policies = [AmazonSSMReadOnlyAccess] + class ManagedPolicy + def initialize(policies) + @policies = policies.compact.flatten.uniq + end + + def standardize + return if @policies.nil? || @policies.empty? + + @policies.map do |policy| + if policy.include?("arn:") + policy + else + "arn:aws:iam::aws:policy/#{policy}" + end + end + end + end +end diff --git a/lib/jets/cfn/iam/policy.rb b/lib/jets/cfn/iam/policy.rb new file mode 100644 index 000000000..995fd1c8d --- /dev/null +++ b/lib/jets/cfn/iam/policy.rb @@ -0,0 +1,121 @@ +module Jets::Cfn::Iam + # Examples: + # config.codebuild.iam.policies = ["s3", "ec2"] + # config.codebuild.iam.policies = [ + # "s3", + # { + # PolicyName: "hello", + # PolicyDocument: { + # Version: "2012-10-17", + # Statement: [{Action: ["s3:*"], Effect: "Allow", Resource: "*"}] + # } + # } + # ] + class Policy + def initialize(policy_name, definitions) + @policy_name, @definitions = policy_name, definitions.compact.flatten.uniq + end + + # Returns a standardize policy document. Example: + # { + # PolicyName: "hello", + # PolicyDocument: { + # Version: "2012-10-17", + # Statement: [{Action: ["s3:*"], Effect: "Allow", Resource: "*"}] + # } + # } + # + # A definition is a String or a Hash. It's very close to a Statement + # String: "s3" + # Hash: {Action: ["s3:*"], Effect: "Allow", Resource: "*"} # Action item + def standardize + return if @definitions.nil? || @definitions.empty? + + if @definitions.is_a?(Hash) + standardize_hash(@definitions) # final policy + else # Array of definitions + statement = statement_from_array(@definitions) # statement is Array + # final policy + # Note since we always extract the statement we ignore the PolicyDocument Version + # and always use 2012-10-17 + { + PolicyName: @policy_name, + PolicyDocument: { + Version: "2012-10-17", + Statement: statement + } + } + end + end + + def statement_from_array(definitions) + if definitions.all? { |definition| definition.is_a?(String) } + statement_from_all_strings(definitions) + else + definitions.map do |definition| + if definition.is_a?(String) + statement_from_string(definition) + else # assume hash + statement_from_hash(definition) # possible Array or Hash + end + end.flatten # due to statement_from_hash + end + end + + def all_actions_colon_star(action) + action.include?(":") ? action : "#{action}:*" + end + + def statement_from_all_strings(definitions) + action = definitions.map do |definition| + all_actions_colon_star(definition) + end + [Action: action, Effect: "Allow", Resource: "*"] + end + + def statement_from_string(definition) + action = [all_actions_colon_star(definition)] + [Action: action, Effect: "Allow", Resource: "*"] + end + + def statement_from_hash(definition) + if definition.key?(:Statement) # full PolicyDocument. Has Version and Statement + # Will have an Array of Statements that needs to be flattened later + definition[:Statement] # This is an Array + elsif definition.key?(:Action) + definition + else + definition.merge(Action: [all_actions_colon_star(definition[:Action])]) + end + end + + # Example return value: + # - Effect: Allow + # Action: '*' + # Resource: '*' + def standardize_hash(hash) + if hash.key?(:Action) + { + PolicyName: @policy_name, + PolicyDocument: { + Version: "2012-10-17", + Statement: [hash] + } + } + elsif hash.key?(:Statement) + { + PolicyName: @policy_name, + PolicyDocument: hash + } + elsif hash.key?(:PolicyDocument) + if hash.key?(:PolicyName) + hash # full hash with both PolicyName and PolicyDocument + else + hash.merge(PolicyName: @policy_name) # almost full hash with PolicyDocument + end + else + raise "Invalid hash format: #{hash.inspect}" + end + end + end +end diff --git a/lib/jets/cfn/params/api/base.rb b/lib/jets/cfn/params/api/base.rb deleted file mode 100644 index 4e576b14f..000000000 --- a/lib/jets/cfn/params/api/base.rb +++ /dev/null @@ -1,39 +0,0 @@ -# Params are an attempt to centralize the logic for building parameters. -# Params are built at two different times: -# 1. Cfn::Builders::Api::Methods.build_pages -# => Api::Pages::Methods => api-methods-1.yml nested stack -# Only uses keys. -# 2. Cfn::Builders::Parent -# => Resource::Nested::Api::Methods => parent.yml logical id ApiMethods1 -# Uses both keys and values. -module Jets::Cfn::Params::Api - class Base - extend Memoist - - def initialize(options={}) - @options = options - @template = load_template # current paged template_path - @params = ActiveSupport::HashWithIndifferentAccess.new - end - - def params - build # interface method - @params - end - memoize :params - - # Nice to be able to use template or template_path so the common Template.load_file - # is centralized. - def load_template - if @options[:template] - # At Cfn::Builders::Api::Methods build time, template is in memory - @options[:template] - else - # At Resource::Nested::Api::Methods build time, template is on disk - Jets::Cfn::Template.load_file(@options[:template_path]) - end - end - - def build; end # noop by default - end -end diff --git a/lib/jets/cfn/params/api/cors.rb b/lib/jets/cfn/params/api/cors.rb deleted file mode 100644 index 2957bfadf..000000000 --- a/lib/jets/cfn/params/api/cors.rb +++ /dev/null @@ -1,10 +0,0 @@ -module Jets::Cfn::Params::Api - class Cors < Base - # interface method - def build - resources = Resources.new(@options).params - methods = Methods.new(@options).params - @params = resources.merge(methods) - end - end -end diff --git a/lib/jets/cfn/params/api/methods.rb b/lib/jets/cfn/params/api/methods.rb deleted file mode 100644 index f0e845cf2..000000000 --- a/lib/jets/cfn/params/api/methods.rb +++ /dev/null @@ -1,69 +0,0 @@ -module Jets::Cfn::Params::Api - class Methods < Base - # interface method - def build - @params.merge!(RestApi: "!GetAtt ApiGateway.Outputs.RestApi") # common - @template[:Resources].each do |logical_id, resource| - create_params_from_resource(resource) - end - end - - def create_params_from_resource(resource) - case resource[:Type] - when "AWS::ApiGateway::Method" - # function name - create_function_name_param(resource) - # authorizer id - create_authorizer_param(resource) - # resource id - resource_id = resource[:Properties][:ResourceId].sub("!Ref ", "") - @params.merge!(resource_id => api_method_stack_value(resource_id)) - when "AWS::Lambda::Permission" - # function name - function_name = resource[:Properties][:FunctionName].sub("!Ref ", "") - @params.merge!(function_name => controller_stack_value(function_name)) - end - end - - def create_authorizer_param(resource) - authorizer_id_ref = resource[:Properties][:AuthorizerId] - return unless authorizer_id_ref - authorizer_id = authorizer_id_ref.sub("!Ref ", "") - @params.merge!(authorizer_id => api_authorizer_stack_value(authorizer_id)) - end - - # IE: !GetAtt MainAuthorizer.Outputs.MainAuthorizerProtectAuthorizer - def api_authorizer_stack_value(authorizer_id) - stack = authorizer_id.sub(/(.*?)Authorizer.*Authorizer$/, '\1') # IE: Main - stack += "Authorizer" # IE: MainAuthorizer - "!GetAtt #{stack}.Outputs.#{authorizer_id}" - end - - def create_function_name_param(resource) - # Type can be: AWS_PROXY or MOCK (cors) - return unless resource[:Properties][:Integration][:Type] == "AWS_PROXY" - uri = resource[:Properties][:Integration][:Uri] - md = uri.match(%r|functions/\${(.*)}/invocations|) - function_name = md[1] - @params.merge!(function_name => controller_stack_value(function_name)) - end - - # function_name: UpControllerIndexLambdaFunction - def controller_stack_value(function_name) - controller = function_name.sub(/Controller.*/, "Controller") - # IE: !GetAtt UpController.Outputs.UpControllerIndexLambdaFunction - "!GetAtt #{controller}.Outputs.#{function_name}" - end - - def api_method_stack_value(resource_id) - # IE: !GetAtt ApiResources1.Outputs.UpApiResource - api_stack = if resource_id == "RootResourceId" - "ApiGateway" - else - Jets::Cfn::Params::Api::Resources.stack_logical_id(resource_id) - end - "!GetAtt #{api_stack}.Outputs.#{resource_id}" - end - end -end - diff --git a/lib/jets/cfn/params/api/resources.rb b/lib/jets/cfn/params/api/resources.rb deleted file mode 100644 index d5c066596..000000000 --- a/lib/jets/cfn/params/api/resources.rb +++ /dev/null @@ -1,28 +0,0 @@ -module Jets::Cfn::Params::Api - class Resources < Base - # interface method - def build - # For the nested ApiResources template defined in the parent template, we need - # grab the parameters from the other paged ApiResources templates if not in - # the current template. - @template[:Parameters].keys.each do |key| - key = key.to_sym - case key.to_s - when "RestApi" - @params.merge!(key => "!GetAtt ApiGateway.Outputs.RestApi") - when "RootResourceId" - @params.merge!(key => "!GetAtt ApiGateway.Outputs.RootResourceId") - else - @params.merge!(key => "!GetAtt #{self.class.stack_logical_id(key)}.Outputs.#{key}") - end - end - end - - class << self - # IE: path: #{Jets.build_root}/templates/api-resources-1.yml" - def stack_logical_id(parameter) - Jets::Cfn::Template.lookup_logical_id("api-resources", parameter) - end - end - end -end diff --git a/lib/jets/cfn/params/common.rb b/lib/jets/cfn/params/common.rb deleted file mode 100644 index 4d663663e..000000000 --- a/lib/jets/cfn/params/common.rb +++ /dev/null @@ -1,14 +0,0 @@ -module Jets::Cfn::Params - module Common - def parameters - parameters = { - IamRole: "!GetAtt IamRole.Arn", - S3Bucket: "!Ref S3Bucket", - } - parameters[:GemLayer] = "!Ref GemLayer" if Jets.gem_layer? - parameters - end - - extend self - end -end diff --git a/lib/jets/cfn/resource/api_gateway/authorizer.rb b/lib/jets/cfn/resource/api_gateway/authorizer.rb deleted file mode 100644 index 609308a30..000000000 --- a/lib/jets/cfn/resource/api_gateway/authorizer.rb +++ /dev/null @@ -1,82 +0,0 @@ -module Jets::Cfn::Resource::ApiGateway - class Authorizer < Jets::Cfn::Base - def initialize(props={}) - @props = props # associated_properties from dsl.rb - end - - def definition - { - authorizer_logical_id => { - Type: "AWS::ApiGateway::Authorizer", - Properties: props, - } - } - end - - def props - default = { - # AuthorizerCredentials: '', - # AuthorizerResultTtlInSeconds: '', - # AuthType: '', - # IdentitySource: '', # required - # IdentityValidationExpression: '', - # Name: '', - # ProviderARNs: [], - RestApiId: '!Ref RestApi', # Required: Yes - Type: '', # Required: Yes - } - - unless @props[:Type].to_s.upcase == 'COGNITO_USER_POOLS' - @props[:AuthorizerUri] = { # Required: Conditional - "Fn::Join" => ['', [ - 'arn:aws:apigateway:', - "!Ref 'AWS::Region'", - ':lambda:path/2015-03-31/functions/', - {"Fn::GetAtt" => ["{namespace}LambdaFunction", "Arn"]}, - '/invocations' - ]] - } - end - @props[:AuthorizerResultTtlInSeconds] = @props.delete(:ttl) if @props[:ttl] # shorthand - - normalize_type!(@props) - normalize_identity_source!(@props) - default.merge(@props) - end - - def authorizer_logical_id - "{namespace}Authorizer" # IE: protect_authorizer - end - - def outputs - # IE: ProtectAuthorizer: !Ref ProtectAuthorizer - { - logical_id => "!Ref #{logical_id}", - } - end - - private - # Also sets a default if it's not provided - def normalize_type!(props) - type = props[:Type] || :request - @props[:Type] = type.to_s.upcase - end - - # Also sets a default if it's not provided - def normalize_identity_source!(props) - identity_source = props[:IdentitySource] || Jets.config.api.authorizers.default_token_source - # request authorizer type can have multiple identity sources. - # token authorizer type has only one identity source. - # We handle both cases. - identity_sources = identity_source.split(',') # to handle multipe - identity_sources.map! do |source| - if source.include?(".") # if '.' is detected assume full identify source provided - source - else - "method.request.header.#{source}" # convention - end - end - @props[:IdentitySource] = identity_sources.join(',') - end - end -end diff --git a/lib/jets/cfn/resource/api_gateway/base_path/function.rb b/lib/jets/cfn/resource/api_gateway/base_path/function.rb deleted file mode 100644 index f3d8e491a..000000000 --- a/lib/jets/cfn/resource/api_gateway/base_path/function.rb +++ /dev/null @@ -1,57 +0,0 @@ -module Jets::Cfn::Resource::ApiGateway::BasePath - class Function < Jets::Cfn::Base - include Jets::Cfn::Resource::Lambda::Function::Environment - - def definition - { - BasePathFunction: { - Type: "AWS::Lambda::Function", - Properties: { - FunctionName: function_name, - Description: "Jets#base_path", - Code: { - S3Bucket: "!Ref S3Bucket", - S3Key: code_s3_key, - }, - Role: "!GetAtt BasePathRole.Arn", - Handler: handler, - Runtime: get_runtime, - Timeout: 60, - MemorySize: 1536, - Environment: env_properties[:Environment], - Layers: layers, - } - } - } - end - - def get_runtime - props = camelize(Jets.application.config.function.to_h) - props[:Runtime] || Jets.ruby_runtime - end - - def layers - return Jets.config.lambda.layers if Jets.config.pro.disable - ["!Ref GemLayer"] + Jets.config.lambda.layers - end - - # JETS_RESET is respected here because CloudFormation Custom Resources - # do not allow updates to the ServiceToken property. - # Changing the base-path Lambda Function name results in a CloudFormation error: - # - # Modifying service token is not allowed - # - def function_name - "#{Jets.project_namespace}-jets-base-path" - end - - def handler - "handlers/functions/jets/base_path.lambda_handler" - end - - def code_s3_key - checksum = Jets::Builders::Md5.checksums["stage/code"] - "jets/code/code-#{checksum}.zip" # s3_key - end - end -end \ No newline at end of file diff --git a/lib/jets/cfn/resource/api_gateway/base_path/mapping.rb b/lib/jets/cfn/resource/api_gateway/base_path/mapping.rb deleted file mode 100644 index dbbdc1dfb..000000000 --- a/lib/jets/cfn/resource/api_gateway/base_path/mapping.rb +++ /dev/null @@ -1,45 +0,0 @@ -# CloudFormation Docs AWS::ApiGateway::DomainName: https://amzn.to/2BsrSqo -# -# Example: -# -# Type: AWS::ApiGateway::BasePathMapping -# Properties: -# BasePath: String -# DomainName: String -# RestApiId: String -# Stage: String -# -# Currently unable to add base path mapping in-place with CloudFormation. -# The workaround for this is to do it post deployment with raw API calls outside -# of CloudFormation. Leaving this around for now in case there's a workaround -# to get this into CloudFormation instead of raw API calls. Some notes: -# * Also tried to change the domain name of to something like demo-dev-[random].mydomain.com -# That does not work because the domain name has to match the route53 record exactly. -# -module Jets::Cfn::Resource::ApiGateway::BasePath - class Mapping < Jets::Cfn::Base - def definition - function_logical_id = "BasePathFunction" # lambda function that supports custom resource - { - BasePathMapping: { - Type: "Custom::BasePathMapping", - Properties: { - ServiceToken: "!GetAtt #{function_logical_id}.Arn", - # A change to any of these properties updates the CloudFormation Custom Resource - # IE: It runs the Lambda function that implements the custom resource - BasePath: Jets.config.domain.base_path, # '' empty path represents root - DomainName: "!Ref DomainName", - RestApiId: "!Ref RestApi", - Stage: Jets::Cfn::Resource::ApiGateway::Deployment.stage_name, - }, - } - } - end - - def outputs - { - BasePathMapping: "!Ref BasePathMapping", - } - end - end -end \ No newline at end of file diff --git a/lib/jets/cfn/resource/api_gateway/base_path/role.rb b/lib/jets/cfn/resource/api_gateway/base_path/role.rb deleted file mode 100644 index 8dc0f7dfd..000000000 --- a/lib/jets/cfn/resource/api_gateway/base_path/role.rb +++ /dev/null @@ -1,70 +0,0 @@ -module Jets::Cfn::Resource::ApiGateway::BasePath - class Role < Jets::Cfn::Base - extend Memoist - include Jets::AwsServices - - def definition - { - BasePathRole: { - Type: "AWS::IAM::Role", - Properties: { - # RoleName: role_name, - Path: "/", - AssumeRolePolicyDocument: { - Version: "2012-10-17", - Statement: [{ - Effect: "Allow", - Principal: {Service: ["lambda.amazonaws.com"]}, - Action: ["sts:AssumeRole"]} - ] - }, - Policies: [ - PolicyName: "base-path-mapping-policy", # cannot be empty - PolicyDocument: policy_document, - ] - }, - } - } - end - - def policy_document - project_namespace = Jets.project_namespace - default_policy_statements = Jets.application.config.default_iam_policy # Array of Hashes - apigateway = [{ - Action: [ "apigateway:*" ], - Effect: "Allow", - Resource: "arn:aws:apigateway:#{Jets.aws.region}::/restapis/*", # scoped to all restapis because this changes - },{ - Action: [ "apigateway:*" ], - Effect: "Allow", - Resource: "arn:aws:apigateway:#{Jets.aws.region}::/domainnames/*", # scoped to all restapis because this changes - }] - cloudformation = [{ - Action: ["cloudformation:DescribeStacks"], - Effect: "Allow", - Resource: "arn:aws:cloudformation:#{Jets.aws.region}:#{Jets.aws.account}:stack/#{project_namespace}*", - }] - - # Combine the statements - { - Version: '2012-10-17', - Statement: default_policy_statements + apigateway + cloudformation - } - end - - # Duplicated in rest_api/change_detection.rb, base_path/role.rb, rest_api/routes.rb - def rest_api_id - stack_name = Jets::Names.parent_stack_name - return "RestApi" unless stack_exists?(stack_name) - - stack = cfn.describe_stacks(stack_name: stack_name).stacks.first - - api_gateway_stack_arn = lookup(stack[:outputs], "ApiGateway") - - # resources = cfn.describe_stack_resources(stack_name: api_gateway_stack_arn).stack_resources - stack = cfn.describe_stacks(stack_name: api_gateway_stack_arn).stacks.first - rest_api_id = lookup(stack[:outputs], "RestApi") - end - memoize :rest_api_id - end -end diff --git a/lib/jets/cfn/resource/api_gateway/deployment.rb b/lib/jets/cfn/resource/api_gateway/deployment.rb deleted file mode 100644 index bbae801fb..000000000 --- a/lib/jets/cfn/resource/api_gateway/deployment.rb +++ /dev/null @@ -1,73 +0,0 @@ -module Jets::Cfn::Resource::ApiGateway - class Deployment < Jets::Cfn::Base - def definition - { - deployment_logical_id => { - Type: "AWS::ApiGateway::Deployment", - Properties: { - Description: "Version #{timestamp} deployed by jets", - RestApiId: "!Ref #{RestApi.logical_id}", - StageName: stage_name, - } - } - } - end - - def parameters - { - RestApi: "RestApi", - } - end - - def outputs(internal=false) - rest_api = internal ? RestApi.internal_logical_id : "RestApi" - { - RestApiUrl: "!Sub 'https://${#{rest_api}}.execute-api.${AWS::Region}.amazonaws.com/#{stage_name}/'", - } - end - - def depends_on - expression = "#{Jets::Names.templates_folder}/*_controller*" - controller_logical_ids = [] - Dir.glob(expression).each do |path| - next unless File.file?(path) - - controller_name = path.sub("#{Jets::Names.templates_folder}/", '').sub('.yml', '') - # map the path to a camelized logical_id. Example: - # /tmp/jets/demo/templates/demo-dev-2-posts_controller.yml to - # PostsController - controller_logical_id = controller_name.underscore.camelize - - controller_logical_ids << controller_logical_id - end - controller_logical_ids - end - - # stage_name: dev, dev-1, dev-2, etc - def stage_name - self.class.stage_name - end - - def self.stage_name - # Stage name only allows a-zA-Z0-9_ - [Jets.short_env, Jets.extra].compact.join('_').gsub('-','_') - end - - def timestamp - self.class.timestamp - end - - @@timestamp = nil - def self.timestamp - @@timestamp ||= Time.now.strftime("%Y%m%d%H%M%S") - end - - def deployment_logical_id - self.class.logical_id.underscore - end - - def self.logical_id - "ApiDeployment#{timestamp}" - end - end -end \ No newline at end of file diff --git a/lib/jets/cfn/resource/api_gateway/domain_name.rb b/lib/jets/cfn/resource/api_gateway/domain_name.rb deleted file mode 100644 index 108b55d3f..000000000 --- a/lib/jets/cfn/resource/api_gateway/domain_name.rb +++ /dev/null @@ -1,65 +0,0 @@ -# CloudFormation Docs AWS::ApiGateway::DomainName: https://amzn.to/2Bsnmbq -# -# Example: -# -# MyDomainName: -# Type: 'AWS::ApiGateway::DomainName' -# Properties: -# DomainName: api.mydomain.com -# CertificateArn: arn:aws:acm:us-east-1:111122223333:certificate/fb1b9770-a305-495d-aefb-27e5e101ff3 -# -module Jets::Cfn::Resource::ApiGateway - class DomainName < Jets::Cfn::Base - def definition - properties = { - DomainName: domain_name, - EndpointConfiguration: { - Types: endpoint_types - } - } - # Can really only be REGIONAL or EDGE - if endpoint_types.include?("REGIONAL") - properties[:RegionalCertificateArn] = cert_arn - end - if endpoint_types.include?("EDGE") - properties[:CertificateArn] = cert_arn - end - - { - DomainName: { - Type: "AWS::ApiGateway::DomainName", - Properties: properties - } - } - end - - def outputs - { - DomainName: "!Ref DomainName", - } - end - - def domain_name - name = Jets.config.domain.name - if Jets.config.domain.apex - name ||= Jets.config.domain.hosted_zone_name - else - subdomain = Jets.project_namespace - managed_domain_name = "#{subdomain}.#{Jets.config.domain.hosted_zone_name}" - name ||= managed_domain_name - end - - # Strip trailing period if there is one set accidentally or else get this error - # Trailing period should be omitted from domain name (Service: AmazonApiGateway; Status Code: 400; Error Code: BadRequestException - name.sub(/\.$/,'') - end - - def endpoint_types - [Jets.config.domain.endpoint_type].flatten - end - - def cert_arn - Jets.config.domain.cert_arn - end - end -end diff --git a/lib/jets/cfn/resource/api_gateway/method.rb b/lib/jets/cfn/resource/api_gateway/method.rb deleted file mode 100644 index b8cd83fb0..000000000 --- a/lib/jets/cfn/resource/api_gateway/method.rb +++ /dev/null @@ -1,92 +0,0 @@ -# Converts a Jets::Route to a CloudFormation Jets::Cfn::Resource::ApiGateway::Method resource -module Jets::Cfn::Resource::ApiGateway - class Method < Jets::Cfn::Base - include Authorization - - # route - Jets::Route - def initialize(route) - @route = route - end - - def definition - { - method_logical_id => { - Type: "AWS::ApiGateway::Method", - Properties: props - } - } - end - - # Note: The {namespace} in - # functions/${{namespace}LambdaFunction.Arn}/invocations - # is replaced by Jets::Cfn::Resource::Replacer - # - # Jets::Cfn::Resource::ApiGateway::Method - # Jets::Cfn::Resource (attributes delegate to resource) - # Jets::Cfn::Resource => replacer - # - def props - function_logical_id = Jets.one_lambda_for_all_controllers? ? - "JetsControllerLambdaFunction" : - "{namespace}LambdaFunction" - - resource_id = ResourceId.new(@route.path).resource_id - props = { - ResourceId: "!Ref #{resource_id}", - RestApiId: "!Ref #{RestApi.logical_id}", - HttpMethod: @route.http_method, - RequestParameters: {}, - AuthorizationType: authorization_type, - ApiKeyRequired: api_key_required?, - Integration: { - IntegrationHttpMethod: "POST", - Type: "AWS_PROXY", - Uri: "!Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${#{function_logical_id}}/invocations" - }, - MethodResponses: [] - } - props[:AuthorizerId] = authorizer_id if authorizer_id - props[:AuthorizationScopes] = authorization_scopes if authorization_scopes - - props - end - - def method_logical_id - # https://stackoverflow.com/questions/6104240/how-do-i-strip-non-alphanumeric-characters-from-a-string-and-keep-spaces - # Add path to the logical id to allow 2 different paths to be connected to the same controller action. - # Example: - # - # root "jets/public#show" - # any "*catchall", to: "jets/public#show" - # - # Without the path in the logical id, the logical id would be ShowApiMethod for both routes and only the - # last one would be created in the CloudFormation template. - path = @route.path.gsub('*','') - .gsub(/[^0-9a-z]/i, ' ') - .gsub(/\s+/, '_') - path = nil if path == '' - http_verb = @route.http_method.downcase - [http_verb, path, "api_method"].compact.join('_') - end - - def replacements - # Mimic task to grab replacements - # Use functions/${namespace} in Uri - resources = [definition] - action_name = Jets.one_lambda_per_controller? ? "lambda_handler" : @route.action_name - task = Jets::Lambda::Definition.new(@route.controller_name, action_name, - resources: resources) - task.replacements - end - - private - - def controller_klass - @controller_klass ||= "#{controller_name}_controller".camelize.constantize - end - - def controller_name - @controller_name ||= @route.to.split('#').first - end - end -end diff --git a/lib/jets/cfn/resource/api_gateway/method/authorization.rb b/lib/jets/cfn/resource/api_gateway/method/authorization.rb deleted file mode 100644 index 0aa41fa90..000000000 --- a/lib/jets/cfn/resource/api_gateway/method/authorization.rb +++ /dev/null @@ -1,41 +0,0 @@ -class Jets::Cfn::Resource::ApiGateway::Method - module Authorization - private - def authorizer_id - if @route.authorizer - logical_id = @route.authorizer_id - elsif controller_klass.authorizer - logical_id = controller_klass.authorizer_logical_id_for(@route.action_name) - end - - "!Ref #{logical_id}" if logical_id - end - - def authorization_type - type = @route.authorization_type || - controller_klass.authorization_type || # Already handles inheritance via class_attribute, applies controller-wide - controller_klass.infer_authorization_type_for(@route.action_name) || # Applies specifically to route - Jets.config.api.authorization_type - type.to_s.upcase - end - - def api_key_required? - api_key_required == true - end - - def api_key_required - @route.api_key_required || - controller_klass.api_key_required || - Jets.config.api.api_key_required - end - - def authorization_scopes - if @route.authorization_scopes - authorization_scopes = @route.authorization_scopes - elsif controller_klass.authorization_scopes - authorization_scopes = controller_klass.authorization_scopes - end - authorization_scopes - end - end -end \ No newline at end of file diff --git a/lib/jets/cfn/resource/api_gateway/resource.rb b/lib/jets/cfn/resource/api_gateway/resource.rb deleted file mode 100644 index 91def294c..000000000 --- a/lib/jets/cfn/resource/api_gateway/resource.rb +++ /dev/null @@ -1,86 +0,0 @@ -module Jets::Cfn::Resource::ApiGateway - class Resource < Jets::Cfn::Base - def initialize(path, internal: false) - # The original implementation uses path without the leading slash. - # Remove it here so the path_part is calculated correctly. - @path = path.delete_prefix('/') # Examples: "posts/:id/edit" or "posts" - @internal = internal - end - - def definition - { - resource_logical_id => { - Type: "AWS::ApiGateway::Resource", - Properties: { - ParentId: parent_id, - PathPart: path_part, - RestApiId: "!Ref #{RestApi.logical_id(@internal)}", - } - } - } - end - - def outputs - { - logical_id => "!Ref #{logical_id}", - } - end - - def resource_logical_id - if @path == '' - # Not including ApiResource in the logical id so it doesn't collide with a - # user-defined ApiResource that happens to be named RootResourceId. - "RootResourceId" - else - Jets::Cfn::Resource.truncate_id(path_logical_id(@path), "ApiResource") - end - end - - # For parameter description - def desc - path.empty? ? 'Homepage route: /' : "Route for: /#{path}" - end - - def parent_path_parameter - if @path.include?('/') # posts/:id or posts/:id/edit - parent_path = @path.split('/')[0..-2].join('/') - parent_logical_id = path_logical_id(parent_path) - Jets::Cfn::Resource.truncate_id(parent_logical_id, "ApiResource") - else - "RootResourceId" - end - end - - def parent_id - "!Ref " + parent_path_parameter - end - - def path_part - last_part = path.split('/').last - last_part.split('/').map {|s| transform_capture(s) }.join('/') if last_part - end - - # Modify the path to conform to API Gateway capture expressions - def path - @path.split('/').map {|s| transform_capture(s) }.join('/') - end - - def transform_capture(text) - if text.starts_with?(':') - text = text.sub(':','') - text = "{#{text}}" # :foo => {foo} - end - if text.starts_with?('*') - text = text.sub('*','') - text = "{#{text}+}" # *foo => {foo+} - end - text - end - - private - # Similar path_logical_id method in resource/route.rb - def path_logical_id(path) - path.gsub('/','_').gsub(':','').gsub('*','').gsub('-','_').gsub('.','_').camelize - end - end -end diff --git a/lib/jets/cfn/resource/api_gateway/resource_id.rb b/lib/jets/cfn/resource/api_gateway/resource_id.rb deleted file mode 100644 index 569e0f497..000000000 --- a/lib/jets/cfn/resource/api_gateway/resource_id.rb +++ /dev/null @@ -1,25 +0,0 @@ -module Jets::Cfn::Resource::ApiGateway - class ResourceId - def initialize(path) - @path = path - end - - # Used by Method and Cors - def resource_id - @path == '/' ? - "RootResourceId" : - resource_logical_id.camelize + "ApiResource" - end - - # Example: Posts - def resource_logical_id - camelized_path.underscore - end - - def camelized_path - path = @path - path = "homepage" if path == '/' - path.gsub('/','_').gsub(':','').gsub('*','').gsub('.','').camelize - end - end -end diff --git a/lib/jets/cfn/resource/api_gateway/rest_api.rb b/lib/jets/cfn/resource/api_gateway/rest_api.rb deleted file mode 100644 index 63fd0131a..000000000 --- a/lib/jets/cfn/resource/api_gateway/rest_api.rb +++ /dev/null @@ -1,74 +0,0 @@ -module Jets::Cfn::Resource::ApiGateway - class RestApi < Jets::Cfn::Base - def definition - properties = { - Name: Jets::Names.gateway_api_name, - EndpointConfiguration: { Types: endpoint_types } - } - properties[:EndpointConfiguration][:VpcEndpointIds] = vpce_ids if vpce_ids - properties[:BinaryMediaTypes] = binary_media_types if binary_media_types - properties[:Policy] = endpoint_policy if endpoint_policy - - { - internal_logical_id => { - Type: "AWS::ApiGateway::RestApi", - Properties: properties - } - } - end - - def internal_logical_id - self.class.logical_id(true) - end - - def self.logical_id(internal=false) - internal ? internal_logical_id : "RestApi" - end - - @@internal_logical_id = nil - def self.internal_logical_id - @@internal_logical_id ||= LogicalId.new.get - end - - def outputs - { - RestApi: "!Ref #{internal_logical_id}", - Region: "!Ref AWS::Region", - RootResourceId: "!GetAtt #{internal_logical_id}.RootResourceId", - } - end - - def endpoint_types - [Jets.config.api.endpoint_type].flatten - end - - # TODO: Looks like there's a bug with CloudFormation. On an API Gateway update - # we need to pass in the escaped version: multipart~1form-data - # On a brand new API Gateway creation, we need to pass in the unescaped form: - # multipart/form-data - # We are handling this with a full API Gateway replacement instead because it - # can be generalized more easily. - def binary_media_types - types = Jets.config.api.binary_media_types - return nil if types.nil? || types.empty? - - [types].flatten - end - - def endpoint_policy - endpoint_policy = Jets.config.api.endpoint_policy - return nil if endpoint_policy.nil? || endpoint_policy.empty? - - endpoint_policy - end - - private - - def vpce_ids - ids = Jets.config.api.vpc_endpoint_ids - return nil if ids.nil? || ids.empty? - - ids - end - end -end diff --git a/lib/jets/cfn/resource/api_gateway/rest_api/change_detection.rb b/lib/jets/cfn/resource/api_gateway/rest_api/change_detection.rb deleted file mode 100644 index ed99090c4..000000000 --- a/lib/jets/cfn/resource/api_gateway/rest_api/change_detection.rb +++ /dev/null @@ -1,10 +0,0 @@ -class Jets::Cfn::Resource::ApiGateway::RestApi - class ChangeDetection - extend Memoist - include Jets::AwsServices - - def changed? - Routes.changed? - end - end -end diff --git a/lib/jets/cfn/resource/api_gateway/rest_api/logical_id.rb b/lib/jets/cfn/resource/api_gateway/rest_api/logical_id.rb deleted file mode 100644 index 27e124a71..000000000 --- a/lib/jets/cfn/resource/api_gateway/rest_api/logical_id.rb +++ /dev/null @@ -1,98 +0,0 @@ -class Jets::Cfn::Resource::ApiGateway::RestApi - class LogicalId - extend Memoist - include Jets::AwsServices - - def get - return default if ENV['JETS_NO_INTERNET'] - return default unless stack_exists?(parent_stack_name) && api_gateway_exists? - - if changed? - auto_replace_prompt - new_id - else - current - end - end - - def auto_replace_prompt - return if ENV['JETS_API_AUTO_REPLACE'] - return unless Jets::Command.original_cli_command == "deploy" - case Jets.config.api.auto_replace - when nil - puts message.routes_changed - puts message.custom_domain - print "Would you like to continue the deployment? (y/N) " - answer = get_answer - exit 1 unless answer =~ /^y/ - when false - puts message.routes_changed - puts message.auto_replace_disabled - exit 1 - end - end - - def message - Message.new - end - memoize :message - - TIMEOUT_PERIOD = 120 - def get_answer - Timeout::timeout(TIMEOUT_PERIOD) do - $stdin.gets - end - rescue Timeout::Error => e - puts "#{e.class}: #{e.message}".color(:red) - puts "Deployment timeout after #{TIMEOUT_PERIOD}s. Waited too long answer. Exiting." - exit 1 - end - - def changed? - change_detection = ChangeDetection.new - change_detection.changed? - end - - # Takes current logical id and increments the number that is appended to it. - # - # Examples: - # - # RestApi => RestApi1 - # RestApi1 => RestApi2 - # RestApi2 => RestApi3 - # RestApi7 => RestApi8 - def new_id - regexp = /(\d+)/ - md = current.match(regexp) - if md - current.gsub(regexp,'') + (md[1].to_i + 1).to_s - else - current + "1" - end - end - - def current - resources = cfn.describe_stack_resources(stack_name: api_gateway_stack_arn).stack_resources - rest_api = resources.find { |r| r.resource_type == 'AWS::ApiGateway::RestApi' } - rest_api.logical_resource_id - end - memoize :current - - def api_gateway_stack_arn - stack = cfn.describe_stacks(stack_name: parent_stack_name).stacks.first - lookup(stack[:outputs], "ApiGateway") # api_gateway_stack_arn - end - - def api_gateway_exists? - !!api_gateway_stack_arn - end - - def parent_stack_name - Jets::Names.parent_stack_name - end - - def default - "RestApi" - end - end -end diff --git a/lib/jets/cfn/resource/api_gateway/rest_api/logical_id/message.rb b/lib/jets/cfn/resource/api_gateway/rest_api/logical_id/message.rb deleted file mode 100644 index cbca1f9dc..000000000 --- a/lib/jets/cfn/resource/api_gateway/rest_api/logical_id/message.rb +++ /dev/null @@ -1,59 +0,0 @@ -class Jets::Cfn::Resource::ApiGateway::RestApi::LogicalId - class Message - def routes_changed - <<~EOL - Routes Change Detection: Jets has detected that a new brand API Gateway is required to be deployed. - IMPORTANT: This will result in the API Gateway endpoint changing. - EOL - end - - def custom_domain - domain_name = Jets.config.domain.name - if domain_name - <<~EOL - It looks like you have already set up a custom domain. - The domain name: #{domain_name} - - So you should be good to go as the custom domain will be updated with the new API Gateway endpoint. - To avoid this prompt in the future, you can configure: - - config/application.rb - - config.api.auto_replace = true - - More info: custom domain docs: https://rubyonjets.com/docs/routing/custom-domain/ - EOL - else - <<~EOL - To avoid this prompt in the future, you can configure: - - config/application.rb - - config.api.auto_replace = true - - However, you should also set up a custom domain for a "stable" endpoint. - - https://rubyonjets.com/docs/routing/custom-domain/ - - EOL - end - end - - def auto_replace_disabled - <<~EOL - It looks like `config.api.auto_replace = false`. IE: - - config/application.rb - - config.api.auto_replace = false - - The deploy will not continue. See: - - * https://rubyonjets.com/docs/app-config/reference/ - * https://rubyonjets.com/docs/routing/custom-domain/ - - EOL - end - end -end - diff --git a/lib/jets/cfn/resource/api_gateway/rest_api/routes.rb b/lib/jets/cfn/resource/api_gateway/rest_api/routes.rb deleted file mode 100644 index 7d605df52..000000000 --- a/lib/jets/cfn/resource/api_gateway/rest_api/routes.rb +++ /dev/null @@ -1,7 +0,0 @@ -class Jets::Cfn::Resource::ApiGateway::RestApi - class Routes - def self.changed? - Change.new.changed? - end - end -end \ No newline at end of file diff --git a/lib/jets/cfn/resource/api_gateway/rest_api/routes/change.rb b/lib/jets/cfn/resource/api_gateway/rest_api/routes/change.rb deleted file mode 100644 index fc8fbe64b..000000000 --- a/lib/jets/cfn/resource/api_gateway/rest_api/routes/change.rb +++ /dev/null @@ -1,27 +0,0 @@ -# Detects route changes -class Jets::Cfn::Resource::ApiGateway::RestApi::Routes - class Change - include Jets::AwsServices - - def changed? - return false unless parent_stack_exists? - return true if reset? - - # Note: Variable.changed? will likely always true in one_apigw_method_for_all_routes mode - # since parent variables are allowed to vary in Jets v5. - if Jets.config.cfn.build.routes == "one_apigw_method_for_all_routes" - MediaTypes.changed? || To.changed? - else - MediaTypes.changed? || To.changed? || Variable.changed? || Page.changed? - end - end - - def reset? - ENV['JETS_RESET'] || ENV['JETS_API_REPLACE'] || ENV['JETS_REPLACE_API'] - end - - def parent_stack_exists? - stack_exists?(Jets::Names.parent_stack_name) - end - end -end diff --git a/lib/jets/cfn/resource/api_gateway/rest_api/routes/change/base.rb b/lib/jets/cfn/resource/api_gateway/rest_api/routes/change/base.rb deleted file mode 100644 index df3a575a0..000000000 --- a/lib/jets/cfn/resource/api_gateway/rest_api/routes/change/base.rb +++ /dev/null @@ -1,110 +0,0 @@ -class Jets::Cfn::Resource::ApiGateway::RestApi::Routes::Change - class Base - extend Memoist - include Jets::AwsServices - - def self.changed? - new.changed? - end - - # Recreate routes from previously deployed stored state in s3 - def deployed_routes - state = Jets::Router::State.new - data = state.load("routes") - return [] if data.nil? - - data.map do |item| - http_method = item.dig('options','method') || item.dig('options','http_method') - Jets::Router::Route.new( - http_method: http_method, - path: item['path'], - to: item['to'], - engine: item['engine'], - internal: item['internal'], - ) - end - end - memoize :deployed_routes - - def to(resource_id, http_method) - uri = method_uri(resource_id, http_method) - recreate_to(uri) unless uri.nil? - end - - def method_uri(resource_id, http_method) - # https://docs.aws.amazon.com/apigateway/latest/developerguide/limits.html - resp = apigateway.get_method( - rest_api_id: rest_api_id, - resource_id: resource_id, - http_method: http_method - ) - resp.method_integration.uri - end - - # Parses method uri and recreates a Route to argument. - # So: - # "arn:aws:apigateway:us-west-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-west-2:112233445566:function:demo-test-posts_controller-new/invocations" - # Returns: - # posts#new - def recreate_to(method_uri) - md = method_uri.match(/function:(.*)\//) - function_arn = md[1] # IE: demo-dev-posts_controller-new - controller, action = get_controller_action(function_arn) - "#{controller}##{action}" # IE: posts#new - end - - def get_controller_action(function_arn) - if function_arn.include?('_controller-') - controller_action_from_string(function_arn) - else - controller_action_from_api(function_arn) - end - end - - # TODO: If this hits the Lambda Rate limit, then list_functions also contains the Lambda - # function description. So we can paginate through list_functions results and store - # description from there if needed. - # Dont think this will be needed though because controller_action_from_string gets called - # most of the time. Also, user might be able to request their Lambda limit to be increased. - def controller_action_from_api(function_arn) - desc = lambda_function_description(function_arn) - controller, action = desc.split('#') - controller = controller.underscore.sub(/_controller$/,'') - [controller, action] - end - - def controller_action_from_string(function_arn) - controller_action = function_arn.sub("#{Jets.project_namespace}-", '') - md = controller_action.match(/(.*)_controller-(.*)/) - controller = md[1] - controller = controller.gsub('-','/') - action = md[2] - [controller, action] - end - - def lambda_function_description(function_arn) - resp = aws_lambda.get_function(function_name: function_arn) - resp.configuration.description # contains full info: PostsController#index - end - - # Duplicated in rest_api/change_detection.rb, base_path/role.rb, rest_api/routes.rb - def rest_api_id - stack_name = Jets::Names.parent_stack_name - return "RestApi" unless stack_exists?(stack_name) - - stack = cfn.describe_stacks(stack_name: stack_name).stacks.first - - api_gateway_stack_arn = lookup(stack[:outputs], "ApiGateway") - - # resources = cfn.describe_stack_resources(stack_name: api_gateway_stack_arn).stack_resources - stack = cfn.describe_stacks(stack_name: api_gateway_stack_arn).stacks.first - lookup(stack[:outputs], "RestApi") # rest_api_id - end - memoize :rest_api_id - - def new_routes - Jets::Router.routes - end - memoize :new_routes - end -end \ No newline at end of file diff --git a/lib/jets/cfn/resource/api_gateway/rest_api/routes/change/media_types.rb b/lib/jets/cfn/resource/api_gateway/rest_api/routes/change/media_types.rb deleted file mode 100644 index ea9593864..000000000 --- a/lib/jets/cfn/resource/api_gateway/rest_api/routes/change/media_types.rb +++ /dev/null @@ -1,36 +0,0 @@ -class Jets::Cfn::Resource::ApiGateway::RestApi::Routes::Change - class MediaTypes < Base - def changed? - current_binary_media_types != new_binary_media_types - end - - def new_binary_media_types - rest_api = Jets::Cfn::Resource::ApiGateway::RestApi.new - rest_api.binary_media_types - end - memoize :new_binary_media_types - - def current_binary_media_types - return nil unless parent_stack_exists? - - stack = cfn.describe_stacks(stack_name: parent_stack_name).stacks.first - - api_gateway_stack_arn = lookup(stack[:outputs], "ApiGateway") - - stack = cfn.describe_stacks(stack_name: api_gateway_stack_arn).stacks.first - rest_api_id = lookup(stack[:outputs], "RestApi") - - resp = apigateway.get_rest_api(rest_api_id: rest_api_id) - resp.binary_media_types - end - memoize :current_binary_media_types - - def parent_stack_exists? - stack_exists?(parent_stack_name) - end - - def parent_stack_name - Jets::Names.parent_stack_name - end - end -end diff --git a/lib/jets/cfn/resource/api_gateway/rest_api/routes/change/page.rb b/lib/jets/cfn/resource/api_gateway/rest_api/routes/change/page.rb deleted file mode 100644 index d4e555355..000000000 --- a/lib/jets/cfn/resource/api_gateway/rest_api/routes/change/page.rb +++ /dev/null @@ -1,93 +0,0 @@ -class Jets::Cfn::Resource::ApiGateway::RestApi::Routes::Change - class Page < Base - def changed? - route_page_moved? || old_api_template? - end - - def route_page_moved? - moved?(new_pages, deployed_pages) - end - - # Routes page to logical ids - def moved?(new_pages, deployed_pages) - not_moved = true # page has not moved - new_pages.each do |logical_id, new_page_number| - if !deployed_pages[logical_id].nil? && deployed_pages[logical_id] != new_page_number - not_moved = false # page has moved - break - end - end - !not_moved # moved - end - - def new_pages - local_logical_ids_map - end - memoize :new_pages - - def deployed_pages - remote_logical_ids_map - end - memoize :deployed_pages - - # logical id to page map - # Important: In Cfn::Builders::ApiGatewayBuilder, the add_gateway_routes and ApiResourcesBuilder needs to run - # before the parent add_gateway_rest_api method. - def local_logical_ids_map(path_expression="#{Jets::Names.templates_folder}/api-resources-*.yml") - logical_ids = {} # logical id => page number - - Dir.glob(path_expression).each do |path| - md = path.match(/-api-resources-(\d+).yml/) - page_number = md[1] - - template = Jets::Cfn::Template.load_file(path) - template[:Resources].keys.each do |logical_id| - logical_ids[logical_id] = page_number - end - end - - logical_ids - end - - # aws cloudformation describe-stack-resources --stack-name demo-dev-ApiResources1-DYGLIEY3VAWT | jq -r '.StackResources[].LogicalResourceId' - def remote_logical_ids_map - logical_ids = {} # logical id => page number - - parent_resources.each do |resource| - stack_name = resource.physical_resource_id # full physical id can be used as stack name also - regexp = Regexp.new("#{Jets.project_namespace}-ApiResources(\\d+)-") # tricky to escape \d pattern - md = stack_name.match(regexp) - if md - page_number = md[1] - - resp = cfn.describe_stack_resources(stack_name: stack_name) - resp.stack_resources.map(&:logical_resource_id).each do |logical_id| - logical_ids[logical_id] = page_number - end - end - end - - logical_ids - end - - def old_api_template? - logical_resource_ids = parent_resources.map(&:logical_resource_id) - - api_gateway_found = logical_resource_ids.detect do |logical_id| - logical_id == "ApiGateway" - end - return false unless api_gateway_found - - api_resources_found = logical_resource_ids.detect do |logical_id| - logical_id.match(/^ApiResources\d+$/) - end - !api_resources_found # if api_resources_found then it's the new structure. so opposite is old structure - end - - def parent_resources - resp = cfn.describe_stack_resources(stack_name: Jets::Names.parent_stack_name) - resp.stack_resources - end - memoize :parent_resources - end -end diff --git a/lib/jets/cfn/resource/api_gateway/rest_api/routes/change/to.rb b/lib/jets/cfn/resource/api_gateway/rest_api/routes/change/to.rb deleted file mode 100644 index e77e0e945..000000000 --- a/lib/jets/cfn/resource/api_gateway/rest_api/routes/change/to.rb +++ /dev/null @@ -1,30 +0,0 @@ -# Detects route to changes -class Jets::Cfn::Resource::ApiGateway::RestApi::Routes::Change - class To < Base - def changed? - deployed_routes.each do |deployed_route| - next if deployed_route.engine # skip engine routes - next if deployed_route.internal # skip internal routes - - new_route = find_comparable_route(deployed_route) - next unless new_route - - if new_route.to != deployed_route.to - # change in already deployed route has been detected, requires bluegreen deploy - return true - end - end - false # Reaching here means no routes have been changed in a way that requires a bluegreen deploy - end - - # Find a route that has the same path and method. This is a comparable route - # Then we will compare the to or controller action to see if an already - # deployed route has been changed. - def find_comparable_route(deployed_route) - new_routes.find do |new_route| - new_route.path == deployed_route.path && - new_route.http_method == deployed_route.http_method - end - end - end -end diff --git a/lib/jets/cfn/resource/api_gateway/rest_api/routes/change/variable.rb b/lib/jets/cfn/resource/api_gateway/rest_api/routes/change/variable.rb deleted file mode 100644 index cbe297afe..000000000 --- a/lib/jets/cfn/resource/api_gateway/rest_api/routes/change/variable.rb +++ /dev/null @@ -1,35 +0,0 @@ -# Detects route variable changes -class Jets::Cfn::Resource::ApiGateway::RestApi::Routes::Change - class Variable < Base - def changed? - changed = false - deployed_routes.each do |deployed_route| - parent = collision.variable_parent(deployed_route.path) - parent_variables = collision.parent_variables(parent, [deployed_route.path]) - new_parent_variables = collision.parent_variables(parent, new_paths) - - changed = parent_variables.size > 0 && new_parent_variables.size > 0 && - parent_variables != new_parent_variables - break if changed - end - changed - end - - # Only consider paths with variables - def new_paths - new_routes.map(&:path).select {|p| p.include?(':')}.uniq - end - - # Only consider deployed routes with variables - def deployed_routes - deployed_routes = super - deployed_routes.select do |route| - route.path.include?(':') - end - end - - def collision - @collision ||= Jets::Cfn::Resource::ApiGateway::RestApi::Routes::Collision.new - end - end -end diff --git a/lib/jets/cfn/resource/api_gateway/rest_api/routes/collision.rb b/lib/jets/cfn/resource/api_gateway/rest_api/routes/collision.rb deleted file mode 100644 index 8f179e2ad..000000000 --- a/lib/jets/cfn/resource/api_gateway/rest_api/routes/collision.rb +++ /dev/null @@ -1,121 +0,0 @@ -# Detects path variable collisions -class Jets::Cfn::Resource::ApiGateway::RestApi::Routes - class Collision - attr_reader :collisions - def initialize - @collisions = [] - end - - def collision?(paths) - paths = paths_with_variables(paths) - parents = variable_parents(paths) - - collide = false - parents.each do |parent| - collide ||= variable_collision_exists?(parent, paths) - end - collide - end - - def exception - VariableException.new(collision_message) - end - - def collision_message - <<~EOL - There are routes with sibling variables under the same parent that collide. - - Collisions: - #{@collisions.join("\n ")} - - API Gateway only allows one unique variable path You must use the same variable name within - the same parent route path. - Example: /posts/:id and /posts/:post_id/reveal should both be /posts/:id and /posts/:id/reveal. - - Please check your `config/routes.rb` and remove the colliding routes. - More info: http://rubyonjets.com/docs/considerations/api-gateway/ - EOL - end - - def variable_collision_exists?(parent, paths) - paths = paths_with_variables(paths) - variables = parent_variables(parent, paths) - collide = variables.uniq.size > 1 - register_collision(parent, variables) if collide - collide - end - - # register collision for later display - # We don't register the full path but this might be more helpful. - def register_collision(parent, variables) - return unless variables.uniq.size # check again just in case - - variables.each do |v| - @collisions << "#{parent}/#{v}" - end - @collisions.uniq! - end - - def parent_variables(parent, paths) - paths = paths.select do |path| - parent?(parent, path) - end - paths.map do |path| - path.sub("#{parent}/",'').gsub(%r{/.*},'') - end.select { |x| x =~ /^:/ }.uniq.sort - end - - def parent?(parent, path) - return false if parent == path - - parent_parts = parent.split('/') - path_parts = path.split('/') - - n = parent_parts.size-1 - parent_parts[0..n] == path_parts[0..n] - end - - def direct_parent?(parent, path) - leaf = variable_leaf(path) - leaf_parent = leaf.split('/')[0..-2].join('/') - parent == leaf_parent - end - - def variable_parents(paths) - parents = [] - paths = paths_with_variables(paths) - paths.each do |path| - parents << variable_parent(path) - end - parents.uniq.sort - end - - # Strips the path down until only the leaf node part is a variable - # Example: users/:user_id/posts/:post_id/edit - # Returns: users/:user_id/posts - def variable_parent(path) - path = variable_leaf(path) - # drop last variable to leave behind the parent - path.split('/')[0..-2].join('/') - end - - # Strips the path down until only the leaf node part is a variable - # Example: users/:user_id/posts/:post_id/edit - # Returns: users/:user_id/posts - def variable_leaf(path) - return '' unless path.include?(':') - - parts = path.split('/') - is_variable = parts.last.include?(':') - until is_variable - parts.pop - is_variable = parts.last.include?(':') - end - parts[0..-1].join('/') # parent - end - - def paths_with_variables(paths) - paths.select { |p| p.include?(':') }.uniq - end - end -end \ No newline at end of file diff --git a/lib/jets/cfn/resource/api_gateway/rest_api/routes/collision/variable_exception.rb b/lib/jets/cfn/resource/api_gateway/rest_api/routes/collision/variable_exception.rb deleted file mode 100644 index e508f6221..000000000 --- a/lib/jets/cfn/resource/api_gateway/rest_api/routes/collision/variable_exception.rb +++ /dev/null @@ -1,7 +0,0 @@ -class Jets::Cfn::Resource::ApiGateway::RestApi::Routes::Collision - class VariableException < RuntimeError - def initialize(message="Route variable collisions") - super(message) - end - end -end diff --git a/lib/jets/cfn/resource/associated.rb b/lib/jets/cfn/resource/associated.rb index 7a2e9ddf0..d95541baf 100644 --- a/lib/jets/cfn/resource/associated.rb +++ b/lib/jets/cfn/resource/associated.rb @@ -34,4 +34,4 @@ def standardized end memoize :standardized end -end \ No newline at end of file +end diff --git a/lib/jets/cfn/resource/codebuild/fleet.rb b/lib/jets/cfn/resource/codebuild/fleet.rb new file mode 100644 index 000000000..e986f2329 --- /dev/null +++ b/lib/jets/cfn/resource/codebuild/fleet.rb @@ -0,0 +1,48 @@ +module Jets::Cfn::Resource::Codebuild + class Fleet < Jets::Cfn::Base + def definition + { + CodebuildFleet: { + Type: "AWS::CodeBuild::Fleet", + Properties: props + } + } + end + + def props + { + BaseCapacity: base_capacity, # Integer + ComputeType: compute_type, # String + EnvironmentType: environment_type # String + # Name: "", # String + # Tags: "", # [ Tag, ... ] + } + end + + def base_capacity + Jets.bootstrap.config.codebuild.fleet.base_capacity + end + + def compute_type + codebuild_properties[:Environment][:ComputeType] + end + + def environment_type + codebuild_properties[:Environment][:Type] + end + + def outputs + { + "CodebuildFleet" => "!Ref CodebuildFleet" + } + end + + private + + def codebuild_properties + project = Jets::Cfn::Resource::Codebuild::Project::Ec2.new + project.properties + end + memoize :codebuild_properties + end +end diff --git a/lib/jets/cfn/resource/codebuild/iam_role.rb b/lib/jets/cfn/resource/codebuild/iam_role.rb new file mode 100644 index 000000000..d15af9525 --- /dev/null +++ b/lib/jets/cfn/resource/codebuild/iam_role.rb @@ -0,0 +1,63 @@ +module Jets::Cfn::Resource::Codebuild + class IamRole < Jets::Cfn::Base + def definition + { + "CodebuildRole" => { + Type: "AWS::IAM::Role", + Properties: props + } + } + end + + # Do not name this method properties as that's a computed method + def props + text = <<~EOL + AssumeRolePolicyDocument: + Statement: + - Action: + - sts:AssumeRole + Effect: Allow + Principal: + Service: + - codebuild.amazonaws.com + Version: '2012-10-17' + Path: "/" + EOL + props = YAML.load(text).deep_symbolize_keys + props[:Policies] = policies + props[:ManagedPolicyArns] = managed_policy_arns + props + end + + def policies + [default_policy, custom_policy, vpc_policy].flatten.compact + end + + def default_policy + Jets::Cfn::Iam::Policy.new("DefaultPolicy", config.codebuild.iam.default_policy).standardize + end + + def vpc_policy + policy = config.codebuild.iam.default_vpc_policy + if !policy.nil? && !policy.empty? + Jets::Cfn::Iam::Policy.new("VpcPolicy", policy).standardize + end + end + + def custom_policy + Jets::Cfn::Iam::Policy.new("CustomPolicy", config.codebuild.iam.policy).standardize + end + + def managed_policy_arns + [default_managed_policy, custom_managed_policy].flatten.compact + end + + def default_managed_policy + Jets::Cfn::Iam::ManagedPolicy.new(config.codebuild.iam.default_managed_policy).standardize + end + + def custom_managed_policy + Jets::Cfn::Iam::ManagedPolicy.new(config.codebuild.iam.managed_policy).standardize + end + end +end diff --git a/lib/jets/cfn/resource/codebuild/project/base.rb b/lib/jets/cfn/resource/codebuild/project/base.rb new file mode 100644 index 000000000..0698bc53f --- /dev/null +++ b/lib/jets/cfn/resource/codebuild/project/base.rb @@ -0,0 +1,4 @@ +module Jets::Cfn::Resource::Codebuild::Project + class Base < Jets::Cfn::Base + end +end diff --git a/lib/jets/cfn/resource/codebuild/project/ec2.rb b/lib/jets/cfn/resource/codebuild/project/ec2.rb new file mode 100644 index 000000000..955074e73 --- /dev/null +++ b/lib/jets/cfn/resource/codebuild/project/ec2.rb @@ -0,0 +1,125 @@ +module Jets::Cfn::Resource::Codebuild::Project + class Ec2 < Base + def definition + { + codebuild_logical_id => { + Type: "AWS::CodeBuild::Project", + Properties: finalize_properties(props) + } + } + end + + # Do not name this method properties as that's a computed method + def props + { + Name: project_name, + Artifacts: { + Type: "NO_ARTIFACTS" + }, + Environment: environment, + Source: { + Type: "NO_SOURCE", + BuildSpec: build_spec + }, + ServiceRole: "!Ref CodebuildRole", + LogsConfig: { + CloudWatchLogs: { + Status: "ENABLED" + } + } + } + end + + # Only supported for EC2 compute type. Lambda compute type does not support. + def timeout_in_minutes + config.codebuild.project.timeout || config.codebuild.project.timeout_in_minutes + end + + # Certain properties are not supported for certain Compute Types + # We set this at the end based on the final Environment Type + # + # PrivilegedMode: Only supported for non-LAMBDA environment types + # Cache: non-LAMBDA environment types + # + # CREATE_FAILED AWS::CodeBuild::Project Codebuild Cannot specify timeoutInMinutes for lambda compute + # TimeoutInMinutes: 20, # TODO: make this configurable + # CREATE_FAILED AWS::CodeBuild::Project + # Codebuild Cannot specify cache for lambda compute. Error Code: InvalidInputException + # + # PrivilegedMode: true, # Note: does not seem to be actually needed for docker support + # When using PrivilegedMode for ARM_LAMBDA_CONTAINER it'll error though. + # Not officially supporting ARM_LAMBDA_CONTAINER when packaging code anyway. + # Just noting for posterity. + def finalize_properties(props) + if environment[:Type].include?("LAMBDA") + props[:Cache] = { + Type: "NO_CACHE" + } + props[:Environment][:PrivilegedMode] = false + else + props[:Cache] = { + Type: "LOCAL", + Modes: ["LOCAL_DOCKER_LAYER_CACHE", "LOCAL_CUSTOM_CACHE"] + } + props[:TimeoutInMinutes] = timeout_in_minutes # not supported for lambda compute + props[:Environment][:PrivilegedMode] = true + end + + if config.codebuild.fleet.enable + props[:Environment][:Fleet] = { + FleetArn: "!Ref CodebuildFleet" + } + end + + props + end + + def build_spec + <<~EOL + version: 0.2 + + phases: + build: + commands: + - uname -a + EOL + end + + # https://docs.aws.amazon.com/codebuild/latest/userguide/build-env-ref-available.html + def environment + env = compute_type + env.deep_merge!(config.codebuild.project.environment) + env.deep_merge!(EnvironmentVariables: environment_variables) if environment_variables + # sort for correct diffing. Deploy#changed? checks the template body diff + env[:EnvironmentVariables].sort_by! { |h| h[:Name] } + env + end + memoize :environment + + def environment_variables + Env.new.vars + end + + def outputs + { + codebuild_logical_id => "!Ref #{codebuild_logical_id}" + } + end + + # interface method + def codebuild_logical_id + "Codebuild" + end + + # interface method + def project_name + "#{Jets.project.namespace}-remote" + end + + # interface method + def compute_type + # Note: PrivilegedMode is set at the end in finalize_properties based on the final Type + config.codebuild.project.compute_type + end + end +end diff --git a/lib/jets/cfn/resource/codebuild/project/env.rb b/lib/jets/cfn/resource/codebuild/project/env.rb new file mode 100644 index 000000000..17b223897 --- /dev/null +++ b/lib/jets/cfn/resource/codebuild/project/env.rb @@ -0,0 +1,55 @@ +module Jets::Cfn::Resource::Codebuild::Project + class Env + include FormatEnv + + # config/jets/bootstrap.rb + # + # Jets.bootstrap.configure do + # config.codebuild.project.env.vars + # + def vars + vars = Jets.bootstrap.config.codebuild.project.env.vars.symbolize_keys! + standardize_env_vars(vars) + end + + # Used for codebuild.start_build in runner.rb + def pass_vars(overrides = {}) + # config/jets/bootstrap.rb defined ENV vars + env = Jets.bootstrap.config.codebuild.project.env + + vars = {} + pass = (env.default_pass + env.pass).uniq + + # pass vars from your local machine to the codebuild remote runner + pass.each do |x| + ENV.each do |k, v| + k = k.to_s + match = x.is_a?(Regexp) ? k =~ x : k == x + if match && v.is_a?(String) + vars[k.to_sym] = v + end + end + end + + # block gets the final say + vars.reject! do |k, v| + k = k.to_s + env.block.any? do |x| + x.is_a?(Regexp) ? k =~ x : k == x + end + end + + vars.merge!(overrides) + + standardize_env_vars(vars, casing: :underscore_keys) + end + + def always_block + %w[ + JETS_APP_SRC + JETS_SIG + JETS_TEMPLATES + ] + end + end +end diff --git a/lib/jets/cfn/resource/codebuild/project/format_env.rb b/lib/jets/cfn/resource/codebuild/project/format_env.rb new file mode 100644 index 000000000..86c39b877 --- /dev/null +++ b/lib/jets/cfn/resource/codebuild/project/format_env.rb @@ -0,0 +1,35 @@ +module Jets::Cfn::Resource::Codebuild::Project + module FormatEnv + def standardize_env_vars(vars, casing: :camelcase_keys) + map = { + PARAMETER_STORE: "PARAMETER_STORE", + SECRET: "SECRETS_MANAGER", + SECRETS_MANAGER: "SECRETS_MANAGER", + SSM: "PARAMETER_STORE" + } + + vars = vars.reject { |k, v| v.nil? } + + # There's no map! method. So using map and then assigning to vars + vars = vars.map do |k, v| + starts_with = v.to_s.split(":").first + value = if map.key?(starts_with.upcase.to_sym) + v.to_s.sub("#{starts_with}:", "") + else + v + end + type = map[starts_with.upcase.to_sym] || "PLAINTEXT" + { + Name: k.to_s, + Value: value, + Type: type + } + end + vars = vars.sort_by { |h| h[:Name].to_s } + if casing == :underscore_keys + vars.map! { |h| h.transform_keys { |k| k.to_s.underscore.to_sym } } + end + vars + end + end +end diff --git a/lib/jets/cfn/resource/codebuild/project/lambda.rb b/lib/jets/cfn/resource/codebuild/project/lambda.rb new file mode 100644 index 000000000..488d063f6 --- /dev/null +++ b/lib/jets/cfn/resource/codebuild/project/lambda.rb @@ -0,0 +1,15 @@ +module Jets::Cfn::Resource::Codebuild::Project + class Lambda < Ec2 + def codebuild_logical_id + "CodebuildLambda" + end + + def project_name + "#{Jets.project.namespace}-remote-lambda" + end + + def compute_type + config.codebuild.lambda.project.compute_type + end + end +end diff --git a/lib/jets/cfn/resource/config/config_rule.rb b/lib/jets/cfn/resource/config/config_rule.rb deleted file mode 100644 index 6e3449e88..000000000 --- a/lib/jets/cfn/resource/config/config_rule.rb +++ /dev/null @@ -1,74 +0,0 @@ -module Jets::Cfn::Resource::Config - class ConfigRule < Jets::Cfn::Base - def initialize(app_class, meth, props={}) - @app_class = app_class.to_s - @meth = meth - @props = props # associated_properties from dsl.rb - end - - def definition - base = { - config_rule_logical_id => { - Type: "AWS::Config::ConfigRule", - Properties: definition_properties - } - } - - # Explicitly set depends_on to help with CloudFormation random race condition. - # Seems to be a new CloudFormation and AWS Config resource issue. - if definition_properties[:Source][:Owner] == 'CUSTOM_LAMBDA' - base[config_rule_logical_id][:DependsOn] = "{namespace}Permission" - end - - base - end - - # Do not name this method properties, that is a computed method of `Jets::Cfn::Resource` - def definition_properties - { - ConfigRuleName: config_rule_name, - Source: { - Owner: "CUSTOM_LAMBDA", - SourceIdentifier: "!GetAtt {namespace}LambdaFunction.Arn", - SourceDetails: [ - { - EventSource: "aws.config", - MessageType: "ConfigurationItemChangeNotification" - }, - { - EventSource: "aws.config", - MessageType: "OversizedConfigurationItemChangeNotification" - } - ] - }, - }.deep_merge(@props) - end - - def config_rule_logical_id - "{namespace}ConfigRule" - end - - def config_rule_name - app_class = @app_class.underscore.gsub(/_rule$/,'') - ns = namespace - ns = nil if ns == false # for compact - [ns, "#{app_class}_#{@meth}"].compact.join('_').dasherize - end - - def namespace - namespace = nil - klass = @app_class.constantize - while klass != Jets::Lambda::Functions - namespace = klass.rule_namespace - break if namespace or namespace == false - klass = klass.superclass - end - - if namespace.nil? - Jets.project_namespace - else - namespace - end - end - end -end \ No newline at end of file diff --git a/lib/jets/cfn/resource/config/managed_rule.rb b/lib/jets/cfn/resource/config/managed_rule.rb deleted file mode 100644 index b8757f7a6..000000000 --- a/lib/jets/cfn/resource/config/managed_rule.rb +++ /dev/null @@ -1,15 +0,0 @@ -# ManagedRule is just different enough to be a separate class vs being part of the -# ConfigRule class itself. -module Jets::Cfn::Resource::Config - class ManagedRule < ConfigRule - def definition_properties - { - ConfigRuleName: config_rule_name, - Source: { - Owner: "AWS", - SourceIdentifier: @meth.upcase, - }, - }.deep_merge(@props) - end - end -end \ No newline at end of file diff --git a/lib/jets/cfn/resource/events/rule.rb b/lib/jets/cfn/resource/events/rule.rb deleted file mode 100644 index bf40d3d3d..000000000 --- a/lib/jets/cfn/resource/events/rule.rb +++ /dev/null @@ -1,31 +0,0 @@ -module Jets::Cfn::Resource::Events - class Rule < Jets::Cfn::Base - def initialize(props={}) - @props = props # associated_properties from dsl.rb - end - - def definition - { - rule_logical_id => { - Type: "AWS::Events::Rule", - Properties: merged_properties - } - } - end - - # Do not name this method properties, that is a computed method of `Jets::Cfn::Resource` - def merged_properties - { - State: "ENABLED", - Targets: [{ - Arn: "!GetAtt {namespace}LambdaFunction.Arn", - Id: "{namespace}RuleTarget" - }] - }.deep_merge(@props) - end - - def rule_logical_id - "{namespace}EventsRule" - end - end -end \ No newline at end of file diff --git a/lib/jets/cfn/resource/iam/application_role.rb b/lib/jets/cfn/resource/iam/application_role.rb deleted file mode 100644 index dcff8fde7..000000000 --- a/lib/jets/cfn/resource/iam/application_role.rb +++ /dev/null @@ -1,27 +0,0 @@ -module Jets::Cfn::Resource::Iam - class ApplicationRole < Jets::Cfn::Base - include BaseRoleDefinition - - def initialize - @policy_definitions = Jets.config.iam_policy # config.iam_policy contains definitions - @policy_definitions = @policy_definitions ? [@policy_definitions].flatten : [] - - @managed_policy_definitions = Jets.config.managed_iam_policy # config.managed_iam_policy contains definitions - @managed_policy_definitions = @managed_policy_definitions ? [@managed_policy_definitions].flatten : [] - end - - def role_logical_id - "IamRole" - end - - def policy_name - "#{Jets.project_namespace}-application-policy" - end - - def outputs - { - logical_id => "!Ref #{logical_id}", - } - end - end -end \ No newline at end of file diff --git a/lib/jets/cfn/resource/iam/base_role_definition.rb b/lib/jets/cfn/resource/iam/base_role_definition.rb deleted file mode 100644 index 895ce1908..000000000 --- a/lib/jets/cfn/resource/iam/base_role_definition.rb +++ /dev/null @@ -1,56 +0,0 @@ -module Jets::Cfn::Resource::Iam - module BaseRoleDefinition - attr_reader :policy_definitions, :managed_policy_definitions - - def definition - logical_id = role_logical_id - - # Do not assign pretty role_name because long controller names might hit the 64-char - # limit. Also, IAM roles are global, so assigning role names prevents cross region deploys. - definition = { - logical_id => { - Type: "AWS::IAM::Role", - Properties: { - Path: "/", - AssumeRolePolicyDocument: { - Version: "2012-10-17", - Statement: [{ - Effect: "Allow", - Principal: {Service: ["lambda.amazonaws.com"]}, - Action: ["sts:AssumeRole"]} - ] - } - } - } - } - - # Add vpc permissions to all policies - definition[logical_id][:Properties][:Policies] = [ - PolicyName: "vpc", # required, limited to 128-chars - PolicyDocument: vpc_policy_document, - ] if vpc_policy_document - - unless managed_policy_arns.empty? - definition[logical_id][:Properties][:ManagedPolicyArns] = managed_policy_arns - end - - definition - end - - def vpc_policy_document - if Jets.config.function.vpc_config - { - Statement: [Jets.config.vpc_iam_policy_statement] - } - end - end - - def policy_document - PolicyDocument.new(@policy_definitions.flatten.uniq).policy_document - end - - def managed_policy_arns - ManagedPolicy.new(@managed_policy_definitions.flatten.uniq).arns - end - end -end \ No newline at end of file diff --git a/lib/jets/cfn/resource/iam/class_role.rb b/lib/jets/cfn/resource/iam/class_role.rb deleted file mode 100644 index 5bbdc8fc0..000000000 --- a/lib/jets/cfn/resource/iam/class_role.rb +++ /dev/null @@ -1,74 +0,0 @@ -module Jets::Cfn::Resource::Iam - class ClassRole < Jets::Cfn::Base - include BaseRoleDefinition - - def initialize(app_class) - @app_class = app_class.to_s # IE: PostsController, HardJob, Hello, HelloFunction - @policy_definitions = lookup_iam_policies - @managed_policy_definitions = lookup_managed_iam_policies - end - - def role_logical_id - "{namespace}IamRole" - end - - def policy_name - class_namespace = replacements[:namespace].underscore.dasherize - "#{Jets.project_namespace}-#{class_namespace}-policy" # camelized because used as template value - end - - def replacements - { - namespace: @app_class.gsub('::','').camelize, # camelized because can be used as value - } - end - - def policy_document - # Handles precedence inheritance from the ApplicationRole to the ClassRole - @policy_definitions += application_role.policy_definitions if inherit? - super - end - - def managed_policy_arns - @managed_policy_definitions += application_role.managed_policy_definitions if inherit? - super - end - - # There are 2 types of inheritance: from superclasses and from higher precedence policies. - # This one manages the inheritance for higher precedence policies. - def inherit? - !@policy_definitions.empty? || !@managed_policy_definitions.empty? - end - - def application_role - Jets::Cfn::Resource::Iam::ApplicationRole.new - end - memoize :application_role - - # Accounts for inherited class_managed_iam_policy from superclasses - def lookup_managed_iam_policies - all_classes.map do |k| - k.class_managed_iam_policy # class_managed_iam_policy contains definitions - end.uniq - end - - # Accounts for inherited class_iam_policies from superclasses - def lookup_iam_policies - all_classes.map do |k| - k.class_iam_policy # class_iam_policy contains definitions - end.uniq - end - - # Class heirachry in top to down order - def all_classes - klass = @app_class.constantize - all_classes = [] - while klass != Object - all_classes << klass - klass = klass.superclass - end - all_classes.reverse - end - memoize :all_classes - end -end \ No newline at end of file diff --git a/lib/jets/cfn/resource/iam/function_role.rb b/lib/jets/cfn/resource/iam/function_role.rb deleted file mode 100644 index 9fe6ed408..000000000 --- a/lib/jets/cfn/resource/iam/function_role.rb +++ /dev/null @@ -1,54 +0,0 @@ -module Jets::Cfn::Resource::Iam - class FunctionRole < Jets::Cfn::Base - include BaseRoleDefinition - - def initialize(task) - @task = task - @policy_definitions = task.iam_policy || [] # iam_policy contains policy definitions - @managed_policy_definitions = task.managed_iam_policy || [] # managed_iam_policy contains policy definitions - end - - def role_logical_id - "{namespace}IamRole" - end - - def policy_name - funcion_namespace = replacements[:namespace].underscore.dasherize - "#{Jets.project_namespace}-#{funcion_namespace}-policy" # camelized because used as template value - end - - def replacements - { - namespace: "#{@task.class_name.gsub('::','')}#{@task.meth.to_s.camelize}", # camelized because can be used as value - } - end - - def policy_document - if inherit? - @policy_definitions += class_role.policy_definitions + application_role.policy_definitions - end - super - end - - def managed_policy_arns - if inherit? - @managed_policy_definitions += class_role.managed_policy_definitions + application_role.managed_policy_definitions - end - super - end - - def inherit? - !@policy_definitions.empty? || !@managed_policy_definitions.empty? - end - - def class_role - Jets::Cfn::Resource::Iam::ClassRole.new(@task.class_name.constantize) - end - memoize :class_role - - def application_role - Jets::Cfn::Resource::Iam::ApplicationRole.new - end - memoize :application_role - end -end \ No newline at end of file diff --git a/lib/jets/cfn/resource/iam/managed_policy.rb b/lib/jets/cfn/resource/iam/managed_policy.rb deleted file mode 100644 index 7d8d0d1ec..000000000 --- a/lib/jets/cfn/resource/iam/managed_policy.rb +++ /dev/null @@ -1,22 +0,0 @@ -module Jets::Cfn::Resource::Iam - class ManagedPolicy - extend Memoist - - attr_reader :definitions - def initialize(*definitions) - @definitions = definitions.flatten.compact - end - - def arns - definitions.map { |definition| standardize(definition) } - end - memoize :arns # only process arns once - - # AmazonEC2ReadOnlyAccess => arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess - def standardize(definition) - return definition if definition.include?('iam::aws:policy') - - "arn:aws:iam::aws:policy/#{definition}" - end - end -end diff --git a/lib/jets/cfn/resource/iam/policy.rb b/lib/jets/cfn/resource/iam/policy.rb deleted file mode 100644 index 7fa39a4f1..000000000 --- a/lib/jets/cfn/resource/iam/policy.rb +++ /dev/null @@ -1,31 +0,0 @@ -module Jets::Cfn::Resource::Iam - class Policy < Jets::Cfn::Base - def initialize(role) - @role = role - end - delegate :policy_document, :policy_name, :role_logical_id, :replacements, to: :@role - - def policy_logical_id - role_logical_id.sub(/Role$/, "Policy") - end - - def definition - logical_id = policy_logical_id - - # Do not assign pretty role_name because long controller names might hit the 64-char - # limit. Also, IAM roles are global, so assigning role names prevents cross region deploys. - definition = { - logical_id => { - Type: "AWS::IAM::Policy", - Properties: { - Roles: [Ref: role_logical_id.camelize], - PolicyName: "#{policy_name[0..127]}", # required, limited to 128-chars - PolicyDocument: policy_document, - } - } - } - - definition - end - end -end diff --git a/lib/jets/cfn/resource/iam/policy_document.rb b/lib/jets/cfn/resource/iam/policy_document.rb deleted file mode 100644 index 1cdfb1684..000000000 --- a/lib/jets/cfn/resource/iam/policy_document.rb +++ /dev/null @@ -1,42 +0,0 @@ -module Jets::Cfn::Resource::Iam - class PolicyDocument - extend Memoist - include Jets::Util::Camelize - - attr_reader :definitions - def initialize(*definitions) - @definitions = definitions.flatten - # empty starting policy that will be altered - @policy = { - Version: "2012-10-17", - Statement: [] - } - end - - def policy_document - definitions.map { |definition| standardize(definition) } - camelize(@policy) - end - memoize :policy_document # only process policy_document once - - def standardize(definition) - case definition - when String - # Expands simple string from: logs => logs:* - definition = "#{definition}:*" unless definition.include?(':') - @policy[:Statement] << { - Action: [definition], - Effect: "Allow", - Resource: "*", - } - when Hash - definition = definition.stringify_keys - if definition.key?("Version") # special case where we replace the policy entirely - @policy = definition - else - @policy[:Statement] << definition - end - end - end - end -end diff --git a/lib/jets/cfn/resource/iot/topic_rule.rb b/lib/jets/cfn/resource/iot/topic_rule.rb deleted file mode 100644 index d194088a8..000000000 --- a/lib/jets/cfn/resource/iot/topic_rule.rb +++ /dev/null @@ -1,34 +0,0 @@ -# CloudFormation AWS::IoT::TopicRule docs: https://amzn.to/2SMBOVm -module Jets::Cfn::Resource::Iot - class TopicRule < Jets::Cfn::Base - def initialize(props={}) - @props = props # associated_properties from dsl.rb - end - - def definition - { - topic_logical_id => { - Type: "AWS::IoT::TopicRule", - Properties: merged_properties, - } - } - end - - # Do not name this method properties, that is a computed method of `Jets::Cfn::Resource` - def merged_properties - { - # required properties - TopicRulePayload: { - Actions: [{ - Lambda: { FunctionArn: "!GetAtt {namespace}LambdaFunction.Arn" } - }], - RuleDisabled: 'false', - } - }.deep_merge(@props) - end - - def topic_logical_id - "{namespace}IotTopicRule" - end - end -end \ No newline at end of file diff --git a/lib/jets/cfn/resource/lambda/event_source_mapping.rb b/lib/jets/cfn/resource/lambda/event_source_mapping.rb deleted file mode 100644 index 3f3c594b8..000000000 --- a/lib/jets/cfn/resource/lambda/event_source_mapping.rb +++ /dev/null @@ -1,31 +0,0 @@ -# Note the Lambda function timeout must be less than or equal to the sqs queue default timeout. -module Jets::Cfn::Resource::Lambda - class EventSourceMapping < Jets::Cfn::Base - def initialize(props={}) - @props = props # associated_properties from dsl.rb - end - - def definition - # CloudFormation Docs: https://amzn.to/2WM6165 - properties = { - # BatchSize: 10, # Defaults: Kinesis 100, DynamoDB Streams: 100, SQS: 10 - # Enabled: boolean, - # EventSourceArn: string, # required - FunctionName: "!Ref {namespace}LambdaFunction", - # StartingPosition: string # reqiured for Required for Amazon Kinesis and Amazon DynamoDB Streams sources - } - properties.merge!(@props) - - { - event_source_mapping_logical_id => { - Type: "AWS::Lambda::EventSourceMapping", - Properties: properties - } - } - end - - def event_source_mapping_logical_id - "{namespace}EventSourceMapping" - end - end -end \ No newline at end of file diff --git a/lib/jets/cfn/resource/lambda/function.rb b/lib/jets/cfn/resource/lambda/function.rb deleted file mode 100644 index df92c90d6..000000000 --- a/lib/jets/cfn/resource/lambda/function.rb +++ /dev/null @@ -1,266 +0,0 @@ -module Jets::Cfn::Resource::Lambda - class Function < Jets::Cfn::Base - MAX_FUNCTION_NAME_SIZE = 64 - include Environment - - def initialize(definition) - @definition = definition - @app_class = definition.class_name.to_s - end - - def definition - { - function_logical_id => { - Type: "AWS::Lambda::Function", - Properties: combined_properties - } - } - end - - # Examples: - # one_lambda_per_controller PostsControllerLambdaHandlerLambdaFunction - # one_lambda_per_method PostsControllerIndexLambdaFunction - def function_logical_id - "{namespace}LambdaFunction" - end - - def replacements - @definition.replacements # has namespace replacement - end - - def combined_properties - props = env_properties - .deep_merge(global_properties) - .deep_merge(class_properties) - .deep_merge(function_properties) - finalize_properties!(props) - end - - # Global properties example: - # jets defaults are in jets/default/application.rb. - # Your application's default config/application.rb then get used. Example: - # - # Jets.application.configure do - # config.function = ActiveSupport::OrderedOptions.new - # config.function.timeout = 30 - # config.function.runtime = "nodejs8.10" - # config.function.memory_size = 1536 - # end - def global_properties - baseline = { - Code: { - S3Bucket: "!Ref S3Bucket", - S3Key: code_s3_key - }, - Role: "!Ref IamRole", - Environment: { Variables: environment }, - } - - application_config = camelize(Jets.application.config.function.to_h) - baseline.merge(application_config) - end - - # Class properties example: - # - # class PostsController < ApplicationController - # class_timeout 22 - # ... - # end - # - # Also handles iam policy override at the class level. Example: - # - # class_iam_policy("logs:*") - # - def class_properties - # klass is PostsController, HardJob, GameRule, Hello or HelloFunction - klass = Jets::Klass.from_definition(@definition) - - class_properties = lookup_class_properties(klass) - if assign_iam_role?(klass) - iam_policy = Jets::Cfn::Resource::Iam::ClassRole.new(klass) - class_properties[:Role] = "!GetAtt #{iam_policy.logical_id}.Arn" - end - - camelize(class_properties) - end - - # The IAM Role is built in the same nested template but determination of - # whether or not to assign the IAM Role is determined by the inheritance. - # The merging of permissions is already handled by Resource::Iam::*Role classes. - # This also avoids having to pass Application IAM role around. - def assign_iam_role?(klass) - assign = false - while klass && klass != Object - if klass&.build_class_iam? - assign = true - break - end - klass = klass.superclass - end - assign - end - - # Accounts for inherited class_properties - def lookup_class_properties(klass) - all_classes = [] - while klass != Object - all_classes << klass - klass = klass.superclass - end - class_properties = {} - # Go back down class heirachry top to down - all_classes.reverse.each do |k| - class_properties.merge!(k.class_properties) - end - class_properties - end - - # Function properties example: - # - # class PostsController < ApplicationController - # timeout 18 - # def index - # ... - # end - # - # Also handles iam policy override at the function level. Example: - # - # iam_policy("ec2:*") - # def new - # render json: params.merge(action: "new") - # end - # - def function_properties - properties = @definition.properties - if @definition.build_function_iam? - iam_policy = Jets::Cfn::Resource::Iam::FunctionRole.new(@definition) - properties[:Role] = "!GetAtt #{iam_policy.logical_id}.Arn" - end - camelize(properties) - end - - # Properties managed by Jets merged with finality. - def finalize_properties!(props) - handler = full_handler(props) - runtime = get_runtime(props) - description = get_descripton(props) - managed = { - Handler: handler, - Runtime: runtime, - Description: description, - } - managed[:FunctionName] = function_name if function_name - layers = get_layers(runtime) - managed[:Layers] = layers if layers - props.merge!(managed) - end - - def get_layers(runtime) - return nil unless runtime =~ /^ruby/ || runtime =~ /^provided/ - return Jets.config.lambda.layers if Jets.config.pro.disable - - ["!Ref GemLayer"] + Jets.config.lambda.layers - end - - def get_runtime(props) - # Only allow custom runtime for ruby so polymorphic support works for python and node - if @definition.lang == :ruby - props[:Runtime] || default_runtime - else - default_runtime - end - end - - def default_runtime - self.class.default_runtimes[@definition.lang] - end - - # Also used by jets/stack/main/dsl/lambda.rb - def self.default_runtimes - { - node: "nodejs18.x", - python: "python3.10", - ruby: Jets.ruby_runtime, - } - end - - def default_handler - map = { - node: @definition.full_handler(:handler), # IE: handlers/controllers/posts/show.handler - python: @definition.full_handler(:lambda_handler), # IE: handlers/controllers/posts/show.lambda_handler - ruby: handler, # IE: handlers/controllers/posts_controllers.index - } - map[@definition.lang] - end - - def handler - handler_value(@definition.meth) # IE: handlers/controllers/posts_controllers.index - end - - # Used for node-shim also - def handler_value(meth) - "handlers/#{@definition.type.pluralize}/#{@app_class.underscore}.#{meth}" - end - - # Ensure that the handler path is normalized. - def full_handler(props) - if props[:Handler] - handler_value(props[:Handler]) - else - default_handler - end - end - - def code_s3_key - checksum = Jets::Builders::Md5.checksums["stage/code"] - "jets/code/code-#{checksum}.zip" # s3_key - end - - # Examples: - # "#{Jets.project_namespace}-sleep_job-perform" - # "demo-dev-sleep_job-perform" - def function_name - return if ENV['JETS_RESET'] # reset mode, let CloudFormation manage the function name - # Example values: - # @app_class: admin/pages_controller - # @definition.meth: index - # method: admin/pages_controller - # method: admin-pages_controller-index - - method = @app_class.underscore - method = method.gsub('/','-').gsub(/[^0-9a-z\-_]/i, '') - unless one_lambda_per_controller? - method += "-#{@definition.meth}" - end - function_name = "#{Jets.project_namespace}-#{method}" - - # Returns nil if function name is too long. - # CloudFormation will managed the the function name in this case. - # A pretty function name won't be generated but the deploy will be successful. - function_name.size > MAX_FUNCTION_NAME_SIZE ? nil : function_name - end - - def get_descripton(props) - props[:Description] || default_description - end - - # Example values: - # @app_class: Admin/PagesController - # @definition.meth: index - # Returns: - # Admin/PagesController - # or - # Admin/PagesController#index - def default_description - if one_lambda_per_controller? - "#{@app_class}" - else - "#{@app_class}##{@definition.meth}" - end - end - - def one_lambda_per_controller? - Jets.one_lambda_per_controller? && @app_class.to_s.ends_with?("Controller") - end - end -end diff --git a/lib/jets/cfn/resource/lambda/function/controller.rb b/lib/jets/cfn/resource/lambda/function/controller.rb deleted file mode 100644 index 7eed7044b..000000000 --- a/lib/jets/cfn/resource/lambda/function/controller.rb +++ /dev/null @@ -1,21 +0,0 @@ -# Handles one_lambda_per_controller -class Jets::Cfn::Resource::Lambda::Function - class Controller < Jets::Cfn::Resource::Lambda::Function - # override - def combined_properties - props = env_properties - .deep_merge(global_properties) - .deep_merge(class_properties) - # .deep_merge(function_properties) # comment left to emphasize controller cannot have function_properties - finalize_properties!(props) - end - - # override - def permission - Jets::Cfn::Resource::Lambda::Permission.new(replacements, self, - Principal: "apigateway.amazonaws.com", - SourceArn: "!Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${RestApi}/*/*", - ) - end - end -end diff --git a/lib/jets/cfn/resource/lambda/function/environment.rb b/lib/jets/cfn/resource/lambda/function/environment.rb deleted file mode 100644 index 1572f9aba..000000000 --- a/lib/jets/cfn/resource/lambda/function/environment.rb +++ /dev/null @@ -1,64 +0,0 @@ -class Jets::Cfn::Resource::Lambda::Function - module Environment - def env_properties - env_vars = Jets::Dotenv.load!(true) - variables = environment.merge(env_vars) - check_reserved_variables!(variables) - {Environment: { Variables: variables }} - end - - def environment - env = Jets.config.function.environment ? Jets.config.function.environment.to_h : {} - env.deep_merge(jets_env) - end - - # These jets env variables are special variables that get included to keeps some state - def jets_env - env = {} - env[:JETS_ENV] = Jets.env.to_s - env[:JETS_EXTRA] = Jets.extra if Jets.extra - env[:JETS_PROJECT_NAME] = ENV['JETS_PROJECT_NAME'] if ENV['JETS_PROJECT_NAME'] - env[:JETS_STAGE] = Jets::Cfn::Resource::ApiGateway::Deployment.stage_name - env[:JETS_AWS_ACCOUNT] = Jets.aws.account - env - end - - private - def check_reserved_variables!(variables) - found_reserved_vars = variables.keys & reserved_variables - return if found_reserved_vars.empty? - - puts "You have configured some environment variables that are reserved by AWS Lambda.".color(:red) - puts found_reserved_vars - puts "The deployment to AWS Lambda will failed when using reserved variables." - puts "Please remove these reserved variables. " - puts "More info: https://docs.aws.amazon.com/lambda/latest/dg/current-supported-versions.html#lambda-environment-variables" - exit 1 - end - - # https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html - def reserved_variables - %w[ - _HANDLER - AWS_DEFAULT_REGION - AWS_REGION - AWS_EXECUTION_ENV - AWS_LAMBDA_FUNCTION_NAME - AWS_LAMBDA_FUNCTION_MEMORY_SIZE - AWS_LAMBDA_FUNCTION_VERSION - AWS_LAMBDA_INITIALIZATION_TYPE - AWS_LAMBDA_LOG_GROUP_NAME - AWS_LAMBDA_LOG_STREAM_NAME - AWS_ACCESS_KEY - AWS_ACCESS_KEY_ID - AWS_SECRET_ACCESS_KEY - AWS_SESSION_TOKEN - LAMBDA_TASK_ROOT - LAMBDA_RUNTIME_DIR - AWS_LAMBDA_RUNTIME_API - ] - end - - - end -end diff --git a/lib/jets/cfn/resource/lambda/gem_layer.rb b/lib/jets/cfn/resource/lambda/gem_layer.rb deleted file mode 100644 index 5dc1d45e1..000000000 --- a/lib/jets/cfn/resource/lambda/gem_layer.rb +++ /dev/null @@ -1,17 +0,0 @@ -module Jets::Cfn::Resource::Lambda - class GemLayer < LayerVersion - def description - "Jets Ruby Gems" - end - - def layer_name - # Do not include the Jets.extra_env to group the layers in same app together - "#{Jets.short_env}-#{Jets.project_name}-gems" - end - - def code_s3_key - checksum = Jets::Builders::Md5.checksums["stage/opt"] - "jets/code/opt-#{checksum}.zip" # s3_key - end - end -end diff --git a/lib/jets/cfn/resource/lambda/layer_version.rb b/lib/jets/cfn/resource/lambda/layer_version.rb deleted file mode 100644 index 74eb71fe0..000000000 --- a/lib/jets/cfn/resource/lambda/layer_version.rb +++ /dev/null @@ -1,44 +0,0 @@ -# Type: "AWS::Lambda::LayerVersion" -# Properties: -# CompatibleRuntimes: -# - String -# - ... -# Content: -# Content -# Description: String -# LayerName: String -# LicenseInfo: String -module Jets::Cfn::Resource::Lambda - class LayerVersion < Jets::Cfn::Base - def definition - { - layer_version_logical_id => { - Type: "AWS::Lambda::LayerVersion", - Properties: { - Content: { - S3Bucket: s3_bucket, - S3Key: code_s3_key, - }, - Description: description, - LayerName: layer_name, - LicenseInfo: "Nonstandard", - } - } - } - end - - def s3_bucket - "!Ref S3Bucket" - end - - def layer_version_logical_id - self.class.name.split('::').last - end - - def outputs - { - logical_id => "!Ref #{logical_id}", - } - end - end -end \ No newline at end of file diff --git a/lib/jets/cfn/resource/lambda/permission.rb b/lib/jets/cfn/resource/lambda/permission.rb deleted file mode 100644 index 30f36460e..000000000 --- a/lib/jets/cfn/resource/lambda/permission.rb +++ /dev/null @@ -1,51 +0,0 @@ -# Resource: Permissions are at the cfn/resource/permission.rb level because the -# principal is determined by the calling or associated resource. -# IE: Principal: events.amazonaws.com for Jets Jobs. -module Jets::Cfn::Resource::Lambda - class Permission < Jets::Cfn::Base - def initialize(replacements, associated_resource, options={}) - @replacements = replacements - @associated_resource = associated_resource - # allow override for Jets::Cfn::Resource::Lambda::Function::Controller permission - @principal = options[:Principal] - @source_arn = options[:SourceArn] - end - - def definition - logical_id = permission_logical_id - - definition = { - logical_id => { - Type: "AWS::Lambda::Permission", - Properties: { - FunctionName: "!Ref {namespace}LambdaFunction", - Action: "lambda:InvokeFunction", - Principal: principal - } - } - } - - # From AWS docs: https://amzn.to/2N0QXQL - # source_arn is "not supported by all event sources" - definition[logical_id][:Properties][:SourceArn] = source_arn if source_arn - - definition - end - - def permission_logical_id - logical_id = "{namespace}Permission" - md = @associated_resource.logical_id.match(/(\d+)$/) - counter = md[1] if md - [logical_id, counter].compact.join('') - end - - # Auto-detect principal from the associated resources. - def principal - @principal || replacer.principal_map(@associated_resource.type) - end - - def source_arn - @source_arn || replacer.source_arn_map(@associated_resource.type) - end - end -end diff --git a/lib/jets/cfn/resource/logs/subscription_filter.rb b/lib/jets/cfn/resource/logs/subscription_filter.rb deleted file mode 100644 index dfa5463bd..000000000 --- a/lib/jets/cfn/resource/logs/subscription_filter.rb +++ /dev/null @@ -1,31 +0,0 @@ -# CloudFormation Log Subscription docs: https://amzn.to/2SNiSpr -module Jets::Cfn::Resource::Logs - class SubscriptionFilter < Jets::Cfn::Base - def initialize(props={}) - @props = props # associated_properties from dsl.rb - end - - def definition - { - log_logical_id => { - Type: "AWS::Logs::SubscriptionFilter", - Properties: merged_properties, - } - } - end - - # Do not name this method properties, that is a computed method of `Jets::Cfn::Resource` - def merged_properties - { - DestinationArn: "!GetAtt {namespace}LambdaFunction.Arn", - FilterPattern: "", # matches everything https://amzn.to/2N3b39I - # LogGroupName: string # will be set by log_event - # RoleArn: string # only required for kinensis, we dont use this for Lambda - }.deep_merge(@props) - end - - def log_logical_id - "{namespace}SubscriptionFilter" - end - end -end \ No newline at end of file diff --git a/lib/jets/cfn/resource/nested/api/base.rb b/lib/jets/cfn/resource/nested/api/base.rb deleted file mode 100644 index e00ee1019..000000000 --- a/lib/jets/cfn/resource/nested/api/base.rb +++ /dev/null @@ -1,4 +0,0 @@ -module Jets::Cfn::Resource::Nested::Api - class Base < Jets::Cfn::Resource::Nested::Base - end -end diff --git a/lib/jets/cfn/resource/nested/api/deployment.rb b/lib/jets/cfn/resource/nested/api/deployment.rb deleted file mode 100644 index a5cf3f315..000000000 --- a/lib/jets/cfn/resource/nested/api/deployment.rb +++ /dev/null @@ -1,53 +0,0 @@ -module Jets::Cfn::Resource::Nested::Api - class Deployment < Base - # interface method - def definition - { - deployment_id => { - Type: "AWS::CloudFormation::Stack", - Properties: { - TemplateURL: template_url, - Parameters: parameters, - }, - DependsOn: depends_on, - } - } - end - - def parameters - { RestApi: "!GetAtt ApiGateway.Outputs.RestApi" } - end - - def depends_on - depends_on_controllers + depends_on_api_methods - end - - def depends_on_api_methods - pages = Jets::Cfn::Builder::Api::Pages::Methods.pages - pages.map do |page| - "ApiMethods#{page.number}" - end - end - - def depends_on_controllers - controller_logical_ids = [] - expression = "#{Jets::Names.templates_folder}/*_controller*" - Dir.glob(expression).each do |path| - next unless File.file?(path) - - # map the path to a camelized logical_id. Example: - # /tmp/jets/demo/templates/demo-dev-2-posts_controller.yml to - # PostsController - regexp = Regexp.new(".*#{Jets::Names.templates_folder}/app-") - controller_name = path.sub(regexp, '').sub('.yml', '') - controller_logical_id = controller_name.underscore.camelize - controller_logical_ids << controller_logical_id - end - controller_logical_ids - end - - def deployment_id - Jets::Cfn::Resource::ApiGateway::Deployment.logical_id - end - end -end diff --git a/lib/jets/cfn/resource/nested/api/gateway.rb b/lib/jets/cfn/resource/nested/api/gateway.rb deleted file mode 100644 index eb9ceeea0..000000000 --- a/lib/jets/cfn/resource/nested/api/gateway.rb +++ /dev/null @@ -1,15 +0,0 @@ -module Jets::Cfn::Resource::Nested::Api - class Gateway < Base - # interface method - def definition - { - ApiGateway: { - Type: "AWS::CloudFormation::Stack", - Properties: { - TemplateURL: template_url, - } - } - } - end - end -end diff --git a/lib/jets/cfn/resource/nested/api/mapping.rb b/lib/jets/cfn/resource/nested/api/mapping.rb deleted file mode 100644 index d71e2683d..000000000 --- a/lib/jets/cfn/resource/nested/api/mapping.rb +++ /dev/null @@ -1,33 +0,0 @@ -module Jets::Cfn::Resource::Nested::Api - class Mapping < Base - # interface method - def definition - { - ApiMapping: { - Type: "AWS::CloudFormation::Stack", - Properties: { - TemplateURL: template_url, - Parameters: parameters, - }, - DependsOn: depends_on, - } - } - end - - def parameters - p = { - GemLayer: "!Ref GemLayer", - IamRole: "!GetAtt IamRole.Arn", - RestApi: "!GetAtt ApiGateway.Outputs.RestApi", - S3Bucket: "!Ref S3Bucket", - } - p[:DomainName] = "!GetAtt ApiGateway.Outputs.DomainName" if Jets.custom_domain? - p[:BasePath] = Jets.config.domain.base_path - p - end - - def depends_on - [Jets::Cfn::Resource::ApiGateway::Deployment.logical_id] - end - end -end diff --git a/lib/jets/cfn/resource/nested/api/methods.rb b/lib/jets/cfn/resource/nested/api/methods.rb deleted file mode 100644 index a47ef0ebd..000000000 --- a/lib/jets/cfn/resource/nested/api/methods.rb +++ /dev/null @@ -1,30 +0,0 @@ -module Jets::Cfn::Resource::Nested::Api - # interface method - class Methods < Page - def definition - { - "ApiMethods#{@page_number}" => { - Type: "AWS::CloudFormation::Stack", - Properties: { - TemplateURL: template_url, - Parameters: parameters, - }, - } - } - end - - # Read current paged ApiMethods1 template and see what parameters it needs. - # Then add them to the params hash from Controller Lambda functions outputs - # Example: - # - # api-methods-1.yml: - # - # params["PostsControllerIndexLambdaFunction"] = "!GetAtt UpController.Outputs.PostsControllerIndexLambdaFunction" - # - def parameters - template_path = Jets::Names.api_methods_template_path(@page_number) - api_methods = Jets::Cfn::Params::Api::Methods.new(template_path: template_path) - api_methods.params - end - end -end diff --git a/lib/jets/cfn/resource/nested/api/page.rb b/lib/jets/cfn/resource/nested/api/page.rb deleted file mode 100644 index 6447d3a96..000000000 --- a/lib/jets/cfn/resource/nested/api/page.rb +++ /dev/null @@ -1,8 +0,0 @@ -module Jets::Cfn::Resource::Nested::Api - class Page < Base - def initialize(options={}) - super - @page_number = options[:page_number] - end - end -end diff --git a/lib/jets/cfn/resource/nested/api/resources.rb b/lib/jets/cfn/resource/nested/api/resources.rb deleted file mode 100644 index 603e764dc..000000000 --- a/lib/jets/cfn/resource/nested/api/resources.rb +++ /dev/null @@ -1,22 +0,0 @@ -module Jets::Cfn::Resource::Nested::Api - class Resources < Page - # interface method - def definition - { - "ApiResources#{@page_number}" => { - Type: "AWS::CloudFormation::Stack", - Properties: { - TemplateURL: template_url, - Parameters: parameters, - } - } - } - end - - def parameters - template_path = Jets::Names.api_resources_template_path(@page_number) - api_resources = Jets::Cfn::Params::Api::Resources.new(template_path: template_path) - api_resources.params - end - end -end diff --git a/lib/jets/cfn/resource/nested/app_class.rb b/lib/jets/cfn/resource/nested/app_class.rb deleted file mode 100644 index a33373b80..000000000 --- a/lib/jets/cfn/resource/nested/app_class.rb +++ /dev/null @@ -1,116 +0,0 @@ -module Jets::Cfn::Resource::Nested - class AppClass < Base - def initialize(options={}) - super - @path = options[:path] - end - - # interface method - def definition - logical_id = app_logical_id - defintion = { - logical_id => { - Type: "AWS::CloudFormation::Stack", - Properties: { - TemplateURL: template_url, - Parameters: parameters, - } - } - } - defintion[logical_id][:DependsOn] = depends.stack_list if depends - defintion - end - - def depends - return if all_depends_on.empty? - Jets::Stack::Depends.new(all_depends_on) - end - memoize :depends - - # Always returns an Array, could be empty - def all_depends_on - depends_on = current_app_class.depends_on || [] # contains Depends::Items - stagger_depends_on = @stagger_depends_on || [] # contains Depends::Items - depends_on + stagger_depends_on - end - - # For staggering. We're abusing depends_on to slow down the update rate. - # - # For this type of depends_on, there are no template parameters or outputs. To use the normal depends at we would - # have to make app classes adhere to what Jets::Stack::Depends requires. This is mainly dependency_outputs and - # output_keys for each class right now. It would not be that difficult but is not needed. So we create the - # Jets::Stack::Depends::Item objects directly. - def add_stagger_depends_on(stacks) - stack_names = stacks.map { |s| s.current_app_class.to_s.underscore } - items = stack_names.map { |name| Jets::Stack::Depends::Item.new(name) } - @stagger_depends_on ||= [] - @stagger_depends_on += items.flatten - end - - def parameters - params = Jets::Cfn::Params::Common.parameters - params.merge!(controller_params) if controller? - params.merge!(depends.params) if depends - params - end - - def controller_params - return {} if Jets::Router.no_routes? - - params = { - RestApi: "!GetAtt ApiGateway.Outputs.RestApi", - } - - template_path = @path - template = Jets::Cfn::Template.load_file(template_path) - template[:Parameters].each do |p,data| - case p - when /Authorizer$/ # AWS::ApiGateway::Authorizer in authorizers templates. IE: demo-dev-authorizers.yml - # Description contains metadata to get the Authorizer logical id - params[p] = "!GetAtt #{authorizer_output(data[:Description])}" - end - end - params - end - - def authorizer_output(desc) - authorizer_stack, authorizer_logical_id = desc.split('.') - # IE: MainAuthorizer.Outputs.ProtectAuthorizer - "#{authorizer_stack}.Outputs.#{authorizer_logical_id}" - end - - def outputs - if controller? or job? - {} - else - super # { logical_id => "!Ref #{logical_id}" } in base.rb - end - end - - def controller? - @path.include?('_controller.yml') - end - - def job? - @path.include?('_job.yml') - end - - def current_app_class - templates_prefix = "#{Jets::Names.templates_folder}/app-" - @path.sub(templates_prefix, '') - .sub(/\.yml$/,'') - .gsub('-','/') - .camelize - .constantize - end - - # map the path to a camelized logical_id. Example: - # /tmp/jets/demo/templates/demo-dev-2-posts_controller.yml to - # PostsController - def app_logical_id - regexp = Regexp.new(".*#{Jets::Names.templates_folder}/app-") - controller_name = @path.sub(regexp, '').sub('.yml', '') - controller_name.underscore.camelize - end - end -end \ No newline at end of file diff --git a/lib/jets/cfn/resource/nested/authorizer.rb b/lib/jets/cfn/resource/nested/authorizer.rb deleted file mode 100644 index 5d7898c7e..000000000 --- a/lib/jets/cfn/resource/nested/authorizer.rb +++ /dev/null @@ -1,34 +0,0 @@ -module Jets::Cfn::Resource::Nested - class Authorizer < Base - def initialize(options={}) - super - @path = options[:path] - end - - def definition - logical_id = authorizer_logical_id - { - logical_id => { - Type: "AWS::CloudFormation::Stack", - Properties: { - TemplateURL: template_url, - Parameters: parameters, - } - } - } - end - - def parameters - params = Jets::Cfn::Params::Common.parameters - params[:RestApi] = "!GetAtt ApiGateway.Outputs.RestApi" - params - end - - # map the path to a camelized logical_id. IE: ProtectAuthorizer - def authorizer_logical_id - regexp = Regexp.new("#{Jets::Names.templates_folder}/authorizers-") - authorizer_name = @path.sub(regexp, '').sub('.yml', '') - authorizer_name.underscore.camelize - end - end -end diff --git a/lib/jets/cfn/resource/nested/base.rb b/lib/jets/cfn/resource/nested/base.rb deleted file mode 100644 index 7c6809b83..000000000 --- a/lib/jets/cfn/resource/nested/base.rb +++ /dev/null @@ -1,38 +0,0 @@ -# Inheriting classes should implement: -# -# definition -# template_filename -# -module Jets::Cfn::Resource::Nested - class Base < Jets::Cfn::Base - def initialize(options={}) - @options = options # not used yet - end - - def outputs - { - logical_id => "!Ref #{logical_id}", - } - end - - def template_url - checksum = Jets::Builders::Md5.checksums["stage/code"] - "https://s3.amazonaws.com/#{Jets.s3_bucket}/jets/cfn-templates/shas/#{checksum}/#{template_filename}" - end - - # Examples: - # api-gateway.yml - # api-resources-1.yml - # api-methods-1.yml - # app-posts_controller.yml - # shared-custom.yml - def template_filename - filename = if @path # AppClass, Authorizer, Shared - @path.sub("#{Jets::Names.templates_folder}/", '').gsub('/','-').sub('.yml', '') - else - self.class.name.to_s.sub(/.*Nested::/,'').underscore.gsub('/','-').dasherize - end - [filename, @page_number].compact.join('-') + '.yml' - end - end -end diff --git a/lib/jets/cfn/resource/nested/one_controller.rb b/lib/jets/cfn/resource/nested/one_controller.rb deleted file mode 100644 index 21fd0ed42..000000000 --- a/lib/jets/cfn/resource/nested/one_controller.rb +++ /dev/null @@ -1,48 +0,0 @@ -module Jets::Cfn::Resource::Nested - class OneController < Base - # interface method - def definition - defintion = { - JetsController: { - Type: "AWS::CloudFormation::Stack", - Properties: { - TemplateURL: template_url, - Parameters: parameters, - } - } - } - defintion - end - - # override - def template_filename - "jets-controller.yml" - end - - def parameters - params = Jets::Cfn::Params::Common.parameters - params.merge!(controller_params) - params - end - - def controller_params - if Jets::Router.no_routes? - {} - else - { - RestApi: "!GetAtt ApiGateway.Outputs.RestApi", - } - end - end - - def authorizer_output(desc) - authorizer_stack, authorizer_logical_id = desc.split('.') - # IE: MainAuthorizer.Outputs.ProtectAuthorizer - "#{authorizer_stack}.Outputs.#{authorizer_logical_id}" - end - - def outputs - {} - end - end -end diff --git a/lib/jets/cfn/resource/nested/shared.rb b/lib/jets/cfn/resource/nested/shared.rb deleted file mode 100644 index 80bdb7c40..000000000 --- a/lib/jets/cfn/resource/nested/shared.rb +++ /dev/null @@ -1,79 +0,0 @@ -# Implements: -# -# definition -# template_filename -# -module Jets::Cfn::Resource::Nested - class Shared < AppClass - def initialize(options={}) - super - @path = options[:path] - end - - def definition - logical_id = shared_logical_id - definition = { - logical_id => { - Type: "AWS::CloudFormation::Stack", - Properties: child_properties - } - } - definition[logical_id][:DependsOn] = depends_on if depends_on - definition - end - - def child_properties - props = { - TemplateURL: template_url, - } - - props[:Parameters] = Jets::Cfn::Params::Common.parameters # common child parameters - # add depends on parameters - depends_on.each do |dependency| - dependency_outputs(dependency).each do |output| - dependency_class = dependency.to_s.camelize - props[:Parameters][output] = "!GetAtt #{dependency_class}.Outputs.#{output}" - end - end if depends_on - - props - end - - # Returns output keys associated with the stack. They are the resource logical ids. - def dependency_outputs(dependency) - dependency.to_s.camelize.constantize.output_keys - end - - def depends_on - return unless current_shared_class.depends_on - current_shared_class.depends_on.map { |x| x.to_s.camelize } - end - - # map the path to a camelized logical_id. Example: - # /tmp/jets/demo/templates/demo-dev-2-shared-resources.yml to - # PostsController - def shared_logical_id - regexp = Regexp.new(".*#{Jets::Names.templates_folder}/shared-") # remove the shared - shared_name = @path.sub(regexp, '').sub('.yml', '') - shared_name.underscore.camelize - end - - # IE: app/resource.rb => Resource - # Returns Resource class object in the example - def current_shared_class - templates_prefix = "#{Jets::Names.templates_folder}/shared-" - @path.sub(templates_prefix, '') - .sub(/\.yml$/,'') - .gsub('-','/') - .camelize - .constantize # returns actual class - end - - # Tells us if there are any resources defined in the shared class. - # - # Returns: Boolean - def resources? - current_shared_class.build? - end - end -end diff --git a/lib/jets/cfn/resource/one/function.rb b/lib/jets/cfn/resource/one/function.rb deleted file mode 100644 index 46dbb6986..000000000 --- a/lib/jets/cfn/resource/one/function.rb +++ /dev/null @@ -1,66 +0,0 @@ -module Jets::Cfn::Resource::One - class Function < Jets::Cfn::Resource::Lambda::Function - include Jets::Cfn::Resource::Lambda::Function::Environment - - # override to use the same function name for all controllers - def initialize - end - - # override - def function_logical_id - "JetsControllerLambdaFunction" - end - - # override - def combined_properties - props = env_properties - .deep_merge(global_properties) - .deep_merge(application_controller_properties) - finalize_properties!(props) - end - - def application_controller_properties - klass = ApplicationController - return {} unless klass.build_class_iam? - - class_properties = lookup_class_properties(klass) - iam_policy = Jets::Cfn::Resource::Iam::ClassRole.new(klass) - class_properties[:Role] = "!GetAtt #{iam_policy.logical_id}.Arn" - camelize(class_properties) - end - - # Properties managed by Jets merged with finality. - def finalize_properties!(props) - handler = "handlers/controller.lambda_handler" - runtime = get_runtime(props) - description = "Jets Lambda function for all controllers" - managed = { - Handler: handler, - Runtime: runtime, - Description: description, - } - managed[:FunctionName] = function_name if function_name - layers = get_layers(runtime) - managed[:Layers] = layers if layers - props.merge!(managed) - end - - # override - def get_runtime(props) - props[:Runtime] || Jets.ruby_runtime - end - - # Examples: - # "#{Jets.project_namespace}-sleep_job-perform" - # "demo-dev-sleep_job-perform" - def function_name - return if ENV['JETS_RESET'] # reset mode, let CloudFormation manage the function name - "#{Jets.project_namespace}-controller" - end - - # override - def replacements - {} - end - end -end diff --git a/lib/jets/cfn/resource/one/permission.rb b/lib/jets/cfn/resource/one/permission.rb deleted file mode 100644 index c6efa09e4..000000000 --- a/lib/jets/cfn/resource/one/permission.rb +++ /dev/null @@ -1,17 +0,0 @@ -module Jets::Cfn::Resource::One - class Permission < Jets::Cfn::Base - def definition - { - JetsControllerPermission: { - Type: "AWS::Lambda::Permission", - Properties: { - FunctionName: "!Ref JetsControllerLambdaFunction", - Action: "lambda:InvokeFunction", - Principal: "apigateway.amazonaws.com", - SourceArn: "!Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${RestApi}/*/*", - } - } - } - end - end -end diff --git a/lib/jets/cfn/resource/replacer.rb b/lib/jets/cfn/resource/replacer.rb index ec0451046..c31188ecc 100644 --- a/lib/jets/cfn/resource/replacer.rb +++ b/lib/jets/cfn/resource/replacer.rb @@ -2,7 +2,7 @@ class Jets::Cfn::Resource class Replacer extend Memoist - def initialize(replacements={}) + def initialize(replacements = {}) @replacements = replacements end @@ -45,7 +45,7 @@ def replace_value(value) # return value unless value.is_a?(String) or value.is_a?(Symbol) value = value.to_s # normalize to String - @replacements.each do |k,v| + @replacements.each do |k, v| # IE: Replaces {namespace} => SecurityJobCheck value = value.gsub("{#{k}}", v) end @@ -58,7 +58,7 @@ def replace_value(value) # "AWS::Config::ConfigRule" => "config.amazonaws.com", # "AWS::ApiGateway::Method" => "apigateway.amazonaws.com" def principal_map(type) - service = type.split('::')[1].downcase + service = type.split("::")[1].downcase service = special_principal_map(service) "#{service}.amazonaws.com" end @@ -66,7 +66,7 @@ def principal_map(type) def special_principal_map(service) # special map # s3_event actually uses sns topic events to trigger a Lambda function - map = { s3: "sns" } + map = {s3: "sns"} map[service.to_sym] || service end @@ -76,7 +76,7 @@ def special_principal_map(service) # When it is not available the resource definition should add it. def source_arn_map(type) map = { - "AWS::ApiGateway::Method" => "!Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${RestApi}/*/*", + "AWS::ApiGateway::Method" => "!Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${RestApi}/*/*" } map[type.to_s] end diff --git a/lib/jets/cfn/resource/route53/record_set.rb b/lib/jets/cfn/resource/route53/record_set.rb deleted file mode 100644 index 3e64ddee9..000000000 --- a/lib/jets/cfn/resource/route53/record_set.rb +++ /dev/null @@ -1,98 +0,0 @@ -# CloudFormation Docs AWS::Route53::RecordSet: https://amzn.to/2BtP9s5 -# -# Example: -# -# DnsRecord: -# Type: AWS::Route53::RecordSet -# Properties: -# HostedZoneName: !Ref 'HostedZoneResource' -# Comment: DNS name for my instance. -# Name: !Join ['', [!Ref 'Ec2Instance', ., !Ref 'AWS::Region', ., !Ref 'HostedZone', .]] -# Type: A -# TTL: '900' -# ResourceRecords: -# - !GetAtt Ec2Instance.PublicIp -module Jets::Cfn::Resource::Route53 - class RecordSet < Jets::Cfn::Base - def definition - { - DnsRecord: { - Type: "AWS::Route53::RecordSet", - Properties: record_set_properties - } - } - end - - def record_set_properties - base = { - Comment: "DNS record managed by Jets", - Name: name, - } - hosted_zone_id = Jets.config.domain.hosted_zone_id - if hosted_zone_id - base[:HostedZoneId] = hosted_zone_id - else - base[:HostedZoneName] = hosted_zone_name - end - - if Jets.config.domain.apex - base.merge( - AliasTarget: { - DNSName: cname, - HostedZoneId: domain_name_hosted_zone, - }, - Type: "A", - ) - else - base.merge({ - Type: "CNAME", - TTL: "60", - ResourceRecords: [cname], - }) - end - end - - def domain_name_hosted_zone - if endpoint_types.include?("REGIONAL") - "!GetAtt DomainName.RegionalHostedZoneId" - else - "!GetAtt DomainName.DistributionHostedZoneId" - end - end - - def cname - if endpoint_types.include?("REGIONAL") - "!GetAtt DomainName.RegionalDomainName" - else - "!GetAtt DomainName.DistributionDomainName" - end - end - - def domain_name - Jets::Cfn::Resource::ApiGateway::DomainName.new - end - memoize :domain_name - - def endpoint_types - domain_name.endpoint_types - end - - # IE: demo-dev.mydomain.com - def name - # Weird looking but correct: domain_name is object and domain_name is also method - domain_name.domain_name - end - - # IE: mydomain.com - def hosted_zone_name - name = Jets.config.domain.hosted_zone_name - name.ends_with?('.') ? name : "#{name}." # add trailing period if missing - end - - def outputs - { - DnsRecord: "!Ref DnsRecord", - } - end - end -end \ No newline at end of file diff --git a/lib/jets/cfn/resource/s3/bucket.rb b/lib/jets/cfn/resource/s3/bucket.rb index 413f02f0f..2e7098384 100644 --- a/lib/jets/cfn/resource/s3/bucket.rb +++ b/lib/jets/cfn/resource/s3/bucket.rb @@ -1,7 +1,7 @@ module Jets::Cfn::Resource::S3 class Bucket < Jets::Cfn::Base attr_reader :bucket_logical_id - def initialize(props={}) + def initialize(props = {}) @props = props # associated_properties from dsl.rb @bucket_logical_id = props.delete(:logical_id) || "{namespace}_s3_bucket" end @@ -10,14 +10,14 @@ def definition { bucket_logical_id => { Type: "AWS::S3::Bucket", - Properties: @props, + Properties: @props } } end def outputs { - bucket_logical_id => "!Ref #{bucket_logical_id.to_s.camelize}", + bucket_logical_id => "!Ref #{bucket_logical_id.to_s.camelize}" } end end diff --git a/lib/jets/cfn/resource/s3/jets_bucket.rb b/lib/jets/cfn/resource/s3/jets_bucket.rb index adb8385b6..10112087e 100644 --- a/lib/jets/cfn/resource/s3/jets_bucket.rb +++ b/lib/jets/cfn/resource/s3/jets_bucket.rb @@ -8,7 +8,7 @@ def initialize def props props = { PublicAccessBlockConfiguration: { - BlockPublicAcls: false, + BlockPublicAcls: false # BlockPublicPolicy: false, # IgnorePublicAcls: false, # RestrictPublicBuckets: false @@ -22,23 +22,13 @@ def props SSEAlgorithm: "AES256" } ] - }, + } } - props[:CorsConfiguration] = cors_configuration # dont check config.api.cors since javascript_importmap_tags also uses + # CorsConfiguration to allow assets to serve from the bucket + props[:CorsConfiguration] = Jets.bootstrap.config.s3_bucket.cors_configuration props end - def cors_configuration - Jets.config.api.s3_cors_configuration || { - CorsRules: [{ - AllowedHeaders: ["*"], - AllowedMethods: ["GET"], - AllowedOrigins: ["*"], - ExposedHeaders: [], - }] - } - end - class << self include Jets::AwsServices @@ -47,21 +37,22 @@ class << self @@name = nil def name return @@name if @@name - return "fake-bucket" if ENV['JETS_NO_INTERNET'] || ENV['JETS_TEMPLATES'] + return "fake-bucket" if ENV["JETS_NO_INTERNET"] || ENV["JETS_TEMPLATES"] resp = nil begin resp = cfn.describe_stacks(stack_name: Jets::Names.parent_stack_name) rescue Aws::CloudFormation::Errors::ValidationError => e - if e.message.include?('does not exist') && Jets::Command.original_cli_command == 'build' # jets build + if e.message.include?("does not exist") return "no-bucket-yet" # for jets build without s3 bucket yet else raise end end - output = resp.stacks[0].outputs.find {|o| o.output_key == 'S3Bucket'} - @@name = output.output_value # cache only once found + output = resp.stacks[0].outputs.find { |o| o.output_key == "S3Bucket" } + # The output can be nil if the stack failed and is in rollback state + @@name = output.output_value if output # cache only once found end end end diff --git a/lib/jets/cfn/resource/sns/subscription.rb b/lib/jets/cfn/resource/sns/subscription.rb deleted file mode 100644 index 2c95a9c56..000000000 --- a/lib/jets/cfn/resource/sns/subscription.rb +++ /dev/null @@ -1,29 +0,0 @@ -# CloudFormation SNS Subscription docs: https://amzn.to/2SJtN3C -module Jets::Cfn::Resource::Sns - class Subscription < Jets::Cfn::Base - def initialize(props={}) - @props = props # associated_properties from dsl.rb - end - - def definition - { - subscription_logical_id => { - Type: "AWS::SNS::Subscription", - Properties: merged_properties, - } - } - end - - # Do not name this method properties, that is a computed method of `Jets::Cfn::Resource` - def merged_properties - { - Endpoint: "!GetAtt {namespace}LambdaFunction.Arn", - Protocol: "lambda", - }.deep_merge(@props) - end - - def subscription_logical_id - "{namespace}SnsSubscription" - end - end -end \ No newline at end of file diff --git a/lib/jets/cfn/resource/sns/topic.rb b/lib/jets/cfn/resource/sns/topic.rb deleted file mode 100644 index 63cdac9a0..000000000 --- a/lib/jets/cfn/resource/sns/topic.rb +++ /dev/null @@ -1,35 +0,0 @@ -# CloudFormation SNS Topic docs: https://amzn.to/2MYbUZc -module Jets::Cfn::Resource::Sns - class Topic < Jets::Cfn::Base - def initialize(props={}) - @props = props # associated_properties from dsl.rb - end - - def definition - { - topic_logical_id => { - Type: "AWS::SNS::Topic", - Properties: merged_properties, - } - } - end - - # Do not name this method properties, that is a computed method of `Jets::Cfn::Resource` - def merged_properties - display_name = "{namespace} Topic"[0..99] # limit is 100 chars - { - DisplayName: display_name, - # Not setting subscription this way but instead with a SNS::Subscription resource so the interface - # is consistent. Leaving comment in here to remind me and in case decide to change this. - # Subscription: [ - # Endpoint: "!GetAtt {namespace}LambdaFunction.Arn", - # Protocol: "lambda" - # ] - }.deep_merge(@props) - end - - def topic_logical_id - "{namespace}SnsTopic" - end - end -end \ No newline at end of file diff --git a/lib/jets/cfn/resource/sns/topic_policy.rb b/lib/jets/cfn/resource/sns/topic_policy.rb deleted file mode 100644 index e215dbde7..000000000 --- a/lib/jets/cfn/resource/sns/topic_policy.rb +++ /dev/null @@ -1,40 +0,0 @@ -# CloudFormation SNS TopicPolicy docs: https://amzn.to/2SBMq9v -module Jets::Cfn::Resource::Sns - class TopicPolicy < Jets::Cfn::Base - def initialize(props={}) - @props = props # associated_properties from dsl.rb - end - - def definition - { - policy_logical_id => { - Type: "AWS::SNS::TopicPolicy", - Properties: merged_properties, - } - } - end - - # Do not name this method properties, that is a computed method of `Jets::Cfn::Resource` - def merged_properties - { - PolicyDocument: { - Version: "2012-10-17", - Statement: { - Effect: "Allow", - Principal: { Service: "s3.amazonaws.com"}, - Action: "sns:Publish", - Resource: "*", # TODO: figure out good syntax to limit easily - # Condition: - # ArnLike: - # aws:SourceArn: arn:aws:s3:::aa-test-95872017 - } - }, - topics: ["!Ref {namespace}SnsTopic"], - }.deep_merge(@props) - end - - def policy_logical_id - "{namespace}SnsTopicPolicy" - end - end -end diff --git a/lib/jets/cfn/resource/sqs/queue.rb b/lib/jets/cfn/resource/sqs/queue.rb deleted file mode 100644 index df38d7c4f..000000000 --- a/lib/jets/cfn/resource/sqs/queue.rb +++ /dev/null @@ -1,21 +0,0 @@ -# CloudFormation SQS Queue docs: https://amzn.to/2MVWk0j -module Jets::Cfn::Resource::Sqs - class Queue < Jets::Cfn::Base - def initialize(props={}) - @props = props # associated_properties from dsl.rb - end - - def definition - { - queue_logical_id => { - Type: "AWS::SQS::Queue", - Properties: @props, - } - } - end - - def queue_logical_id - "{namespace}SqsQueue" - end - end -end \ No newline at end of file diff --git a/lib/jets/cfn/resource/standardizer.rb b/lib/jets/cfn/resource/standardizer.rb index baf375957..0a43673eb 100644 --- a/lib/jets/cfn/resource/standardizer.rb +++ b/lib/jets/cfn/resource/standardizer.rb @@ -15,21 +15,23 @@ def standarize(definition) definition = camelize(definition) first, second, third, _ = definition if definition.size == 1 && first.is_a?(Hash) # long form + attrs = first.values.first + attrs.delete(:Properties) if attrs[:Properties].blank? first # pass through elsif definition.size == 2 && second.is_a?(Hash) # medium form logical_id, attributes = first, second - attributes.delete(:Properties) if attributes[:Properties].nil? || attributes[:Properties].empty? - { logical_id => attributes } + attributes.delete(:Properties) if attributes[:Properties].blank? + {logical_id => attributes} elsif definition.size == 2 && second.is_a?(String) # short form logical_id, type = first, second - { logical_id => { - Type: type + {logical_id => { + Type: type }} - elsif definition.size == 3 && (second.is_a?(String) || second.is_a?(NilClass))# short form + elsif definition.size == 3 && (second.is_a?(String) || second.is_a?(NilClass)) # short form logical_id, type, properties = first, second, third - template = { logical_id => { - Type: type - }} + template = {logical_id => { + Type: type + }} attributes = template.values.first attributes[:Properties] = properties unless properties.empty? template @@ -38,4 +40,4 @@ def standarize(definition) end end end -end \ No newline at end of file +end diff --git a/lib/jets/cfn/rollback.rb b/lib/jets/cfn/rollback.rb new file mode 100644 index 000000000..52bcb0bda --- /dev/null +++ b/lib/jets/cfn/rollback.rb @@ -0,0 +1,8 @@ +module Jets::Cfn + class Rollback < Stack + def run + check_deployable! + Jets::Remote::Runner.new(@options.merge(dummy: true, command: "rollback")).run + end + end +end diff --git a/lib/jets/cfn/ship.rb b/lib/jets/cfn/ship.rb deleted file mode 100644 index a8f8ac850..000000000 --- a/lib/jets/cfn/ship.rb +++ /dev/null @@ -1,192 +0,0 @@ -module Jets::Cfn - class Ship - extend Memoist - include Jets::AwsServices - - def initialize(options) - @options = options - @parent_stack_name = Jets::Names.parent_stack_name - end - - def run - # s3 bucket is available only when stack_type is full - upload_to_s3 if @options[:stack_type] == :full - - stack_in_progress?(@parent_stack_name) - - puts "Deploying CloudFormation stack with jets app!" - begin - set_resource_tags - save_stack - rescue Aws::CloudFormation::Errors::InsufficientCapabilitiesException => e - capabilities = e.message.match(/\[(.*)\]/)[1] - confirm = prompt_for_iam(capabilities) - if confirm =~ /^y/ - @options.merge!(capabilities: [capabilities]) - puts "Re-running: #{command_with_iam(capabilities).color(:green)}" - retry - else - puts "Exited" - exit 1 - end - end - - # waits for /(_COMPLETE|_FAILED)$/ status - cfn_status = Jets::Cfn::Status.new - success = cfn_status.wait - unless success - cfn_status.failure_message! - end - - save_apigw_state - prewarm - clean_deploy_logs - show_api_endpoint - show_custom_domain - create_deployment_record - end - - def create_deployment_record - return if @options[:stack_type] == :minimal - resp = Jets::Cfn::Deployment.new(@options.merge(stack_name: @parent_stack_name)).create - if resp - version = resp["version"] - Jets::Cfn::Upload.new.upload_cfn_templates(version) if version - end - end - - def set_resource_tags - @tags = Jets.config.cfn.build.resource_tags.map { |key, value| { key: key, value: value } } - end - - def save_stack - if stack_exists?(@parent_stack_name) - update_stack - else - create_stack - end - end - - def create_stack - # parent stack template is on filesystem and child stacks templates is on s3 - cfn.create_stack(stack_options) - end - - def update_stack - begin - cfn.update_stack(stack_options) - rescue Aws::CloudFormation::Errors::ValidationError => e - puts "ERROR: #{e.message}".color(:red) - true # error - end - end - - # options common to both create_stack and update_stack - def stack_options - { - stack_name: @parent_stack_name, - capabilities: capabilities, # ["CAPABILITY_IAM", "CAPABILITY_NAMED_IAM"] - # disable_rollback: !@options[:rollback], - tags: @tags, - }.merge!(template.stack_option) - end - - def template - @template ||= Template.new(Jets::Names.parent_template_path, @options) - end - - def save_apigw_state - Jets::Router::State.save_apigw_state - end - - def prewarm - if ENV['SKIP_PREWARMING'] - puts "Skipping prewarming" # useful for testing - return - end - return unless @options[:stack_type] == :full # s3 bucket is available - return unless Jets.config.prewarm.enable - return unless Jets.gem_layer? - - puts "Prewarming application." - Jets::PreheatJob.prewarm! - end - - def clean_deploy_logs - Jets::Commands::Clean::Log.new(@options).clean_deploys - end - - def endpoint_unavailable? - return true unless @options[:stack_type] == :full # s3 bucket is available - return true if Jets::Router.no_routes? - _, status = stack_status - return true if status.include?("ROLLBACK") - return true unless api_gateway - end - - # Do not memoize this because on first stack run it will be nil - # It only gets called one more time so just let it get called. - def api_gateway - resp = cfn.describe_stack_resources(stack_name: @parent_stack_name) - resources = resp.stack_resources - resources.find { |resource| resource.logical_resource_id == "ApiGateway" } - end - memoize :api_gateway - - def endpoint_available? - !endpoint_unavailable? - end - - def show_api_endpoint - return unless endpoint_available? - - stack_id = api_gateway["physical_resource_id"] - - resp = cfn.describe_stacks(stack_name: stack_id) - stack = resp["stacks"].first - output = stack["outputs"].find { |o| o["output_key"] == "RestApiUrl" } - endpoint = output["output_value"] - puts "API Gateway Endpoint: #{endpoint}" - end - - def show_custom_domain - return unless endpoint_available? && Jets.custom_domain? && Jets.config.domain.route53 - - domain_name = Jets::Cfn::Resource::ApiGateway::DomainName.new - # Looks funny but its right. - # domain_name is a method on the Jets::Cfn::Resource::ApiGateway::Domain instance - url = "https://#{domain_name.domain_name}" - puts "Custom Domain: #{url}" - puts "App Domain: https://#{Jets.config.app.domain}" if Jets.config.app.domain - end - - # All CloudFormation states listed here: - # http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-describing-stacks.html - def stack_status - resp = cfn.describe_stacks(stack_name: @parent_stack_name) - status = resp.stacks[0].stack_status - [resp, status] - end - - def prompt_for_iam(capabilities) - puts "This stack will create IAM resources. Please approve to run the command again with #{capabilities} capabilities." - puts " #{command_with_iam(capabilities)}" - - puts "Please confirm (y/n)" - $stdin.gets # confirm - end - - def command_with_iam(capabilities) - "#{File.basename($0)} #{ARGV.join(' ')} --capabilities #{capabilities}" - end - - def capabilities - ["CAPABILITY_IAM", "CAPABILITY_NAMED_IAM"] - end - - # Upload both code and child templates to s3 - def upload_to_s3 - Upload.new.upload - end - end -end diff --git a/lib/jets/cfn/stack.rb b/lib/jets/cfn/stack.rb new file mode 100644 index 000000000..2aeebce11 --- /dev/null +++ b/lib/jets/cfn/stack.rb @@ -0,0 +1,24 @@ +module Jets::Cfn + class Stack + extend Memoist + include Jets::AwsServices + include Jets::Util::Logging + include Rollback # delete_rollback_complete! + include Deployable # check_deployable! + + def initialize(options) + @options = options + end + + def stack_name + Jets.project.namespace + end + + def check_stack_exist! + stack = find_stack(stack_name) + return if stack + puts "ERROR: Stack #{stack_name} does not exist".color(:red) + exit 1 + end + end +end diff --git a/lib/jets/cfn/stack/deployable.rb b/lib/jets/cfn/stack/deployable.rb new file mode 100644 index 000000000..e9410145d --- /dev/null +++ b/lib/jets/cfn/stack/deployable.rb @@ -0,0 +1,16 @@ +class Jets::Cfn::Stack + module Deployable + # All CloudFormation states listed here: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-describing-stacks.html + def check_deployable! + return if !stack_exists?(stack_name) + + resp = cfn.describe_stacks(stack_name: stack_name) + status = resp.stacks[0].stack_status + if /_IN_PROGRESS$/.match?(status) + log.error "ERROR: The '#{stack_name}' stack status is #{status}".color(:red) + log.error "Please wait until the stack is ready and try again." + exit 1 + end + end + end +end diff --git a/lib/jets/cfn/stack/rollback.rb b/lib/jets/cfn/stack/rollback.rb new file mode 100644 index 000000000..c45043c70 --- /dev/null +++ b/lib/jets/cfn/stack/rollback.rb @@ -0,0 +1,59 @@ +class Jets::Cfn::Stack + module Rollback + include Jets::AwsServices::AwsHelpers + include Jets::Util::Logging + + # Super edge case: UPDATE_ROLLBACK_FAILED status + # The continue_update_rollback! addresses this edge case. + # Note: We do not provide any resources to skip. + # Also, tough to reproduce this edge case. Unsure how got to it. + # + # Related: + # Gist with Error: https://gist.github.com/tongueroo/d752186375ea95ed310e6735de24a324 + # AWS Troubleshooting Update rollback failed: https://go.aws/49m3Ji3 + # AWS cli continue-update-rollback: https://go.aws/43OiLw2 + def continue_update_rollback! + return unless update_rollback_failed? + + log.info "Continuing update rollback" + cfn.continue_update_rollback(stack_name: stack_name) + cfn_status.wait + cfn_status.reset + end + + def update_rollback_failed? + stack = find_stack(stack_name) + return false unless stack + + stack.stack_status == "UPDATE_ROLLBACK_FAILED" + end + + # Delete existing rollback stack from previous bad bootstrap deploy + def delete_rollback_complete! + return unless rollback_complete? + + log.info "Existing stack is in ROLLBACK_COMPLETE" + log.info "Deleting stack before continuing" + cfn.delete_stack(stack_name: stack_name) + cfn_status.wait + cfn_status.reset + end + + # Checks for a few things before deciding to delete the parent stack + # + # * Parent stack status status is ROLLBACK_COMPLETE + # * Parent resources are in the DELETE_COMPLETE state + # + def rollback_complete? + stack = find_stack(stack_name) + return false unless stack + + return false unless stack.stack_status == "ROLLBACK_COMPLETE" + + # Finally check if all the minimal resources in the parent template have been deleted + resp = cfn.describe_stack_resources(stack_name: stack_name) + resource_statuses = resp.stack_resources.map(&:resource_status).uniq + resource_statuses == ["DELETE_COMPLETE"] + end + end +end diff --git a/lib/jets/cfn/stack/template.rb b/lib/jets/cfn/stack/template.rb new file mode 100644 index 000000000..27a98e368 --- /dev/null +++ b/lib/jets/cfn/stack/template.rb @@ -0,0 +1,47 @@ +class Jets::Cfn::Stack + class Template + extend Memoist + include Jets::AwsServices + + delegate :s3_bucket, to: "Jets.project" + + attr_reader :path, :stack_name + def initialize(options = {}) + @options = options + @path = Jets::Names.parent_template_path + @stack_name = Jets::Names.parent_stack_name + end + + def template_option + if upload_to_s3? + {template_url: url} + else + {template_body: body} + end + end + + # uploads to s3 lazily on first call + def url + s3_key = "jets/cfn/#{File.basename(path)}" + object = s3_resource.bucket(s3_bucket).object(s3_key) + object.upload_file(path) + "https://s3.amazonaws.com/#{s3_bucket}/#{s3_key}" + end + memoize :url + + # Only use filesystem on initial bootstrap + def body + IO.read(path) + end + memoize :body + + private + + # Should not upload template to s3 and always use local template for bootstrap deploy. + # This is because for finale deletion stack, uploading the parent.yml to s3 + # prevents a clean deletion of the s3 bucket resource since it's not empty. + def upload_to_s3? + !@options[:bootstrap] + end + end +end diff --git a/lib/jets/cfn/stack/yamler.rb b/lib/jets/cfn/stack/yamler.rb new file mode 100644 index 000000000..14fa6c5ea --- /dev/null +++ b/lib/jets/cfn/stack/yamler.rb @@ -0,0 +1,53 @@ +require "yaml" + +class Jets::Cfn::Stack + class Yamler + def self.load(text) + new(text).load + end + + def initialize(text) + @text = text + end + + def load + add_domain_types! + YAML.load(@text) + end + + private + + def add_domain_types! + intrinsic_functions.each do |name| + YAML.add_domain_type("", name) do |type, val| + key = type.split("::").last + key = "Fn::" + key unless name == "Ref" + {key => val} + end + end + end + + def intrinsic_functions + %w[ + And + Base64 + Cidr + Equals + FindInMap + GetAtt + GetAZs + If + If + ImportValue + Join + Not + Or + Ref + Select + Split + Sub + Transform + ] + end + end +end diff --git a/lib/jets/cfn/status.rb b/lib/jets/cfn/status.rb index d20676bb6..276664e08 100644 --- a/lib/jets/cfn/status.rb +++ b/lib/jets/cfn/status.rb @@ -1,14 +1,14 @@ -require 'cfn_status' +require "cfn_status" module Jets::Cfn class Status < CfnStatus extend Memoist - def initialize(stack_name = Jets::Names.parent_stack_name, options={}) + def initialize(stack_name = Jets::Names.parent_stack_name, options = {}) super(stack_name, options) end - def failure_message! + def failure_message puts <<~EOL The Jets application failed to deploy. Jets creates a few CloudFormation stacks to deploy your application. The logs above show the CloudFormation parent stack @@ -22,16 +22,19 @@ def failure_message! EOL show_nested_stack_error - - exit 1 + show_update_update_rollback_failed end def show_nested_stack_error event = find_failed_event return unless event + # When error is not a nested stack return early + return if event.resource_type != "AWS::CloudFormation::Stack" + puts "-" * 80 puts "Here's the nested stack error details: #{event.resource_status_reason}" - self.class.new(event.physical_resource_id, start_index_before_delete: true).run + nested_status = self.class.new(event.physical_resource_id, start_index_before_delete: true) + nested_status.run region = Jets.aws.region puts <<~EOL @@ -40,6 +43,28 @@ def show_nested_stack_error https://#{region}.console.aws.amazon.com/cloudformation/home?region=#{region}#/stacks/events?filteringText=&filteringStatus=active&viewNested=true&stackId=#{event.physical_resource_id} EOL + + nested_status.events.each do |event| + if event.resource_status == "CREATE_FAILED" && event.resource_status_reason =~ /already exists in stack/ + debug_tip_already_exist_in_stack(event) + break + end + end + end + + def show_update_update_rollback_failed + resp = cfn.describe_stacks(stack_name: @stack_name) + status = resp.stacks.first.stack_status + return unless status == "UPDATE_ROLLBACK_FAILED" + + puts <<~EOL + The parent stack is in UPDATE_ROLLBACK_FAILED status. + + Also, running jets deploy again will attempt a continue-update-rollback operation to try to recover from this state. + However, if the continue-update-rollback operation fails, you may need to manually resolve the issue. + You might be able to resolve this with the AWS console continue update rollback + and skipping the resources that are causing the issue. + EOL end def find_failed_event @@ -49,5 +74,15 @@ def find_failed_event events = cfn_status.events[0..i].reverse # events are in reverse chronological order events.find { |e| e.resource_status =~ /FAILED/ } # first failed event end + + # 04:44:53AM CREATE_FAILED AWS::Lambda::Function RecordLambdaFunction rails-demo-dev-temperature_event-record already exists in stack arn:aws:cloudformation:us-west-2:112233445566:stack/rails-demo-dev-TemperatureEvent-1EPIUF1EN4LSQ/0f373000-f229-11ee-9f5f-0a3e531c7fd5 + def debug_tip_already_exist_in_stack(event) + puts <<~EOL + The error message indicates that the resource already exists in another stack. + + See: https://docs.rubyonjets.com/docs/debug/resource-already-exists/ + + EOL + end end end diff --git a/lib/jets/cfn/teardown.rb b/lib/jets/cfn/teardown.rb new file mode 100644 index 000000000..f94c29040 --- /dev/null +++ b/lib/jets/cfn/teardown.rb @@ -0,0 +1,20 @@ +module Jets::Cfn + class Teardown < Stack + def run + check_stack_exist! + log.info "Final Delete Phase" + Bucket.new.empty! + remaining_resources + end + + def remaining_resources + stack_resources = cfn.describe_stack_resources(stack_name: stack_name).stack_resources + resources = stack_resources.map(&:logical_resource_id).join(", ") + log.debug "Remaining resources: #{resources}" + log.debug "Final delete #{Jets.project.namespace.color(:green)}" + cfn.delete_stack(stack_name: stack_name) + cfn_status = Jets::Cfn::Status.new + cfn_status.wait + end + end +end diff --git a/lib/jets/cfn/teardown/bucket.rb b/lib/jets/cfn/teardown/bucket.rb new file mode 100644 index 000000000..927320cc5 --- /dev/null +++ b/lib/jets/cfn/teardown/bucket.rb @@ -0,0 +1,79 @@ +class Jets::Cfn::Teardown + class Bucket + include Jets::AwsServices + include Jets::Util::Logging + delegate :s3_bucket, to: "Jets.project" + + def empty! + delete_objects + wait_for_emptiness + end + + def delete_objects(call_count = 0) + return unless bucket_exists? + + # Must first remove all objects from s3 bucket in order to delete stack + log.info "Emptying s3 bucket #{s3_bucket}" if s3_bucket && call_count == 0 + + resp = s3.list_objects(bucket: s3_bucket) + if resp.contents.size > 0 + # IE: objects = [{key: "objectkey1"}, {key: "objectkey2"}] + objects = resp.contents.map { |item| {key: item.key} } + s3.delete_objects( + bucket: s3_bucket, + delete: { + objects: objects, + quiet: false + } + ) + delete_objects(call_count + 1) # recursive call to keep deleting objects until bucket is empty + end + end + + # Think there can be a race condition since delete_objects is async + # So we wait for the bucket to be empty + # Before we continue with the delete stack + def wait_for_emptiness + return unless bucket_exists? + + @empty_bucket_checks ||= 0 + max_checks = 5 + while @empty_bucket_checks < max_checks + is_empty = s3.list_objects(bucket: s3_bucket).contents.empty? + if is_empty + log.debug "Bucket '#{s3_bucket}' is empty." + return + else + log.debug "Bucket '#{s3_bucket}' is not empty. Waiting..." + sleep 2 + @empty_bucket_checks += 1 + end + end + + log.debug "Timeout: Bucket '#{s3_bucket}' is not empty after waiting." + end + + # Thanks: https://docs.aws.amazon.com/sdk-for-ruby/v3/developer-guide/s3-example-does-bucket-exist.html + def bucket_exists? + return false unless s3_bucket + s3.head_bucket(bucket: s3_bucket, use_accelerate_endpoint: false) + true + rescue Aws::S3::Errors::NotFound + false + end + + def are_you_sure? + if @options[:yes] + sure = "y" + else + log.debug "Are you sure you want to delete the #{Jets.project.namespace.color(:green)} project? (y/N)" + sure = $stdin.gets + end + + unless /^y/.match?(sure) + log.debug "Phew! Jets #{Jets.project.namespace.color(:green)} project was not deleted." + exit 0 + end + end + end +end diff --git a/lib/jets/cfn/template.rb b/lib/jets/cfn/template.rb deleted file mode 100644 index cbe1167c7..000000000 --- a/lib/jets/cfn/template.rb +++ /dev/null @@ -1,89 +0,0 @@ -module Jets::Cfn - class Template - include Jets::AwsServices - - attr_reader :path - - def initialize(path, options={}) - @path = path - @options = options - end - - def body - @body ||= IO.read(path) - end - - def url - @url ||= upload_file_to_s3 - end - - def stack_option - if upload_to_s3? - from_s3 - else - from_path - end - end - - private - def upload_to_s3? - return false if @options[:stack_type] == :minimal # bucket not yet available - bucket_name.present? - end - - def bucket_name - Jets.s3_bucket - end - - def from_s3 - { - template_url: url - } - end - - def from_path - { - template_body: body - } - end - - def upload_file_to_s3 - obj = s3_resource.bucket(bucket_name).object(s3_key) - obj.upload_file(path) - - "https://s3.amazonaws.com/#{bucket_name}/#{s3_key}" - end - - def s3_key - @s3_key ||= "jets/cfn-templates/#{File.basename(path)}" - end - - class << self - # Caches reduce filesystem IO calls - @@cache = {} - def load_file(path) - if @@cache[path] - @@cache[path] - else - @@cache[path] = Jets::Util::Yamler.load_file(path).deep_symbolize_keys - end - end - - # Jets::Cfn::Template.lookup_logical_id(template_name, key) - # Jets::Cfn::Template.lookup_logical_id("api-resources", "UpApiResource") - def lookup_logical_id(template_name, key) - expr = "#{Jets::Names.templates_folder}/#{template_name}-*" - template_paths = Dir.glob(expr).sort.to_a - found_template = template_paths.detect do |path| - next unless File.file?(path) - - template = Jets::Cfn::Template.load_file(path) - template[:Outputs].keys.include?(key.to_sym) - end - - name = File.basename(found_template).sub(/\.yml$/,'') - name.underscore.camelize # IE: ApiResources1 - end - end - end -end diff --git a/lib/jets/cfn/upload.rb b/lib/jets/cfn/upload.rb deleted file mode 100644 index 460ea4768..000000000 --- a/lib/jets/cfn/upload.rb +++ /dev/null @@ -1,163 +0,0 @@ -require 'active_support/number_helper' -require 'digest' -require 'rack/mime' - -module Jets::Cfn - class Upload - include Jets::AwsServices - include ActiveSupport::NumberHelper # number_to_human_size - - def upload - upload_cfn_templates - upload_zip_files - upload_assets - end - - def bucket_name - Jets.s3_bucket - end - - def upload_cfn_templates(version=nil) - puts "Uploading CloudFormation templates to S3." unless version # hide message when version is passed in - expression = "#{Jets::Names.templates_folder}/*" - if version # outside of each loop to avoid repeating - version = "versions/#{version}" - else - checksum = Jets::Builders::Md5.checksums["stage/code"] - version = "shas/#{checksum}" - end - checksum = Jets::Builders::Md5.checksums["stage/code"] - Dir.glob(expression).each do |path| - next unless File.file?(path) - key = ["jets/cfn-templates", version, File.basename(path)].compact.join('/') - obj = s3_resource.bucket(bucket_name).object(key) - puts "Uploading #{path} to s3://#{bucket_name}/#{key}".color(:green) if ENV['JETS_DEBUG'] - obj.upload_file(path) - end - end - - def upload_zip_files - puts "Uploading code zip files to S3." - zip_area = "#{Jets.build_root}/stage/zips" - Dir.glob("#{zip_area}/*").each do |file| - upload_zip_file(file) - end - end - - def upload_zip_file(path) - file_size = number_to_human_size(File.size(path)) - - puts "Uploading #{path} (#{file_size}) to S3" - start_time = Time.now - s3_key = "jets/code/#{File.basename(path)}" - obj = s3_resource.bucket(bucket_name).object(s3_key) - obj.upload_file(path) - puts "Uploaded to s3://#{bucket_name}/#{s3_key}".color(:green) - puts "Time to upload code to s3: #{pretty_time(Time.now-start_time).color(:green)}" - end - - def upload_assets - puts "Checking for modified public assets and uploading to S3." - start_time = Time.now - upload_public_assets - puts "Time for public assets to s3: #{pretty_time(Time.now-start_time).color(:green)}" - end - - def upload_public_assets - public_folders = %w[public] - public_folders.each do |folder| - upload_asset_folder(folder) - end - end - - def upload_asset_folder(folder) - expression = "#{Jets.root}/#{folder}/**/*" - group_size = 10 - Dir.glob(expression).each_slice(group_size) do |paths| - threads = [] - paths.each do |full_path| - next unless File.file?(full_path) - - threads << Thread.new do - upload_to_s3(full_path) - end - end - threads.each(&:join) - end - end - - def upload_to_s3(full_path) - if identical_on_s3?(full_path) && !ENV['JETS_ASSET_UPLOAD_FORCE'] - puts "Asset is identical on s3: #{full_path}" if ENV['JETS_DEBUG_ASSETS'] - return - end - - key = s3_key(full_path) - obj = s3_resource.bucket(bucket_name).object(key) - content_type = content_type_headers(full_path) - if ENV['JETS_DEBUG_ASSETS'] - puts "Uploading and setting content type for s3://#{bucket_name}/#{key} content_type #{content_type[:content_type].inspect}" - end - obj.upload_file(full_path, { acl: "public-read", cache_control: cache_control }.merge(content_type)) - end - - CONTENT_TYPES_BY_EXTENSION = { - '.css' => 'text/css', - '.html' => 'text/html', - '.js' => 'application/javascript', - } - def content_type_headers(full_path) - ext = File.extname(full_path) - content_type = CONTENT_TYPES_BY_EXTENSION[ext] || Rack::Mime.mime_type(ext) - if content_type - { content_type: content_type } - else - {} - end - end - - def s3_key(full_path) - relative_path = full_path.sub("#{Jets.root}/", '') - "jets/#{relative_path}" - end - - def identical_on_s3?(full_path) - local_md5 = ::Digest::MD5.file(full_path) - key = s3_key(full_path) - begin - resp = s3.head_object(bucket: bucket_name, key: key) - rescue Aws::S3::Errors::NotFound - return false - end - - remote_md5 = resp.etag.delete_prefix('"').delete_suffix('"') - local_md5 == remote_md5 - end - - # If cache_control is provided, then it will set the entire cache-control header. - # If only max_age is provided, then we'll generate a cache_control header. - # Using max_age is the shorter and simply way of setting the cache_control header. - def cache_control - # default when sprockets-jets not installed - return "public, max-age=3600" unless Jets.config.respond_to?(:assets) - - cache_control = Jets.config.assets.cache_control - unless cache_control - max_age = Jets.config.assets.max_age # defaults to 3600 in jets/application.rb - cache_control = "public, max-age=#{max_age}" - end - cache_control - end - - # http://stackoverflow.com/questions/4175733/convert-duration-to-hoursminutesseconds-or-similar-in-rails-3-or-ruby - def pretty_time(total_seconds) - minutes = (total_seconds / 60) % 60 - seconds = total_seconds % 60 - if total_seconds < 60 - "#{seconds.to_i}s" - else - "#{minutes.to_i}m #{seconds.to_i}s" - end - end - end -end diff --git a/lib/jets/cli.rb b/lib/jets/cli.rb index dfea1bfa1..769522c01 100644 --- a/lib/jets/cli.rb +++ b/lib/jets/cli.rb @@ -1,33 +1,185 @@ -# frozen_string_literal: true - -require "jets/app_loader" - -# If we are inside a Jets application this method performs an exec and thus -# the rest of this script is not run. -# exec_app either runs bin/jets or an inline version of it. -Jets::AppLoader.exec_app - -# Allow running jets with local source code when bin/jets does not exist -# Useful for development: jets build -# Also allows jets to work without a bin/jets file in the project. -if File.exist?("config/application.rb") - APP_PATH = File.expand_path("config/application", Dir.pwd) - require "jets" - require "jets/commands" - return -end +module Jets + class CLI < Jets::Thor::Base + desc "ci SUBCOMMAND", "ci subcommands" + subcommand "ci", Ci + + desc "concurrency SUBCOMMAND", "concurrency subcommands" + subcommand "concurrency", Concurrency + + desc "dotenv SUBCOMMAND", "dotenv subcommands" + subcommand "dotenv", Dotenv + + desc "env SUBCOMMAND", "env subcommands" + subcommand "env", Env + + desc "generate SUBCOMMAND", "generate subcommands" + subcommand "generate", Generate + + desc "maintenance SUBCOMMAND", "maintenance subcommands" + subcommand "maintenance", Maintenance + + desc "package SUBCOMMAND", "package subcommands" + subcommand "package", Package + + desc "release SUBCOMMAND", "release subcommands" + subcommand "release", Release + + desc "schedule SUBCOMMAND", "schedule subcommands" + subcommand "schedule", Schedule + + desc "waf SUBCOMMAND", "waf subcommands" + subcommand "waf", Waf + + desc "build", "Build deployment" + option :templates, type: :boolean, desc: "Build only cfn templates. Skip docker build" + def build + Build.new(options).run + end + + desc "bootstrap", "Bootstrap deployment", hide: true + yes_option + def bootstrap + Bootstrap.new(options).run + end + + desc "clean", "Clean local build folder" + def clean + Clean.new(options).run + end + + desc "deploy", "Deploy stack" + yes_option + option :templates, type: :boolean, hide: true, desc: "Deploy only cfn templates. Skip docker build. Experimental option. May be removed." + def deploy + Deploy.new(options).run + end + + desc "delete", "Delete stack" + yes_option + def delete + Delete.new(options).run + end + + desc "functions", "List functions" + option :full, default: false, type: :boolean, desc: "Show full function names with the project namespace" + def functions + Functions.new(options).run + end + map "funs" => :functions + # use string "funs", otherwise `jets fun` results in Thor sort error + + Init.cli_options.each { |args| option(*args) } + register(Init, "init", "init", "Initialize project for Jets") + + desc "ping", "Ping", hide: true + def ping + Ping.new(options).run + end + + desc "projects", "List projects" + paging_options + format_option(default: "space") + def projects + Projects.new(options).run + end + + desc "login [TOKEN]", "login" + def login(token = nil) + Login.new(options.merge(token: token)).run + end + + desc "logout", "logout" + def logout + Logout.new(options).run + end + + desc "logs", "Tail the logs" + option :since, desc: "From what time to begin displaying logs. By default, logs will be displayed starting from 10m in the past. The value provided can be an ISO 8601 timestamp or a relative time. Examples: 10m 2d 2w" + option :follow, aliases: :f, default: false, type: :boolean, desc: " Whether to continuously poll for new logs. To exit from this mode, use Control-C." + option :format, default: "plain", desc: "The format to display the logs. IE: detailed, short, plain. For plain, no timestamp are shown." + option :filter_pattern, desc: "The filter pattern to use. If not provided, all the events are matched" + # option :log_group_name, aliases: :n, desc: "The log group name. Default: /aws/lambda/#{Jets.project.namespace}-controller" + option :log_group_name, aliases: :n, desc: "The log group name. Default: /aws/lambda/NAMESPACE-controller" + option :refresh_rate, default: 1, type: :numeric, desc: "How often to refresh the logs in seconds." + option :wait_exists, default: true, type: :boolean, desc: "Whether to wait until the log group exists. By default, it will wait." + def logs + Logs.new(options).run + end + + desc "call", "Call Lambda function" + function_name_option + verbose_option + option :event, aliases: :e, default: "{}", desc: "JSON event to provide to Lambda function as input" + option :invocation_type, aliases: :t, default: "RequestResponse", desc: "Invocation type. IE: RequestResponse, Event, DryRun" + def call + $stdout.sync = $stderr.sync = true + $stdout = $stderr + Call.new(options).run + rescue Jets::CLI::Call::Error => e + puts "ERROR: #{e.message}".color(:red) + abort "Unable to find the function. Please check the function name and try again." + end + + desc "curl PATH", "Curl Lambda function" + function_name_option + verbose_option + option :request, aliases: :X, default: "GET", desc: "HTTP request method. IE: GET, POST, PUT, DELETE, etc. Default: GET" + option :data, aliases: :d, desc: "HTTP request data. The @ character is used to read data from a file. IE: @data.json" + option :headers, aliases: :H, default: {}, type: :hash, desc: "HTTP request header. IE: -H 'Content-Type: application/json'" + option :cookie, aliases: :b, desc: "HTTP request cookie. IE: -b 'yummy_cookie=choco; tasty_cookie=strawberry'. If no '=' is used, it is treated as a file with the name of the cookie" + option :cookie_jar, aliases: :c, desc: "HTTP request cookie jar. IE: -c cookie.jar" + option :trim, aliases: :t, default: nil, type: :boolean, desc: "Trim large values in the response" + def curl(path) + Curl.new(options.merge(path: path)).run + end + + desc "exec", "REPL or execute commands on AWS Lambda" + function_name_option + verbose_option + def exec(*command) + Exec.new(options.merge(command: command)).run + end + + # Shorthand command for jets release:rollback which works too but is hidden + desc "rollback VERSION", "Rollback to a previous release" + option :yes, aliases: :y, type: :boolean, desc: "Skip are you sure prompt" + def rollback(version) + Jets::CLI::Release::Rollback.new(options.merge(version: version)).run + end + + desc "stacks", "List deployed stacks" + paging_options + option :all_projects, desc: "Show all stacks across all projects", type: :boolean, default: false + format_option(default: "space") + def stacks + Stacks.new(options).run + end -# The rest of the script runs if outside of Jets application. IE: -# jets new demo + desc "stop", "Stops the deploy" + yes_option + def stop + Stop.new(options).run + end -require "jets/ruby_version_check" -Signal.trap("INT") { puts; exit(1) } + # Normally should not have to use this. This is why it's hidden. + # Only useful if some reason the remote delete leaves remaining resources behind. + desc "teardown", "Teardown stack", hide: true + yes_option + def teardown + warn "WARN: You should use `jets delete` instead of `jets teardown`".color(:yellow) + warn "This is for debugging and will not delete the Jets API deployment record" + Teardown.new(options).run + end -require "jets/command" + desc "url", "App url" + format_option(default: "space") + def url + Url.new(options).run + end -if ARGV.first == "plugin" - ARGV.shift - Jets::Command.invoke :plugin, ARGV -else - Jets::Command.invoke :application, ARGV + desc "version", "Prints version" + def version + puts "Jets #{VERSION}" + end + end end diff --git a/lib/jets/cli/base.rb b/lib/jets/cli/base.rb new file mode 100644 index 000000000..7b268ddf8 --- /dev/null +++ b/lib/jets/cli/base.rb @@ -0,0 +1,46 @@ +class Jets::CLI + class Base + extend Memoist + include Jets::Api + include Jets::AwsServices + include Jets::Util::Logging + include Jets::Util::Sure + + attr_reader :options + def initialize(options = {}) + @options = options + Jets.boot + end + + def paging_params + params = {} + params[:limit] = @options[:limit] if @options[:limit] + params[:order] = @options[:order] if @options[:order] + params[:page] = @options[:page] if @options[:page] + params + end + + def paginate(resp) + return unless resp[:total_pages] > 1 + warn "\npage #{resp[:current_page]} of #{resp[:total_pages]}" + end + + class << self + def rescue_api_error(*methods) + methods = [:run] if methods.empty? + mod = Module.new do + methods.each do |method_name| + define_method(method_name) do |*args, &block| + super(*args, &block) + rescue Jets::Api::Error => e + warn "Jets API Error. #{e.message}".color(:red) + log.debug e.backtrace.join("\n") + exit 1 + end + end + end + prepend mod + end + end + end +end diff --git a/lib/jets/cli/bootstrap.rb b/lib/jets/cli/bootstrap.rb new file mode 100644 index 000000000..614ecbad8 --- /dev/null +++ b/lib/jets/cli/bootstrap.rb @@ -0,0 +1,17 @@ +class Jets::CLI + class Bootstrap < Base + def run + are_you_sure? + Jets::Cfn::Bootstrap.new(@options).run + end + + def are_you_sure? + return if @options[:yes] + sure?("Will bootstrap #{stack_name.color(:green)}") + end + + def stack_name + Jets::Names.parent_stack_name + end + end +end diff --git a/lib/jets/cli/build.rb b/lib/jets/cli/build.rb new file mode 100644 index 000000000..14fe880e7 --- /dev/null +++ b/lib/jets/cli/build.rb @@ -0,0 +1,8 @@ +class Jets::CLI + class Build < Base + def run + Jets::Cfn::Bootstrap.new(@options).run + Jets::Remote::Runner.new(@options.merge(command: "build")).run + end + end +end diff --git a/lib/jets/cli/call.rb b/lib/jets/cli/call.rb new file mode 100644 index 000000000..14d1cccea --- /dev/null +++ b/lib/jets/cli/call.rb @@ -0,0 +1,133 @@ +class Jets::CLI + class Call + class Error < StandardError; end + + extend Memoist + include Jets::AwsServices + include Jets::Util::Logging + + attr_reader :event + def initialize(options = {}) + @options = options + @log_type = options[:log_type] || "Tail" + @event = options[:event] || "{}" # json string + end + + def run + warn "Calling Lambda function #{function_name}" + result = invoke + warn "Response:".color(:green) + # only thing that goes to stdout. so can pipe to commands like jq + puts JSON.pretty_generate(result) + end + + def invoke + params = { + function_name: function_name, + invocation_type: invocation_type, # Event or RequestResponse + log_type: @log_type, # pretty sweet + payload: payload # json string + # qualifier: @qualifier # "1", version or alias published version. not used yet + } + + resp = nil + begin + resp = lambda_client.invoke(params) + # Capture @log_last_4kb for log_last_4kb method + # log_last_4kb is an interface method used by Exec::Command + @log_last_4kb = resp.log_result + rescue Aws::Lambda::Errors::ResourceNotFoundException + warn "ERROR: function #{function_name} not found".color(:red) + warn "Maybe check the spelling or the AWS_PROFILE?" + return resp + end + + if !/^2/.match?(resp[:status_code].to_s) + warn "ERROR: Lambda function #{function_name} returned status code: #{resp[:status_code]}".color(:red) + warn resp + end + + if verbose? && invocation_type != "Event" + log_last_4kb + end + + if invocation_type == "Event" + resp + else + text = resp.payload.read # already been normalized/JSON.dump by AWS + data = JSON.parse(text) + ActiveSupport::HashWithIndifferentAccess.new(data) + end + end + + # interface method + def log_last_4kb(header_message = "Showing last 4KB of log from x-amz-log-result header:") + return unless @log_last_4kb + warn header_message + warn Base64.decode64(@log_last_4kb) + end + + # Event invocation returns a "202 Accepted" response. + # It means the request has accepted for processing, but the processing has not + # been completed. Event invocation types are asynchronous. + # The resp.payload.read is a empty string and is not JSON parseable. + # We the raw resp object so the caller can inspect the status code and headers. + # Example: + # + # { + # status_code: 202, + # function_error: nil, + # log_result: nil, + # payload: "[FILTERED]", + # executed_version: nil + # } + # + # Event invocation only use by Jets::Preheat.perform + def invocation_type + @options[:invocation_type] || "RequestResponse" + end + + def verbose? + @options[:verbose] || @options[:logs] + end + + # payload returns a JSON String for the lambda.invoke payload. + # It can be the file:// notation + # interface method + def payload + text = @event + if text&.include?("file://") + text = load_event_from_file(text) + end + text + end + memoize :payload + + def load_event_from_file(text) + path = text.gsub("file://", "") + path = "#{Jets.root}/#{path}" unless path[0..0] == "/" + unless File.exist?(path) + puts "File #{path} does not exist. Are you sure the file exists?".color(:red) + exit 1 + end + text = IO.read(path) + check_valid_json!(text) + text + end + + # Exits with friendly error message when user provides bad just + def check_valid_json!(text) + JSON.parse(text) + rescue JSON::ParserError => e + puts "Invalid json provided:\n '#{text}'" + puts "Exiting... Please try again and provide valid json." + exit 1 + end + + def function_name + name = @options[:function] || "controller" + Jets::CLI::Lambda::Lookup.function(name) + end + memoize :function_name + end +end diff --git a/lib/jets/cli/ci.rb b/lib/jets/cli/ci.rb new file mode 100644 index 000000000..c7980969a --- /dev/null +++ b/lib/jets/cli/ci.rb @@ -0,0 +1,55 @@ +class Jets::CLI + class Ci < Jets::Thor::Base + Init.cli_options.each { |args| option(*args) } + register(Init, "init", "init", "CI init creates config/jets/ci.rb") + + desc "build", "CI build cfn template" + def build + Build.new(options).run + end + + desc "deploy", "CI deploy cfn stack" + yes_option + def deploy + Deploy.new(options).run + end + + desc "delete", "CI delete cfn stack" + yes_option + def delete + Delete.new(options).run + end + + desc "info", "CI info" + format_option(default: "info") + def info + Info.new(options).run + end + + desc "start", "CI start build" + yes_option + option :buildspec_override, desc: "Path to buildspec override file" + option :branch, aliases: "b", desc: "git branch" # Default is nil. Will use what's configured on AWS CodeBuild project settings. + option :env_vars, aliases: "e", type: :array, desc: "env var overrides. IE: KEY1=VALUE1 KEY2=VALUE2" + def start + Start.new(options).run + end + + desc "status", "CI status of build" + def status + Status.new(options).run + end + + desc "stop", "CI stop build" + yes_option + def stop + Stop.new(options).run + end + + desc "logs", "CI logs" + yes_option + def logs + Logs.new(options).run + end + end +end diff --git a/lib/jets/cli/ci/base.rb b/lib/jets/cli/ci/base.rb new file mode 100644 index 000000000..91e11e0e6 --- /dev/null +++ b/lib/jets/cli/ci/base.rb @@ -0,0 +1,54 @@ +class Jets::CLI::Ci + class Base < Jets::CLI::Base + def stack_name + "#{Jets.project.namespace}-ci" + end + alias_method :project_name, :stack_name + + def run_with_exception_handling + yield + rescue Aws::CodeBuild::Errors::ResourceNotFoundException => e + puts "ERROR: #{e.class}: #{e.message}".color(:red) + puts "CodeBuild project #{project_name} not found." + rescue Aws::CodeBuild::Errors::InvalidInputException => e + puts "ERROR: #{e.class}: #{e.message}".color(:red) + end + + def stop_build + build = codebuild.batch_get_builds(ids: [build_id]).builds.first + if build.build_status == "IN_PROGRESS" + codebuild.stop_build(id: build_id) + true + else + log.info "Not in progress. Status is #{build.build_status}. Cannot stop: #{build_id}" + false + end + end + + def build_id + return @options[:build_id] if @options[:build_id] + find_build + end + memoize :build_id + + def find_build + resp = codebuild.list_builds_for_project(project_name: project_name) + resp.ids.first # most recent build_id + rescue Aws::CodeBuild::Errors::ResourceNotFoundException => e + logger.error "ERROR: #{e.class} #{e.message}".color(:red) + exit 1 + end + + def check_build_id! + return if build_id + puts "WARN: No builds found for #{project_name.color(:green)} project" + exit + end + + def show_console_log_url(build_id) + log.info "Console Log Url:" + build_id = build_id.split(":").last + log.info "https://#{Jets.aws.region}.console.aws.amazon.com/codesuite/codebuild/projects/#{project_name}/build/#{project_name}%3A#{build_id}/log" + end + end +end diff --git a/lib/jets/cli/ci/build.rb b/lib/jets/cli/ci/build.rb new file mode 100644 index 000000000..08a96812f --- /dev/null +++ b/lib/jets/cli/ci/build.rb @@ -0,0 +1,8 @@ +class Jets::CLI::Ci + class Build < Base + def run + Jets::Cfn::Bootstrap.new(@options).run + Jets::Remote::Runner.new(@options.merge(command: "ci:build")).run + end + end +end diff --git a/lib/jets/cli/ci/delete.rb b/lib/jets/cli/ci/delete.rb new file mode 100644 index 000000000..04798befe --- /dev/null +++ b/lib/jets/cli/ci/delete.rb @@ -0,0 +1,25 @@ +class Jets::CLI::Ci + class Delete < Base + def run + are_you_sure? + check_exist! + Jets::Cfn::Bootstrap.new(@options).run + Jets::Remote::Runner.new(@options.merge(command: "ci:delete")).run + end + + private + + def are_you_sure? + unless @options[:yes] + sure?("Will delete #{stack_name.color(:green)}") + end + end + + def check_exist! + unless stack_exists?(stack_name) + puts "Stack does not exist: #{stack_name.color(:green)}" + exit 1 + end + end + end +end diff --git a/lib/jets/cli/ci/deploy.rb b/lib/jets/cli/ci/deploy.rb new file mode 100644 index 000000000..c2830a9cb --- /dev/null +++ b/lib/jets/cli/ci/deploy.rb @@ -0,0 +1,17 @@ +class Jets::CLI::Ci + class Deploy < Base + def run + are_you_sure? + Jets::Cfn::Bootstrap.new(@options).run + Jets::Remote::Runner.new(@options.merge(command: "ci:deploy")).run + end + + def are_you_sure? + sure? <<~EOL + Will deploy stack #{stack_name.color(:green)} + + Uses remote runner to deploy a separate stack for CI resources. + EOL + end + end +end diff --git a/lib/jets/cli/ci/info.rb b/lib/jets/cli/ci/info.rb new file mode 100644 index 000000000..663153cbc --- /dev/null +++ b/lib/jets/cli/ci/info.rb @@ -0,0 +1,40 @@ +class Jets::CLI::Ci + class Info < Base + include Jets::AwsServices + + def run + resp = codebuild.batch_get_projects(names: [project_name]) + project = resp.projects.first + present(project) + end + + private + + def present(project) + presenter = CliFormat::Presenter.new(@options) + presenter.empty_message = "Project not found: #{project_name}" + + data = if project.nil? + [] + else + [ + ["Name", project.name], + ["Arn", project.arn], + ["Description", project.description], + ["Service Role", project.service_role], + ["Source", project.source.type], + ["Environment", project.environment.type], + ["Compute Type", project.environment.compute_type], + ["Image", project.environment.image], + ["Timeout", "#{project.timeout_in_minutes} minutes"], + ["Created", project.created] + ] + end + + data.each do |row| + presenter.rows << row + end + presenter.show + end + end +end diff --git a/lib/jets/cli/ci/init.rb b/lib/jets/cli/ci/init.rb new file mode 100644 index 000000000..7fab3be95 --- /dev/null +++ b/lib/jets/cli/ci/init.rb @@ -0,0 +1,97 @@ +class Jets::CLI::Ci + class Init < Jets::CLI::Group::Base + include Jets::Util::Sure + + def self.cli_options + [ + [:force, aliases: :f, type: :boolean, desc: "Bypass overwrite are you sure prompt for existing files"], + [:yes, aliases: :y, type: :boolean, desc: "Skip are you sure prompt"] + ] + end + cli_options.each { |args| class_option(*args) } + + source_root "#{__dir__}/templates" + + private + + def sure_message + <<~EOL + This will set up some initial Jets CI project settings. + + It will make changes to your project source code. + + Please make sure you have backed up and committed your changes first. + EOL + end + + def git_info + @git_info ||= Jets::Git::Info.new + end + + def git_default_branch + git_info.params[:git_default_branch] || "master" + end + + def repo_location + git_url = git_info.params[:git_url] || "https://github.com/ORG/REPO" + if git_url.starts_with?("git@") + git_url.sub(":", "/").sub("git@", "https://").sub(".git", "") + else + git_url + end + end + + def repo_type + case repo_location + when /github/ + "GITHUB" + when /gitlab/ + "GITLAB" + when /bitbucket/ + "BITBUCKET" + when /codecommit/ + "CODECOMMIT" + else + "REPLACE_ME" + end + end + + public + + def check_jets_initialized + unless File.exist?("config/jets/deploy.rb") + puts "config/jets/deploy.rb not found." + puts "Please run: jets init" + exit 1 + end + end + + def check_already_initialized + if File.exist?("config/jets/ci.rb") + puts "Found config/jets/ci.rb" + puts "It looks like the Jets project is already initialized for CI" + exit + end + + lines = IO.readlines("config/jets/deploy.rb") + found = lines.detect do |l| + l.include?("config.deploy.") && !l.match?(/^\s*#/) + end + if found + puts "Found config.ci in config/jets/deploy.rb" + puts "It looks like the Jets project has already been set up for CI" + exit + end + end + + def are_you_sure? + return if options[:yes] + sure?(sure_message) + end + + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codebuild-project-source.html#cfn-codebuild-project-source-type + def config_jets_ci + template "ci.rb.tt", "config/jets/ci.rb" + end + end +end diff --git a/lib/jets/cli/ci/logs.rb b/lib/jets/cli/ci/logs.rb new file mode 100644 index 000000000..5666376a6 --- /dev/null +++ b/lib/jets/cli/ci/logs.rb @@ -0,0 +1,10 @@ +class Jets::CLI::Ci + class Logs < Base + def run + check_build_id! + run_with_exception_handling do + Tailer.new(@options, build_id).run + end + end + end +end diff --git a/lib/jets/cli/ci/start.rb b/lib/jets/cli/ci/start.rb new file mode 100644 index 000000000..34c1161c9 --- /dev/null +++ b/lib/jets/cli/ci/start.rb @@ -0,0 +1,121 @@ +class Jets::CLI::Ci + class Start < Base + def run + are_you_sure? + + params = {project_name: project_name} + source_version = @options[:branch] + params[:source_version] = source_version if source_version # when nil. uses branch configured on CodeBuild project settings + params[:secondary_sources_version_override] = secondary_sources_version_override if secondary_sources_version_override # when nil. uses branch configured on CodeBuild project settings + params[:environment_variables_override] = environment_variables_override if @options[:env_vars] + params.merge!(@options[:overrides]) if @options[:overrides] + branch_info + params[:buildspec_override] = buildspec_override + log.debug("params: #{params}") + resp = start_build(params) + + log.info "Build started for #{project_name}" + show_console_log_url(resp.build.id) + tail_logs(resp.build.id) + + resp = codebuild.batch_get_builds(ids: [resp.build.id]) + success = resp.builds.first.build_status == "SUCCEEDED" + exit 1 unless success + success + end + + def start_build(params) + codebuild.start_build(params) + rescue Aws::CodeBuild::Errors::ResourceNotFoundException => e + log.error "Error: #{e.message}".color(:red) + log.error <<~EOL + AWS CodeBuild Project #{project_name} not found. Are you sure it exists? + Maybe double-check your AWS config settings + EOL + exit 1 + end + + def buildspec_override + return unless @options[:buildspec_override] + IO.read(@options[:buildspec_override]) + end + + def tail_logs(build_id) + Tailer.new(@options, build_id).run + end + + def environment_variables_override + @options[:env_vars].map do |s| + k, v = s.split("=") + ssm = false + if /^ssm:(.*)/ =~ v + v = $1 + ssm = true + end + + { + name: k, + value: v, + type: ssm ? "PARAMETER_STORE" : "PLAINTEXT" + } + end + end + + def branch_info + if @options[:branch] + log.info "Branch: #{@options[:branch]}" + end + if @options[:secondary_branches] + log.info "Branches: #{@options[:secondary_branches]}" + end + end + + # https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/CodeBuild/Client.html#start_build-instance_method + # Map Hash to secondary_sources_version_override Structure. IE: + # From: {Main: "feature1"} + # To: [{source_identifier: "Main", source_version: "feature"}] + def secondary_sources_version_override + secondary_branches = @options[:secondary_branches] + return unless secondary_branches + secondary_branches.map do |id, version| + {source_identifier: id, source_version: version} + end + end + + def are_you_sure? + message = "Will start build and deploy #{project_name.color(:green)}\n" + message << "#{git_dirty_message}\n" if git_dirty? + sure? message + end + + def git_dirty_message + if git_dirty? + <<~EOL.strip + Warning: Git is dirty. The CI build will not include the latest changes. + If you want to include the latest changes, please commit and push them first. + EOL + elsif !git_changes_pushed? + <<~EOL.strip + Warning: The latest changes have not been pushed to the remote repository. + If you want to include the latest changes, please push them first. + EOL + end + end + + def git_changes_pushed? + current_branch = `git branch --show-current`.strip + local_commit = `git rev-parse #{current_branch}`.strip + remote_commit = `git rev-parse origin/#{current_branch}`.strip + local_commit == remote_commit + end + + def git_dirty_note + "(git is dirty)" if git_dirty? + end + + def git_dirty? + `git status --porcelain`.strip != "" + end + memoize :git_dirty? + end +end diff --git a/lib/jets/cli/ci/status.rb b/lib/jets/cli/ci/status.rb new file mode 100644 index 000000000..27d4cc0e4 --- /dev/null +++ b/lib/jets/cli/ci/status.rb @@ -0,0 +1,29 @@ +class Jets::CLI::Ci + class Status < Base + def run + check_build_id! + run_with_exception_handling do + puts "Build id: #{build_id}" + resp = codebuild.batch_get_builds(ids: [build_id]) + build = resp.builds.first + puts "Build status: #{colored(build.build_status)}" + end + end + + private + + def colored(status) + # one of SUCCEEDED FAILED FAULT TIMED_OUT IN_PROGRESS STOPPED + case status + when "SUCCEEDED" + status.color(:green) + when "FAILED", "FAULT", "TIMED_OUT" + status.color(:red) + when "IN_PROGRESS" + status.color(:yellow) + else + status + end + end + end +end diff --git a/lib/jets/cli/ci/stop.rb b/lib/jets/cli/ci/stop.rb new file mode 100644 index 000000000..a1e27ab56 --- /dev/null +++ b/lib/jets/cli/ci/stop.rb @@ -0,0 +1,24 @@ +class Jets::CLI::Ci + class Stop < Base + def run + are_you_sure? + check_build_id! + run_with_exception_handling do + stopped = stop_build + log.info "Build has been stopped: #{build_id}" if stopped + show_console_log_url(build_id) + end + end + + private + + def are_you_sure? + message = "Will stop build for project #{project_name.color(:green)} build_id #{build_id.color(:green)}" + if @options[:yes] + logger.info message + else + sure?(message) + end + end + end +end diff --git a/lib/jets/cli/ci/tailer.rb b/lib/jets/cli/ci/tailer.rb new file mode 100644 index 000000000..f7aab77be --- /dev/null +++ b/lib/jets/cli/ci/tailer.rb @@ -0,0 +1,17 @@ +class Jets::CLI::Ci + class Tailer < Jets::Remote::Tailer + def show_if + return true unless Jets.bootstrap.config.codebuild.logging.show == "filtered" + + start_marker = "Entering phase BUILD" + end_marker = "Phase complete: BUILD" + proc do |event| + @display_showing ||= event.message.include?(start_marker) + if @display_showing && event.message.include?(end_marker) + @display_showing = false + end + @display_showing && !event.message.include?(start_marker) + end + end + end +end diff --git a/lib/jets/cli/ci/templates/ci.rb.tt b/lib/jets/cli/ci/templates/ci.rb.tt new file mode 100644 index 000000000..5ff4ca1ec --- /dev/null +++ b/lib/jets/cli/ci/templates/ci.rb.tt @@ -0,0 +1,19 @@ +Jets.deploy.configure do + # CI Docs: https://docs.rubyonjets.com/docs/ci/getting-started/ + config.ci.source = { + Type: "<%= repo_type %>", + Location: "<%= repo_location %>" + } + config.ci.source_version = "<%= git_default_branch %>" + # triggers means that the CI git push to these branches + config.ci.triggers = ["<%= git_default_branch %>"] + + # Note: JETS_API_KEY is required. The CI runner needs it to run: jets deploy + config.ci.env.vars = { + # BUNDLE_GITHUB__COM: "SSM:/#{ssm_env}/BUNDLE_GITHUB__COM", + JETS_API_KEY: "SSM:/#{ssm_env}/JETS_API_KEY" + } + # config.ci.schedule.enable = true + # config.ci.schedule.rate = "1d" + # config.ci.schedule.cron = "8 0 * * ? *" # every day at 12:08am UTC +end diff --git a/lib/jets/cli/clean.rb b/lib/jets/cli/clean.rb new file mode 100644 index 000000000..32cd336fa --- /dev/null +++ b/lib/jets/cli/clean.rb @@ -0,0 +1,10 @@ +require "fileutils" + +class Jets::CLI + class Clean < Base + def run + FileUtils.rm_rf(Jets.build_root) + puts "Removed #{Jets.build_root}" + end + end +end diff --git a/lib/jets/cli/concurrency.rb b/lib/jets/cli/concurrency.rb new file mode 100644 index 000000000..7eb3a222e --- /dev/null +++ b/lib/jets/cli/concurrency.rb @@ -0,0 +1,33 @@ +class Jets::CLI + class Concurrency < Jets::Thor::Base + desc "info", "Concurrency info" + format_option(default: "table") + def info + Info.new(options).run + end + + desc "get", "Get concurrency for function" + function_name_option + def get + Get.new(options).run + end + + desc "set", "Set concurrency for function" + function_name_option + option :reserved, type: :numeric, desc: "Reserved concurrency" + option :provisioned, type: :numeric, desc: "Provisioned concurrency" + yes_option + def set + Set.new(options).run + end + + desc "unset", "Unset concurrency for function" + function_name_option + option :reserved, type: :boolean, desc: "Reserved concurrency" + option :provisioned, type: :boolean, desc: "Provisioned concurrency" + yes_option + def unset + Unset.new(options).run + end + end +end diff --git a/lib/jets/cli/concurrency/base.rb b/lib/jets/cli/concurrency/base.rb new file mode 100644 index 000000000..2231335ab --- /dev/null +++ b/lib/jets/cli/concurrency/base.rb @@ -0,0 +1,17 @@ +class Jets::CLI::Concurrency + class Base < Jets::CLI::Base + include Jets::CLI::Lambda::Checks + include Jets::Util::Truthy + + def initialize(options = {}) + super + check_deployed! + end + + def account_limit + response = lambda_client.get_account_settings + response.account_limit + end + memoize :account_limit + end +end diff --git a/lib/jets/cli/concurrency/get.rb b/lib/jets/cli/concurrency/get.rb new file mode 100644 index 000000000..1fdc2d381 --- /dev/null +++ b/lib/jets/cli/concurrency/get.rb @@ -0,0 +1,37 @@ +class Jets::CLI::Concurrency + class Get < Base + def initialize(options = {}) + super + function_name = Jets::CLI::Lambda::Lookup.function(@options[:function]) + @lambda_function = Jets::CLI::Lambda::Function.new(function_name) + end + + def run + puts <<~EOL + Settings for Function: #{@lambda_function.name} + Reserved concurreny: #{reserved_concurrency} + Provisioned concurrency: #{provisioned_concurrency} + EOL + end + + def provisioned_concurrency + info = @lambda_function.provisioned_concurrency_info + + if @lambda_function.provisioned_concurrency.nil? + "not set" + elsif info[:status] == "IN_PROGRESS" + "#{info[:allocated]}/#{info[:requested]} (In progress)" + else + @lambda_function.provisioned_concurrency + end + end + + def reserved_concurrency + if @lambda_function.reserved_concurrency.nil? + "not set. Will scale to unreserved limit: #{account_limit.unreserved_concurrent_executions}" + else + @lambda_function.reserved_concurrency + end + end + end +end diff --git a/lib/jets/cli/concurrency/info.rb b/lib/jets/cli/concurrency/info.rb new file mode 100644 index 000000000..bc77f41a1 --- /dev/null +++ b/lib/jets/cli/concurrency/info.rb @@ -0,0 +1,86 @@ +class Jets::CLI::Concurrency + class Info < Base + include Jets::CLI::Lambda::Functions + + def run + concurrency_info + account_limit_info + end + + def concurrency_info + warn "Concurrency for #{Jets.project.namespace}" + + presenter = CliFormat::Presenter.new(@options) + presenter.empty_message = "No Lambda Functions found" + presenter.header = ["Function"] + presenter.header << "Reserved" if has_reserved? + presenter.header << "Provisioned" if has_provisioned? + concurrency_settings.each do |function_name, concurrency_info| + row = [] + row << function_name.gsub("#{Jets.project.namespace}-", "") + if has_reserved? + reserved = concurrency_info[:reserved_concurrency] + @reserved_concurrency_total ||= 0 + @reserved_concurrency_total += reserved.to_i + row << reserved + end + if has_provisioned? + provisioned = concurrency_info[:provisioned_concurrency] + @provisioned_concurrency_total ||= 0 + @provisioned_concurrency_total += provisioned.to_i + row << provisioned + end + presenter.rows << row + end + + # Totals row + unless presenter.rows.empty? + totals = ["total"] + totals << @reserved_concurrency_total if @reserved_concurrency_total + totals << @provisioned_concurrency_total if @provisioned_concurrency_total + presenter.rows << totals + end + presenter.show + end + + def concurrency_settings + concurrency_settings = {} + + lambda_functions.each do |lambda_function| + concurrency_info = { + reserved_concurrency: lambda_function.reserved_concurrency, + provisioned_concurrency: lambda_function.provisioned_concurrency + } + concurrency_info.delete_if { |_, v| v.nil? } + concurrency_settings[lambda_function.name] = concurrency_info + end + + concurrency_settings + end + memoize :concurrency_settings + + def has_reserved? + concurrency_settings.any? { |_, info| info.key?(:reserved_concurrency) } + end + + def has_provisioned? + concurrency_settings.any? { |_, info| info.key?(:provisioned_concurrency) } + end + + def account_limit_info + message = <<~EOL + Account Limits + Concurrent Executions: #{account_limit.concurrent_executions} + Unreserved Concurrent Executions: #{account_limit.unreserved_concurrent_executions} + EOL + if has_unreserved? + message << "Functions with no reserved limit scale to Unreserved Concurrent Executions\n" + end + warn message + end + + def has_unreserved? + concurrency_settings.any? { |_, info| !info.key?(:reserved_concurrency) } + end + end +end diff --git a/lib/jets/cli/concurrency/set.rb b/lib/jets/cli/concurrency/set.rb new file mode 100644 index 000000000..777f07268 --- /dev/null +++ b/lib/jets/cli/concurrency/set.rb @@ -0,0 +1,31 @@ +class Jets::CLI::Concurrency + class Set < Get + def run + sure? "Will update the concurrency settings for #{Jets.project.namespace}" + puts "Updating concurrency settings for #{Jets.project.namespace}" + + if @options[:reserved] + @lambda_function.reserved_concurrency = @options[:reserved] + puts "Set reserved concurrency to #{@options[:reserved]}" + end + + if @options[:provisioned] + success = set_provisioned_concurrency(@options[:provisioned]) + puts "Set provisioned concurrency to #{@options[:provisioned]}" if success + end + + Jets::CLI::Tip.show(:concurrency_change) + end + + def set_provisioned_concurrency(value) + @lambda_function.provisioned_concurrency = value + true # success + rescue Aws::Lambda::Errors::ResourceNotFoundException => e + # Can happen for Events Lambda Lambda Functions where Wersion and Alias resources are only created when specified in config/jets + # For controller Lambda Function the Alias and Version resource is always created. + if e.message.include?("Cannot find alias") + puts "ERROR: The live alias does not exist for the function. Please deploy the function an initial provisioned concurrency first.".color(:red) + end + end + end +end diff --git a/lib/jets/cli/concurrency/unset.rb b/lib/jets/cli/concurrency/unset.rb new file mode 100644 index 000000000..e98f9aa71 --- /dev/null +++ b/lib/jets/cli/concurrency/unset.rb @@ -0,0 +1,21 @@ +class Jets::CLI::Concurrency + class Unset < Set + def run + sure? "Will unset the concurrency settings for #{Jets.project.namespace}" + puts "Unsetting concurrency settings for #{Jets.project.namespace}" + + if @options[:reserved] + @lambda_function.reserved_concurrency = nil + puts "Removed reserved concurrency" + puts "Will scale to your AWS account unreserved limit. Currently: #{account_limit.unreserved_concurrent_executions}" + end + + if @options[:provisioned] + success = set_provisioned_concurrency(0) + puts "Removed provisioned concurrency" if success + end + + Jets::CLI::Tip.show(:concurrency_change) + end + end +end diff --git a/lib/jets/cli/curl.rb b/lib/jets/cli/curl.rb new file mode 100644 index 000000000..c3bc880e3 --- /dev/null +++ b/lib/jets/cli/curl.rb @@ -0,0 +1,9 @@ +class Jets::CLI + class Curl < Base + def run + result = Request.new(options).run + # only thing that goes to stdout. so can pipe to commands like jq + puts JSON.pretty_generate(result) + end + end +end diff --git a/lib/jets/cli/curl/adapter/base.rb b/lib/jets/cli/curl/adapter/base.rb new file mode 100644 index 000000000..6eafa46ee --- /dev/null +++ b/lib/jets/cli/curl/adapter/base.rb @@ -0,0 +1,8 @@ +module Jets::CLI::Curl::Adapter + class Base + extend Memoist + def initialize(options) + @options = options + end + end +end diff --git a/lib/jets/cli/curl/adapter/cookies/jar.rb b/lib/jets/cli/curl/adapter/cookies/jar.rb new file mode 100644 index 000000000..414a4714b --- /dev/null +++ b/lib/jets/cli/curl/adapter/cookies/jar.rb @@ -0,0 +1,28 @@ +module Jets::CLI::Curl::Adapter::Cookies + class Jar + include Jets::Util::Logging + + def initialize(result, filename) + @result, @filename = result, filename + end + + def write_to_file + cookies = @result[:cookies] + if cookies.nil? || cookies.empty? + log.debug "No cookies found in the result." + return + end + + File.open(@filename, "w") do |file| + cookies.each do |cookie| + file.puts("# HTTP Cookie File") + file.puts("# Created by jets curl #{Jets::VERSION}") + file.puts("# Date: #{Time.now}\n\n") + file.puts("#{cookie}\n") + end + end + + log.debug "Cookies written to #{@filename}." + end + end +end diff --git a/lib/jets/cli/curl/adapter/cookies/parser.rb b/lib/jets/cli/curl/adapter/cookies/parser.rb new file mode 100644 index 000000000..bd45abbd0 --- /dev/null +++ b/lib/jets/cli/curl/adapter/cookies/parser.rb @@ -0,0 +1,48 @@ +module Jets::CLI::Curl::Adapter::Cookies + class Parser + def initialize(cookie_string) + @cookie_string = cookie_string + end + + def parse + if @cookie_string.include?("=") + parse_inline_cookies + else + parse_cookies_from_file + end + end + + private + + def skip_line?(line) + line.empty? || line.start_with?("#") + end + + def parse_inline_cookies + cookies = [] + + @cookie_string.split(";").each do |cookie| + cookie = cookie.strip + cookies << cookie unless skip_line?(cookie) + end + + cookies + end + + def parse_cookies_from_file + cookies = [] + + if File.exist?(@cookie_string) + File.open(@cookie_string, "r").each_line do |line| + line = line.chomp.strip + cookies << line unless skip_line?(line) + end + else + warn "Error: File '#{@cookie_string}' not found." + exit 1 + end + + cookies + end + end +end diff --git a/lib/jets/cli/curl/adapter/lambda.rb b/lib/jets/cli/curl/adapter/lambda.rb new file mode 100644 index 000000000..e7a0f2d98 --- /dev/null +++ b/lib/jets/cli/curl/adapter/lambda.rb @@ -0,0 +1,142 @@ +require "rack/utils" + +module Jets::CLI::Curl::Adapter + class Lambda < Base + def convert + { + version: "2.0", + routeKey: "$default", + rawPath: path, + rawQueryString: raw_query_string, + cookies: cookies, + body: body, + headers: headers, + requestContext: request_context, + isBase64Encoded: false + }.delete_if { |k, v| v.nil? }.to_json + end + + def cookies + Cookies::Parser.new(@options[:cookie]).parse if @options[:cookie] + end + + def body + data = @options[:data] + return unless data + if data.starts_with?("@") + file = data[1..] + file = "#{Jets.root}/#{file}" unless file.starts_with?("/") + IO.read(file) # IE: @data.json + else + data + end + end + + def headers + default_headers.merge(headers_option) + end + + def default_headers + { + host: host, + "x-forwarded-proto": "https", + "x-forwarded-port": "443", + "x-forwarded-for": "127.0.0.1", + "user-agent": "jets curl (#{Jets::VERSION})" + } + end + + def headers_option + headers = @options[:headers] || {} + headers["user-agent"] = user_agent if user_agent + headers.transform_keys(&:downcase).transform_values(&:strip) + end + + def host + return default_host unless @options[:headers] + deleted_host = @options[:headers].delete("Host")&.strip + host = @options[:headers][:host] || deleted_host + host || default_host + end + + def default_host + host = uri.host + if host && (host.include?("amazonaws.com") || host.include?(".aws")) + host + else + "#{api_id}.lambda-url.#{aws_region}.on.aws" + end + end + + def user_agent + return unless @options[:headers] + deleted_user_agent = @options[:headers].delete("User-Agent")&.strip + @options[:headers]["user-agent"] || deleted_user_agent + end + memoize :user_agent # run only since .delete will change the headers_option + + def request_context + now = Time.now.utc + { + accountId: "anonymous", + apiId: api_id, + domainName: host, + domainPrefix: api_id, + http: { + method: http_method, + path: path, + protocol: "HTTP/1.1", + sourceIp: "127.0.0.1", + userAgent: user_agent + }, + requestId: "3fca3afe-2fb7-4e93-ac3b-949519408c39", + routeKey: "$default", + stage: "$default", + time: now.strftime("%d/%b/%Y:%H:%M:%S %z"), + timeEpoch: now.to_i + } + end + + def api_id + "dummy" + end + + def aws_region + Jets.aws.region + end + + def http_method + @options[:request] || "GET" + end + + def uri + URI.parse(@options[:path]) + end + + def path + uri.path + end + + def raw_query_string + uri.query + end + + # AWS Lambda 2.0 Behavior + # https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html + def parse_query_string(raw_query_string) + query_params = {} + + raw_query_string.split("&").each do |param| + key, value = param.split("=") + key = key.to_sym + if query_params.key?(key) + query_params[key] += ",#{value}" + else + query_params[key] = value + end + end + + query_params.transform_values! { |value| value.include?(",") ? value.split(",") : value } + end + end +end diff --git a/lib/jets/cli/curl/request.rb b/lib/jets/cli/curl/request.rb new file mode 100644 index 000000000..01426feaa --- /dev/null +++ b/lib/jets/cli/curl/request.rb @@ -0,0 +1,62 @@ +require "active_support" +require "active_support/core_ext/string/filters" + +class Jets::CLI::Curl + class Request < Jets::CLI::Call + TRIM_MAX = ENV["JETS_CURL_TRIM_MAX"] || 64 + def run + warn "Calling Lambda function #{function_name}" + show_body + result = invoke + + if result[:cookies] && @options[:cookie_jar] + cookie_jar = Adapter::Cookies::Jar.new(result, @options[:cookie_jar]) + cookie_jar.write_to_file + end + if @options[:trim] || ENV["JETS_CURL_TRIM"] + trim!(result, TRIM_MAX) + else + result + end + end + + def show_body + return unless @options[:verbose] && @options[:data] + hash = JSON.parse(payload) + body = hash["body"] + text = begin + JSON.pretty_generate(JSON.parse(body)) + rescue JSON::ParserError + body + end + + warn "Request Body:" + warn text + end + + # interface method: override to convert cli curl-like options to Call payload + def payload + adapter.convert + end + + def trim!(hash, max_length) + hash.transform_values! do |value| + if value.is_a?(String) && value.length > max_length + value.truncate(max_length) + elsif value.is_a?(Hash) + trim!(value, max_length) + elsif value.is_a?(Array) + value.map! { |v| (v.is_a?(String) && v.length > max_length) ? v.truncate(max_length) : v } + else + value + end + end + end + + def adapter + # Only support Lambda URL for now. + Adapter::Lambda.new(@options) + end + memoize :adapter + end +end diff --git a/lib/jets/cli/delete.rb b/lib/jets/cli/delete.rb new file mode 100644 index 000000000..8a030092d --- /dev/null +++ b/lib/jets/cli/delete.rb @@ -0,0 +1,23 @@ +class Jets::CLI + class Delete < Base + def run + are_you_sure? + Jets::Cfn::Bootstrap.new(@options).run + Jets::Cfn::Delete.new(options).run + end + + def are_you_sure? + stack_name = Jets.project.namespace + message = "Will delete #{stack_name.color(:green)}" + unless stack_exists?(stack_name) + message << <<~EOL + + Note: It looks like the stack #{stack_name} has already been deleted. + Jets will create a dummy stack to delete the API deployment record. + The dummy stack will be deleted immediately after. + EOL + end + sure?(message) + end + end +end diff --git a/lib/jets/cli/deploy.rb b/lib/jets/cli/deploy.rb new file mode 100644 index 000000000..7f01b8dc0 --- /dev/null +++ b/lib/jets/cli/deploy.rb @@ -0,0 +1,18 @@ +class Jets::CLI + class Deploy < Base + def run + dev_mode_check! + sure?("Will deploy #{Jets.project.namespace.color(:green)}") + Tip.show(:faster_deploy) + + Jets::Cfn::Bootstrap.new(@options).run + Jets::Remote::Runner.new(@options.merge(command: "deploy")).run + end + + def dev_mode_check! + if File.exist?("#{Jets.root}/dev.mode") && !ENV["JETS_SKIP_DEV_MODE_CHECK"] + abort "The dev.mode file exists. Please removed it and run bundle update before you deploy.".color(:red) + end + end + end +end diff --git a/lib/jets/cli/dotenv.rb b/lib/jets/cli/dotenv.rb new file mode 100644 index 000000000..794e509ab --- /dev/null +++ b/lib/jets/cli/dotenv.rb @@ -0,0 +1,28 @@ +class Jets::CLI + class Dotenv < Jets::Thor::Base + desc "list", "Parse and list dotenv vars" + format_option(default: "dotenv") + option :reveal, type: :boolean, default: false, desc: "Reveal values also" + def list + List.new(options).run + end + + desc "get NAME", "Get env var from local files and SSM" + def get(name) + Get.new(options.merge(name: name)).run + end + + desc "set VALUES", "Set SSM env vars for function" + yes_option + option :secure, type: :boolean, default: true, desc: "Whether or not to use SSM parameter type SecureString or String" + def set(*values) + Set.new(options.merge(values: values)).run + end + + desc "unset NAMES", "Unset SSM env vars for function" + yes_option + def unset(*names) + Unset.new(options.merge(names: names)).run + end + end +end diff --git a/lib/jets/cli/dotenv/base.rb b/lib/jets/cli/dotenv/base.rb new file mode 100644 index 000000000..d8d33e57f --- /dev/null +++ b/lib/jets/cli/dotenv/base.rb @@ -0,0 +1,4 @@ +class Jets::CLI::Dotenv + class Base < Jets::CLI::Base + end +end diff --git a/lib/jets/cli/dotenv/get.rb b/lib/jets/cli/dotenv/get.rb new file mode 100644 index 000000000..11a2d13f7 --- /dev/null +++ b/lib/jets/cli/dotenv/get.rb @@ -0,0 +1,14 @@ +class Jets::CLI::Dotenv + class Get < Base + def run + vars = Jets::Dotenv.parse + vars.find do |name, value| + if name == @options[:name] + puts value + exit # success + end + end + exit 1 # not found + end + end +end diff --git a/lib/jets/cli/dotenv/list.rb b/lib/jets/cli/dotenv/list.rb new file mode 100644 index 000000000..9ed51132d --- /dev/null +++ b/lib/jets/cli/dotenv/list.rb @@ -0,0 +1,32 @@ +require "shellwords" + +class Jets::CLI::Dotenv + class List < Base + def run + presenter = CliFormat::Presenter.new(@options) + warn "# Env from config/jets/env files and SSM parameters" + warn "# Values are not used locally. They are only used for the Lambda Function" + unless @options[:reveal] + warn "# To show values also, use the --reveal option" + end + presenter.empty_message = "# No env vars found" + unless @options[:format] == "dotenv" + header = ["Name"] + header << "Value" if @options[:reveal] + presenter.header = header + end + vars = Jets::Dotenv.parse + vars.each do |key, value| + v = inspect?(value) ? value.inspect : value + row = [key] + row << v if @options[:reveal] + presenter.rows << row + end + presenter.show + end + + def inspect?(value) + value.include?("\n") || Shellwords.escape(value) != value + end + end +end diff --git a/lib/jets/cli/dotenv/set.rb b/lib/jets/cli/dotenv/set.rb new file mode 100644 index 000000000..b591d2593 --- /dev/null +++ b/lib/jets/cli/dotenv/set.rb @@ -0,0 +1,50 @@ +class Jets::CLI::Dotenv + class Set < Base + include Jets::CLI::Env::Parse + + def run + sure? sure_message + puts "Setting SSM vars for #{Jets.project.namespace}" + + perform # interface method + Jets::CLI::Tip.show(:ssm_change) + end + + def perform + ssm_manager.set(vars) + end + + def vars + parse_cli_env_values(@options[option_key]) + end + + # interface method + def option_key + :values + end + + def ssm_method + name = self.class.name.demodulize # Set or Unset + (name == "Set") ? :set : :delete + end + + def names + vars.keys.map(&:to_s) + end + + def sure_message + <<~EOL + Will #{ssm_method} the SSM vars for #{Jets.project.namespace} + Note: SSM changes do not update the Lambda function env vars. + You will need run jets deploy to update the env vars. + + #{ssm_manager.preview_list(names)} + EOL + end + + def ssm_manager + Jets::CLI::Dotenv::Ssm.new(@options) + end + memoize :ssm_manager + end +end diff --git a/lib/jets/cli/dotenv/ssm.rb b/lib/jets/cli/dotenv/ssm.rb new file mode 100644 index 000000000..923131909 --- /dev/null +++ b/lib/jets/cli/dotenv/ssm.rb @@ -0,0 +1,60 @@ +class Jets::CLI::Dotenv + # The update logic is here and not a part of Jets::Dotenv::Ssm to emphasize + # that it's only used for jets dotenv commands. + # This class is responsible for updating. + # The other Jets::Dotenv class is only repsonsible for reading. + # The one part of the other class that is used is Jets::Dotenv::Convention + class Ssm + include Jets::AwsServices + include Jets::Util::Logging + + def initialize(options = {}) + @options = options + @parameter_type = options[:secure] ? "SecureString" : "String" + end + + def set(params) + # Loop through the hash and update each parameter + params.each do |name, value| + name = conventional_name(name) + ssm.put_parameter( + name: name, # The name of the parameter + value: value, # The new value for the parameter + type: @parameter_type, # Set type to 'SecureString' if secure, else 'String' + overwrite: true # Allows overwriting an existing parameter + ) + log.info "SSM Parameter set: #{name}" + end + end + + # There's a delete_parameters method also that can delete 10 at a time. + # Use simple one-by-one deletion for clarity and to surface errors. + # Will allow program to continue for ParameterNotFound error. + # In case use wants to keep trying to delete all parameters. + def delete(names) + names.each do |name| + name = conventional_name(name) + ssm.delete_parameter(name: name) + log.info "SSM Parameter deleted: #{name}" + rescue Aws::SSM::Errors::ParameterNotFound => e + log.warn "WARN: Failed to delete parameter #{name}: #{e.message}" + end + end + + def preview_list(names) + names = names.is_a?(Hash) ? names.keys.map(&:to_s) : names + names.map do |name| + " #{conventional_name(name)}" + end.join("\n") + end + + def conventional_name(name) + # add conventional prefix + # "/#{Jets.project.name}/#{Jets.env}/#{value}" + unless name.include?("/") + name = Jets::Dotenv::Convention.new.ssm_name(name) + end + name + end + end +end diff --git a/lib/jets/cli/dotenv/unset.rb b/lib/jets/cli/dotenv/unset.rb new file mode 100644 index 000000000..ee770d6b0 --- /dev/null +++ b/lib/jets/cli/dotenv/unset.rb @@ -0,0 +1,11 @@ +class Jets::CLI::Dotenv + class Unset < Set + def perform + ssm_manager.delete(names) + end + + def names + @options[:names] + end + end +end diff --git a/lib/jets/cli/env.rb b/lib/jets/cli/env.rb new file mode 100644 index 000000000..809e1e467 --- /dev/null +++ b/lib/jets/cli/env.rb @@ -0,0 +1,29 @@ +class Jets::CLI + class Env < Jets::Thor::Base + class_option :function, aliases: :n, default: "controller", desc: "Lambda Function name" + + desc "list", "List and show env vars" + format_option(default: "dotenv") + option :reveal, type: :boolean, default: false, desc: "Reveal values also" + def list + List.new(options).run + end + + desc "get NAME", "Get env vars for function" + def get(name) + Get.new(options.merge(key: name)).run + end + + desc "set VALUES", "Set env vars for function" + yes_option + def set(*values) + Set.new(options.merge(values: values)).run + end + + desc "unset NAMES", "Unset env vars for function" + yes_option + def unset(*names) + Unset.new(options.merge(names: names)).run + end + end +end diff --git a/lib/jets/cli/env/base.rb b/lib/jets/cli/env/base.rb new file mode 100644 index 000000000..6194ebf54 --- /dev/null +++ b/lib/jets/cli/env/base.rb @@ -0,0 +1,13 @@ +class Jets::CLI::Env + class Base < Jets::CLI::Base + include Jets::CLI::Lambda::Checks + include Jets::Util::Truthy + + def initialize(options = {}) + super + check_deployed! + function_name = Jets::CLI::Lambda::Lookup.function(options[:function]) + @lambda_function = Jets::CLI::Lambda::Function.new(function_name) + end + end +end diff --git a/lib/jets/cli/env/get.rb b/lib/jets/cli/env/get.rb new file mode 100644 index 000000000..60a048777 --- /dev/null +++ b/lib/jets/cli/env/get.rb @@ -0,0 +1,13 @@ +class Jets::CLI::Env + class Get < Base + def run + @lambda_function.environment_variables.find do |key, value| + if key == @options[:key] + puts value + exit # success + end + end + exit 1 # not found + end + end +end diff --git a/lib/jets/cli/env/list.rb b/lib/jets/cli/env/list.rb new file mode 100644 index 000000000..8a1d05c61 --- /dev/null +++ b/lib/jets/cli/env/list.rb @@ -0,0 +1,19 @@ +class Jets::CLI::Env + class List < Base + def run + warn "# Env Variables for #{Jets.project.namespace}" + unless @options[:reveal] + warn "# To show values also, use the --reveal option" + end + vars = @lambda_function.environment_variables + + presenter = CliFormat::Presenter.new(@options) + vars.each do |key, value| + row = [key] + row << value if @options[:reveal] + presenter.rows << row + end + presenter.show + end + end +end diff --git a/lib/jets/cli/env/parse.rb b/lib/jets/cli/env/parse.rb new file mode 100644 index 000000000..54daaa954 --- /dev/null +++ b/lib/jets/cli/env/parse.rb @@ -0,0 +1,11 @@ +class Jets::CLI::Env + module Parse + def parse_cli_env_values(pairs) + # Use each_with_object to iterate over the pairs and insert them into a hash + pairs.each_with_object({}) do |pair, hash| + key, value = pair.split("=") + hash[key] = value + end + end + end +end diff --git a/lib/jets/cli/env/set.rb b/lib/jets/cli/env/set.rb new file mode 100644 index 000000000..0f9f1e4e0 --- /dev/null +++ b/lib/jets/cli/env/set.rb @@ -0,0 +1,25 @@ +class Jets::CLI::Env + class Set < Base + include Parse + + def run + are_you_sure? + puts "Setting env vars for #{@lambda_function.name}" + + @lambda_function.environment_variables = environment_variables + Jets::CLI::Tip.show(:env_change) + end + + def environment_variables + parse_cli_env_values(@options[:values]) + end + + def are_you_sure? + name = self.class.to_s.demodulize.underscore.humanize.downcase + sure? <<~EOL + Will #{name} env vars for #{@lambda_function.name} + The Lambda Function will immediately use the new env vars. + EOL + end + end +end diff --git a/lib/jets/cli/env/unset.rb b/lib/jets/cli/env/unset.rb new file mode 100644 index 000000000..5f29a50f3 --- /dev/null +++ b/lib/jets/cli/env/unset.rb @@ -0,0 +1,17 @@ +class Jets::CLI::Env + class Unset < Set + def run + are_you_sure? + puts "Unsetting env vars for #{@lambda_function.name}" + + @lambda_function.environment_variables = environment_variables + Jets::CLI::Tip.show(:env_change) + end + + def environment_variables + @options[:names].each_with_object({}) do |name, hash| + hash[name] = nil + end + end + end +end diff --git a/lib/jets/cli/exec.rb b/lib/jets/cli/exec.rb new file mode 100644 index 000000000..086d40d84 --- /dev/null +++ b/lib/jets/cli/exec.rb @@ -0,0 +1,14 @@ +class Jets::CLI + class Exec < Base + def run + if @options[:command].empty? + Repl.new(options).start + else + Command.new(options).run + end + rescue Jets::CLI::Call::Error => e + puts "ERROR: #{e.message}".color(:red) + abort "Unable to find the function. Please check the function name and try again." + end + end +end diff --git a/lib/jets/cli/exec/command.rb b/lib/jets/cli/exec/command.rb new file mode 100644 index 000000000..31dc485d2 --- /dev/null +++ b/lib/jets/cli/exec/command.rb @@ -0,0 +1,61 @@ +class Jets::CLI::Exec + class Command < Jets::CLI::Call + # override behavior + def run + result = invoke + if result["errorMessage"] + # Note: + # errorType is Function and not useful + # stackTrace is also not useful. IE: [{}, {}, {}, {}, {}, {}, {}] + # Actual stackTrace only shows up in the logs + log.error "ERROR: #{result["errorMessage"]}".color(:red) + useless_stacktrace = result["stackTrace"]&.all? { |line| line == {} } + if useless_stacktrace + # Logs can come from: + # + # 1. Lambda invoke host log: shows cold-start and Jets.boot errors + # 2. Lambda runtime container log: shows errors inside handler + # + # The result seems to hide the cold-start/Jets.boot errors. + # Guessing AWS does this for security reasons and hides it with {}. + # + # Since result["stackTrace"] since only shows errors within the handler. + # Errors that outside the handler at cold-start/Jets.boot time are not in + # result["stackTrace"]. They show up in the logs though. + # So we show the logs that are available from the header. + log_last_4kb(<<~EOL) + Showing last 4KB of logs from x-amz-log-result header for errors. + + You can check for more logs with. + + jets logs -n #{friendly_function_name} + + Last 4KB of logs: + EOL + elsif result["stackTrace"] + log.error "Stack Trace:" + result["stackTrace"].each do |line| + log.error line + end + else # fallback to errorMessage + # No stack trace available. + # Example: result: {"errorMessage"=>"2024-04-18T19:42:51.819Z cdbcd9f2-6d25-4672-8a83-676643698fa0 Task timed out after 3.05 seconds"} + log.error "errorMessage: #{result["errorMessage"]}" + end + else + $stdout.print result["stdout"] + $stderr.print result["stderr"] # same as $stderr.puts + end + result + end + + def friendly_function_name + function_name.sub("#{Jets.project.namespace}-", "") + end + + # interface method + def payload + {command: @options[:command]}.to_json + end + end +end diff --git a/lib/jets/cli/exec/repl.rb b/lib/jets/cli/exec/repl.rb new file mode 100644 index 000000000..1acd7887d --- /dev/null +++ b/lib/jets/cli/exec/repl.rb @@ -0,0 +1,122 @@ +require "fileutils" +require "open3" +require "readline" + +class Jets::CLI::Exec + class Repl + extend Memoist + include Jets::Util::Logging + + attr_reader :history + def initialize(options = {}) + @options = options + @history = History.new(@options) # load for history to work upon start + trap_signals + end + + def start + welcome + + loop do + input = Readline.readline("$ ", true) + + if input.nil? || input.downcase == "exit" + puts "Exiting..." + history.save + break + elsif input.strip.empty? + next + elsif input.strip.start_with? "history" + num = input.split(" ")[1] + history.display(num) + next + elsif input.strip.start_with? "!" + execute_from_history(input) + next + elsif input.strip == "status" + display_last_status + next + elsif %w[_ result].include?(input.strip) + display_last_result + next + elsif %w[help ?].include?(input.strip) + display_help + next + end + + if history.list.empty? || input != history.list.last + history.add(input) + end + execute_command(input) + end + end + + private + + def welcome + function_name = Command.new(@options).function_name + puts <<~EOL + Jets REPL (#{Jets::VERSION}). Commands will be executed on Lambda. + Lambda function: #{function_name} + Type 'help' for help, 'exit' to exit. + EOL + end + + def execute_command(input) + result = Command.new(@options.merge(command: input)).run + @last_status = result["errorMessage"] ? 1 : result["status"] + @last_result = result + end + + def execute_from_history(input) + index = input[1..-1].to_i - 1 + if index >= 0 && index < history.list.length + input = history.list[index] + puts "> #{input}" + execute_command(input) + else + puts "Invalid history number" + end + end + + def display_last_status + case @last_status + when nil + puts "No command has been executed on Lambda yet." + when 0 + puts "Last command had a status of success (0)." + else + puts "Last command had a status other than success (#{@last_status})." + end + end + + def display_last_result + if @last_result + puts "Last result:" + puts JSON.pretty_generate(@last_result) + else + puts "No command has been executed on Lambda yet." + end + end + + def display_help + puts <<~HELP + Available commands: + - history [n or 'all']: Display the last n commands or the all command history. (Default: 20) + - status: Display the status of the last command executed on Lambda. + - result or _: Show previous command result. + - help: Display this help message. + - !: Execute the command from the history by number. + - exit: Exit the REPL. You can also use Control-D. + HELP + end + + def trap_signals + Signal.trap("INT") do + puts "\nExiting..." + history.save + exit + end + end + end +end diff --git a/lib/jets/cli/exec/repl/history.rb b/lib/jets/cli/exec/repl/history.rb new file mode 100644 index 000000000..f7a3bb904 --- /dev/null +++ b/lib/jets/cli/exec/repl/history.rb @@ -0,0 +1,42 @@ +class Jets::CLI::Exec::Repl + class History + MAX_SIZE = 10_000 + + attr_reader :list + def initialize(options = {}) + @options = options + @file = "#{ENV["HOME"]}/.jets/history" + @list = load + end + + def add(cmd) + @list << cmd + @list.shift if @list.size > MAX_SIZE + end + + def display(num = nil) + num ||= 20 + num = (num == "all") ? @list.length : num.to_i + num = [@list.length, num].min + start_index = [@list.length - num, 0].max + @list[start_index..].each_with_index { |cmd, index| puts "#{start_index + index + 1}: #{cmd}" } + end + + def load + history = if File.exist?(@file) + File.readlines(@file).map(&:chomp) + else + [] + end + history.each { |cmd| Readline::HISTORY << cmd } + history + end + + def save + FileUtils.mkdir_p(File.dirname(@file)) + File.open(@file, "w") do |file| + @list.each { |cmd| file.puts cmd } + end + end + end +end diff --git a/lib/jets/cli/functions.rb b/lib/jets/cli/functions.rb new file mode 100644 index 000000000..02afc0557 --- /dev/null +++ b/lib/jets/cli/functions.rb @@ -0,0 +1,39 @@ +class Jets::CLI + class Functions < Base + def run + functions = all + puts functions.sort + end + + def all + functions = [] + nested_stack_resources.each do |resource| + stack_name = resource.physical_resource_id + # Custom resource stacks may not have output with function name. + # So use describe_stack_resources to get the function names. + resources = cfn.describe_stack_resources(stack_name: stack_name).stack_resources + resources.each do |r| + if r.resource_type == "AWS::Lambda::Function" + functions << r.physical_resource_id + end + end + end + unless @options[:full] + functions = functions.map { |f| f.sub("#{Jets.project.namespace}-", "") } + end + functions + end + + def nested_stack_resources + stack_name = Jets::Names.parent_stack_name + resp = cfn.describe_stack_resources(stack_name: stack_name) + resp.stack_resources.select { |r| r.resource_type == "AWS::CloudFormation::Stack" } + rescue Aws::CloudFormation::Errors::ValidationError => e + if e.message.include?("does not exist") + abort "The stack #{stack_name} does not exist. Have you deployed yet?".color(:red) + else + raise + end + end + end +end diff --git a/lib/jets/cli/generate.rb b/lib/jets/cli/generate.rb new file mode 100644 index 000000000..2bf30a2de --- /dev/null +++ b/lib/jets/cli/generate.rb @@ -0,0 +1,6 @@ +class Jets::CLI + class Generate < Jets::Thor::Base + Event.cli_options.each { |args| option(*args) } + register(Event, "event", "event NAME", "Generate event app code") + end +end diff --git a/lib/jets/cli/generate/event.rb b/lib/jets/cli/generate/event.rb new file mode 100644 index 000000000..0fed117dc --- /dev/null +++ b/lib/jets/cli/generate/event.rb @@ -0,0 +1,29 @@ +class Jets::CLI::Generate + class Event < Jets::CLI::Group::Base + argument :name, required: true, desc: "Event name. Example: cool" + + def self.cli_options + [ + [:force, aliases: :f, type: :boolean, desc: "Bypass overwrite are you sure prompt for existing files"], + [:method, aliases: :m, desc: "Method name", default: "handle"], + [:trigger, aliases: :t, desc: "Event trigger", default: "scheduled"] + ] + end + cli_options.each { |args| class_option(*args) } + + source_root "#{__dir__}/templates/event_types" + + public + + def application_event + template "application_event.rb", "app/events/application_event.rb", skip: true + end + + def event + trigger = options[:trigger] + trigger = "scheduled" if trigger == "schedule" # allow both to work + template_path = "#{trigger}.rb.tt" + template template_path, "app/events/#{underscore_name}_event.rb" + end + end +end diff --git a/lib/jets/cli/generate/templates/application_event.rb.tt b/lib/jets/cli/generate/templates/application_event.rb.tt new file mode 100644 index 000000000..c40cdc948 --- /dev/null +++ b/lib/jets/cli/generate/templates/application_event.rb.tt @@ -0,0 +1,6 @@ +<% module_namespacing do -%> +class ApplicationEvent < Jets::Event::Base + # Adjust default timeout for all Event classes + class_timeout 15.minutes +end +<% end -%> diff --git a/lib/jets/cli/generate/templates/event_types/application_event.rb b/lib/jets/cli/generate/templates/event_types/application_event.rb new file mode 100644 index 000000000..4e2423b5b --- /dev/null +++ b/lib/jets/cli/generate/templates/event_types/application_event.rb @@ -0,0 +1,4 @@ +class ApplicationEvent < Jets::Event::Base + # Adjust default timeout for all Event classes + class_timeout 15.minutes +end diff --git a/lib/jets/cli/generate/templates/event_types/dynamodb.rb.tt b/lib/jets/cli/generate/templates/event_types/dynamodb.rb.tt new file mode 100644 index 000000000..8a5f6cb0f --- /dev/null +++ b/lib/jets/cli/generate/templates/event_types/dynamodb.rb.tt @@ -0,0 +1,6 @@ +class <%= class_name %>Event < ApplicationEvent + dynamodb_event "test-table" # existing namespaced table: IE: demo-dev-test-table + def <%= options[:method] %> + puts "event #{JSON.dump(event)}" + end +end diff --git a/lib/jets/cli/generate/templates/event_types/iot.rb.tt b/lib/jets/cli/generate/templates/event_types/iot.rb.tt new file mode 100644 index 000000000..d096fba1a --- /dev/null +++ b/lib/jets/cli/generate/templates/event_types/iot.rb.tt @@ -0,0 +1,6 @@ +class <%= class_name %>Event < ApplicationEvent + iot_event "SELECT * FROM 'my/topic'" + def <%= options[:method] %> + puts "event #{JSON.dump(event)}" + end +end diff --git a/lib/jets/cli/generate/templates/event_types/kinesis.rb.tt b/lib/jets/cli/generate/templates/event_types/kinesis.rb.tt new file mode 100644 index 000000000..b9c6279bc --- /dev/null +++ b/lib/jets/cli/generate/templates/event_types/kinesis.rb.tt @@ -0,0 +1,7 @@ +class <%= class_name %>Event < ApplicationEvent + kinesis_event "my-stream" # existing stream + def <%= options[:method] %> + puts "event #{JSON.dump(event)}" + puts "kinesis_data #{JSON.dump(kinesis_data)}" + end +end diff --git a/lib/jets/cli/generate/templates/event_types/log.rb.tt b/lib/jets/cli/generate/templates/event_types/log.rb.tt new file mode 100644 index 000000000..147014e2c --- /dev/null +++ b/lib/jets/cli/generate/templates/event_types/log.rb.tt @@ -0,0 +1,7 @@ +class <%= class_name %>Event < ApplicationEvent + log_event "/aws/lambda/hello" + def <%= options[:method] %> + puts "event #{JSON.dump(event)}" + puts "log_event #{JSON.dump(log_event)}" + end +end diff --git a/lib/jets/generators/job/templates/event_types/rule.rb.tt b/lib/jets/cli/generate/templates/event_types/rule.rb.tt similarity index 82% rename from lib/jets/generators/job/templates/event_types/rule.rb.tt rename to lib/jets/cli/generate/templates/event_types/rule.rb.tt index 7f6523d56..294663d07 100644 --- a/lib/jets/generators/job/templates/event_types/rule.rb.tt +++ b/lib/jets/cli/generate/templates/event_types/rule.rb.tt @@ -1,5 +1,4 @@ -<% module_namespacing do -%> -class <%= class_name %>Job < ApplicationJob +class <%= class_name %>Event < ApplicationEvent rule_event( description: "Checks for security group changes", detail_type: ["AWS API Call via CloudTrail"], @@ -15,9 +14,8 @@ class <%= class_name %>Job < ApplicationJob ] } ) - def <%= options[:name] %> + def <%= options[:method] %> puts "event: #{JSON.dump(event)}" # event is available # your logic end end -<% end -%> diff --git a/lib/jets/cli/generate/templates/event_types/s3.rb.tt b/lib/jets/cli/generate/templates/event_types/s3.rb.tt new file mode 100644 index 000000000..f8f6ac7fb --- /dev/null +++ b/lib/jets/cli/generate/templates/event_types/s3.rb.tt @@ -0,0 +1,13 @@ +class <%= class_name %>Event < ApplicationEvent + # Please read the Considerations section before using s3_event + s3_event "my-bucket" # <= CHANGE ME: new or existing bucket + def <%= options[:method] %> + puts "event #{JSON.dump(event)}" + puts "s3_events #{JSON.dump(s3_events)}" + puts "s3_objects #{JSON.dump(s3_objects)}" + # s3_files.each do |file| + # puts "file.filename #{file.filename}" + # puts "file.content #{file.content}" + # end + end +end diff --git a/lib/jets/cli/generate/templates/event_types/scheduled.rb.tt b/lib/jets/cli/generate/templates/event_types/scheduled.rb.tt new file mode 100644 index 000000000..542955cbe --- /dev/null +++ b/lib/jets/cli/generate/templates/event_types/scheduled.rb.tt @@ -0,0 +1,6 @@ +class <%= class_name %>Event < ApplicationEvent + rate "10 hours" + def <%= options[:method] %> + puts "Do something with event #{JSON.dump(event)}" + end +end diff --git a/lib/jets/cli/generate/templates/event_types/sns.rb.tt b/lib/jets/cli/generate/templates/event_types/sns.rb.tt new file mode 100644 index 000000000..2eb393865 --- /dev/null +++ b/lib/jets/cli/generate/templates/event_types/sns.rb.tt @@ -0,0 +1,8 @@ +class <%= class_name %>Event < ApplicationEvent + sns_event "hello-topic" + def <%= options[:method] %> + puts "event #{JSON.dump(event)}" + puts "sns_events #{JSON.dump(sns_events)}" + puts "sns_events? #{JSON.dump(sns_events?)}" + end +end diff --git a/lib/jets/cli/generate/templates/event_types/sqs.rb.tt b/lib/jets/cli/generate/templates/event_types/sqs.rb.tt new file mode 100644 index 000000000..d277a9371 --- /dev/null +++ b/lib/jets/cli/generate/templates/event_types/sqs.rb.tt @@ -0,0 +1,9 @@ +class <%= class_name %>Event < ApplicationEvent + class_timeout 30 # Lambda Function timeout must be less than or equal to the SQS Visibility timeout + sqs_event "hello-queue" + def <%= options[:method] %> + puts "event #{JSON.dump(event)}" + puts "sqs_events #{JSON.dump(sqs_events)}" + puts "sqs_events? #{JSON.dump(sqs_events?)}" + end +end diff --git a/lib/jets/cli/group/actions.rb b/lib/jets/cli/group/actions.rb new file mode 100644 index 000000000..620757cb9 --- /dev/null +++ b/lib/jets/cli/group/actions.rb @@ -0,0 +1,57 @@ +module Jets::CLI::Group + module Actions + extend Memoist + + def config_environment(data, options = {}) + config_file = if options[:env] + "config/environments/#{options[:env]}.rb" + else + "config/application.rb" + end + lines = IO.readlines(config_file) + # remove comment lines + lines.reject! { |line| line =~ /^\s*#/ } + found = lines.any? { |line| line.include?(data) } + environment(data, options) unless found + end + + def environment(data = nil, options = {}) + sentinel = "class Application < Rails::Application\n" + env_file_sentinel = "Rails.application.configure do\n" + data ||= yield if block_given? + + in_root do + if options[:env].nil? + inject_into_file "config/application.rb", optimize_indentation(data, 4), after: sentinel, verbose: true + else + Array(options[:env]).each do |env| + inject_into_file "config/environments/#{env}.rb", optimize_indentation(data, 2), after: env_file_sentinel, verbose: true + end + end + end + end + alias_method :application, :environment + + def optimize_indentation(value, amount = 0) # :doc: + return "#{value}\n" unless value.is_a?(String) + "#{value.strip_heredoc.indent(amount).chomp}\n" + end + alias_method :rebase_indentation, :optimize_indentation + + def comment_out_line(string, options = {}) + file = if options[:env] + "config/environments/#{options[:env]}.rb" + else + "config/application.rb" + end + lines = IO.readlines(file).map do |l| + if l.include?(string) && !l.strip.start_with?("#") + " # #{l.strip}" + else + l + end + end + IO.write(file, lines.join) + end + end +end diff --git a/lib/jets/cli/group/base.rb b/lib/jets/cli/group/base.rb new file mode 100644 index 000000000..c893e5a05 --- /dev/null +++ b/lib/jets/cli/group/base.rb @@ -0,0 +1,12 @@ +require "fileutils" +require "thor" + +module Jets::CLI::Group + class Base < Thor::Group + include Thor::Actions + include Actions + include Helpers + + add_runtime_options! # force, pretend, quiet, skip options + end +end diff --git a/lib/jets/cli/group/helpers.rb b/lib/jets/cli/group/helpers.rb new file mode 100644 index 000000000..ee67be985 --- /dev/null +++ b/lib/jets/cli/group/helpers.rb @@ -0,0 +1,26 @@ +module Jets::CLI::Group + module Helpers + extend Memoist + + def class_name + name.camelize + end + + def underscore_name + name.underscore + end + + def init_project_name + Jets.project.name # inferred from the folder name + end + + def framework + Jets::CLI::Init::Detect.new.framework + end + memoize :framework + + def package_type + (framework == "rails") ? "image" : "zip" + end + end +end diff --git a/lib/jets/cli/help.rb b/lib/jets/cli/help.rb new file mode 100644 index 000000000..eb98286c9 --- /dev/null +++ b/lib/jets/cli/help.rb @@ -0,0 +1,16 @@ +class Jets::CLI + module Help + extend self + def text(namespaced_command) + file = namespaced_command.to_s.tr(":", "/") + path = File.expand_path("../help/#{file}.md", __FILE__) + return IO.read(path) if File.exist?(path) + + # Also look up for a help folder within the current command folder + called_from = caller(1..1).first.split(":").first + unnamespaced_command = namespaced_command.to_s.split(":").last + path = File.expand_path("../help/#{unnamespaced_command}.md", called_from) + IO.read(path) if File.exist?(path) + end + end +end diff --git a/lib/jets/cli/help/build.md b/lib/jets/cli/help/build.md new file mode 100644 index 000000000..94b2ed8c0 --- /dev/null +++ b/lib/jets/cli/help/build.md @@ -0,0 +1 @@ +Build the package and CloudFormation templates but does not deploy it. This can be helpful for development. \ No newline at end of file diff --git a/lib/jets/cli/help/call.md b/lib/jets/cli/help/call.md new file mode 100644 index 000000000..6a7cf2b35 --- /dev/null +++ b/lib/jets/cli/help/call.md @@ -0,0 +1,28 @@ +## Remote mode + +Invoke the lambda function on AWS. + +## Examples Cheatsheet + + jets call -n cool_event-party -e '{"test":1}' + jets call -n cool_event-party -e '{"test":1}' | jq . + jets call -n cool_event-party -e '{"test":1}' --verbose | jq + jets call -n cool_event-party file://event.json | jq . # load event with a file + jets call -n jets-prewarm_event-handle -e '{"invocation_type": "RequestResponse"}' + +The equivalent AWS Lambda CLI command: + + aws lambda invoke --function-name demo-dev-cool_event-party --payload -e '{"test":1}' outfile.txt + cat outfile.txt | jq '.' + +## Logs + +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`. + +## Controller Note + +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. + + jets call -n controller --event file://lambda.json + +The `jets curl` handles this more automatically is recommended over the `jets call` command for calling Jets controller. diff --git a/lib/jets/cli/help/curl.md b/lib/jets/cli/help/curl.md new file mode 100644 index 000000000..8a5e2fd8f --- /dev/null +++ b/lib/jets/cli/help/curl.md @@ -0,0 +1,82 @@ +The `jets curl` command mimics the curl interface and directly invokes the AWS Lambda function. It is helpful in debugging the underlying Lambda event and response structure. + +Key points: + +* The `jets curl` command calls the AWS Lambda function with the `invoke` API call. +* It translates the curl-like options into a Lambda Function URL event payload. +* The event is sent to your Jets controller Lambda function. +* The **raw** Lambda Function response is sent back. +* You can pipe this to `jq` for pretty output. + +Example: + + ❯ jets curl / | jq + Calling Lambda function event-app-dev-controller + Response: + { + "statusCode": 200, + "headers": { + "Content-Type": "text/plain" + }, + "body": "Hello, World!", + "cookies": [ + "my_cookie=my_value; Path=/" + ], + "isBase64Encoded": false + } + +Only a path is needed. IE: When you don't need to specify a hostname, Jets mimics with a dummy one, IE: dummy.lambda-url.us-west-2.on.aws. If you specify the hostname, `jets curl` will use that value instead of a dummy url. + +The `jets curl` command provides some of the curl interfaces but does not cover all curl options exhaustively. + +## Examples Cheatsheet + + jets curl / | jq + jets curl /posts | jq + jets curl / --cookie cookies.txt | jq + jets curl / --cookie "cookie1=value1; cookie2=value2" + jets curl / --cookie-jar cookies.txt | jq + jets curl / -H Host:example.com + jets curl / -X POST -d "foo=bar" + jets curl foobar.lambda-url.us-west-2.on.aws/ + jets curl / --verbose | jq + jets curl / --verbose --trim | jq + +## Multiple Headers + +The Thor CLI parses multiple options differently than curl does. Here's how you pass multiple headers. + + jets curl / -H User-Agent:james-bond Host:example.com + +## Trim Option + +The `--trim` options "trims" values from the Lambda Response Hash to shortened the output so that it's more human-readable. The default trim max length is 64. You can adjust it with the `JETS_CURL_TRIM_MAX` env var. + + export JETS_CURL_TRIM_MAX=32 + jets curl / | jq + +## Cookies + +You can pass it request cookies with `--cookie` or `-b`. + +The `--cookie` option takes a string with values like so: + + jets curl / --cookie "cookie1=value1; cookie2=value2" + +To write the response cookies to a file, use the `--cookie-jar` or `-c` option. Example: + + jets curl / --verbose --trim -c cookies.txt + +Creates: + +cookies.txt + + # HTTP Cookie File + dummy.lambda-url.us-west-2.on.aws FALSE / FALSE 0 yummy1 value1 + dummy.lambda-url.us-west-2.on.aws FALSE / FALSE 0 yummy2 value2 + +You can also see the response cookies in the Lambda response hash structure. + +Saving the cookies.txt is useful to send it later. + + jets curl / -b cookies.txt \ No newline at end of file diff --git a/lib/jets/cli/help/delete.md b/lib/jets/cli/help/delete.md new file mode 100644 index 000000000..a3ae67124 --- /dev/null +++ b/lib/jets/cli/help/delete.md @@ -0,0 +1,22 @@ +Deletes the Jets deployment and all resources associated with it. + +## Examples + + $ jets delete + Deleting project... + Are you sure you want to want to delete the 'demo-dev' project? (y/N) + y + Emptying s3 bucket demo-dev-s3bucket-89jrrj60c7bj + Deleting demo-dev... + 05:14:09AM DELETE_IN_PROGRESS AWS::CloudFormation::Stack demo-dev User Initiated + ... + 05:14:23AM DELETE_IN_PROGRESS AWS::CloudFormation::Stack PostsController + 05:15:31AM DELETE_COMPLETE AWS::S3::Bucket S3Bucket + Stack demo-dev deleted. + Time took for deletion: 1m 27s. + Project demo-dev deleted! + $ + +You can bypass the are you sure prompt with the `-y` flag. + + $ jets delete --y diff --git a/lib/jets/cli/help/deploy.md b/lib/jets/cli/help/deploy.md new file mode 100644 index 000000000..a5cf784b2 --- /dev/null +++ b/lib/jets/cli/help/deploy.md @@ -0,0 +1,5 @@ +Deploys project to AWS Lambda. + +## Example + + $ jets deploy diff --git a/lib/jets/cli/help/dotenv/get.md b/lib/jets/cli/help/dotenv/get.md new file mode 100644 index 000000000..076210a65 --- /dev/null +++ b/lib/jets/cli/help/dotenv/get.md @@ -0,0 +1,4 @@ +## Example + + ❯ jets dotenv:get NAME1 + value1 diff --git a/lib/jets/cli/help/dotenv/list.md b/lib/jets/cli/help/dotenv/list.md new file mode 100644 index 000000000..1a85cbec3 --- /dev/null +++ b/lib/jets/cli/help/dotenv/list.md @@ -0,0 +1,5 @@ +## Example + + ❯ jets dotenv:list + NAME1=value1 + NAME2=value2 \ No newline at end of file diff --git a/lib/jets/cli/help/dotenv/set.md b/lib/jets/cli/help/dotenv/set.md new file mode 100644 index 000000000..f20398fe7 --- /dev/null +++ b/lib/jets/cli/help/dotenv/set.md @@ -0,0 +1,25 @@ +## Examples + +An SSM Parameter name is conventionally used based on JETS_ENV and the Jets project in `config/jets/project.rb`. Example: + + ❯ jets dotenv:set NAME1=value1 NAME2=value2 + Will set the SSM vars for demo-dev + + /demo/dev/NAME1 + /demo/dev/NAME2 + + Are you sure? (y/N) y + Setting SSM vars for demo-dev + SSM Parameter set: /demo/dev/NAME1 + SSM Parameter set: /demo/dev/NAME2 + +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. + + ❯ jets dotenv:set /dev/NAME1=value1 + Will set the SSM vars for sinatra-dev + + /dev/NAME1 + + Are you sure? (y/N) y + Setting SSM vars for sinatra-dev + SSM Parameter set: /dev/NAME1 diff --git a/lib/jets/cli/help/dotenv/unset.md b/lib/jets/cli/help/dotenv/unset.md new file mode 100644 index 000000000..037a56f06 --- /dev/null +++ b/lib/jets/cli/help/dotenv/unset.md @@ -0,0 +1,12 @@ +## Example + + ❯ jets dotenv:unset NAME1 NAME2 + Will delete the SSM vars for demo-dev + + /demo/dev/NAME1 + /demo/dev/NAME2 + + Are you sure? (y/N) y + Setting SSM vars for demo-dev + SSM Parameter deleted: /demo/dev/NAME1 + SSM Parameter deleted: /demo/dev/NAME2 diff --git a/lib/jets/cli/help/exec.md b/lib/jets/cli/help/exec.md new file mode 100644 index 000000000..191179150 --- /dev/null +++ b/lib/jets/cli/help/exec.md @@ -0,0 +1,53 @@ +Execute commands on AWS Lambda environment. + +## REPL + +When no command is provided a REPL is started. + + ❯ jets exec + > pwd + /var/task + > whoami + sbx_user1051 + > echo $AWS_REGION + us-west-2 + > env | sort + +More examples: + + ❯ jets exec + > env | sort + > cat /etc/os-release + > du -sh * | sort -sh + +## Execute Commands + + ❯ jets exec uname -a + Linux ... GNU/Linux + +## Status and Result + +You can see the status and result of the last command executed on AWS Lambda. Example: + + ❯ jets exec + > whoami + sbx_user1051 + > status + Last command had a status of success (0). + > result + Last result: + { + "stdout": "sbx_user1051\n", + "stderr": "", + "status": 0 + } + > echo "This is an error message" >&2 + This is an error message + > result + Last result: + { + "stdout": "", + "stderr": "This is an error message\n", + "status": 0 + } + > \ No newline at end of file diff --git a/lib/jets/cli/help/generate/event.md b/lib/jets/cli/help/generate/event.md new file mode 100644 index 000000000..c9fd30123 --- /dev/null +++ b/lib/jets/cli/help/generate/event.md @@ -0,0 +1,11 @@ +## Examples + + jets generate:event log --trigger log --method report + jets generate:event cool --trigger scheduled + jets generate:event security --trigger rule --method detect_security_group_changes + jets generate:event clerk --trigger dynamodb --method file + jets generate:event thermostat --trigger iot --method measure + jets generate:event data --trigger kinesis --method file + jets generate:event upload --trigger s3 + jets generate:event messenger --trigger sns --method deliver + jets generate:event waiter --trigger sqs --method order diff --git a/lib/jets/command/help/logs.md b/lib/jets/cli/help/logs.md similarity index 77% rename from lib/jets/command/help/logs.md rename to lib/jets/cli/help/logs.md index 644e53ad9..1fa2ebafe 100644 --- a/lib/jets/command/help/logs.md +++ b/lib/jets/cli/help/logs.md @@ -1,6 +1,6 @@ Show logs from Lambda function CloudWatch log group. -This defaults to the controller Lambda function in the `one_lambda_for_all_controllers` mode. Example: +This defaults to the controller Lambda function. Example: ❯ jets logs Showing logs for /aws/lambda/demo-dev-controller @@ -12,7 +12,7 @@ If you want to follow the logs use the `-f` flag. If you want to see the production logs: - ❯ JETS_ENV=production jets logs -f + ❯ JETS_ENV=prod jets logs -f Tailing logs for /aws/lambda/demo-prod-controller If you want to see logs for a job, specify the job and method. diff --git a/lib/jets/command/help/projects.md b/lib/jets/cli/help/projects.md similarity index 100% rename from lib/jets/command/help/projects.md rename to lib/jets/cli/help/projects.md diff --git a/lib/jets/cli/help/release/history.md b/lib/jets/cli/help/release/history.md new file mode 100644 index 000000000..32793fa76 --- /dev/null +++ b/lib/jets/cli/help/release/history.md @@ -0,0 +1,19 @@ +## Examples + + $ jets release:history + Releases for stack: demo-dev + +---------+-----------------+--------------+---------+ + | Version | Status | Released At | Message | + +---------+-----------------+--------------+---------+ + | 3 | UPDATE_COMPLETE | 10 hours ago | Deploy | + | 2 | UPDATE_COMPLETE | 18 hours ago | Deploy | + | 1 | UPDATE_COMPLETE | 22 hours ago | Deploy | + +---------+-----------------+--------------+---------+ + +The shown releases are paginated. If you need to see more releases you can use the `--page` option. + + $ jets release:history --page 2 + +## Other Commands + + release:info View detailed information for a release diff --git a/lib/jets/cli/help/rollback.md b/lib/jets/cli/help/rollback.md new file mode 100644 index 000000000..3d279bb71 --- /dev/null +++ b/lib/jets/cli/help/rollback.md @@ -0,0 +1,5 @@ +## Examples + + jets rollback 8 + +Use the [jets release:history](/reference/jets-release-history/) command to view release history. diff --git a/lib/jets/cli/help/schedule/validate.md b/lib/jets/cli/help/schedule/validate.md new file mode 100644 index 000000000..0e0ad4df8 --- /dev/null +++ b/lib/jets/cli/help/schedule/validate.md @@ -0,0 +1 @@ +Validate config/jets/schedule.yml by creating and deleting live event rules. \ No newline at end of file diff --git a/lib/jets/command/help/stacks.md b/lib/jets/cli/help/stacks.md similarity index 100% rename from lib/jets/command/help/stacks.md rename to lib/jets/cli/help/stacks.md diff --git a/lib/jets/command/help/status.md b/lib/jets/cli/help/status.md similarity index 98% rename from lib/jets/command/help/status.md rename to lib/jets/cli/help/status.md index dbc0e7c8f..9b84fda13 100644 --- a/lib/jets/command/help/status.md +++ b/lib/jets/cli/help/status.md @@ -8,7 +8,7 @@ The CloudFormation stack status info. Essentially the events of the CloudFormati 05:21:42AM UPDATE_IN_PROGRESS AWS::CloudFormation::Stack demo-dev User Initiated 05:21:45AM CREATE_IN_PROGRESS AWS::CloudFormation::Stack ApiGateway ... - 05:23:22AM CREATE_COMPLETE AWS::CloudFormation::Stack JetsPreheatJob + 05:23:22AM CREATE_COMPLETE AWS::CloudFormation::Stack JetsPrewarmEvent 05:23:25AM UPDATE_COMPLETE_CLEANUP_IN_PROGRESS AWS::CloudFormation::Stack demo-dev 05:23:25AM UPDATE_COMPLETE AWS::CloudFormation::Stack demo-dev $ \ No newline at end of file diff --git a/lib/jets/command/help/url.md b/lib/jets/cli/help/url.md similarity index 100% rename from lib/jets/command/help/url.md rename to lib/jets/cli/help/url.md diff --git a/lib/jets/cli/init.rb b/lib/jets/cli/init.rb new file mode 100644 index 000000000..fa5c620ff --- /dev/null +++ b/lib/jets/cli/init.rb @@ -0,0 +1,66 @@ +class Jets::CLI + class Init < Jets::CLI::Group::Base + include Jets::Util::Sure + + def self.cli_options + [ + [:env, type: :boolean, desc: "Generate config/jets/env/.env example file"], + [:force, aliases: :f, type: :boolean, desc: "Bypass overwrite are you sure prompt for existing files"], + [:yes, aliases: :y, type: :boolean, desc: "Skip are you sure prompt"] + ] + end + cli_options.each { |args| class_option(*args) } + + source_root "#{__dir__}/init/templates" + + private + + def sure_message + detected_message = "Detected #{framework} framework.\n" if framework + <<~EOL + #{detected_message}This will initialize the project for Jets. + + It will make changes to your project source code. + + Please make sure you have backed up and committed your changes first. + EOL + end + + public + + def are_you_sure? + return if options[:yes] || options[:force] + sure?(sure_message) + end + + def env_example + # need to create .env for RAILS_ENV=production + create_env = @options[:env].nil? ? framework == "rails" : @options[:env] + if create_env + template "env/.env.tt", "config/jets/env/.env" + end + end + + def config_jets + directory "config/jets", "config/jets" + end + + def configure_environment + return unless framework == "rails" + # config/environments/production.rb adjustments + # Comment out public_file_server.enabled on new Rails 7.0 apps + # config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present? + comment_out_line("config.public_file_server.enabled = ENV", env: "production") + config_environment("config.public_file_server.enabled = false", env: "production") + # Comment out config.asset_host = "http://assets.example.com" on new Rails 7.0 apps + # config.asset_host = "http://assets.example.com" + comment_out_line('config.asset_host = "', env: "production") + config_environment('config.asset_host = ENV["JETS_ASSET_HOST"] unless ENV["JETS_ASSET_HOST"].blank?', env: "production") + jets_job = <<~EOL + # Docs: https://docs.rubyonjets.com/docs/jobs/enable/ + # config.active_job.queue_adapter = :jets_job + EOL + config_environment(jets_job, env: "production") + end + end +end diff --git a/lib/jets/cli/init/detect.rb b/lib/jets/cli/init/detect.rb new file mode 100644 index 000000000..2f9bc7538 --- /dev/null +++ b/lib/jets/cli/init/detect.rb @@ -0,0 +1,30 @@ +class Jets::CLI::Init + class Detect + def framework + gems.each do |gem| + frameworks.each do |framework| + return framework if gem == framework + end + end + nil + end + + def frameworks + %w[ + rails + sinatra + hanami + grape + ] + end + + def gems + return [] unless File.exist?("Gemfile") + Bundler.with_unbundled_env do + gemfile_content = File.read("Gemfile") + dsl = Bundler::Dsl.evaluate(Bundler.default_gemfile, gemfile_content, {}) + dsl.dependencies.map(&:name) + end + end + end +end diff --git a/lib/jets/cli/init/templates/config/jets/bootstrap.rb.tt b/lib/jets/cli/init/templates/config/jets/bootstrap.rb.tt new file mode 100644 index 000000000..a92055c36 --- /dev/null +++ b/lib/jets/cli/init/templates/config/jets/bootstrap.rb.tt @@ -0,0 +1,16 @@ +Jets.bootstrap.configure do + # Docs: http://rubyonjets.com/docs/remote/codebuild/ + # config.codebuild.project.env.vars = { + # BUNDLE_GITHUB__COM: "SSM:/#{ssm_env}/BUNDLE_GITHUB__COM", + # DOCKER_PASS: "SSM:/#{ssm_env}/DOCKER_PASS", + # DOCKER_USER: "SSM:/#{ssm_env}/DOCKER_USER", + # # Tip: Use your own Docker host for faster deploys + # DOCKER_HOST: "SSM:/#{ssm_env}/DOCKER_HOST", + # JETS_SSH_KEY: "SSM:/#{ssm_env}/JETS_SSH_KEY", + # JETS_SSH_KNOWN: "SSM:/#{ssm_env}/JETS_SSH_KNOWN" + # } + + # Additional codebuild remote-lambda runner for commands that can leverage it + # Docs: https://docs.rubyonjets.com/docs/remote/codebuild/compute-type/#lambda-compute-type + # config.codebuild.lambda.enable = true +end diff --git a/lib/jets/cli/init/templates/config/jets/deploy.rb.tt b/lib/jets/cli/init/templates/config/jets/deploy.rb.tt new file mode 100644 index 000000000..5753e13f2 --- /dev/null +++ b/lib/jets/cli/init/templates/config/jets/deploy.rb.tt @@ -0,0 +1,43 @@ +Jets.deploy.configure do +<% unless framework == "rails" -%> + # https://docs.rubyonjets.com/docs/config/package-type/ + # config.package_type = "image" # default: image IE: image or zip + +<% end -%> +<% if framework == "rails" -%> + # https://docs.rubyonjets.com/docs/jobs/ + # config.job.enable = true + +<% end -%> + # Docker https://docs.rubyonjets.com/docs/docker/dockerfile/managed/ + # config.dockerfile.packages.apt.build_stage = ["default-libmysqlclient-dev"] + # config.dockerfile.packages.apt.deployment_stage = ["default-mysql-client"] + + # CloudFront Lambda URL https://docs.rubyonjets.com/docs/routing/lambda/cloudfront/distribution/ + # config.lambda.url.cloudfront.enable = true + # config.lambda.url.cloudfront.cert.arn = acm_cert_arn("domain.com", region: "us-east-1") + # config.lambda.url.cloudfront.route53.enable = true + + # WAF https://docs.rubyonjets.com/docs/routing/lambda/cloudfront/waf/ + # config.lambda.url.cloudfront.web_acl_id = web_acl_arn(ssm_env, scope: "CLOUDFRONT") + # config.waf.rules = ["AWSManagedRulesSQLiRuleSet"] + + # Scaling https://docs.rubyonjets.com/docs/config/provisioned-concurrency/ + # config.lambda.controller.provisioned_concurrency = 1 # costs money, no cold start + # config.lambda.controller.reserved_concurrency = 10 # free and limits scaling + + # Release phase https://docs.rubyonjets.com/docs/hooks/remote/release/ + # config.release.phase.command = "bundle exec rails db:migrate" + + # CI https://docs.rubyonjets.com/docs/ci/getting-started/ + # use jets ci:init to add these settings automatically + # config.ci.source = { + # Type: "GITHUB", + # Location: "https://github.com/ORG/REPO" + # } + # config.ci.source_version = "main" + # config.ci.env.vars = { + # BUNDLE_GITHUB__COM: "SSM:/#{ssm_env}/BUNDLE_GITHUB__COM", + # JETS_API_KEY: "SSM:/#{ssm_env}/JETS_API_KEY" + # } +end diff --git a/lib/jets/cli/init/templates/config/jets/project.rb.tt b/lib/jets/cli/init/templates/config/jets/project.rb.tt new file mode 100644 index 000000000..6857b84df --- /dev/null +++ b/lib/jets/cli/init/templates/config/jets/project.rb.tt @@ -0,0 +1,3 @@ +Jets.project.configure do + config.name = "<%= init_project_name %>" +end diff --git a/lib/jets/cli/init/templates/env/.env.tt b/lib/jets/cli/init/templates/env/.env.tt new file mode 100644 index 000000000..76f8742fc --- /dev/null +++ b/lib/jets/cli/init/templates/env/.env.tt @@ -0,0 +1,18 @@ +# Docs: https://docs.rubyonjets.com/docs/env/files/ +# This file is optional. +# It's recommended to use SSM parameters with conventional paths instead. +# See: https://docs.rubyonjets.com/docs/env/ssm/conventions/ +<% if framework == "rails" -%> +RAILS_ENV=production +RAILS_LOG_TO_STDOUT=1 +# SECRET_KEY_BASE=SSM # SSM:/<%= init_project_name %>/dev/SECRET_KEY_BASE remember to set a secret key base +<% end -%> + +# Examples: +# No Conventions: +# KEY1=value1 # hard coded value +# KEY2=SSM:/abs/path/to/KEY2 +# Conventions https://docs.rubyonjets/docs/env/ssm/conventions/ +# SSM path under /<%= init_project_name %>/dev/ are autoloaded. +# You can also control this behavior and then manually declare the SSM parameter. +# DATABASE_URL=SSM # SSM:/<%= init_project_name %>/dev/DATABASE_URL diff --git a/lib/jets/cli/lambda/checks.rb b/lib/jets/cli/lambda/checks.rb new file mode 100644 index 000000000..2247e3449 --- /dev/null +++ b/lib/jets/cli/lambda/checks.rb @@ -0,0 +1,22 @@ +module Jets::CLI::Lambda + module Checks + extend Memoist + + def check_deployed! + return if stack_exists?(Jets.project.namespace) + warn "ERROR: Project has not been deployed".color(:red) + exit 1 + end + + def check_workers! + return if workers_deployed? + warn "No worker functions deployed" + exit 1 + end + + def workers_deployed? + Jets::CLI::Maintenance::Worker::Saver.new(@options).lambda_functions.size > 0 + end + memoize :workers_deployed? + end +end diff --git a/lib/jets/cli/lambda/function.rb b/lib/jets/cli/lambda/function.rb new file mode 100644 index 000000000..e34d395e2 --- /dev/null +++ b/lib/jets/cli/lambda/function.rb @@ -0,0 +1,104 @@ +module Jets::CLI::Lambda + class Function + include Jets::AwsServices + attr_reader :function_name + alias_method :name, :function_name + + def initialize(function_name) + @function_name = function_name + end + + # Environment Variables + def environment_variables + response = lambda_client.get_function_configuration(function_name: function_name) + response.environment.variables.sort.to_h + end + + def environment_variables=(env_vars) + current_env = environment_variables + + # Update existing vars and remove vars set to nil + updated_env = current_env.merge(env_vars.stringify_keys) { |key, old_val, new_val| new_val.nil? ? nil : new_val } + updated_env.compact! # Removes all key-value pairs where value is nil + + lambda_client.update_function_configuration( + function_name: function_name, + environment: {variables: updated_env} + ) + end + + # Reserved Concurrency + def reserved_concurrency + response = lambda_client.get_function_concurrency(function_name: function_name) + response.reserved_concurrent_executions + rescue Aws::Lambda::Errors::ResourceNotFoundException + nil # No reserved concurrency set implies no limit + end + + def reserved_concurrency=(concurrency) + if concurrency.nil? + lambda_client.delete_function_concurrency(function_name: function_name) + else + lambda_client.put_function_concurrency( + function_name: function_name, + reserved_concurrent_executions: concurrency + ) + end + end + + def provisioned_concurrency(qualifier = "live") + info = provisioned_concurrency_info(qualifier) + (info[:status] == "not set") ? nil : info[:requested] + end + + # Provisioned Concurrency + def provisioned_concurrency_info(qualifier = "live") + response = lambda_client.get_provisioned_concurrency_config( + function_name: function_name, + qualifier: qualifier + ) + { + requested: response.requested_provisioned_concurrent_executions, + allocated: response.allocated_provisioned_concurrent_executions, + status: response.status + } + rescue Aws::Lambda::Errors::ResourceNotFoundException, + Aws::Lambda::Errors::ProvisionedConcurrencyConfigNotFoundException + { + status: "not set" + } + end + + def provisioned_concurrency=(concurrency, qualifier = "live") + if concurrency.nil? || concurrency == 0 + begin + lambda_client.delete_provisioned_concurrency_config( + function_name: function_name, + qualifier: qualifier + ) + rescue Aws::Lambda::Errors::ResourceNotFoundException + end + else + lambda_client.put_provisioned_concurrency_config( + function_name: function_name, + qualifier: qualifier, + provisioned_concurrent_executions: concurrency + ) + end + end + + # Check if reserved concurrency is zero or not set + def reserved_concurrency_zero? + reserved_concurrency == 0 + end + + # Check if provisioned concurrency is unset + def provisioned_concurrency_unset?(qualifier = "live") + pc = provisioned_concurrency(qualifier) + pc.nil? || (pc[:requested] == 0 && pc[:allocated] == 0) + rescue Aws::Lambda::Errors::ResourceNotFoundException, + Aws::Lambda::Errors::ProvisionedConcurrencyConfigNotFoundException + true # Treat not found as unset + end + end +end diff --git a/lib/jets/cli/lambda/functions.rb b/lib/jets/cli/lambda/functions.rb new file mode 100644 index 000000000..2d5898b02 --- /dev/null +++ b/lib/jets/cli/lambda/functions.rb @@ -0,0 +1,10 @@ +module Jets::CLI::Lambda + module Functions + extend Memoist + def lambda_functions + names = Jets::CLI::Functions.new(full: true).all + names.map { |name| Jets::CLI::Lambda::Function.new(name) } + end + memoize :lambda_functions + end +end diff --git a/lib/jets/cli/lambda/lookup.rb b/lib/jets/cli/lambda/lookup.rb new file mode 100644 index 000000000..5d4d9394e --- /dev/null +++ b/lib/jets/cli/lambda/lookup.rb @@ -0,0 +1,68 @@ +module Jets::CLI::Lambda + class Lookup + class Error < StandardError + class ParentStack < self; end + + class Output < self; end + + class ChildStack < self; end + end + + class << self + def function(name) + new(name).lookup + end + end + + include Jets::AwsServices + def initialize(name) + @name = name + end + + MAX_FUNCTION_NAME_SIZE = 64 + def function_name + name = if @name.starts_with?(Jets.project.namespace) + @name # fully qualified function name + elsif !ENV["JETS_RESET"] + [Jets.project.namespace, @name].join("-") + else + lookup + end + (name.size > MAX_FUNCTION_NAME_SIZE) ? lookup : name + end + + def lookup + if @name == "controller" + class_name, meth = "Controller", "" + else + # IE: jets-prewarm_event-handle + # class_name => "JetsPrewarm" - no colons :: + # meth => "Handle" + parts = @name.split("-") + meth = parts.pop.tr("-", "_").camelize + class_name = parts.join("_").camelize + end + + parent_name = Jets::Names.parent_stack_name + parent = cfn.describe_stacks(stack_name: parent_name).stacks.first + unless parent + raise Error::ParentStack, "Unable to find parent stack #{parent_name}" + end + + # Can occur while stack is initially creating for the first time + output = parent.outputs.find { |o| o.output_key == class_name } + unless output + raise Error::Output, "Unable to find output #{class_name} in parent stack #{parent_name}" + end + + # Can occur while stack is initially creating for the first time + child_name = output.output_value + child = cfn.describe_stacks(stack_name: child_name).stacks.first + unless child + raise Error::ChildStack, "Unable to find child stack #{parent_name}" + end + output = child.outputs.find { |o| o.output_key == "#{meth}LambdaFunction" } + output.output_value + end + end +end diff --git a/lib/jets/cli/login.rb b/lib/jets/cli/login.rb new file mode 100644 index 000000000..1282e7d59 --- /dev/null +++ b/lib/jets/cli/login.rb @@ -0,0 +1,7 @@ +class Jets::CLI + class Login < Base + def run + Jets::Api::Config.instance.update_api_key(@options[:token]) + end + end +end diff --git a/lib/jets/cli/logout.rb b/lib/jets/cli/logout.rb new file mode 100644 index 000000000..17f9288f9 --- /dev/null +++ b/lib/jets/cli/logout.rb @@ -0,0 +1,7 @@ +class Jets::CLI + class Logout < Base + def run + Jets::Api::Config.instance.clear_token + end + end +end diff --git a/lib/jets/cli/logs.rb b/lib/jets/cli/logs.rb new file mode 100644 index 000000000..0447fc835 --- /dev/null +++ b/lib/jets/cli/logs.rb @@ -0,0 +1,40 @@ +require "aws-logs" + +class Jets::CLI + class Logs < Base + include Jets::AwsServices::AwsHelpers + + def run + options = @options.dup # so it can be modified + options[:log_group_name] = log_group_name + options[:since] ||= "10m" # by default, start search 10m in the past + options[:wait_exists_retries] = 60 # 300 seconds = 300 / 5 = 60 retries + options[:wait_exists_seconds] = 5 + + verb = options[:follow] ? "Tailing" : "Showing" + warn "#{verb} logs for #{options[:log_group_name]}" + + tail = AwsLogs::Tail.new(options) + tail.run + end + + def log_group_name + log_group_name = @options[:log_group_name] # can be nil + if log_group_name.nil? + begin + log_group_name = Jets::CLI::Lambda::Lookup.function("controller") # function_name + rescue Jets::CLI::Call::Error => e + puts "ERROR: #{e.message}" + abort "Unable to determine log group name by looking it up. Can you double check it?" + end + end + unless log_group_name.include?(Jets.project.namespace) + log_group_name = "#{Jets.project.namespace}-#{log_group_name}" + end + unless log_group_name.include?("aws/lambda") + log_group_name = "/aws/lambda/#{log_group_name}" + end + log_group_name + end + end +end diff --git a/lib/jets/cli/maintenance.rb b/lib/jets/cli/maintenance.rb new file mode 100644 index 000000000..da346c2b7 --- /dev/null +++ b/lib/jets/cli/maintenance.rb @@ -0,0 +1,32 @@ +class Jets::CLI + class Maintenance < Jets::Thor::Base + class_option :role, aliases: [:r], default: "web", desc: "Role to apply the maintenance mode to. IE: web worker" + + desc "on", "Turn on maintenance mode" + long_desc Help.text("maintenance/on") + yes_option + def on + Mode.new(options).on + end + + # Note: --yes or -y is not used for the off command. + # User will not be prompted for confirmation. + # The option is allowed in case users accidentally use it. + # Example: + # jets maintenance off -y + # This is why the option is hidden. This makes the user experience better. + desc "off", "Turn off maintenance mode" + long_desc Help.text("maintenance/off") + option :yes, aliases: [:y], type: :boolean, desc: "Skip are you sure prompt", hide: true + def off + Mode.new(options).off + end + + desc "status", "Show maintenance mode status" + long_desc Help.text("maintenance/status") + option :all, aliases: [:a], type: :boolean, desc: "Show status for all roles. Takes precedence over --role option", default: false + def status + Mode.new(options).status + end + end +end diff --git a/lib/jets/cli/maintenance/base.rb b/lib/jets/cli/maintenance/base.rb new file mode 100644 index 000000000..3afcf8885 --- /dev/null +++ b/lib/jets/cli/maintenance/base.rb @@ -0,0 +1,15 @@ +class Jets::CLI::Maintenance + class Base < Jets::CLI::Base + include Jets::CLI::Lambda::Checks + include Jets::Util::Truthy + + def initialize(options = {}) + super + check_deployed! + end + + def status + on? ? "on" : "off" + end + end +end diff --git a/lib/jets/cli/maintenance/mode.rb b/lib/jets/cli/maintenance/mode.rb new file mode 100644 index 000000000..b3111379e --- /dev/null +++ b/lib/jets/cli/maintenance/mode.rb @@ -0,0 +1,44 @@ +class Jets::CLI::Maintenance + class Mode < Base + def on + are_you_sure? + warn "Enabling #{role_info} maintenance mode #{for_info}" + role.on + end + + def off + warn "Disabling #{role_info} maintenance mode #{for_info}" + role.off + end + + def status + if @options[:all] + warn "Maintenance status for #{Jets.project.namespace}" + puts "web #{Web.new.status}" + puts "worker #{Worker.new.status}" + else + warn "#{role_info.titleize} maintenance status #{for_info}" + puts role.status + end + end + + def are_you_sure? + sure?("Will enable #{role_info} maintenance mode #{for_info}") + end + + def role_info + @options[:role] + end + + def for_info + "for #{Jets.project.namespace}" + end + + def role + # IE: Web or Worker + klass = Jets::CLI::Maintenance.const_get(@options[:role].camelize) + klass.new(@options) + end + memoize :role + end +end diff --git a/lib/jets/cli/maintenance/web.rb b/lib/jets/cli/maintenance/web.rb new file mode 100644 index 000000000..83ce71af2 --- /dev/null +++ b/lib/jets/cli/maintenance/web.rb @@ -0,0 +1,31 @@ +class Jets::CLI::Maintenance + class Web < Base + def initialize(options = {}) + super + function_name = Jets::CLI::Lambda::Lookup.function("controller") + @lambda_function = Jets::CLI::Lambda::Function.new(function_name) + end + + def on + if on? + warn "Web maintenance is already on" + else + @lambda_function.environment_variables = {JETS_MAINTENANCE: "on"} + warn "Web maintenance has been turned on" + end + end + + def off + if on? + @lambda_function.environment_variables = {JETS_MAINTENANCE: nil} + warn "Web maintenance has been turned off" + else + warn "Web maintenance is already off" + end + end + + def on? + truthy?(@lambda_function.environment_variables["JETS_MAINTENANCE"]) + end + end +end diff --git a/lib/jets/cli/maintenance/worker.rb b/lib/jets/cli/maintenance/worker.rb new file mode 100644 index 000000000..f95e14c40 --- /dev/null +++ b/lib/jets/cli/maintenance/worker.rb @@ -0,0 +1,31 @@ +class Jets::CLI::Maintenance + class Worker < Base + def on + check_workers! + + if on? + warn "Worker maintenance is already on" + else + Saver.new(@options).save_concurrency_settings + Zeroer.new(@options).zero_all_concurrency + warn "Worker maintenance has been turned on" + end + end + + def off + check_workers! + + if on? + Restorer.new(@options).restore_concurrency_settings + warn "Worker maintenance has been turned off" + else + warn "Worker maintenance is already off" + end + end + + def on? + check_workers! + Zeroer.new(@options).all_zeroed? + end + end +end diff --git a/lib/jets/cli/maintenance/worker/base.rb b/lib/jets/cli/maintenance/worker/base.rb new file mode 100644 index 000000000..11e82ca13 --- /dev/null +++ b/lib/jets/cli/maintenance/worker/base.rb @@ -0,0 +1,22 @@ +class Jets::CLI::Maintenance::Worker + class Base < Jets::CLI::Base + include Jets::CLI::Lambda::Functions + + attr_reader :s3_bucket + def initialize(options = {}) + super + @s3_bucket = Jets.aws.s3_bucket + end + + def state_file + "jets/state/maintenance/lambda_concurrency_settings.json" + end + + def lambda_functions + super.select do |lambda_function| + # Accounts for both app/events and app/jobs (from jets geneneration) + lambda_function.name.match(/_event-/) + end + end + end +end diff --git a/lib/jets/cli/maintenance/worker/restorer.rb b/lib/jets/cli/maintenance/worker/restorer.rb new file mode 100644 index 000000000..d13aca26a --- /dev/null +++ b/lib/jets/cli/maintenance/worker/restorer.rb @@ -0,0 +1,19 @@ +class Jets::CLI::Maintenance::Worker + class Restorer < Base + def restore_concurrency_settings + data = read_from_s3 + data.each do |function_name, settings| + lambda_function = Jets::CLI::Lambda::Function.new(function_name) + lambda_function.reserved_concurrency = settings["reserved_concurrency"] if settings["reserved_concurrency"] + lambda_function.provisioned_concurrency = settings["provisioned_concurrency"] if settings["provisioned_concurrency"] + end + end + + private + + def read_from_s3 + response = s3.get_object(bucket: s3_bucket, key: state_file) + JSON.parse(response.body.read) + end + end +end diff --git a/lib/jets/cli/maintenance/worker/saver.rb b/lib/jets/cli/maintenance/worker/saver.rb new file mode 100644 index 000000000..f8725b865 --- /dev/null +++ b/lib/jets/cli/maintenance/worker/saver.rb @@ -0,0 +1,15 @@ +class Jets::CLI::Maintenance::Worker + class Saver < Base + def save_concurrency_settings + concurrency_settings = Jets::CLI::Concurrency::Info.new(@options).concurrency_settings + save_to_s3(concurrency_settings) + end + + private + + def save_to_s3(data) + s3.put_object(bucket: s3_bucket, key: state_file, body: data.to_json) + log.debug "Saved concurrency settings to s3://#{s3_bucket}/#{state_file}" + end + end +end diff --git a/lib/jets/cli/maintenance/worker/zeroer.rb b/lib/jets/cli/maintenance/worker/zeroer.rb new file mode 100644 index 000000000..5251cab42 --- /dev/null +++ b/lib/jets/cli/maintenance/worker/zeroer.rb @@ -0,0 +1,19 @@ +class Jets::CLI::Maintenance::Worker + class Zeroer < Base + def zero_all_concurrency + lambda_functions.each do |lambda_function| + # must zero provisioned before reserved + lambda_function.provisioned_concurrency = nil + lambda_function.reserved_concurrency = 0 + end + end + + def all_zeroed? + lambda_functions.all? do |lambda_function| + # check both reserved and provisioned + lambda_function.provisioned_concurrency_unset? && + lambda_function.reserved_concurrency_zero? + end + end + end +end diff --git a/lib/jets/cli/package.rb b/lib/jets/cli/package.rb new file mode 100644 index 000000000..234c03ea3 --- /dev/null +++ b/lib/jets/cli/package.rb @@ -0,0 +1,8 @@ +class Jets::CLI + class Package < Jets::Thor::Base + desc "dockerfile", "Build dockerfile" + def dockerfile + Dockerfile.new(options).run + end + end +end diff --git a/lib/jets/cli/package/dockerfile.rb b/lib/jets/cli/package/dockerfile.rb new file mode 100644 index 000000000..3b2a499b4 --- /dev/null +++ b/lib/jets/cli/package/dockerfile.rb @@ -0,0 +1,9 @@ +class Jets::CLI::Package + class Dockerfile < Jets::CLI::Base + def run + sure?("Will build a Dockerfile for #{Jets.project.namespace.color(:green)}") + Jets::Cfn::Bootstrap.new(@options).run + Jets::Remote::Runner.new(@options.merge(command: "package:dockerfile")).run + end + end +end diff --git a/lib/jets/cli/ping.rb b/lib/jets/cli/ping.rb new file mode 100644 index 000000000..e5e80f117 --- /dev/null +++ b/lib/jets/cli/ping.rb @@ -0,0 +1,10 @@ +class Jets::CLI + class Ping < Base + rescue_api_error + + def run + Jets::Api::Ping.create + puts "Auth check successful" + end + end +end diff --git a/lib/jets/cli/projects.rb b/lib/jets/cli/projects.rb new file mode 100644 index 000000000..77ee2b4f2 --- /dev/null +++ b/lib/jets/cli/projects.rb @@ -0,0 +1,22 @@ +class Jets::CLI + class Projects < Base + rescue_api_error + + def run + resp = Jets::Api::Project.list(paging_params) + present(resp[:data]) + paginate(resp) + end + + private + + def present(items) + presenter = CliFormat::Presenter.new(@options) + presenter.empty_message = "No projects found" + items.each do |item| + presenter.rows << [item[:name]] + end + presenter.show + end + end +end diff --git a/lib/jets/cli/release.rb b/lib/jets/cli/release.rb new file mode 100644 index 000000000..02e2061a0 --- /dev/null +++ b/lib/jets/cli/release.rb @@ -0,0 +1,23 @@ +class Jets::CLI + class Release < Jets::Thor::Base + desc "history", "Release history" + paging_options(order: "desc", limit: 10) + def history + History.new(options).run + end + map list: :history + + desc "info", "Release detailed information" + format_option(default: "info") + def info(version = nil) + Info.new(options.merge(version: version)).run + end + map show: :info + + desc "rollback VERSION", "Rollback to a previous release", hide: true + option :yes, aliases: :y, type: :boolean, desc: "Skip are you sure prompt" + def rollback(version) + Rollback.new(options.merge(version: version)).run + end + end +end diff --git a/lib/jets/cli/release/base.rb b/lib/jets/cli/release/base.rb new file mode 100644 index 000000000..995dcf759 --- /dev/null +++ b/lib/jets/cli/release/base.rb @@ -0,0 +1,4 @@ +class Jets::CLI::Release + class Base < Jets::CLI::Base + end +end diff --git a/lib/jets/cli/release/history.rb b/lib/jets/cli/release/history.rb new file mode 100644 index 000000000..097e28dc9 --- /dev/null +++ b/lib/jets/cli/release/history.rb @@ -0,0 +1,68 @@ +require "date" +require "tzinfo" + +class Jets::CLI::Release + class History < Base + rescue_api_error + + def run + resp = Jets::Api::Stack.retrieve(:current) + + name = "#{resp[:name]} #{resp[:location]}" + resp = Jets::Api::Release.list(@options) + + data = resp[:data] + if data.empty? + log.info "No releases found for stack: #{name}" + else + log.info "Releases for stack: #{name}" + show_items(data) + paginate(resp) + end + end + + def show_items(items) + presenter = CliFormat::Presenter.new(@options) + header = ["Version", "Status", "Released At", "Message"] + header << "Git Sha" if @options[:sha] + presenter.header = header + items.each do |item| + version = item[:version] + status = item[:stack_status] + released_at = item[:pretty_created_at] || item[:created_at] + message = item[:message] || "Deployed" + message = message[0..50] + + row = [version, status, format_time(released_at), message] + if @options[:sha] + sha = item[:git_sha].to_s[0..7] if item[:git_sha] + row << sha + end + presenter.rows << row + end + presenter.show + end + + def format_time(string) + if string.include?("ago") # IE: 5 minutes ago + string + else + utc = DateTime.parse(string) + + tz_override = ENV["JETS_TZ"] # IE: America/Los_Angeles + local = if tz_override + tz = TZInfo::Timezone.get(tz_override) + tz.utc_to_local(utc) + else + utc.new_offset(DateTime.now.offset) # local time + end + + if tz_override + local.strftime("%b %-d, %Y %-l:%M:%S%P") + else + local.strftime("%b %-d, %Y %H:%M:%S") + end + end + end + end +end diff --git a/lib/jets/cli/release/info.rb b/lib/jets/cli/release/info.rb new file mode 100644 index 000000000..89780f1a0 --- /dev/null +++ b/lib/jets/cli/release/info.rb @@ -0,0 +1,59 @@ +class Jets::CLI::Release + class Info < History + rescue_api_error + + def run + release = get(@options[:version]) + + release_fields = %w[ + version + status + created_at + message + deploy_user + jets_env + jets_extra + jets_version + jets_remote + ruby_version + region + docker_image + zip_location + git_branch + git_sha + git_url + git_message + ].map(&:to_sym) + data = release_fields.map do |field| + # special cases for values + value = if field == :created_at + time_string = release[:pretty_created_at] || release[:created_at] + format_time(time_string) + else + release[field] + end + + label = field.to_s.titleize + [label, value] + end + + release.endpoints.each do |endpoint| + name = endpoint[:name].titleize + data << [name, endpoint[:url]] + end + + warn Jets.project.namespace.color(:green) + presenter = CliFormat::Presenter.new(@options) + presenter.empty_message = "Release not found for: #{Jets.project.namespace}" + data.each do |row| + presenter.rows << row + end + presenter.show + end + + def get(version = nil) + version ||= "latest" + Jets::Api::Release.retrieve(version) + end + end +end diff --git a/lib/jets/cli/release/rollback.rb b/lib/jets/cli/release/rollback.rb new file mode 100644 index 000000000..a9f886b9e --- /dev/null +++ b/lib/jets/cli/release/rollback.rb @@ -0,0 +1,15 @@ +class Jets::CLI::Release + class Rollback < Base + rescue_api_error + + def run + version = @options[:version] + resp = Info.new(@options).get(version) + unless resp[:version] + puts "ERROR: version #{version} not found".color(:red) + exit 1 + end + Jets::Cfn::Rollback.new(@options).run + end + end +end diff --git a/lib/jets/cli/schedule.rb b/lib/jets/cli/schedule.rb new file mode 100644 index 000000000..a9462b601 --- /dev/null +++ b/lib/jets/cli/schedule.rb @@ -0,0 +1,15 @@ +class Jets::CLI + class Schedule < Jets::Thor::Base + desc "translate", "Translate Sidekiq Schedule to Jets" + yes_option + def translate + Translate.new(options).run + end + + desc "validate", "Validate config/jets/schedule.yml" + yes_option + def validate + Validate.new(options).run + end + end +end diff --git a/lib/jets/cli/schedule/base.rb b/lib/jets/cli/schedule/base.rb new file mode 100644 index 000000000..01fa7026f --- /dev/null +++ b/lib/jets/cli/schedule/base.rb @@ -0,0 +1,5 @@ +class Jets::CLI::Schedule + class Base < Jets::CLI::Base + include Jets::Event::Dsl::RateExpression + end +end diff --git a/lib/jets/cli/schedule/translate.rb b/lib/jets/cli/schedule/translate.rb new file mode 100644 index 000000000..54f093f18 --- /dev/null +++ b/lib/jets/cli/schedule/translate.rb @@ -0,0 +1,168 @@ +class Jets::CLI::Schedule + class Translate < Base + def run + are_you_sure? + check_exist! + success = perform + if success + finish_message + else + log.error "Translation failed." + exit 1 + end + end + + # interface method: used by deploy to translate on_deploy + def perform + # Currently only sidekiq is supported + sidekiq = YAML.load_file("config/sidekiq.yml") + sidekiq = ActiveSupport::HashWithIndifferentAccess.new(sidekiq) + schedule = sidekiq[:schedule] + unless schedule + log.error "config/sidekiq.yml does not have a schedule key. Nothing to translate." + return false + end + + log.info "Translating config/sidekiq.yml => config/jets/schedule.yml" + mapped = schedule.map do |k, v| + translate_item(k, v) + end + schedule = mapped.inject({}) { |h, v| h.merge(v) } + text = schedule.deep_stringify_keys! + + unless @@has_errors + FileUtils.mkdir_p("config/jets") + IO.write("config/jets/schedule.yml", YAML.dump(text)) + end + !@@has_errors + end + + JETS_FIELDS = %w[ + args + class + cron + rate + splat_args + ] + SUPPORTED_FIELDS = JETS_FIELDS + %w[ + keyword_arguments + every + interval + ] + + # Example: + # hard_worker: + # class: "HardWorker" + # args: ["bob", 5] + # description: "This is a description of the job" + # cron: "30 6 * * + def translate_item(key, original) + result = {} + result[:args] = original[:args] if original[:args] + result[:class] = original[:class] if original[:class] + result[:splat_args] = true if original[:keyword_arguments] + add_schedule_expression!(result, original) + unsupported_fields!(result, original) + {key => result} + end + + def add_schedule_expression!(result, original) + if original[:cron] + result[:cron] = cron(original[:cron]) + elsif original[:every] + handle_every_expression!(result, original[:every]) + elsif original[:interval] + handle_every_expression!(result, original[:interval]) + end + end + + @@has_errors = false + def cron(expr) + parts = expr.split(" ") + # https://github.com/sidekiq-scheduler/sidekiq-scheduler + # Sidekiq scheduler has 6 fields. The first is for seconds. Cron does not support seconds. + # We'll remove the seconds first field. + if parts.size == 6 + parts.shift + end + if parts.size != 5 + log.info "ERROR: Unsupported cron expression. Too many fields." + log.info "There are #{parts.size} fields. Should have exactly 5 fields" + log.info "Offending cron expr: #{expr}" + @@has_errors = true + end + # AWS Cron expressions require ? for the day of the week field + parts[-1] = "?" if parts[-1] == "*" + if parts.size == 5 + # AWS Cron expressions require a 6th year field + parts << "*" + end + parts.join(" ") + end + + def handle_every_expression!(result, expr) + if simple_every?(expr) + result[:rate] = rate_expression(expr) + else + # complex every expression translate to cron + expr = Fugit::Nat.parse("every #{expr}").to_cron_s + result[:cron] = cron(expr) + end + end + + def simple_every?(expr) + # IE: 1h, 1d, 1w, 1m, 45m, 45 minutes, 1 hour, 1 day, 1 week, 1 month + expr =~ /\d+\s?\w+/ + end + + def unsupported_fields!(result, original) + unsupported = original.keys - SUPPORTED_FIELDS + unsupported.each do |field| + result[:"unsupported_#{field}"] = original[field] + end + result + end + + def check_exist! + unless File.exist?("config/sidekiq.yml") + abort "ERROR: config/sidekiq.yml does not exist. Unable to translate".color(:red) + end + end + + def are_you_sure? + message = <<~EOL + This script will make changes to your project source code. + + It will try to translate the schedule items in + + config/sidekiq.yml => config/jets/schedule.yml + + Note: It's unfeasible to account for all cases perform miracle translations. + It's a best-effort script, and the hope is that this script gets you pretty far + and is helpful. 😄 + + Please make sure you have backed up and committed your changes first. + EOL + sure?(message) + end + + def finish_message + # Created message part of finish message because only want it to show as part of CLI run + log.info "\n Created: config/jets/schedule.yml\n".color(:green) + log.info <<~EOL + Translation complete! Please double check the schedule to make sure it looks correct. + Remember, this is a best-effort tool. It does not cover all cases. + + You can validate the config/jets/schedule.yml + + jets schedule:validate + + If that looks good, try deploying the config/jets/schedule.yml + It should create a *JetsScheduledEvent* child stack + with the schedule event rules. + + jets deploy + EOL + end + end +end diff --git a/lib/jets/cli/schedule/validate.rb b/lib/jets/cli/schedule/validate.rb new file mode 100644 index 000000000..69d21e68f --- /dev/null +++ b/lib/jets/cli/schedule/validate.rb @@ -0,0 +1,95 @@ +require "aws-sdk-cloudwatchevents" + +class Jets::CLI::Schedule + class Validate < Base + def run + are_you_sure? + check_exist! + valid = perform + if valid + log.info "Validation passed. config/jets/schedule.yml is valid" + if File.exist?("config/sidekiq.yml") + log.info <<~EOL + You can remove config/sidekiq.yml now. It is no longer needed. + + rm config/sidekiq.yml + EOL + end + else + log.info "Validation failed. Please fix the errors and try again." + log.info "Docs: https://docs.rubyonjets.com/docs/jobs/schedule/" + exit 1 + end + end + + # interface method: used by deploy to translate on_deploy + def perform + items = YAML.load_file("config/jets/schedule.yml") + items = ActiveSupport::HashWithIndifferentAccess.new(items) + items.each do |key, value| + validate_item(key, value) + end + !@@has_errors + end + + def check_exist! + unless File.exist?("config/jets/schedule.yml") + abort "config/jets/schedule.yml does not exist. Nothing to validate." + end + end + + @@rule_name = "validation_rule_#{Time.now.to_i}_#{rand(1000)}" + @@has_errors = false + def validate_item(key, value) + log.debug "To validate, creating live event rule for #{key}" + log.debug "value: #{value.inspect}" + schedule_expression = if value[:cron] + "cron(#{value[:cron]})" + elsif value[:rate] + expr = rate_expression(value[:rate]) + "rate(#{expr})" + else + raise "No schedule expression found for: #{key}" + end + + # Create the rule with the provided cron expression + client.put_rule( + name: @@rule_name, + schedule_expression: schedule_expression, + state: "DISABLED" # Disable the rule to prevent it from being triggered + ) + + # log.info "Valid rule: #{key}" + # log.info " Schedule expression: #{schedule_expression}" + true # If no error is raised, the cron expression is valid + rescue Aws::CloudWatchEvents::Errors::ValidationException => e + log.error "Invalid rule: #{key}" + log.error "Schedule expression: #{schedule_expression}" + # log.error "Validation Error: #{e.message}" # commented out to reduce noise, not useful + @@has_errors ||= true + false # If ValidationException is raised, the cron expression is invalid + ensure + # Delete the rule after validation (whether it's valid or not) + begin + client.delete_rule(name: @@rule_name) + rescue + nil + end + end + + def are_you_sure? + message = <<~EOL + Will validate: config/jets/schedule.yml + + It does this by creating a live test event rule for each entry in schedule.yml + and then deleting it. + EOL + sure?(message) + end + + def client + Aws::CloudWatchEvents::Client.new + end + memoize :client + end +end diff --git a/lib/jets/cli/stacks.rb b/lib/jets/cli/stacks.rb new file mode 100644 index 000000000..2dc83ff38 --- /dev/null +++ b/lib/jets/cli/stacks.rb @@ -0,0 +1,33 @@ +class Jets::CLI + class Stacks < Base + rescue_api_error + + def run + params = paging_params.merge(options) + resp = Jets::Api::Stack.list(params) + log.info stacks_for_message + present(resp[:data]) + paginate(resp) + end + + private + + def stacks_for_message + if options[:all_projects] + "Stacks for all projects:" + else + "Stacks for project: #{Jets.project.name}" + end + end + + def present(items) + presenter = CliFormat::Presenter.new(@options) + presenter.empty_message = "No stacks found" + items.each do |item| + row = "#{item[:name]} #{item[:location]}" + presenter.rows << [row] + end + presenter.show + end + end +end diff --git a/lib/jets/cli/stop.rb b/lib/jets/cli/stop.rb new file mode 100644 index 000000000..b027c8592 --- /dev/null +++ b/lib/jets/cli/stop.rb @@ -0,0 +1,31 @@ +class Jets::CLI + class Stop < Jets::CLI::Ci::Base + def run + are_you_sure? + check_build_id! + run_with_exception_handling do + stopped = stop_build + if stopped + log.info <<~EOL + Deploy has been stopped: #{build_id} + Note: If the deploy has already started the CloudFormation update, + it will continue. Please check the logs. + + EOL + show_console_log_url(build_id) + end + end + end + + def stack_name + "#{Jets.project.namespace}-remote" + end + alias_method :project_name, :stack_name + + def are_you_sure? + unless @options[:yes] + sure? "Will attempt to stop the deploy for project #{project_name.color(:green)} build_id #{build_id.color(:green)}" + end + end + end +end diff --git a/lib/jets/cli/teardown.rb b/lib/jets/cli/teardown.rb new file mode 100644 index 000000000..3dae811fd --- /dev/null +++ b/lib/jets/cli/teardown.rb @@ -0,0 +1,8 @@ +class Jets::CLI + class Teardown < Base + def run + sure?("Will teardown the stack #{Jets.project.namespace}") + Jets::Cfn::Teardown.new(@options).run + end + end +end diff --git a/lib/jets/cli/tip.rb b/lib/jets/cli/tip.rb new file mode 100644 index 000000000..6003d879e --- /dev/null +++ b/lib/jets/cli/tip.rb @@ -0,0 +1,86 @@ +class Jets::CLI + class Tip + class << self + def show(name, options = {}) + new(name).show(options) + end + end + + delegate :config, to: "Jets.project" + + def initialize(name) + @name = name + end + + def show(options = {}) + return if already_configured? + return unless enabled? + puts send(@name) + puts disable_howto unless options[:disable_howto] == false + end + + def already_configured? + if @name == :faster_deploy + config = Jets.bootstrap.config + config.codebuild.project.env.vars.key?(:DOCKER_HOST) || + config.codebuild.fleet.enable || + config.codebuild.fleet_override + else + false + end + end + + def faster_deploy + <<~EOL + Tip: You can speed the deploy with one of these options: + + * Docker Remote Host: https://docs.rubyonjets.com/docs/remote/codebuild/docker/ + * CodeBuild Fleet: https://docs.rubyonjets.com/docs/remote/codebuild/fleet/ + + Enabling of those options will also remove this message. + EOL + end + + def concurrency_change + <<~EOL + + Note: CLI changes to concurrency are outside of deployment + EOL + end + + def env_change + <<~EOL + + Note: CLI changes to env vars are outside of deployment + See: https://docs.rubyonjets.com/env/cli/ + EOL + end + + def ssm_change + <<~EOL + After deleting a parameter, wait for at least 30 seconds + to create a parameter with the same name + EOL + end + + def disable_howto + <<~EOL + To disable this tip: + + * set config.tips.#{@name} = false in config/jets/project.rb + * See: https://docs.rubyonjets.com/docs/more/cli-tips/ + EOL + end + + def remote_run + <<~EOL + Ctrl-C will stop showing logs. Jets will continue to run remotely. + If you want to stop the remote process, use: jets stop + EOL + end + + def enabled? + config.tips.enable && config.tips[@name] + end + end +end diff --git a/lib/jets/cli/url.rb b/lib/jets/cli/url.rb new file mode 100644 index 000000000..2d27252c4 --- /dev/null +++ b/lib/jets/cli/url.rb @@ -0,0 +1,41 @@ +require "cli-format" + +class Jets::CLI + class Url < Base + rescue_api_error + + def run + if @options[:format] == "json" + puts data.to_json # simpler json format allows for: jets url | jq + else + present(data) + end + end + + private + + def present(items) + presenter = CliFormat::Presenter.new(@options) + presenter.empty_message = "No url info found" + presenter.header = ["Name", "Value"] if @options[:header] # default: false + data.keys.sort.each do |name| + next if name.to_s == "queue_url" # dont show Queue Url + name_url = name.to_s.titleize + value = data[name] + row = [name_url, value] + presenter.rows << row + end + presenter.show + end + + def data + release = Release::Info.new(@options).get + data = release.endpoints.inject({}) do |acc, endpoint| + acc.merge!(endpoint[:name] => endpoint[:url]) + end + data.delete_if { |k, v| v.nil? } # remove nil values + data + end + memoize :data + end +end diff --git a/lib/jets/cli/waf.rb b/lib/jets/cli/waf.rb new file mode 100644 index 000000000..b54a8b945 --- /dev/null +++ b/lib/jets/cli/waf.rb @@ -0,0 +1,38 @@ +class Jets::CLI + class Waf < Jets::Thor::Base + class << self + # interface method + def waf_name + [Jets.env, Jets.extra].compact.join("-") + end + end + + Init.cli_options.each { |args| option(*args) } + register(Init, "init", "init", "WAF init creates config/jets/waf.rb") + + desc "build", "WAF build" + yes_option + def build + Build.new(options).run + end + + desc "deploy", "WAF deploy" + yes_option + def deploy + Deploy.new(options).run + end + + desc "delete", "WAF delete" + yes_option + def delete + Delete.new(options).run + end + + desc "info", "WAF info" + format_option(default: "info") + option :name, aliases: :n, default: waf_name, desc: "Web ACL name" + def info + Info.new(options).run + end + end +end diff --git a/lib/jets/cli/waf/base.rb b/lib/jets/cli/waf/base.rb new file mode 100644 index 000000000..3fb8001a8 --- /dev/null +++ b/lib/jets/cli/waf/base.rb @@ -0,0 +1,7 @@ +class Jets::CLI::Waf + class Base < Jets::CLI::Base + def stack_name + [Jets::CLI::Waf.waf_name, "waf"].compact.join("-") + end + end +end diff --git a/lib/jets/cli/waf/build.rb b/lib/jets/cli/waf/build.rb new file mode 100644 index 000000000..6ea633c93 --- /dev/null +++ b/lib/jets/cli/waf/build.rb @@ -0,0 +1,8 @@ +class Jets::CLI::Waf + class Build < Base + def run + Jets::Cfn::Bootstrap.new(@options).run + Jets::Remote::Runner.new(@options.merge(command: "waf:build")).run + end + end +end diff --git a/lib/jets/cli/waf/delete.rb b/lib/jets/cli/waf/delete.rb new file mode 100644 index 000000000..495954112 --- /dev/null +++ b/lib/jets/cli/waf/delete.rb @@ -0,0 +1,17 @@ +class Jets::CLI::Waf + class Delete < Base + def run + are_you_sure? + Jets::Cfn::Bootstrap.new(@options).run + Jets::Remote::Runner.new(@options.merge(command: "waf:delete")).run + end + + private + + def are_you_sure? + unless @options[:yes] + sure?("Will delete #{stack_name.color(:green)} in us-east-1") + end + end + end +end diff --git a/lib/jets/cli/waf/deploy.rb b/lib/jets/cli/waf/deploy.rb new file mode 100644 index 000000000..82a6e39cc --- /dev/null +++ b/lib/jets/cli/waf/deploy.rb @@ -0,0 +1,17 @@ +class Jets::CLI::Waf + class Deploy < Base + def run + are_you_sure? + Jets::Cfn::Bootstrap.new(@options).run + Jets::Remote::Runner.new(@options.merge(command: "waf:deploy")).run + end + + def are_you_sure? + sure? <<~EOL + Will deploy stack #{stack_name.color(:green)} to us-east-1 + + Uses #{Jets.project.namespace} remote runner to deploy a separate stack for WAF resources. + EOL + end + end +end diff --git a/lib/jets/cli/waf/info.rb b/lib/jets/cli/waf/info.rb new file mode 100644 index 000000000..98a33cf41 --- /dev/null +++ b/lib/jets/cli/waf/info.rb @@ -0,0 +1,128 @@ +class Jets::CLI::Waf + class Info < Base + include Jets::AwsServices + + def web_acl_name + @options[:name] || Jets::CLI::Waf.waf_name + end + + def run + ENV["AWS_REGION"] = "us-east-1" # wafv2 only works in us-east-1 + + web_acl_summary = web_acl_summaries.find do |i| + web_acl_name == i.name + end + + unless web_acl_summary + puts "Web ACL not found: #{web_acl_name}" + return + end + + resp = wafv2.get_web_acl( + id: web_acl_summary.id, + name: web_acl_summary.name, + scope: "CLOUDFRONT" + ) + web_acl = resp.web_acl + present(web_acl) + end + + def web_acl_summaries + Enumerator.new do |y| + next_token = :start + while next_token + params = {} + params[:next_token] = next_token if next_token && next_token != :start + params[:scope] = "CLOUDFRONT" + resp = wafv2.list_web_acls(params) + y.yield resp.web_acls # acl summaries + # Looks like if there's only 1 page next_token is not even availablel + break unless resp.respond_to?(:next_token) + next_token = resp.next_token + end + end.lazy.flat_map { |i| i } + end + + private + + def present(web_acl) + presenter = CliFormat::Presenter.new(@options) + presenter.empty_message = "Web ACL not found: #{web_acl_name}" + + data = [ + ["Name", web_acl.name], + ["Description", web_acl.description], + ["Id", web_acl.id], + ["Capacity", web_acl.capacity] + ] + rules = web_acl.rules.sort_by!(&:priority) + + rules.each_with_index do |rule, i| + name = rule_name(rule) + data << ["Rule #{i + 1}", name] + end + + metric = metric_name(web_acl) + data << ["Metric", metric] if metric + + # Additional info + data << ["Logging", logging(web_acl)] + rules.each do |rule| + rate_limited_ips(web_acl, rule) + end + + data.each do |row| + presenter.rows << row + end + presenter.show + end + + def rule_name(rule) + override_action = rule.override_action.to_h.keys.first + action = rule.action.to_h.keys.first + + label = (override_action == :none || override_action.nil?) ? action : override_action + label ||= :block + label = :monitoring if label == :count + "#{rule.name} (#{label})" + end + + def metric_name(web_acl) + web_acl&.visibility_config&.metric_name + end + + def logging(web_acl) + resp = wafv2.get_logging_configuration(resource_arn: web_acl.arn) + # log_destination_configs= + # ["arn:aws:logs:us-east-1:112233445555:log-group:aws-waf-logs-dev:*"], + configs = resp.logging_configuration.log_destination_configs + configs.map! do |config| + # arn:aws:logs:us-east-1:112233445555:log-group:aws-waf-logs-dev:* + # only keep everything after the AWS account id which is exactly 12 digits + c = config.sub(/arn:aws:logs:.*:\d{12}:/, "") + c.sub(/:\*$/, "") # remove trailing colon + end + configs.join(", ") + rescue Aws::WAFV2::Errors::WAFNonexistentItemException + "disabled" + end + + def rate_limited_ips(web_acl, rule) + return unless rule.statement.rate_based_statement + + resp = wafv2.get_rate_based_statement_managed_keys( + scope: "CLOUDFRONT", + web_acl_name: web_acl.name, + web_acl_id: web_acl.id, + rule_name: rule.name + ) + addresses = resp.managed_keys_ipv4.addresses + resp.managed_keys_ipv6.addresses + return if addresses.empty? + + puts "Rule #{rule.name} rate limited IPs:" + addresses.each do |address| + puts " #{address}" + end + end + end +end diff --git a/lib/jets/cli/waf/init.rb b/lib/jets/cli/waf/init.rb new file mode 100644 index 000000000..8cb9f1c13 --- /dev/null +++ b/lib/jets/cli/waf/init.rb @@ -0,0 +1,40 @@ +class Jets::CLI::Waf + class Init < Jets::CLI::Group::Base + include Jets::Util::Sure + + def self.cli_options + [ + [:force, aliases: :f, type: :boolean, desc: "Bypass overwrite are you sure prompt for existing files"], + [:yes, aliases: :y, type: :boolean, desc: "Skip are you sure prompt"] + ] + end + cli_options.each { |args| class_option(*args) } + + source_root "#{__dir__}/templates" + + private + + def sure_message + <<~EOL + This will create a config/jets/waf.rb file with initial waf settings. + + The waf is designed to be a shared resource used by multiple projects. + Having a separate project that manages the waf stack may make sense. + + Please make sure you have backed up and committed your changes first. + EOL + end + + public + + def are_you_sure? + return if options[:yes] + sure?(sure_message) + end + + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codebuild-project-source.html#cfn-codebuild-project-source-type + def config_jets_ci + template "waf.rb.tt", "config/jets/waf.rb" + end + end +end diff --git a/lib/jets/cli/waf/templates/waf.rb.tt b/lib/jets/cli/waf/templates/waf.rb.tt new file mode 100644 index 000000000..6c5498a04 --- /dev/null +++ b/lib/jets/cli/waf/templates/waf.rb.tt @@ -0,0 +1,26 @@ +Jets.deploy.configure do + # Logging and Monitoring mode + # config.waf.logging.enable = true # default: false + # config.waf.monitoring = true # default: false + + # Add custom rules + # https://docs.rubyonjets.com/docs/routing/lambda/cloudfront/waf/custom-rules/ + # https://docs.rubyonjets.com/docs/routing/lambda/cloudfront/waf/default-rules/ + # config.waf.rules = [] + + # Jets WAF Managed Rules + # config.waf.managed_rules.blanket_rate_limiter.enable = true # default: true + # config.waf.managed_rules.blanket_rate_limiter.limit = 1000 + # config.waf.managed_rules.blanket_rate_limiter.evaluation_window_sec = 300 + # config.waf.managed_rules.blanket_rate_limiter.aggregate_key_type = "IP" + + # config.waf.managed_rules.uri_rate_limiter.enable = true # default: false + # config.waf.managed_rules.uri_rate_limiter.limit = 100 + # config.waf.managed_rules.uri_rate_limiter.logical_statement = "Or" + # config.waf.managed_rules.uri_rate_limiter.paths = ["/login", "/signup"] # default: ["/"] + # config.waf.managed_rules.uri_rate_limiter.evaluation_window_sec = 300 + # config.waf.managed_rules.uri_rate_limiter.aggregate_key_type = "IP" + # config.waf.managed_rules.uri_rate_limiter.string_match_condition = "STARTS_WITH" + + # Docs: https://docs.rubyonjets.com/docs/routing/lambda/cloudfront/waf/ +end diff --git a/lib/jets/code.rb b/lib/jets/code.rb new file mode 100644 index 000000000..7d5573592 --- /dev/null +++ b/lib/jets/code.rb @@ -0,0 +1,80 @@ +module Jets + class Code + include Jets::AwsServices + include Jets::Util::Logging + include Jets::Util::Sh + + delegate :s3_bucket, to: "Jets.project" + delegate :build_root, to: Jets + + def initialize(options = {}) + @options = options + end + + def zip_and_upload + command = ARGV.reject { |s| s.match(/^-/) }.join(":") # remove options + log.info "Packaging code for #{command}: #{Jets.project.namespace}" + stage + zip + upload + end + + def zip_path + "#{build_root}/code/code.zip" + end + + @@s3_key = nil + def s3_key + return @@s3_key if @@s3_key + + timestamp = Time.now.utc.strftime("%Y-%m-%dT%H-%M-%SZ") + git = Jets::Git::Info.new + branch, sha, dirty = git.params.values_at(:git_branch, :git_sha, :git_dirty) + sha = sha[0..6] if sha # short sha + dirty = dirty ? "dirty" : nil + key = ["jets/code/#{timestamp}", branch, sha, dirty].compact.join("-") + key = key.gsub(/[^a-zA-Z0-9\-_:\/]/, "-").squeeze("-") # sanitize + @@s3_key = key + ".zip" + end + + def s3_location + "s3://#{s3_bucket}/#{s3_key}" + end + + def upload + s3_resource.bucket(s3_bucket).object(s3_key).upload_file(zip_path) + logger.debug "Uploaded code to s3://#{s3_bucket}/#{s3_key}" + s3_location # Important to return the s3_location + end + + def stage + if @options[:dummy] + Dummy.new.build + else + Stager.new.build + end + end + + def zip + # Check if the folder exists + unless Dir.exist?("#{build_root}/stage/code") + logger.info "Error: Source folder not found: #{build_root}/stage/code" + return + end + + # Setup + FileUtils.mkdir_p(File.dirname(zip_path)) + FileUtils.rm_f(zip_path) # remove existing zip file. Else files are added to it + + # Zip + quiet_sh "cd #{build_root}/stage/code && zip --symlinks -rq #{zip_path} ." + + # Check if the zip operation was successful + unless $?.success? + logger.info "Error: Failed to zip the folder: #{build_root}/stage/code" + end + + zip_path + end + end +end diff --git a/lib/jets/code/copy/base.rb b/lib/jets/code/copy/base.rb new file mode 100644 index 000000000..c66f6f0d5 --- /dev/null +++ b/lib/jets/code/copy/base.rb @@ -0,0 +1,84 @@ +module Jets::Code::Copy + class Base + extend Memoist + include Jets::Util::Git + include Jets::Util::Logging + include Jets::Util::Sh + + delegate :build_root, to: Jets + delegate :config, to: "Jets.bootstrap.config" + + def self.run + new.run + end + + def run + create_temp_zip # interface method + extract_code # Now have a working area: stage/code + remove_temp_files + always_keep + always_remove + save_gitinfo + "#{build_root}/stage/code" + end + + # Extract code-temp.zip immediately + def extract_code + quiet_sh "unzip -q #{build_root}/stage/code-temp.zip -d #{build_root}/stage/code" + end + + # Remove code-temp files. Not needed anymore. + def remove_temp_files + FileUtils.rm_f "#{build_root}/stage/code-temp.zip" + FileUtils.rm_rf "#{build_root}/stage/code-temp" + end + + def save_gitinfo + dest = "#{build_root}/stage/code/.jets/gitinfo.yml" + FileUtils.mkdir_p(File.dirname(dest)) + if File.exist?(".jets/gitinfo.yml") + # already copied over + FileUtils.cp(".jets/gitinfo.yml", dest) + else + IO.write(dest, git_info.params.deep_stringify_keys.to_yaml) + end + end + + def git_info + Jets::Git::Info.new + end + memoize :git_info + + # Ensure gitconfig to avoid the Author identity unknown git warning. + def gitconfig + home = ENV["HOME"] || "/root" + git_user = capture "git config user.name || true" + return unless git_user.blank? + + IO.write("#{home}/.gitconfig", <<~EOL) + [user] + name = Nobody + email = "nobody@email.com" + EOL + end + + # Copy always_keep files and folders like .deploy for remote runner + # regardless of .gitignore. Allows user to use .deploy/.env files for remote runner + # And have a .gitignore for .env at the project root level. + def always_keep + config.code.copy.always_keep.each do |path| + next unless File.exist?(path) + # Remove in case not gitignore. IE: avoid creating .deploy/.deploy + FileUtils.rm_rf("#{build_root}/stage/code/#{path}") + FileUtils.mkdir_p("#{build_root}/stage/code") + FileUtils.cp_r(path, "#{build_root}/stage/code/#{path}") + end + end + + def always_remove + config.code.copy.always_remove.each do |path| + FileUtils.rm_rf("#{build_root}/stage/code/#{path}") + end + end + end +end diff --git a/lib/jets/code/copy/full.rb b/lib/jets/code/copy/full.rb new file mode 100644 index 000000000..e300f58c4 --- /dev/null +++ b/lib/jets/code/copy/full.rb @@ -0,0 +1,9 @@ +module Jets::Code::Copy + # Inherits from Base for build_root and class run method + class Full < Base + # Completely override run since Full has complete different behavior + def run + FileUtils.cp_r(Jets.root, "#{build_root}/stage/code") + end + end +end diff --git a/lib/jets/code/copy/git_copy.rb b/lib/jets/code/copy/git_copy.rb new file mode 100644 index 000000000..62a207b37 --- /dev/null +++ b/lib/jets/code/copy/git_copy.rb @@ -0,0 +1,26 @@ +module Jets::Code::Copy + class GitCopy < Base + # interface method + def create_temp_zip + copy_to_code_temp # interface method + + # We leverage git archive for gitignore settings. + Dir.chdir("#{build_root}/stage/code-temp") do + if git_info.params[:git_dirty] + quiet_sh "git add ." + gitconfig + quiet_sh "git commit -m 'add working tree files' > /dev/null || true" + end + quiet_sh "git archive -o #{build_root}/stage/code-temp.zip HEAD" + end + end + + # Copy project to stage/code-temp to use git add and archive + # without affecting up the current git repo. + # interface method. overriden by GitRsync + def copy_to_code_temp + FileUtils.mkdir_p("#{build_root}/stage") + FileUtils.cp_r(Jets.root, "#{build_root}/stage/code-temp") + end + end +end diff --git a/lib/jets/code/copy/git_inline.rb b/lib/jets/code/copy/git_inline.rb new file mode 100644 index 000000000..be88c3470 --- /dev/null +++ b/lib/jets/code/copy/git_inline.rb @@ -0,0 +1,13 @@ +module Jets::Code::Copy + class GitInline < Base + def create_temp_zip + # Git add and commit to current repo. Affects the working dir but faster. + if git_info.params[:git_dirty] + quiet_sh "git add ." + gitconfig + quiet_sh "git commit -m 'commit for deploy' > /dev/null || true" + end + quiet_sh "git archive -o #{build_root}/stage/code-temp.zip HEAD" + end + end +end diff --git a/lib/jets/code/copy/rsync.rb b/lib/jets/code/copy/rsync.rb new file mode 100644 index 000000000..4ab90fdd8 --- /dev/null +++ b/lib/jets/code/copy/rsync.rb @@ -0,0 +1,19 @@ +module Jets::Code::Copy + class Rsync < Base + # interface method + def create_temp_zip + FileUtils.mkdir_p("#{build_root}/stage/code-temp") + excludes = " --exclude .git" + excludes << " --exclude-from=#{Jets.root}/.gitignore" if File.exist?("#{Jets.root}/.gitignore") + quiet_sh "rsync -aq#{excludes} #{Jets.root}/ #{build_root}/stage/code-temp" + # Create zip file + check_zip_installed! + quiet_sh "cd #{build_root}/stage/code-temp && zip -q -r #{build_root}/stage/code-temp.zip ." + end + + def check_zip_installed! + return if system("type zip > /dev/null 2>&1") + abort "ERROR: zip command is required. Please install zip command." + end + end +end diff --git a/lib/jets/code/dummy.rb b/lib/jets/code/dummy.rb new file mode 100644 index 000000000..a51657d2d --- /dev/null +++ b/lib/jets/code/dummy.rb @@ -0,0 +1,11 @@ +class Jets::Code + class Dummy < Stager + def stage_code + # copies jets config + return unless File.exist?("#{Jets.root}/config/jets") + FileUtils.rm_rf("#{build_root}/stage/code/config/jets") + FileUtils.mkdir_p("#{build_root}/stage/code/config") + FileUtils.cp_r("#{Jets.root}/config/jets", "#{build_root}/stage/code/config/jets") + end + end +end diff --git a/lib/jets/code/stager.rb b/lib/jets/code/stager.rb new file mode 100644 index 000000000..c56c1683d --- /dev/null +++ b/lib/jets/code/stager.rb @@ -0,0 +1,124 @@ +require "active_support" +require "active_support/number_helper" + +class Jets::Code + class Stager + extend Memoist + include Jets::Util::Git + include Jets::Util::Logging + include Jets::Util::Sh + + delegate :build_root, to: Jets + delegate :config, to: "Jets.bootstrap.config" + + def build + clean + warn_large_codebase + stage_code # interface method + save_deploy_user + save_project_name + save_ruby_version + touch_deployed_at + end + + # interface method: overridden by Dummy + def stage_code + log.debug "Copying project to #{build_root}/stage/code" + log.debug "Copy strategy: #{copy_strategy}" + copy_strategy.run + end + + def copy_strategy + strategy = config.code.copy.strategy.to_s + if strategy == "auto" + auto_strategy + else # whatever user overrides it to + class_name = strategy.camelize + Copy.const_get(class_name) # Copy::GitInline + end + end + memoize :copy_strategy + + def auto_strategy + if File.exist?("#{Jets.root}/.git") && rsync_installed? + Copy::Rsync + elsif git? # .git folder exists and git command available + Copy::GitCopy + else # full copy + Copy::Full # FileUtils.cp_r(Jets.root, "#{build_root}/stage/code") + end + end + + @@rsync_installed = nil # only check once + def rsync_installed? + return @@rsync_installed unless @@rsync_installed.nil? + @@rsync_installed = system "type rsync > /dev/null 2>&1" + end + + def save_deploy_user + User.new.save + end + + # Save the project_name in .jets/info.yml if inferred + # Better to write the file here instead of within the Jets::Core::Config::Info class. + # Because Info assumes Jets.root is Dir.pwd + # We need to ensure it's written to stage/code not the current project root. + def save_project_name + return unless Jets.project.name_inferred? + + jets_info = Jets::Core::Config::Info.instance + data = jets_info.data.merge(project_name: Jets.project.name) + dest = "#{build_root}/stage/code/#{jets_info.path}" + FileUtils.mkdir_p(File.dirname(dest)) + IO.write(dest, data.to_h.to_yaml) + end + + # Save by moving .ruby-version to .jets/ruby-version + # So it does not affect codebuild ruby version. + def save_ruby_version + Dir.chdir("#{build_root}/stage/code") do + move_ruby_version(".ruby-version") + move_ruby_version(".tool-versions") # future proofing for asdf + end + end + + def move_ruby_version(filename = ".ruby-version") + if File.exist?(filename) + FileUtils.mkdir_p(".jets") + FileUtils.mv(filename, ".jets/#{filename}") + end + end + + def touch_deployed_at + dest = "#{build_root}/stage/code/.jets/deployed_at" + FileUtils.mkdir_p(File.dirname(dest)) + IO.write(dest, Time.now.utc.to_s) + end + + def clean + FileUtils.rm_rf("#{build_root}/stage/code") + FileUtils.rm_rf("#{build_root}/stage/code-temp") + FileUtils.mkdir_p(File.dirname("#{build_root}/stage/code")) + end + + def warn_large_codebase + return unless Jets.bootstrap.code.copy.warn_large + return unless system("type du > /dev/null 2>&1") + bytes = `du -bs #{Jets.root}`.split("\t").first.to_i + if bytes > 1024 * 1024 * 1024 # 1GB + size = ActiveSupport::NumberHelper.number_to_human_size(bytes) + log.info <<~EOL + WARNING: Large codebase detected: #{size} + Will take some time to zip and upload. Please be patient. + You can turn off this warning with + + config/jets/bootstrap.rb + + Jets.bootstrap.configure do + config.code.copy.warn_large = false + end + EOL + end + end + end +end diff --git a/lib/jets/code/user.rb b/lib/jets/code/user.rb new file mode 100644 index 000000000..03a68f13e --- /dev/null +++ b/lib/jets/code/user.rb @@ -0,0 +1,24 @@ +require "aws-sdk-iam" + +class Jets::Code + class User + delegate :build_root, to: Jets + + def save + user = iam_user || ENV["USER"] || ENV["JETS_DEPLOY_USER"] + FileUtils.mkdir_p(File.dirname(user_file)) + IO.write(user_file, user) + user + end + + def user_file + "#{build_root}/stage/code/.jets/deploy_user" + end + + def iam_user + @iam ||= Aws::IAM::Client.new + @iam.get_user.user.user_name + rescue Aws::IAM::Errors::ValidationError + end + end +end diff --git a/lib/jets/command.rb b/lib/jets/command.rb deleted file mode 100644 index 9c0d3d051..000000000 --- a/lib/jets/command.rb +++ /dev/null @@ -1,142 +0,0 @@ -# frozen_string_literal: true - -require "active_support" -require "active_support/core_ext/enumerable" -require "active_support/core_ext/object/blank" - -require "thor" - -module Jets - module Command - extend ActiveSupport::Autoload - - autoload :Behavior - autoload :Base - - include Behavior - - HELP_MAPPINGS = %w(-h -? --help) - - cattr_accessor :original_cli_command - - class << self - def hidden_commands # :nodoc: - @hidden_commands ||= [] - end - - def environment # :nodoc: - ENV["JETS_ENV"].presence || ENV["RACK_ENV"].presence || "development" - end - - # Receives a namespace, arguments, and the behavior to invoke the command. - def invoke(full_namespace, args = [], **config) - namespace = full_namespace = full_namespace.to_s - Jets::Command.original_cli_command = full_namespace - - if char = namespace =~ /:(\w+)$/ - command_name, namespace = $1, namespace.slice(0, char) - else - command_name = namespace - end - - command_name, namespace = "help", "help" if command_name.blank? || HELP_MAPPINGS.include?(command_name) - command_name, namespace, args = "application", "application", ["--help"] if jets_new_with_no_path?(args) - command_name, namespace = "version", "version" if %w( -v --version ).include?(command_name) - - original_argv = ARGV.dup - ARGV.replace(args) - - command = find_by_namespace(namespace, command_name) - if command && command.all_commands[command_name] - command.perform(full_namespace, command_name, args, config) - else - # Decorate the rake [] method in order to rescue the error and print out - # a user friendly message. This works to catch rake errors but - # unsure why cannot just rescue RuntimeError here. - # More details in the RakeDecorate module. - require "rake/application" - Rake::Application.send(:include, RakeDecorate) - args = ["--describe", full_namespace] if HELP_MAPPINGS.include?(args[0]) - find_by_namespace("rake").perform(full_namespace, args, config) - end - ensure - ARGV.replace(original_argv) - end - - # Jets finds namespaces similar to Thor, it only adds one rule: - # - # Command names must end with "_command.rb". This is required because Jets - # looks in load paths and loads the command just before it's going to be used. - # - # find_by_namespace :webrat, :integration - # - # Will search for the following commands: - # - # "webrat", "webrat:integration", "jets:webrat", "jets:webrat:integration" - # - def find_by_namespace(namespace, command_name = nil) # :nodoc: - lookups = [ namespace ] - lookups << "#{namespace}:#{command_name}" if command_name - lookups.concat lookups.map { |lookup| "jets:#{lookup}" } - - lookup(lookups) - - namespaces = subclasses.index_by(&:namespace) - namespaces[(lookups & namespaces.keys).first] - end - - # Returns the root of the Jets engine or app running the command. - def root - if defined?(ENGINE_ROOT) - Pathname.new(ENGINE_ROOT) - elsif defined?(APP_PATH) - Pathname.new(File.expand_path("../..", APP_PATH)) - end - end - - def print_commands # :nodoc: - commands.each { |command| puts(" #{command}") } - end - - COMMANDS_IN_USAGE = %w( - generate - console - server - deploy - logs - new - ) - private_constant :COMMANDS_IN_USAGE - - PRO_COMMANDS = %w( - projects - stacks - releases - rollback - ) - - def jets_new_with_no_path?(args) - args == ["new"] - end - - def commands - lookup! - visible_commands = (subclasses - hidden_commands).flat_map(&:printing_commands) - (visible_commands - COMMANDS_IN_USAGE - PRO_COMMANDS).sort - end - - private - def command_type # :doc: - @command_type ||= "command" - end - - def lookup_paths # :doc: - @lookup_paths ||= %w( jets/commands commands ) - end - - def file_lookup_paths # :doc: - @file_lookup_paths ||= [ "{#{lookup_paths.join(',')}}", "**", "*_command.rb" ] - end - end - end -end diff --git a/lib/jets/command/actions.rb b/lib/jets/command/actions.rb deleted file mode 100644 index a17c58fa1..000000000 --- a/lib/jets/command/actions.rb +++ /dev/null @@ -1,49 +0,0 @@ -# frozen_string_literal: true - -module Jets - module Command - module Actions - # Change to the application's path if there is no config.ru file in current directory. - # This allows us to run jets server from other directories, but still get - # the main config.ru and properly set the tmp directory. - def set_application_directory! - Dir.chdir(File.expand_path("../..", APP_PATH)) unless File.exist?(File.expand_path("config.ru")) - end - - def require_application_and_environment! - Jets.boot - end - - def require_application! - require ENGINE_PATH if defined?(ENGINE_PATH) - - if defined?(APP_PATH) - require APP_PATH - end - end - - if defined?(ENGINE_PATH) - def load_tasks - Rake.application.init("jets") - Rake.application.load_rakefile - end - - def load_generators - engine = ::Jets::Engine.find(ENGINE_ROOT) - Jets::Generators.namespace = engine.railtie_namespace - engine.load_generators - end - else - def load_tasks - Jets.boot # hack - Jets.application.load_tasks - end - - def load_generators - Jets.application.load_generators - end - end - end - end -end - diff --git a/lib/jets/command/api_helpers.rb b/lib/jets/command/api_helpers.rb deleted file mode 100644 index 1cf9eeea1..000000000 --- a/lib/jets/command/api_helpers.rb +++ /dev/null @@ -1,42 +0,0 @@ -module Jets - module Command - module ApiHelpers # :nodoc: - extend ActiveSupport::Concern - - include Jets::Api - - def no_token_exit! - return if Jets::Api.token - puts "ERROR: This command requires Jets Api".color(:red) - puts "Please run `jets configure` first" - exit 1 - end - - def check_for_error_message!(resp) - # IE: {"error":"Invalid token. Please check your token in ~/.jets/config.yml"} - if resp && resp["error"] - $stderr.puts "ERROR: #{resp["error"]}" - exit 1 - end - resp - end - - def paging_params - Jets.boot - params = {} - params[:page] = @options[:page] if @options[:page] - params[:order] = @options[:order] if @options[:order] - params - end - - module ClassMethods - def paging_options(defaults={}) - Proc.new do - option :page, aliases: :p, type: :numeric, desc: "Page number" - option :order, default: defaults[:order] || 'asc', desc: "Order: asc or desc" - end - end - end - end - end -end diff --git a/lib/jets/command/aws_helpers.rb b/lib/jets/command/aws_helpers.rb deleted file mode 100644 index 8a909a5df..000000000 --- a/lib/jets/command/aws_helpers.rb +++ /dev/null @@ -1,26 +0,0 @@ -module Jets - module Command - module AwsHelpers # :nodoc: - extend ActiveSupport::Concern - - include Jets::AwsServices - - def first_run? - return false if ENV['JETS_TEMPLATES'] - !stack_exists?(Jets::Names.parent_stack_name) - end - - def find_stack(stack_name) - resp = cfn.describe_stacks(stack_name: stack_name) - resp.stacks.first - rescue Aws::CloudFormation::Errors::ValidationError => e - # example: Stack with id demo-dev does not exist - if e.message =~ /Stack with/ && e.message =~ /does not exist/ - nil - else - raise - end - end - end - end -end diff --git a/lib/jets/command/base.rb b/lib/jets/command/base.rb deleted file mode 100644 index e0f7e5784..000000000 --- a/lib/jets/command/base.rb +++ /dev/null @@ -1,209 +0,0 @@ -# frozen_string_literal: true - -require "thor" -require "erb" - -require "active_support/core_ext/string/filters" -require "active_support/core_ext/string/inflections" - -require "jets/command/actions" - -module Jets - module Command - class Base < Thor - extend Memoist - - class Error < Thor::Error # :nodoc: - end - - class CorrectableError < Error # :nodoc: - attr_reader :key, :options - - def initialize(message, key, options) - @key = key - @options = options - super(message) - end - - if defined?(DidYouMean::SpellChecker) && defined?(DidYouMean::Correctable) - include DidYouMean::Correctable - - def corrections - @corrections ||= DidYouMean::SpellChecker.new(dictionary: options).correct(key) - end - end - end - - include Actions - include AwsHelpers - include ApiHelpers - - no_commands do - cattr_accessor :full_namespace - end - - class << self - def long_desc(long_description, options = {}) - options[:wrap] = false - super - end - - def exit_on_failure? # :nodoc: - false - end - - # Returns true when the app is a Jets engine. - def engine? - defined?(ENGINE_ROOT) - end - - # Tries to get the description from a USAGE file one folder above the command - # root. - def desc(usage = nil, description = nil, options = {}) - if usage - super - else - @desc ||= ERB.new(File.read(usage_path), trim_mode: "-").result(binding) if usage_path - end - end - - # Convenience method to get the namespace from the class name. It's the - # same as Thor default except that the Command at the end of the class - # is removed. - def namespace(name = nil) - if name - super - else - @namespace ||= super.chomp("_command").sub(/:command:/, ":") - end - end - - # Convenience method to hide this command from the available ones when - # running jets command. - def hide_command! - Jets::Command.hidden_commands << self - end - - def inherited(base) # :nodoc: - super - - if base.name && !base.name.end_with?("Base") - Jets::Command.subclasses << base - end - end - - def perform(full_namespace, command, args, config) # :nodoc: - if Jets::Command::HELP_MAPPINGS.include?(args.first) - command, args = "help", [] - self.full_namespace = full_namespace # store for help. clean:log => log - end - - dispatch(command, args.dup, nil, config) - rescue Thor::InvocationError => e - puts e.message.color(:red) # message already has ERROR prefix - self.full_namespace = full_namespace # store for help. clean:log => log - dispatch("help", [], nil, config) - exit 1 - end - - def printing_commands - namespaced_commands - end - - def executable - "jets #{full_namespace || command_name}" - end - - # Use Jets' default banner. - def banner(*) - command_name = full_namespace ? full_namespace.split(':').last : command_name - command = commands[command_name] - options_arg = '[options]' unless command && command.options.empty? - output = "#{executable} #{arguments.map(&:usage).join(' ')} #{options_arg}".squish - " #{output}" # add 2 more spaces in front - end - - # Sets the base_name taking into account the current class namespace. - # - # Jets::Command::TestCommand.base_name # => 'jets' - def base_name - @base_name ||= if base = name.to_s.split("::").first - base.underscore - end - end - - # Return command name without namespaces. - # - # Jets::Command::TestCommand.command_name # => 'test' - def command_name - @command_name ||= if command = name.to_s.split("::").last - command.chomp!("Command") - command.underscore - end - end - - # Path to lookup a USAGE description in a file. - def usage_path - if default_command_root - path = File.join(default_command_root, "USAGE") - path if File.exist?(path) - end - end - - # Default file root to place extra files a command might need, placed - # one folder above the command file. - # - # For a Jets::Command::TestCommand placed in jets/command/test_command.rb - # would return jets/test. - def default_command_root - path = File.expand_path(relative_command_path, __dir__) - path if File.exist?(path) - end - - private - # Allow the command method to be called perform. - def create_command(meth) - if meth == "perform" - alias_method command_name, meth - else - # Prevent exception about command without usage. - # Some commands define their documentation differently. - @usage ||= "" - @desc ||= "" - - super - end - end - - def command_root_namespace - (namespace.split(":") - %w(jets)).join(":") - end - - def relative_command_path - File.join("../commands", *command_root_namespace.split(":")) - end - - def namespaced_commands - commands.keys.map do |key| - if command_root_namespace.match?(/(\A|:)#{key}\z/) - command_root_namespace - else - "#{command_root_namespace}:#{key}" - end - end - end - end - - def help - if full_namespace = self.class.full_namespace - command_name = full_namespace.split(':').last # clean:log => log - self.class.command_help(shell, command_name) - elsif command_name = self.class.command_name - self.class.command_help(shell, command_name) - else - super - end - end - end - end -end diff --git a/lib/jets/command/behavior.rb b/lib/jets/command/behavior.rb deleted file mode 100644 index c6154d7ee..000000000 --- a/lib/jets/command/behavior.rb +++ /dev/null @@ -1,83 +0,0 @@ -# frozen_string_literal: true - -require "active_support" - -module Jets - module Command - module Behavior # :nodoc: - extend ActiveSupport::Concern - - class_methods do - # Remove the color from output. - def no_color! - Thor::Base.shell = Thor::Shell::Basic - end - - # Track all command subclasses. - def subclasses - @subclasses ||= [] - end - - private - # Prints a list of generators. - def print_list(base, namespaces) - return if namespaces.empty? - puts "#{base.camelize}:" - - namespaces.each do |namespace| - puts(" #{namespace}") - end - - puts - end - - # Receives namespaces in an array and tries to find matching generators - # in the load path. - def lookup(namespaces) - paths = namespaces_to_paths(namespaces) - paths.each do |raw_path| - lookup_paths.each do |base| - path = "#{base}/#{raw_path}_#{command_type}" - - begin - require path - return - rescue LoadError => e - raise unless /#{Regexp.escape(path)}$/.match?(e.message) - rescue Exception => e - warn "[WARNING] Could not load #{command_type} #{path.inspect}. Error: #{e.message}.\n#{e.backtrace.join("\n")}" - end - end - end - end - - # This will try to load any command in the load path to show in help. - def lookup! - $LOAD_PATH.each do |base| - Dir[File.join(base, *file_lookup_paths)].each do |path| - path = path.delete_prefix("#{base}/") - require path - rescue Exception - # No problem - end - end - end - - # Convert namespaces to paths by replacing ":" for "/" and adding - # an extra lookup. For example, "jets:model" should be searched - # in both: "jets/model/model_generator" and "jets/model_generator". - def namespaces_to_paths(namespaces) - paths = [] - namespaces.each do |namespace| - pieces = namespace.split(":") - path = pieces.join("/") - paths << "#{path}/#{pieces.last}" - paths << path - end - paths.uniq! - paths - end - end - end - end -end diff --git a/lib/jets/command/environment_argument.rb b/lib/jets/command/environment_argument.rb deleted file mode 100644 index 52f7a0428..000000000 --- a/lib/jets/command/environment_argument.rb +++ /dev/null @@ -1,42 +0,0 @@ -# frozen_string_literal: true - -require "active_support" -require "active_support/core_ext/class/attribute" - -module Jets - module Command - module EnvironmentArgument # :nodoc: - extend ActiveSupport::Concern - - included do - no_commands do - class_attribute :environment_desc, default: "Specifies the environment to run this #{self.command_name} under (test/development/production)." - end - class_option :environment, aliases: "-e", type: :string, desc: environment_desc - end - - private - def extract_environment_option_from_argument(default_environment: Jets::Command.environment) - if options[:environment] - self.options = options.merge(environment: acceptable_environment(options[:environment])) - else - self.options = options.merge(environment: default_environment) - end - # JETS_ENV needs to be set before config/application is required. - ENV["JETS_ENV"] = options[:environment] - end - - def acceptable_environment(env = nil) - if available_environments.include? env - env - else - %w( production development test ).detect { |e| /^#{env}/.match?(e) } || env - end - end - - def available_environments - Dir["config/environments/*.rb"].map { |fname| File.basename(fname, ".*") } - end - end - end -end diff --git a/lib/jets/command/help.rb b/lib/jets/command/help.rb deleted file mode 100644 index 20ad4a29a..000000000 --- a/lib/jets/command/help.rb +++ /dev/null @@ -1,17 +0,0 @@ -module Jets::Command - class Help - class << self - def text(namespaced_command) - file = namespaced_command.to_s.gsub(':','/') - path = File.expand_path("../help/#{file}.md", __FILE__) - return IO.read(path) if File.exist?(path) - - # Also look up for a help folder within the current command folder - called_from = caller.first.split(':').first - unnamespaced_command = namespaced_command.to_s.split(':').last - path = File.expand_path("../help/#{unnamespaced_command}.md", called_from) - return IO.read(path) if File.exist?(path) - end - end - end -end diff --git a/lib/jets/command/help/build.md b/lib/jets/command/help/build.md deleted file mode 100644 index d1d88c285..000000000 --- a/lib/jets/command/help/build.md +++ /dev/null @@ -1,6 +0,0 @@ -Builds a zip file package to be uploaded to AWS Lambda. This allows you to build the project without deploying and inspect the zip file that gets deployed to AWS Lambda. The package contains: - -* your application code -* generated shims - -If the application has no Ruby code and only uses Polymorphic functions, then gems are not bundled up. diff --git a/lib/jets/command/help/call.md b/lib/jets/command/help/call.md deleted file mode 100644 index 6b4ef5573..000000000 --- a/lib/jets/command/help/call.md +++ /dev/null @@ -1,48 +0,0 @@ -## Remote mode - -Invoke the lambda function on AWS. - -## Examples - -### Controller Examples - - jets call controller '{"path": "/up", "httpMethod": "GET"}' --show-logs | jq - -You'll need to specify enough of the event payload so that the Jets shim handler can find the route and send it to the right controller action. - - jets call controller '{"path":"/posts"}' --show-logs | jq . - jets call controller 'file://event.json' --show-logs | jq . - -## Job Examples - - jets call hard_job-drive '{"test":1}' - jets call hard_job-drive '{"test":1}' | jq . - jets call hard_job-drive file://event.json | jq . # load event with a file - - -The equivalent AWS Lambda CLI command: - - aws lambda invoke --function-name demo-dev-hard_job-dig --payload '{"path":"/posts","test":1}' outfile.txt - cat outfile.txt | jq '.' - -Jets figures out what functions to call by evaluating the app code and finds if the class and method exists. If you want to turn guess mode off and want to always explicitly provide the method name use the `--no-guess` option. The function name will then have to match the lambda function without the namespace. Example: - - jets call controller --no-guess - -If you want to call a function which runs too long time, you can set `read_timeout`. - - jets call some_long_job-index --read_timeout 900 - -And you can set `retry_limit`. If you don't want to retry you can set 0. - - jets call some_long_job-index --retry_limit 0 - -## Local mode - -Instead of calling AWS lambda remote, you can also have `jets call` use the code directly on your machine. To enable this, use the `--local` flag. Example: - - jets call hard_job-drive --local - -## Logs - -The `jets call` command can also print out the last 4KB of the lambda logs with the `--show-logs` 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`. diff --git a/lib/jets/command/help/clean/build.md b/lib/jets/command/help/clean/build.md deleted file mode 100644 index c141a9e2c..000000000 --- a/lib/jets/command/help/clean/build.md +++ /dev/null @@ -1,5 +0,0 @@ -Removes the build files that jets creates. Essentially, deletes `/tmp/jets`. This will remove all build files for all jets projects. This is safe as jets uses `/tmp/jets` merely as a cache to speed up incrementally builds. Cleaning this out can clean up cruft in the `/tmp/jets` directory that builds over time. - -## Example - - jets clean:build \ No newline at end of file diff --git a/lib/jets/command/help/clean/log.md b/lib/jets/command/help/clean/log.md deleted file mode 100644 index db1e40daa..000000000 --- a/lib/jets/command/help/clean/log.md +++ /dev/null @@ -1,5 +0,0 @@ -Essentially removes the CloudWatch groups assocated with the app. Lambda requests re-create the log groups so this is pretty safe to do. - -## Example - - jets log:clean \ No newline at end of file diff --git a/lib/jets/command/help/console.md b/lib/jets/command/help/console.md deleted file mode 100644 index 0c50f5b93..000000000 --- a/lib/jets/command/help/console.md +++ /dev/null @@ -1,18 +0,0 @@ -## Example - - $ jets console - >> Post.table_name - => "posts" - >> ActiveRecord::Base.connection.tables - => ["schema_migrations", "ar_internal_metadata", "posts"] - >> Jets.env - => "development" - >> - -## Use .env.development.remote - -To use the remote values also in the `jets console` you can use the `JETS_ENV_REMOTE=1` env variable. Example: - - JETS_ENV_REMOTE=1 jets console - -More info at [Env Files](http://rubyonjets.com/docs/env-files/) \ No newline at end of file diff --git a/lib/jets/command/help/db/generate.md b/lib/jets/command/help/db/generate.md deleted file mode 100644 index 595df454b..000000000 --- a/lib/jets/command/help/db/generate.md +++ /dev/null @@ -1,8 +0,0 @@ -Generates migration in `db/migrate` - -## Examples - - jets db:generate create_articles title:string user_id:integer - jets db:generate AddTitleBodyToPost title:string body:text published:boolean - -This task delegates to Rails `rails generate migration`. For more examples, refer to the [Active Record Migrations Rails Guide](https://edgeguides.rubyonrails.org/active_record_migrations.html). \ No newline at end of file diff --git a/lib/jets/command/help/dbconsole.md b/lib/jets/command/help/dbconsole.md deleted file mode 100644 index 6176b8e14..000000000 --- a/lib/jets/command/help/dbconsole.md +++ /dev/null @@ -1,9 +0,0 @@ -This is like running the psql command with the `config/database.yml` values and set for you. - -## Example - - $ jets dbconsole - psql (9.2.24) - Type "help" for help. - - demo_dev=> \ No newline at end of file diff --git a/lib/jets/command/help/degenerate.md b/lib/jets/command/help/degenerate.md deleted file mode 100644 index 8f1519db6..000000000 --- a/lib/jets/command/help/degenerate.md +++ /dev/null @@ -1,16 +0,0 @@ -This piggy backs off of the [rails scaffold destroy](https://guides.rubyonrails.org/command_line.html#rails-destroy). - -## Example - - $ jets degenerate scaffold post title:string body:text published:boolean - invoke active_record - remove db/migrate/20190225231821_create_posts.rb - remove app/models/post.rb - invoke resource_route - route resources :posts - invoke scaffold_controller - remove app/controllers/posts_controller.rb - invoke erb - invoke helper - remove app/helpers/posts_helper.rb - $ diff --git a/lib/jets/command/help/delete.md b/lib/jets/command/help/delete.md deleted file mode 100644 index 38bbd7f37..000000000 --- a/lib/jets/command/help/delete.md +++ /dev/null @@ -1,22 +0,0 @@ -This deletes the all the contents in the internal s3 bucket that jets uses and the associated CloudFormation stacks. - -## Examples - - $ jets delete - Deleting project... - Are you sure you want to want to delete the 'demo-dev' project? (y/N) - y - First, deleting objects in s3 bucket demo-dev-s3bucket-89jrrj60c7bj - Deleting demo-dev... - 05:14:09AM DELETE_IN_PROGRESS AWS::CloudFormation::Stack demo-dev User Initiated - ... - 05:14:23AM DELETE_IN_PROGRESS AWS::CloudFormation::Stack PostsController - 05:15:31AM DELETE_COMPLETE AWS::S3::Bucket S3Bucket - Stack demo-dev deleted. - Time took for deletion: 1m 27s. - Project demo-dev deleted! - $ - -You can bypass the are you sure prompt with the `--yes` flag. - - $ jets delete --yes diff --git a/lib/jets/command/help/deploy.md b/lib/jets/command/help/deploy.md deleted file mode 100644 index 090ba515c..000000000 --- a/lib/jets/command/help/deploy.md +++ /dev/null @@ -1,32 +0,0 @@ -This builds the project and deploys it AWS Lambda. The deployment is mainly handled by CloudFormation. To check on the status of the deploy you can also check the CloudFormation console. - -![](http://rubyonjets.com/img/cli/deploy-cloudformation-status.png) - -## Example - - $ jets deploy - Deploying stack demo-dev ... - => Compling assets in current project directory - => Copying current project directory to temporary build area: /tmp/jets/demo/app_root - => Tidying project: removing ignored files to reduce package size. - => Generating node shims in the handlers folder. - => Bundling: running bundle install in cache area: /tmp/jets/demo/cache. - => Setting up a vendored copy of ruby. - => Replacing compiled gems with AWS Lambda Linux compiled versions. - Creating zip file. - => cd /tmp/jets/demo/app_root && zip --symlinks -rq /tmp/jets/demo/code/code-temp.zip . - Building CloudFormation templates. - Deploying CloudFormation stack with jets app! - Uploading /tmp/jets/demo/code/code-7169d0ac.zip (88.8 MB) to S3 - Time to upload code to s3: 1s - Deploying CloudFormation stack with jets app! - 02:08:20AM UPDATE_IN_PROGRESS AWS::CloudFormation::Stack demo-dev User Initiated - 02:08:23AM CREATE_IN_PROGRESS AWS::CloudFormation::Stack ApiGateway - ... - 02:08:48AM CREATE_IN_PROGRESS AWS::CloudFormation::Stack PostsController - 02:10:03AM UPDATE_COMPLETE AWS::CloudFormation::Stack demo-dev - Stack success status: UPDATE_COMPLETE - Time took for stack deployment: 1m 46s. - Prewarming application. - API Gateway Endpoint: https://ewwnealfk0.execute-api.us-west-2.amazonaws.com/dev/ - $ diff --git a/lib/jets/command/help/gems/check.md b/lib/jets/command/help/gems/check.md deleted file mode 100644 index e605a0d0b..000000000 --- a/lib/jets/command/help/gems/check.md +++ /dev/null @@ -1 +0,0 @@ -Check if precompiled gems are available from Jets Pro. diff --git a/lib/jets/command/help/generate.md b/lib/jets/command/help/generate.md deleted file mode 100644 index bd34efdf4..000000000 --- a/lib/jets/command/help/generate.md +++ /dev/null @@ -1,28 +0,0 @@ -This piggy backs off of the [rails scaffold generator](https://guides.rubyonrails.org/command_line.html#rails-generate). - -## General options - - -h, [--help] # Print generator's options and usage - -p, [--pretend] # Run but do not make any changes - -f, [--force] # Overwrite files that already exist - -s, [--skip] # Skip files that already exist - -q, [--quiet] # Suppress status output - -Please choose a generator below. - -Jets: - - application_record - controller - helper - job - migration - model - resource - scaffold - scaffold_controller - task - -ActiveRecord: - - active_record:application_record diff --git a/lib/jets/command/help/import/rack.md b/lib/jets/command/help/import/rack.md deleted file mode 100644 index 6272107d5..000000000 --- a/lib/jets/command/help/import/rack.md +++ /dev/null @@ -1,13 +0,0 @@ -Imports a generic Rack application into a Jets project and configures it for [Mega Mode](http://rubyonjets.com/docs/megamode/). - -Note, generic rack projects will likely need some adjustments to take into account API Gateway stages and logging. For more info refer to [Mega Mode Considerations](http://rubyonjets.com//megamode-details/). - -## Example - - jets import:rack http://github.com/tongueroo/jets-mega-rails.git - -## More Examples - - jets import:rack tongueroo/jets-mega-rails # expands to github - jets import:rack git@github.com:tongueroo/jets-mega-rails.git - jets import:rack /path/to/folder/jets-mega-rails diff --git a/lib/jets/command/help/import/rails.md b/lib/jets/command/help/import/rails.md deleted file mode 100644 index 9bd72e12a..000000000 --- a/lib/jets/command/help/import/rails.md +++ /dev/null @@ -1,11 +0,0 @@ -Imports a Rails application into a Jets project and configures it for [Mega Mode](http://rubyonjets.com/docs/rails-support/). - -## Example - - jets import:rails http://github.com/tongueroo/demo-rails.git - -## More Examples - - jets import:rails tongueroo/demo-rails # expands to github - jets import:rails git@github.com:tongueroo/demo-rails.git - jets import:rails /path/to/folder/jets-examples-rails diff --git a/lib/jets/command/help/new.md b/lib/jets/command/help/new.md deleted file mode 100644 index 3c06c4454..000000000 --- a/lib/jets/command/help/new.md +++ /dev/null @@ -1,41 +0,0 @@ -## Examples - - $ jets new demo - create app/controllers/application_controller.rb - create app/helpers/application_helper.rb - create app/jobs/application_job.rb - ... - ================================================================ - Congrats You have successfully created a Jets project. - - Cd into the project directory: - cd demo - - To start a server and test locally: - jets server # localhost:8888 should have the Jets welcome page - - Scaffold example: - jets generate scaffold post title:string body:text published:boolean - - To deploy to AWS Lambda: - jets deploy - $ - -## Mode Option - -The `--mode` is a notable option. With it, you can generate different starter Jets projects. Examples: - - jets new demo --mode html # default - jets new api --mode api - jets new cron --mode job - -* The html mode generates a starter app useful for html web application. -* The api mode is useful for building an API. -* The job mode creates a very lightweight project. It is useful when you just need to run a Lambda function. - -## Repo Option -Use the `--repo` flag to clone an example project from GitHub instead. With this flag, jets new command clones a jets project repo from GitHub: - - $ jets new blog --repo tongueroo/tutorial - $ jets new todos --repo tongueroo/todos - $ jets new whatever --repo user/repo # any github repo diff --git a/lib/jets/command/help/releases.md b/lib/jets/command/help/releases.md deleted file mode 100644 index 15db88564..000000000 --- a/lib/jets/command/help/releases.md +++ /dev/null @@ -1,19 +0,0 @@ -## Examples - - $ jets releases - Releases for stack: demo-dev - +---------+-----------------+--------------+---------+ - | Version | Status | Released At | Message | - +---------+-----------------+--------------+---------+ - | 3 | UPDATE_COMPLETE | 10 hours ago | Deploy | - | 2 | UPDATE_COMPLETE | 18 hours ago | Deploy | - | 1 | UPDATE_COMPLETE | 22 hours ago | Deploy | - +---------+-----------------+--------------+---------+ - -The shown releases are paginated. If you need to see more releases you can use the `--page` option. - - $ jets releases --page 2 - -## Other Commands - - releases:info View detailed information for a release diff --git a/lib/jets/command/help/rollback.md b/lib/jets/command/help/rollback.md deleted file mode 100644 index cf53674f1..000000000 --- a/lib/jets/command/help/rollback.md +++ /dev/null @@ -1,5 +0,0 @@ -## Examples - - jets rollback 8 - -Use the [jets releases](/reference/jets-releases/) command to view release history. diff --git a/lib/jets/command/help/routes.md b/lib/jets/command/help/routes.md deleted file mode 100644 index 58d22b05e..000000000 --- a/lib/jets/command/help/routes.md +++ /dev/null @@ -1,15 +0,0 @@ -## Example - - $ jets routes - +-------------------+--------+--------------------+--------------------+ - | As (Prefix) | Verb | Path (URI Pattern) | Controller#action | - +-------------------+--------+--------------------+--------------------+ - | posts | GET | /posts | posts#index | - | posts | POST | /posts | posts#create | - | new_post | GET | /posts/new | posts#new | - | edit_post | GET | /posts/:id/edit | posts#edit | - | post | GET | /posts/:id | posts#show | - | post | PUT | /posts/:id | posts#update | - | post | PATCH | /posts/:id | posts#update | - | post | DELETE | /posts/:id | posts#destroy | - +-------------------+--------+--------------------+--------------------+ diff --git a/lib/jets/command/help/runner.md b/lib/jets/command/help/runner.md deleted file mode 100644 index 52c1d6d0a..000000000 --- a/lib/jets/command/help/runner.md +++ /dev/null @@ -1,40 +0,0 @@ -## Examples - - $ jets runner 'puts "hi"' - hi - $ jets runner 'puts Jets.env' - development - -Using a script in a file. Let's say you have a script: - -script.rb: - -```ruby -puts "hello world: #{Jets.env}" -``` - - $ jets runner file://script.rb - hello world: development - - -Optionally pass in an argument on the command line: - - Usage: jets runner file|Ruby code [args] - -The argument will be assigned to the `args` variable. - -Example: - - $ jets runner 'puts "hello world with args: #{args}"' 123 - hello world with args: 123 - - -Example with script.rb: - -```ruby -puts "hello world with args: #{args}" -``` - - $ jets runner file://script.rb 123 - hello world with args: 123 - diff --git a/lib/jets/command/help/server.md b/lib/jets/command/help/server.md deleted file mode 100644 index 526029af8..000000000 --- a/lib/jets/command/help/server.md +++ /dev/null @@ -1,15 +0,0 @@ -The local server for mimics API Gateway and provides a way to test your app locally without deploying to AWS. - -## Examples - - $ jets server - => bundle exec shotgun --port 8888 --host 127.0.0.1 - Jets booting up in development mode! - == Shotgun/WEBrick on http://127.0.0.1:8888/ - [2018-08-17 05:31:33] INFO WEBrick 1.4.2 - [2018-08-17 05:31:33] INFO ruby 2.5.1 (2018-03-29) [x86_64-linux] - [2018-08-17 05:31:33] INFO WEBrick::HTTPServer#start: pid=27433 port=8888 - -Start up server binding to host `0.0.0.0`: - - jets server --host 0.0.0.0 diff --git a/lib/jets/command/help/upgrade.md b/lib/jets/command/help/upgrade.md deleted file mode 100644 index 0b9540048..000000000 --- a/lib/jets/command/help/upgrade.md +++ /dev/null @@ -1,7 +0,0 @@ -Upgrades the Jets project structure to the latest version. This command is designed to be idempotent, so it should be safe to run this command multiple times. - -## Example - - $ jets upgrade - -Refer to https://rubyonjets.com/docs/extras/upgrading/ \ No newline at end of file diff --git a/lib/jets/command/helpers/editor.rb b/lib/jets/command/helpers/editor.rb deleted file mode 100644 index d8a546cc1..000000000 --- a/lib/jets/command/helpers/editor.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -require "active_support/encrypted_file" - -module Jets - module Command - module Helpers - module Editor - private - def ensure_editor_available(command:) - if ENV["EDITOR"].to_s.empty? - say "No $EDITOR to open file in. Assign one like this:" - say "" - say %(EDITOR="mate --wait" #{command}) - say "" - say "For editors that fork and exit immediately, it's important to pass a wait flag," - say "otherwise the credentials will be saved immediately with no chance to edit." - - false - else - true - end - end - - def catch_editing_exceptions - yield - rescue Interrupt - say "Aborted changing file: nothing saved." - rescue ActiveSupport::EncryptedFile::MissingKeyError => error - say error.message - end - end - end - end -end diff --git a/lib/jets/command/rake_decorate.rb b/lib/jets/command/rake_decorate.rb deleted file mode 100644 index 246f29f67..000000000 --- a/lib/jets/command/rake_decorate.rb +++ /dev/null @@ -1,38 +0,0 @@ -module Jets::Command - module RakeDecorate - # Decorate this method because this does not get called until runtime. - # It's "lazy loaded" so we can avoid the Rails const being defined in general. - def [](task_name, scopes=nil) - super # => Rake::TaskManager#[] - rescue RuntimeError => e - # We require dummy/rails since this time because all the rake tasks have been loaded - # and we need to load dummy/rails to get the database configurations. Normally, - # we do not want to require dummy/rails because it defines the Rails. - # However, a "command not found" error, more accurately, - # a "rake task not found" error, has already been encountered. - # Also: - # require "dummy/rails" to prevent another error. - # from lib/active_record/railties/databases.rake - # - # NoMethodError: undefined method `env' for Rails:Module (NoMethodError) - # database_configs = ActiveRecord::DatabaseConfigurations.new(databases).configs_for(env_name: Rails.env) - # - require "jets/overrides/dummy/rails" - - # Original error message from rake is something like this - # - # Don't know how to build task 'foo:bar' (See the list of available tasks with `jets --tasks`) - # - # With an ugly backtrace. - # We override the error message to be more user friendly. - # - # All of that in order for - # jets foo:bar - # to show a pretty error message. - $stderr.puts "ERROR: Could not find command: #{task_name.inspect}".color(:red) - require "jets/commands/help/help_command" - Jets::Command::HelpCommand.new.help - exit 1 - end - end -end diff --git a/lib/jets/commands.rb b/lib/jets/commands.rb deleted file mode 100644 index 79545937b..000000000 --- a/lib/jets/commands.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -require "jets/command" - -aliases = { - "g" => "generate", - "c" => "console", - "s" => "server", - "db" => "dbconsole", - "r" => "runner", - "t" => "test" -} - -command = ARGV.shift -command = aliases[command] || command - -Jets::Command.invoke command, ARGV diff --git a/lib/jets/commands/application/application_command.rb b/lib/jets/commands/application/application_command.rb deleted file mode 100644 index 001135665..000000000 --- a/lib/jets/commands/application/application_command.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -module Jets - module Command - class ApplicationCommand < Base # :nodoc: - hide_command! - - self.bin = "jets" if respond_to?(:bin=) # Rails 7.1 - - def help - perform # Punt help output to the generator. - end - - # This is: - # - # bundle exec jets application:help => jets new --help - # bundle exec jets application => jets new --help - # - def perform(*args) - version_flags = %w[-v --version] & args - unless version_flags.empty? - puts "Jets #{Jets::VERSION}" - return - end - - # require lazily so that Rails constant is only defined within generators - require "jets/generators/overrides/app/app_generator" - override_exit_on_failure? - argv = Rails::Generators::ARGVScrubber.new(args).prepare! - Jets::Generators::AppGenerator.start argv - end - - private - # We override this way because Jets require generators lazily - def override_exit_on_failure? - Jets::Generators::AppGenerator.class_eval do - # We want to exit on failure to be kind to other libraries - # This is only when accessing via CLI - def self.exit_on_failure? - true - end - end - end - end - end -end diff --git a/lib/jets/commands/build/build_command.rb b/lib/jets/commands/build/build_command.rb deleted file mode 100644 index aaaeef76d..000000000 --- a/lib/jets/commands/build/build_command.rb +++ /dev/null @@ -1,33 +0,0 @@ -module Jets::Command - class BuildCommand < Base # :nodoc: - include EnvironmentArgument - - option :templates, type: :boolean, desc: "Build CloudFormation templates only" - - desc "build", "Builds and packages project for AWS Lambda" - long_desc Help.text(:build) - def perform - ENV['JETS_TEMPLATES'] = '1' if options[:templates] - - extract_environment_option_from_argument - require_application_and_environment! - - puts "Building project for Lambda..." - # run gets called from the CLI and does not have all the stack_options yet. - # We compute it and change the options early here. - @options.merge!(stack_type: stack_type, s3_bucket: Jets.s3_bucket) - do_build - end - - private - # Note: build is picked up as a command so naming it do_build - def do_build - Jets::Builders::CodeBuilder.new.build unless ENV['JETS_TEMPLATES'] - Jets::Cfn::Builder.new(@options).build - end - - def stack_type - first_run? ? :minimal : :full - end - end -end diff --git a/lib/jets/commands/call/anonymous_guesser.rb b/lib/jets/commands/call/anonymous_guesser.rb deleted file mode 100644 index 5cede2829..000000000 --- a/lib/jets/commands/call/anonymous_guesser.rb +++ /dev/null @@ -1,96 +0,0 @@ -module Jets::Commands::Call - class AnonymousGuesser < BaseGuesser - def detect_class_name - found_path = function_paths.find do |path| - File.exist?("#{Jets.root}/#{path}") - end - - klass = Jets::Klass.from_path(found_path) if found_path - klass.to_s - end - - def method_name - return @method_name if defined?(@method_name) - - full_function_name = @provided_function_name.underscore - underscored_class_name = class_name.underscore - meth = full_function_name.sub("#{underscored_class_name}_","") - - if meth == class_name.constantize.handler.to_s - @method_name = meth - else - @method_name_error = "#{class_name} class found but #{meth} method not found" - @method_name = nil - end - end - - # Useful to printing out what was attempted to look up - def error_message - guess_paths = function_paths - puts "Unable to find the function to call." - if class_name and !method_name - puts @method_name_error - else - puts "Tried: #{guess_paths.join(', ')}" - end - end - - def function_filenames(meth=nil, primary_namespace=nil) - guesses = [] - parts = meth.split('_') - - if primary_namespace.nil? - guesses << meth - - if parts.size == 1 # already on final_primary_namespace - return guesses # end of recursion - else - next_primary_namespace = parts.first - guesses += function_filenames(meth, next_primary_namespace) # start of recursion - return guesses # return early - end - end - - next_meth = meth.sub("#{primary_namespace}_", '') - next_parts = next_meth.split('_') - - # Takes the next_parts and creates guesses with the parts joined by '/' - # with the primary_namespace prepended. So if next_parts is - # ["long", "name", "function"] and primary_namespace is "complex" - # - # guesses that get added: - # - # [ - # "complex/long_name_function", - # "complex/long/name_function", - # "complex/long/name/function", - # ] - n = next_parts.size + 1 - next_parts.size.times do |i| - namespace = i == 0 ? nil : next_parts[0..i-1].join('/') - class_path = next_parts[i..-1].join('_') - guesses << [primary_namespace, namespace, class_path].compact.join('/') - end - - final_primary_namespace = parts[0..-2].join('_') - if primary_namespace == final_primary_namespace - return guesses # end of recursion - else - namespace_size = parts.size - next_parts.size - next_primary_namespace = parts[0..namespace_size].join('_') - guesses += function_filenames(meth, next_primary_namespace) - return guesses - end - end - - def function_paths - # drop the last word for starting filename - starting_filename = @provided_function_name.underscore.split('_')[0..-2].join('_') - filenames = function_filenames(starting_filename) - filenames.map do |name| - "app/functions/#{name}.rb" - end - end - - end -end diff --git a/lib/jets/commands/call/autoload_guesser.rb b/lib/jets/commands/call/autoload_guesser.rb deleted file mode 100644 index d96ea869a..000000000 --- a/lib/jets/commands/call/autoload_guesser.rb +++ /dev/null @@ -1,136 +0,0 @@ -module Jets::Commands::Call - class AutoloadGuesser < BaseGuesser - def detect_class_name - # First guess the class name assuming simple - class_name = simple_detect_class_name - return class_name if class_name - - guess_classes.each do |class_name_guess| - begin - class_name_guess.constantize - return class_name_guess # if there's no error then the class is found - rescue NameError - if out_of_guesses(class_name_guess) - # puts "Unable to find the class to call. Tried guessing: #{guess_classes[0..-2].join(', ')}." - # raise # re-raise NameError for now but maybe better to provide - # a custom error class, so we can rescue it and provide a - # friendly message to the user - else - next - end - end - end - - nil - end - - # Can guess name by looking at underscore and dashes. - # - # Example: - # - # @provided_function_name: first_module-lost_controller-index - # return: FirstModule::LostController - # - def simple_detect_class_name - parts = @provided_function_name.split('-') - namespace = parts[0..-3].join('/') - controller = parts[-2..-2] # dont include the action - class_name = [namespace, controller].reject {|s| s.empty?}.compact.join('/').camelize - begin - class_name.constantize - return class_name - rescue NameError - nil - end - end - - def method_name - return @method_name if defined?(@method_name) - return nil unless class_name - - underscored_class_name = class_name.underscore.gsub('/','_') - underscored_function_name = @provided_function_name.underscore.gsub('/','_') - meth = underscored_function_name.sub(underscored_class_name, '') - meth = meth.sub(/^[-_]/,'') # remove leading _ or - - - if class_name.constantize.definitions.map(&:meth).include?(meth.to_sym) - @method_name = meth - else - @method_name_error ="#{class_name} class found but #{meth} method not found" - @method_name = nil - end - end - - # Useful to printing out what was attempted to look up - def error_message - guesses = guess_classes - puts "Unable to find the function to call." - if class_name and !method_name - puts @method_name_error - else - puts "Tried: #{guesses.join(', ')}" - end - end - - def process_type - if @provided_function_name =~ /[-_]controller/ - "controller" - elsif @provided_function_name =~ /[-_]job/ - "job" - elsif @provided_function_name =~ /[-_]rule/ - "rule" - else - "function" - end - end - - def process_type_pattern - Regexp.new("[-_]#{process_type}[-_](.*)") - end - - # Strips the action because we dont want it to guess the class name - # So: - # admin-related-pages => admin_related_pages_controller - def underscored_name - # strip action and concidentally the _controller_ string - name = @provided_function_name.sub(process_type_pattern,'') - # Ensure _controller or _job at the end except for simple functions - unless process_type == "function" - name = name.gsub('-','_') + "_#{process_type}" - end - name - end - - # Guesses autoload paths. - # - # underscored_name: admin_related_pages_controller - # Returns: - # [ - # "admin_related_pages_controller", - # "admin/related_pages_controller", - # "admin_related/pages_controller", - # "admin_related_pages/controller", - # ] - def autoload_paths - guesses = [] - - parts = underscored_name.split('_') - parts.size.times do |i| - namespace = i == 0 ? nil : parts[0..i-1].join('/') - class_path = parts[i..-1].join('_') - guesses << [namespace, class_path].compact.join('/') - end - - guesses - end - - def guess_classes - autoload_paths.map(&:classify) - end - - def out_of_guesses(guess) - guess.include?("::Controller") - end - - end -end diff --git a/lib/jets/commands/call/base_guesser.rb b/lib/jets/commands/call/base_guesser.rb deleted file mode 100644 index a2a37c437..000000000 --- a/lib/jets/commands/call/base_guesser.rb +++ /dev/null @@ -1,62 +0,0 @@ -# Subclasses of BaseGuessor must implement interface: -# detect_class_name -# method_name -# error_message -# -module Jets::Commands::Call - class BaseGuesser - include Jets::AwsServices - - # provided_function_name: - # admin/related_pages_controller-list_all - # admin-related-pages-controller-list-all - def initialize(provided_function_name) - @provided_function_name = provided_function_name - end - - def class_name - return @class_name if @detection_ran - - @class_name = detect_class_name - @detection_ran = true - @class_name - end - - def function_name - # Strip the project namespace if the user has accidentally added it - # Since we're going to automatically add it no matter what at the end - # and dont want the namespace to be included twice - @provided_function_name = @provided_function_name.sub("#{Jets.project_namespace}-", "") - - code_path = class_name.underscore.gsub('/','-') - function_name = [Jets.project_namespace, code_path, method_name].join('-') - generated_function_name(function_name) - end - - def generated_function_name(function_name) - if function_name.size > Jets::Cfn::Resource::Lambda::Function::MAX_FUNCTION_NAME_SIZE # name generated by CloudFormation - logical_id = @class_name.gsub('::','') - - parent_resources = stack_resources(parent_stack.stack_id) - app_stack = parent_resources.find { |s| s.logical_resource_id == logical_id } - - resources = stack_resources(app_stack.physical_resource_id) - resource = resources.find { |r| r.logical_resource_id == method_name.camelize + "LambdaFunction" } # method_name only contains the method - resource.physical_resource_id # actual function name - else - function_name - end - end - - # Class variable caches - @@stack_resources = {} - def stack_resources(stack_name) - @@stack_resources[stack_name] ||= cfn.describe_stack_resources(stack_name: stack_name).stack_resources - end - - @@parent_stack = nil - def parent_stack - @@parent_stack ||= cfn.describe_stacks(stack_name: Jets::Names.parent_stack_name).stacks.first - end - end -end diff --git a/lib/jets/commands/call/call_command.rb b/lib/jets/commands/call/call_command.rb deleted file mode 100644 index f33e17f3e..000000000 --- a/lib/jets/commands/call/call_command.rb +++ /dev/null @@ -1,27 +0,0 @@ -require "base64" -require "json" - -module Jets::Command - class CallCommand < Base # :nodoc: - include Jets::AwsServices - - desc "call [function] [event]", "Call a lambda function on AWS or locally" - long_desc Help.text(:call) - option :invocation_type, default: "RequestResponse", desc: "RequestResponse, Event, or DryRun" - option :log_type, default: "Tail", desc: "Works if invocation_type set to RequestResponse" - option :qualifier, desc: "Lambda function version or alias name" - option :show_log, type: :boolean, desc: "Shows last 4KB of log in the x-amz-log-result header" - option :show_logs, type: :boolean, desc: "Shows last 4KB of log in the x-amz-log-result header" - option :lambda_proxy, type: :boolean, default: true, desc: "Enables automatic Lambda proxy transformation of the event payload" - option :guess, type: :boolean, default: true, desc: "Enables guess mode. Uses inference to allows use of all dashes to specify functions. Guess mode verifies that the function exists in the code base." - option :local, type: :boolean, desc: "Enables local mode. Instead of invoke the AWS Lambda function, the method gets called locally with current app code. With local mode guess mode is always used." - option :retry_limit, type: :numeric, default: nil, desc: "Retry count of invoking function. It work with remote call" - option :read_timeout, type: :numeric, default: nil, desc: " The number of seconds to wait for response data. It work with remote call" - def perform(function_name, payload='') - $stdout.sync = true - $stderr.sync = true - $stdout = $stderr # jets call operation - Jets::Commands::Call::Caller.new(function_name, payload, options).run - end - end -end diff --git a/lib/jets/commands/call/caller.rb b/lib/jets/commands/call/caller.rb deleted file mode 100644 index 34f0798ce..000000000 --- a/lib/jets/commands/call/caller.rb +++ /dev/null @@ -1,184 +0,0 @@ -module Jets::Commands::Call - class Caller - include Jets::AwsServices - - def initialize(provided_function_name, event, options={}) - @options = options - @guess = @options[:guess].nil? ? true : @options[:guess] - - @provided_function_name = provided_function_name - @event = event - - @invocation_type = options[:invocation_type] || "RequestResponse" - @log_type = options[:log_type] || "Tail" - @qualifier = @qualifier - end - - def function_name - if @provided_function_name == 'controller' - "#{Jets.project_namespace}-controller" - elsif @provided_function_name.starts_with?(Jets.project_namespace) - @provided_function_name # fully qualified function name - elsif @guess - ensure_guesses_found! # possibly exits here - guesser.function_name # guesser adds namespace already - else - [Jets.project_namespace, @provided_function_name].join('-') - end - end - - def run - @options[:local] ? local_run : remote_run - end - - # With local mode there is no way to bypass the guesser - def local_run - puts "Local mode enabled!" - ensure_guesses_found! # possibly exits here - klass = guesser.class_name.constantize - # Example: - # PostsController.process(event, context, meth) - event = JSON.load(transformed_event) || {} # transformed_event is JSON text String - - fun = Jets::Poly.new(klass, guesser.method_name) - result = fun.run(event) # check the logs for polymorphic function errors - # Note: even though data might not always be json, the JSON.dump does a - # good job of not bombing, so always calling it to simplify code. - - text = Jets::Util.normalize_result(result) - STDOUT.puts text - end - - def remote_run - puts "Calling lambda function #{function_name} on AWS" unless @options[:mute] - return if @options[:noop] - - options = { - # client_context: client_context, - function_name: function_name, - invocation_type: @invocation_type, # "Event", # RequestResponse - log_type: @log_type, # pretty sweet - payload: transformed_event, # "fileb://file-path/input.json", <= JSON - qualifier: @qualifier, # "1", - } - - begin - resp = lambda_client.invoke(options) - rescue Aws::Lambda::Errors::ResourceNotFoundException - puts "The function #{function_name} was not found. Maybe check the spelling or the AWS_PROFILE?".color(:red) - return - end - - if @options[:show_logs] || @options[:show_log] - puts "Last 4KB of log in the x-amz-log-result header:".color(:green) - puts Base64.decode64(resp.log_result) - end - - add_console_link_to_clipboard - result = resp.payload.read # already been normalized/JSON.dump by AWS - unless @options[:mute_output] - STDOUT.puts result # only thing that goes to stdout - end - end - - def guesser - @guesser ||= Guesser.new(@provided_function_name) - end - - def ensure_guesses_found! - unless guesser.class_name and guesser.method_name - puts guesser.error_message - exit - end - end - - # @event is String because it can be the file:// notation - # Returns text String for the lambda.invoke payload. - def transformed_event - text = @event - if text && text.include?("file://") - text = load_event_from_file(text) - end - - check_valid_json!(text) - - puts "Function name: #{function_name.color(:green)}" unless @options[:mute] - return text unless function_name.include?("_controller-") - return text if @options[:lambda_proxy] == false - - event = JSON.load(text) - lambda_proxy = {"queryStringParameters" => event} - JSON.dump(lambda_proxy) - end - - def load_event_from_file(text) - path = text.gsub('file://','') - path = "#{Jets.root}/#{path}" unless path[0..0] == '/' - unless File.exist?(path) - puts "File #{path} does not exist. Are you sure the file exists?".color(:red) - exit - end - text = IO.read(path) - end - - # Exits with friendly error message when user provides bad just - def check_valid_json!(text) - JSON.load(text) - rescue JSON::ParserError => e - puts "Invalid json provided:\n '#{text}'" - puts "Exiting... Please try again and provide valid json." - exit 1 - end - - # So use can quickly paste this into their browser if they want to see the function - # via the Lambda console - def add_console_link_to_clipboard - return unless RUBY_PLATFORM =~ /darwin/ - return unless system("type pbcopy > /dev/null") - - # TODO: for add_console_link_to_clipboard get the region from the ~/.aws/config and AWS_PROFILE setting - region = Aws::S3::Client.new.config.region || ENV["AWS_REGION"] ||'us-east-1' - link = "https://console.aws.amazon.com/lambda/home?region=#{region}#/functions/#{function_name}?tab=configuration" - system("echo #{link} | pbcopy") - puts "Pro tip: The Lambda Console Link to the #{function_name} function has been added to your clipboard." unless @options[:mute] - end - - # TODO: Hook client_context up and make sure it works. Think I've figure out how to sign client_context below. - # Client context must be a valid Base64-encoded JSON object - # Example: http://docs.aws.amazon.com/mobileanalytics/latest/ug/PutEvents.html - def client_context - context = { - "client" => { - "client_id" => "Jets", - "app_title" => "jets call cli", - "app_version_name" => Jets::VERSION, - }, - "custom" => {}, - "env" =>{ - "platform" => RUBY_PLATFORM, - "platform_version" => RUBY_VERSION, - } - } - Base64.encode64(JSON.dump(context)) - end - - # For this class redirect puts to stderr so user can pipe output to tools like - # jq. Example: - # jets call posts_controller-index '{"test":1}' | jq . - def puts(text) - $stderr.puts(text) - end - - def lambda_client - opt = {} - opt = opt.merge({retry_limit: @options[:retry_limit]}) if @options[:retry_limit].present? - opt = opt.merge({http_read_timeout: @options[:read_timeout]}) if @options[:read_timeout].present? - - if opt.empty? - aws_lambda - else - Aws::Lambda::Client.new(opt) - end - end - end -end diff --git a/lib/jets/commands/call/guesser.rb b/lib/jets/commands/call/guesser.rb deleted file mode 100644 index 60d0ab4cf..000000000 --- a/lib/jets/commands/call/guesser.rb +++ /dev/null @@ -1,48 +0,0 @@ -# Guesser transforms the user provided function name to the actual lambda -# function name. -# -# Allow for variety of different inputs to work: -# Simple: -# admin/pages_controller-index => admin-pages_controller-index -# admin-pages_controller-index => admin-pages_controller-index -# -# Complex, requires detecting the right class name: -# admin/related_pages_controller-list_all -# admin-related-pages-controller-list-all -# -# All still result in: admin-related_pages_controller-index -# -# The detection process follows. Given worse case: -# admin-related-pages-controller-list-all -# -# Know that the action comes after controller, try: -# AdminRelatedPagesController -# Admin::RelatedPagesController <= found stop guessing -# -# admin/related_pages_controller <= underscored -# admin/related_pages_controller-list_all <= add action back on -# admin-related_pages_controller-list_all <= gsub / - DONE -# -# Now we're at a point where we can start guessing -# function_name = detect_function_name(function_name) -module Jets::Commands::Call - class Guesser - delegate :class_name, :method_name, :error_message, :function_name, - to: :delegate_guesser - - # Example of provided_function_name: - # admin/related_pages_controller-list_all - # admin-related-pages-controller-list-all - def initialize(provided_function_name) - @provided_function_name = provided_function_name - end - - def delegate_guesser - @delegate_guesser ||= if @provided_function_name =~ /[-_](controller|job|rule)/ - AutoloadGuesser.new(@provided_function_name) - else - AnonymousGuesser.new(@provided_function_name) - end - end - end -end diff --git a/lib/jets/commands/clean/base.rb b/lib/jets/commands/clean/base.rb deleted file mode 100644 index bf3731d1b..000000000 --- a/lib/jets/commands/clean/base.rb +++ /dev/null @@ -1,24 +0,0 @@ -module Jets::Commands::Clean - class Base - def initialize(options={}) - @options = options - end - - private - def say(message) - prefix = 'NOOP ' if @options[:noop] - puts "#{prefix}#{message}" unless @options[:mute] - end - - def are_you_sure?(message) - return true if @options[:yes] - - puts "Are you sure that you want to #{message}? (y/N)" - yes = $stdin.gets.strip - unless yes =~ /^y/ - puts "Phew that was close!" - exit 0 - end - end - end -end \ No newline at end of file diff --git a/lib/jets/commands/clean/build.rb b/lib/jets/commands/clean/build.rb deleted file mode 100644 index ecedd4404..000000000 --- a/lib/jets/commands/clean/build.rb +++ /dev/null @@ -1,13 +0,0 @@ -require 'fileutils' - -module Jets::Commands::Clean - class Build < Base - def clean - are_you_sure?("delete /tmp/jets") - - say "Removing /tmp/jets..." - FileUtils.rm_rf("/tmp/jets") unless @options[:noop] - say "Removed /tmp/jets" - end - end -end diff --git a/lib/jets/commands/clean/clean_command.rb b/lib/jets/commands/clean/clean_command.rb deleted file mode 100644 index f18413361..000000000 --- a/lib/jets/commands/clean/clean_command.rb +++ /dev/null @@ -1,15 +0,0 @@ -module Jets::Command - class CleanCommand < Base # :nodoc: - desc "log", "Cleans CloudWatch log groups assocated with app" - long_desc Help.text("clean/log") - def log - Jets::Commands::Clean::Log.new(options).clean - end - - desc "build", "Cleans jets build" - long_desc Help.text("clean/build") - def build - Jets::Commands::Clean::Build.new(options).clean - end - end -end diff --git a/lib/jets/commands/clean/log.rb b/lib/jets/commands/clean/log.rb deleted file mode 100644 index 74889d7b0..000000000 --- a/lib/jets/commands/clean/log.rb +++ /dev/null @@ -1,111 +0,0 @@ -# The thing that limits this implementation is that there needs to be at least -# one lambda function created from an internal jets function. Example: -# -# /aws/lambda/demo-dev-2-jets-preheat_job-warm -# /aws/lambda/demo-dev-2-jets-public_controller-show -# -# We're doing this because Jets.extra environments can create additional matching -# log groups and we don't want to overly-aggressively delete them. -# -# The `keep_prefixes(log_group_names)` method calcuates the log groups to keep. -module Jets::Commands::Clean - class Log < Base - extend Memoist - include Jets::AwsServices - - def clean - are_you_sure?("delete CloudWatch logs") - - say "Removing CloudWatch logs for #{prefix_guess}..." - log_groups.each do |g| - next if keep_log_group?(g.log_group_name) - delete_log_group(g.log_group_name) unless @options[:noop] - say "Removed log group: #{g.log_group_name}" - end - say "Removed CloudWatch logs for #{prefix_guess}" - end - - def delete_log_group(log_group_name) - logs.delete_log_group(log_group_name: log_group_name) - end - - def clean_deploys - groups = deploy_log_groups.sort_by do |g| - g.log_group_name - end - # Keep the last 2 recent log groups so we can see the deleted logic - groups = groups[0..-3] - groups.each do |g| - logs.delete_log_group(log_group_name: g.log_group_name) unless @options[:noop] - end - end - - def deploy_log_groups - log_groups.select do |g| - !keep_log_group?(g.log_group_name) && - g.log_group_name.include?('jets-base-path') - end - end - - private - def prefix_guess - Jets::Names.parent_stack_name - end - - def log_groups - groups, next_token = [], true - while next_token - next_token = nil if next_token == true # just at start the loop - resp = logs.describe_log_groups( - log_group_name_prefix: "/aws/lambda/#{prefix_guess}-", - next_token: next_token, - ) - groups += resp.log_groups - next_token = resp.next_token - end - groups - end - memoize :log_groups - - def log_group_names - log_groups.map(&:log_group_name) - end - - def all_prefixes(log_group_names) - log_prefixes(log_group_names) - end - memoize :all_prefixes - - # Check for the prefixes to keep. The slightly tricky thing to watch for is - # for the prefix matching addiitonal log groups that belong to other - # JETS_EXTRA=xxx created environments. - # - # We find and store the prefixes to keep so we don't over aggressively delete - # log groups. - def keep_prefixes(log_group_names) - names = log_group_names.reject do |name| - name =~ %r{/aws/lambda/#{prefix_guess}-jets} - end - log_prefixes(names) - end - memoize :keep_prefixes - - # Strips -jets.* from the full log group name to leave only the prefix behind - def log_prefixes(names) - names = names.select do |name| - name.match(Regexp.new("#{prefix_guess}-.*jets")) - end - names.map do |name| - name.sub(/-jets.*/,'') - end.uniq.sort - end - - # Check if it is safe to delete the log group - def keep_log_group?(log_group_name) - keep_prefixes = keep_prefixes(log_group_names) - !!keep_prefixes.detect do |keep_prefix| - log_group_name =~ Regexp.new(keep_prefix) - end - end - end -end diff --git a/lib/jets/commands/configure/configure_command.rb b/lib/jets/commands/configure/configure_command.rb deleted file mode 100644 index b4a600ab5..000000000 --- a/lib/jets/commands/configure/configure_command.rb +++ /dev/null @@ -1,9 +0,0 @@ -module Jets::Command - class ConfigureCommand < Base # :nodoc: - desc "configure [TOKEN]", "configure token and updates ~/.jets/config.yml" - long_desc Help.text(:configure) - def perform(token=nil) - Jets::Api::Config.instance.update_token(token) - end - end -end diff --git a/lib/jets/commands/console/console_command.rb b/lib/jets/commands/console/console_command.rb deleted file mode 100644 index 6cbcb71d4..000000000 --- a/lib/jets/commands/console/console_command.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -require "irb" -require "irb/completion" - -module Jets::Command - class ConsoleCommand < Base # :nodoc: - include EnvironmentArgument - - desc "console", "REPL console with Jets environment loaded" - long_desc Help.text(:console) - def perform - extract_environment_option_from_argument - require_application_and_environment! - Console.new(options).run - end - end - - class Console - attr_reader :environment - - def initialize(environment) - @environment = environment - end - - def run - puts Jets::Booter.message - - # Thanks: https://mutelight.org/bin-console - require "irb" - require "irb/completion" - - ARGV.clear # https://stackoverflow.com/questions/33070092/irb-start-not-starting/33136762 - IRB.start - end - end - -end diff --git a/lib/jets/commands/credentials/USAGE b/lib/jets/commands/credentials/USAGE deleted file mode 100644 index 932f37f89..000000000 --- a/lib/jets/commands/credentials/USAGE +++ /dev/null @@ -1,103 +0,0 @@ -## Storing Encrypted Credentials in Source Control - -The Jets `credentials` commands provide access to encrypted credentials, -so you can safely store access tokens, database passwords, and the like -safely inside the app without relying on a mess of ENVs. - -This also allows for atomic deploys: no need to coordinate key changes -to get everything working as the keys are shipped with the code. - -## Setup - -Applications after Jets 5 automatically have a basic credentials file generated -that just contains the secret_key_base used by MessageVerifiers/MessageEncryptors, like the ones -signing and encrypting cookies. - -For applications created prior to Jets 5, we'll automatically generate a new -credentials file in `config/credentials.yml.enc` the first time you run `bin/jets credentials:edit`. -If you didn't have a master key saved in `config/master.key`, that'll be created too. - -Don't lose this master key! Put it in a password manager your team can access. -Should you lose it no one, including you, will be able to access any encrypted -credentials. - -Don't commit the key! Add `config/master.key` to your source control's -ignore file. If you use Git, Jets handles this for you. - -Jets also looks for the master key in `ENV["JETS_MASTER_KEY"]`, if that's easier to manage. - -You could prepend that to your server's start command like this: - - JETS_MASTER_KEY="very-secret-and-secure" server.start - -## Set up Git to Diff Credentials - -Jets provides `bin/jets credentials:diff --enroll` to instruct Git to call -`bin/jets credentials:diff` when `git diff` is run on a credentials file. - -Running the command enrolls the project such that all credentials files use the -"jets_credentials" diff driver in .gitattributes. - -Additionally since Git requires the driver itself to be set up in a config file -that isn't tracked Jets automatically ensures it's configured when running -`credentials:edit`. - -Otherwise each co-worker would have to run enable manually, including on each new -repo clone. - -To disenroll from this feature, run `bin/jets credentials:diff --disenroll`. - -## Editing Credentials - -This will open a temporary file in `$EDITOR` with the decrypted contents to edit -the encrypted credentials. - -When the temporary file is next saved the contents are encrypted and written to -`config/credentials.yml.enc` while the file itself is destroyed to prevent credentials -from leaking. - -## Environment Specific Credentials - -The `credentials` command supports passing an `--environment` option to create an -environment specific override. That override will take precedence over the -global `config/credentials.yml.enc` file when running in that environment. So: - - bin/jets credentials:edit --environment development - -will create `config/credentials/development.yml.enc` with the corresponding -encryption key in `config/credentials/development.key` if the credentials file -doesn't exist. - -The encryption key can also be put in `ENV["JETS_MASTER_KEY"]`, which takes -precedence over the file encryption key. - -In addition to that, the default credentials lookup paths can be overridden through -`config.credentials.content_path` and `config.credentials.key_path`. - -## Editor wait - -For editors that fork and exit immediately, it's important to pass a wait flag, -otherwise the credentials will be saved immediately with no chance to edit. - - EDITOR="code --wait" jets credentials:edit - -## Accessing in App - -Let's say you have credentials like so: - - ❯ jets credentials:show - secret_key_base: somesecretvalue - foo: bar - -You can access them in the app like so: - - ❯ jets console - > Jets.application.credentials.secret_key_base - => "somesecretvalue" - > Jets.application.credentials.foo - => "bar" - > Jets.application.credentials.does_not_exist - => nil - -Remember, when you deploy this to Lambda you should set the `JETS_MASTER_KEY`. You set it with [Env Files](https://rubyonjets.com/docs/env-files/). Otherwise the `Jets.application.credentials.xx` values will return `nil`. - diff --git a/lib/jets/commands/credentials/credentials_command.rb b/lib/jets/commands/credentials/credentials_command.rb deleted file mode 100644 index 7c9d769f5..000000000 --- a/lib/jets/commands/credentials/credentials_command.rb +++ /dev/null @@ -1,139 +0,0 @@ -# frozen_string_literal: true - -require "pathname" -require "shellwords" -require "active_support" - -module Jets - module Command - class CredentialsCommand < Jets::Command::Base # :nodoc: - include Helpers::Editor - include EnvironmentArgument - - require_relative "credentials_command/diffing" - include Diffing - - self.environment_desc = "Uses credentials from config/credentials/:environment.yml.enc encrypted by config/credentials/:environment.key key" - - no_commands do - def help - say "Usage:\n #{self.class.banner}" - say "" - say self.class.desc - end - end - - def edit - extract_environment_option_from_argument(default_environment: nil) - require_application! - - ensure_editor_available(command: "bin/jets credentials:edit") || (return) - - ensure_encryption_key_has_been_added if credentials.key.nil? - ensure_credentials_have_been_added - ensure_diffing_driver_is_configured - - catch_editing_exceptions do - change_credentials_in_system_editor - end - - say "File encrypted and saved." - rescue ActiveSupport::MessageEncryptor::InvalidMessage - say "Couldn't decrypt #{content_path}. Perhaps you passed the wrong key?" - end - - def show - extract_environment_option_from_argument(default_environment: nil) - require_application! - - say credentials.read.presence || missing_credentials_message - end - - option :enroll, type: :boolean, default: false, - desc: "Enrolls project in credentials file diffing with `git diff`" - - option :disenroll, type: :boolean, default: false, - desc: "Disenrolls project from credentials file diffing" - - def diff(content_path = nil) - if @content_path = content_path - extract_environment_option_from_argument(default_environment: extract_environment_from_path(content_path)) - require_application! - - say credentials.read.presence || credentials.content_path.read - else - require_application! - disenroll_project_from_credentials_diffing if options[:disenroll] - enroll_project_in_credentials_diffing if options[:enroll] - end - rescue ActiveSupport::MessageEncryptor::InvalidMessage - say credentials.content_path.read - end - - private - def credentials - Jets.application.encrypted(content_path, key_path: key_path) - end - - def ensure_encryption_key_has_been_added - encryption_key_file_generator.add_key_file(key_path) - encryption_key_file_generator.ignore_key_file(key_path) - end - - def ensure_credentials_have_been_added - if options[:environment] - encrypted_file_generator.add_encrypted_file_silently(content_path, key_path) - else - credentials_generator.add_credentials_file_silently - end - end - - def change_credentials_in_system_editor - credentials.change do |tmp_path| - system(*Shellwords.split(ENV["EDITOR"]), tmp_path.to_s) - end - end - - def missing_credentials_message - if credentials.key.nil? - "Missing '#{key_path}' to decrypt credentials. See `bin/jets credentials:help`" - else - "File '#{content_path}' does not exist. Use `bin/jets credentials:edit` to change that." - end - end - - def content_path - @content_path ||= options[:environment] ? "config/credentials/#{options[:environment]}.yml.enc" : "config/credentials.yml.enc" - end - - def key_path - options[:environment] ? "config/credentials/#{options[:environment]}.key" : "config/master.key" - end - - def extract_environment_from_path(path) - available_environments.find { |env| path.include? env } if path.end_with?(".yml.enc") - end - - def encryption_key_file_generator - require "rails/generators" - require "rails/generators/rails/encryption_key_file/encryption_key_file_generator" - - Rails::Generators::EncryptionKeyFileGenerator.new - end - - def encrypted_file_generator - require "rails/generators" - require "rails/generators/rails/encrypted_file/encrypted_file_generator" - - Rails::Generators::EncryptedFileGenerator.new - end - - def credentials_generator - require "rails/generators" - require "rails/generators/rails/credentials/credentials_generator" - - Rails::Generators::CredentialsGenerator.new - end - end - end -end diff --git a/lib/jets/commands/credentials/credentials_command/diffing.rb b/lib/jets/commands/credentials/credentials_command/diffing.rb deleted file mode 100644 index e3d7f1dac..000000000 --- a/lib/jets/commands/credentials/credentials_command/diffing.rb +++ /dev/null @@ -1,51 +0,0 @@ -# frozen_string_literal: true - -module Jets::Command::CredentialsCommand::Diffing # :nodoc: - GITATTRIBUTES_ENTRY = <<~END - config/credentials/*.yml.enc diff=jets_credentials - config/credentials.yml.enc diff=jets_credentials - END - - def enroll_project_in_credentials_diffing - if enrolled_in_credentials_diffing? - say "Project is already enrolled in credentials file diffing." - else - gitattributes.write(GITATTRIBUTES_ENTRY, mode: "a") - - say "Enrolled project in credentials file diffing!" - say "Jets ensures the jets_credentials diff driver is set when running `credentials:edit`. See `credentials:help` for more." - end - end - - def disenroll_project_from_credentials_diffing - if enrolled_in_credentials_diffing? - gitattributes.write(gitattributes.read.gsub(GITATTRIBUTES_ENTRY, "")) - gitattributes.delete if gitattributes.empty? - - say "Disenrolled project from credentials file diffing!" - else - say "Project is not enrolled in credentials file diffing." - end - end - - def ensure_diffing_driver_is_configured - configure_diffing_driver if enrolled_in_credentials_diffing? && !diffing_driver_configured? - end - - private - def enrolled_in_credentials_diffing? - gitattributes.file? && gitattributes.read.include?(GITATTRIBUTES_ENTRY) - end - - def diffing_driver_configured? - system "git config --get diff.jets_credentials.textconv", out: File::NULL - end - - def configure_diffing_driver - system "git config diff.jets_credentials.textconv 'bin/jets credentials:diff'" - end - - def gitattributes - Jets.root.join(".gitattributes") - end -end diff --git a/lib/jets/commands/db/system/change/change_command.rb b/lib/jets/commands/db/system/change/change_command.rb deleted file mode 100644 index f8f206468..000000000 --- a/lib/jets/commands/db/system/change/change_command.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -module Jets - module Command - module Db - module System - class ChangeCommand < Base # :nodoc: - class_option :to, desc: "The database system to switch to." - - def initialize(positional_args, option_args, *) - @argv = positional_args + option_args - super - end - - def perform - require "rails/generators" - require "rails/generators/rails/db/system/change/change_generator" - Rails::Generators::Db::System::ChangeGenerator.start(@argv) - end - end - end - end - end -end diff --git a/lib/jets/commands/dbconsole/dbconsole_command.rb b/lib/jets/commands/dbconsole/dbconsole_command.rb deleted file mode 100644 index 1a016385b..000000000 --- a/lib/jets/commands/dbconsole/dbconsole_command.rb +++ /dev/null @@ -1,184 +0,0 @@ -# frozen_string_literal: true - -require "active_support/core_ext/string/filters" -require "active_support/deprecation" - -module Jets - class DBConsole - def self.start(*args) - new(*args).start - end - - def initialize(options = {}) - @options = options - end - - def start - ENV["JETS_ENV"] ||= @options[:environment] || environment - config = db_config.configuration_hash - - case db_config.adapter - when /^(jdbc)?mysql/ - args = { - host: "--host", - port: "--port", - socket: "--socket", - username: "--user", - encoding: "--default-character-set", - sslca: "--ssl-ca", - sslcert: "--ssl-cert", - sslcapath: "--ssl-capath", - sslcipher: "--ssl-cipher", - sslkey: "--ssl-key" - }.filter_map { |opt, arg| "#{arg}=#{config[opt]}" if config[opt] } - - if config[:password] && @options[:include_password] - args << "--password=#{config[:password]}" - elsif config[:password] && !config[:password].to_s.empty? - args << "-p" - end - - args << db_config.database - - find_cmd_and_exec(["mysql", "mysql5"], *args) - - when /^postgres|^postgis/ - ENV["PGUSER"] = config[:username] if config[:username] - ENV["PGHOST"] = config[:host] if config[:host] - ENV["PGPORT"] = config[:port].to_s if config[:port] - ENV["PGPASSWORD"] = config[:password].to_s if config[:password] && @options[:include_password] - ENV["PGSSLMODE"] = config[:sslmode].to_s if config[:sslmode] - ENV["PGSSLCERT"] = config[:sslcert].to_s if config[:sslcert] - ENV["PGSSLKEY"] = config[:sslkey].to_s if config[:sslkey] - ENV["PGSSLROOTCERT"] = config[:sslrootcert].to_s if config[:sslrootcert] - find_cmd_and_exec("psql", db_config.database) - - when "sqlite3" - args = [] - - args << "-#{@options[:mode]}" if @options[:mode] - args << "-header" if @options[:header] - args << File.expand_path(db_config.database, Jets.respond_to?(:root) ? Jets.root : nil) - - find_cmd_and_exec("sqlite3", *args) - - when "oracle", "oracle_enhanced" - logon = "" - - if config[:username] - logon = config[:username].dup - logon << "/#{config[:password]}" if config[:password] && @options[:include_password] - logon << "@#{db_config.database}" if db_config.database - end - - find_cmd_and_exec("sqlplus", logon) - - when "sqlserver" - args = [] - - args += ["-d", "#{db_config.database}"] if db_config.database - args += ["-U", "#{config[:username]}"] if config[:username] - args += ["-P", "#{config[:password]}"] if config[:password] - - if config[:host] - host_arg = +"tcp:#{config[:host]}" - host_arg << ",#{config[:port]}" if config[:port] - args += ["-S", host_arg] - end - - find_cmd_and_exec("sqlcmd", *args) - - else - abort "Unknown command-line client for #{db_config.database}." - end - end - - def db_config - return @db_config if defined?(@db_config) - - # If the user provided a database, use that. Otherwise find - # the first config in the database.yml - if database - @db_config = configurations.configs_for(env_name: environment, name: database, include_hidden: true) - else - @db_config = configurations.find_db_config(environment) - end - - unless @db_config - missing_db = database ? "'#{database}' database is not" : "No databases are" - raise ActiveRecord::AdapterNotSpecified, - "#{missing_db} configured for '#{environment}'. Available configuration: #{configurations.inspect}" - end - - @db_config - end - - def environment - Jets.respond_to?(:env) ? Jets.env : Jets::Command.environment - end - - def database - @options[:database] - end - - private - def configurations # :doc: - # require APP_PATH - ActiveRecord::Base.configurations = Jets.application.config.database_configuration - ActiveRecord::Base.configurations - end - - def find_cmd_and_exec(commands, *args) # :doc: - commands = Array(commands) - - dirs_on_path = ENV["PATH"].to_s.split(File::PATH_SEPARATOR) - unless (ext = RbConfig::CONFIG["EXEEXT"]).empty? - commands = commands.map { |cmd| "#{cmd}#{ext}" } - end - - full_path_command = nil - found = commands.detect do |cmd| - dirs_on_path.detect do |path| - full_path_command = File.join(path, cmd) - begin - stat = File.stat(full_path_command) - rescue SystemCallError - else - stat.file? && stat.executable? - end - end - end - - if found - exec full_path_command, *args - else - abort("Couldn't find database client: #{commands.join(', ')}. Check your $PATH and try again.") - end - end - end - - module Command - class DbconsoleCommand < Base # :nodoc: - include EnvironmentArgument - - class_option :include_password, aliases: "-p", type: :boolean, - desc: "Automatically provide the password from database.yml" - - class_option :mode, enum: %w( html list line column ), type: :string, - desc: "Automatically put the sqlite3 database in the specified mode (html, list, line, column)." - - class_option :header, type: :boolean - - class_option :database, aliases: "--db", type: :string, - desc: "Specifies the database to use." - - desc "dbconsole", "Starts DB REPL console" - long_desc Help.text(:dbconsole) - def perform - extract_environment_option_from_argument - require_application_and_environment! - Jets::DBConsole.start(options) - end - end - end -end diff --git a/lib/jets/commands/degenerate/degenerate_command.rb b/lib/jets/commands/degenerate/degenerate_command.rb deleted file mode 100644 index d091564c8..000000000 --- a/lib/jets/commands/degenerate/degenerate_command.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -require "jets/generators" -require "jets/commands/generate/generate_command" - -module Jets - module Command - class DegenerateCommand < GenerateCommand # :nodoc: - desc "Degenerate", "Opposite of generate. Removes the generated code." - long_desc Help.text(:degenerate) - def perform(*) - super - end - - private - def behavior - :revoke - end - end - end -end diff --git a/lib/jets/commands/delete/delete_command.rb b/lib/jets/commands/delete/delete_command.rb deleted file mode 100644 index a4464d079..000000000 --- a/lib/jets/commands/delete/delete_command.rb +++ /dev/null @@ -1,154 +0,0 @@ -module Jets::Command - class DeleteCommand < Base # :nodoc: - desc "delete", "Delete the Jets project and all its resources" - long_desc Help.text(:delete) - option :yes, aliases: %w[y], type: :boolean, desc: "Skip are you sure prompt." - option :wait, type: :boolean, default: true, desc: "Wait for stack deletion to complete." - def perform - Delete.new(options).run - end - end - - class Delete - include Jets::AwsServices - include AwsHelpers - - def initialize(options={}) - @options = options - end - - def run - puts("Deleting project...") - return if @options[:noop] - - are_you_sure? - confirm_project_exists - - # Must first remove all objects from s3 bucket in order to delete stack - puts "First, deleting objects in s3 bucket #{s3_bucket_name}" if s3_bucket_name - empty_s3_bucket - - stack_in_progress?(parent_stack_name) - - cfn.delete_stack(stack_name: parent_stack_name) - puts "Deleting #{Jets.project_namespace.color(:green)}..." - - stack = find_stack(parent_stack_name) - if @options[:wait] - wait_for_stack - end - Jets::Cfn::Deployment.new(stack_name: stack.stack_id).delete - - delete_logs - puts "Project #{Jets.project_namespace.color(:green)} deleted!" - end - - def wait_for_stack - status = Jets::Cfn::Status.new - start_time = Time.now - status.wait - took = Time.now - start_time - puts "Time took for deletion: #{status.pretty_time(took).color(:green)}." - end - - def delete_logs - puts "Deleting CloudWatch logs" - log = Jets::Commands::Clean::Log.new(mute: true, yes: true) - log.clean - end - - def confirm_project_exists - stack = find_stack(parent_stack_name) - return if stack - puts "ERROR: Stack #{parent_stack_name} does not exist".color(:red) - exit 1 - end - - def empty_s3_bucket - return unless s3_bucket_name # Happens when minimal stack fails to build - return unless bucket_exists?(s3_bucket_name) - - resp = s3.list_objects(bucket: s3_bucket_name) - if resp.contents.size > 0 - # IE: objects = [{key: "objectkey1"}, {key: "objectkey2"}] - objects = resp.contents.map { |item| {key: item.key} } - s3.delete_objects( - bucket: s3_bucket_name, - delete: { - objects: objects, - quiet: false, - } - ) - empty_s3_bucket # keep deleting objects until bucket is empty - end - end - - def s3_bucket_name - return @s3_bucket_name if defined?(@s3_bucket_name) - - resp = cfn.describe_stacks(stack_name: parent_stack_name) - outputs = resp.stacks[0].outputs - if outputs.empty? - @s3_bucket_name = false - else - @s3_bucket_name = outputs.find {|o| o.output_key == 'S3Bucket'}.output_value - end - end - - # Thanks: https://docs.aws.amazon.com/sdk-for-ruby/v3/developer-guide/s3-example-does-bucket-exist.html - def bucket_exists?(bucket_name) - bucket_exists = false - begin - resp = s3.head_bucket(bucket: bucket_name, use_accelerate_endpoint: false) - bucket_exists = true - rescue - end - bucket_exists - end - - def parent_stack_name - Jets::Names.parent_stack_name - end - - def are_you_sure? - if @options[:yes] - sure = 'y' - else - puts "Are you sure you want to want to delete the #{Jets.project_namespace.color(:green)} project? (y/N)" - sure = $stdin.gets - end - - unless sure =~ /^y/ - puts "Phew! Jets #{Jets.project_namespace.color(:green)} project was not deleted." - exit 0 - end - end - - # All CloudFormation states listed here: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-describing-stacks.html - # - # Returns resp so we can use it to grab data about the stack without calling api again. - def check_deleteable_status - return true if !stack_exists?(@parent_stack_name) - - # Assumes stack exists - resp = cfn.describe_stacks(stack_name: @parent_stack_name) - status = resp.stacks[0].stack_status - - return true if status == 'ROLLBACK_COMPLETE' - - if status =~ /_IN_PROGRESS$/ - puts "The '#{@parent_stack_name}' stack status is #{status}. " \ - "It is not in an updateable status. Please wait until the stack is ready and try again.".color(:red) - exit 0 - elsif resp.stacks[0].outputs.empty? - # This Happens when the miminal stack fails at the very beginning. - # There is no s3 bucket at all. User should delete the stack. - puts "The minimal stack failed to create. Please delete the stack first and try again. " \ - "You can delete the CloudFormation stack or use the `jets delete` command" - exit 0 - else - true - end - end - end -end diff --git a/lib/jets/commands/deploy/deploy_command.rb b/lib/jets/commands/deploy/deploy_command.rb deleted file mode 100644 index 81f414e27..000000000 --- a/lib/jets/commands/deploy/deploy_command.rb +++ /dev/null @@ -1,118 +0,0 @@ -module Jets::Command - class DeployCommand < Base # :nodoc: - include EnvironmentArgument - - desc "deploy", "Builds and deploys project to AWS Lambda" - long_desc Help.text(:deploy) - option :message, aliases: :m, desc: "Custom message to use for the deployment message" - def perform - extract_environment_option_from_argument - require_application_and_environment! - - stack = Jets.project_namespace.color(:green) - puts "Deploying stack #{stack} ..." - return if @options[:noop] - - check_dev_mode - validate_routes! - - # Delete existing rollback stack from previous bad minimal deploy - delete_minimal_stack if minimal_rollback_complete? - exit_unless_updateable! # Stack could be in a weird rollback state or in progress state - - if first_run? - ship(stack_type: :minimal) - end - - # Build code after the minimal stack because need s3 bucket for assets on_aws? and s3_base_url logic - # TODO: possible deploy hook point: before_build - Jets::Builders::CodeBuilder.new.build - - # TODO: possible deploy hook point: before_ship - create_s3_event_buckets - ship(stack_type: :full, s3_bucket: Jets.s3_bucket) - end - - private - def create_s3_event_buckets - buckets = Jets::Job::Base._s3_events.keys - buckets.each do |bucket| - Jets::AwsServices::S3Bucket.ensure_exists(bucket) - end - end - - def delete_minimal_stack - puts "Existing stack is in ROLLBACK_COMPLETE state from a previous failed minimal deploy. Deleting stack and continuing." - cfn.delete_stack(stack_name: stack_name) - cfn_status.wait - cfn_status.reset - end - - def check_dev_mode - if File.exist?("#{Jets.root}/dev.mode") && !ENV['JETS_SKIP_DEV_MODE_CHECK'] - puts "The dev.mode file exists. Please removed it and run bundle update before you deploy.".color(:red) - exit 1 - end - end - - def validate_routes! - valid = Jets::Router.validate_routes! - return if valid - - puts "Deploy fail: The jets application contain invalid routes.".color(:red) - exit 1 - end - - def ship(stack_options) - options = @options.merge(stack_options) # includes stack_type - Jets::Cfn::Builder.new(options).build - Jets::Cfn::Ship.new(options).run - end - - def cfn_status - @cfn_status ||= Jets::Cfn::Status.new(stack_name) - end - - def stack_name - Jets::Names.parent_stack_name - end - - # Checks for a few things before deciding to delete the parent stack - # - # * Parent stack status status is ROLLBACK_COMPLETE - # * Parent resources are in the DELETE_COMPLETE state - # - def minimal_rollback_complete? - stack = find_stack(stack_name) - return false unless stack - - return false unless stack.stack_status == 'ROLLBACK_COMPLETE' - - # Finally check if all the minimal resources in the parent template have been deleted - resp = cfn.describe_stack_resources(stack_name: stack_name) - resource_statuses = resp.stack_resources.map(&:resource_status).uniq - resource_statuses == ['DELETE_COMPLETE'] - end - - # All CloudFormation states listed here: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-describing-stacks.html - def exit_unless_updateable! - return if ENV['JETS_FORCE_UPDATEABLE'] # useful for debugging if stack stack updating - - stack_name = Jets::Names.parent_stack_name - exists = stack_exists?(stack_name) - return unless exists # continue because stack could be updating - - stack = cfn.describe_stacks(stack_name: stack_name).stacks.first - status = stack["stack_status"] - if status =~ /^ROLLBACK_/ || - status =~ /_IN_PROGRESS$/ - region = `aws configure get region`.strip rescue "us-east-1" - url = "https://console.aws.amazon.com/cloudformation/home?region=#{region}#/stacks" - puts "The parent stack of the #{Jets.project_name.color(:green)} project is not in an updateable state." - puts "Stack name #{stack_name.color(:yellow)} status #{stack["stack_status"].color(:yellow)}" - puts "Here's the CloudFormation url to check for more details #{url}" - exit 1 - end - end - end -end diff --git a/lib/jets/commands/dev/dev_command.rb b/lib/jets/commands/dev/dev_command.rb deleted file mode 100644 index 48f88b0ab..000000000 --- a/lib/jets/commands/dev/dev_command.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -require "jets/dev_caching" - -module Jets - module Command - class DevCommand < Base # :nodoc: - no_commands do - def help - say "jets dev:cache # Toggle development mode caching on/off." - end - end - - def cache - Jets::DevCaching.enable_by_file - end - end - end -end diff --git a/lib/jets/commands/dotenv/dotenv_command.rb b/lib/jets/commands/dotenv/dotenv_command.rb deleted file mode 100644 index e541b2807..000000000 --- a/lib/jets/commands/dotenv/dotenv_command.rb +++ /dev/null @@ -1,9 +0,0 @@ -module Jets::Command - class DotenvCommand < Base # :nodoc: - desc "show", "Shows evaulated dotenv values" - long_desc Help.text('dotenv:show') - def show - Jets::Dotenv::Show.list - end - end -end diff --git a/lib/jets/commands/encrypted/USAGE b/lib/jets/commands/encrypted/USAGE deleted file mode 100644 index 7cde12df8..000000000 --- a/lib/jets/commands/encrypted/USAGE +++ /dev/null @@ -1,46 +0,0 @@ -## Storing Encrypted Files in Source Control - -The Jets `encrypted` commands provide access to encrypted files or configurations. -See the `Jets.application.encrypted` documentation for using them in your app. - -This is a more generalized concept of `jets credentials`. - -## Encryption Keys - -By default, Jets looks for the encryption key in `config/master.key` or -`ENV["JETS_MASTER_KEY"]`, but that lookup can be overridden with `--key`: - - jets encrypted:edit config/encrypted_file.yml.enc --key config/encrypted_file.key - -Don't commit the key! Add it to your source control's ignore file. If you use -Git, Jets handles this for you. - -## Editing Files - -To edit or create an encrypted file use: - - jets encrypted:edit config/encrypted_file.yml.enc - -This opens a temporary file in `$EDITOR` with the decrypted contents for editing. - -## Viewing Files - -To print the decrypted contents of an encrypted file use: - - jets encrypted:show config/encrypted_file.yml.enc - -## Access in App - -Let's say you have encrypted like so: - - ❯ jets encrypted:show config/encrypted_file.yml.enc - cloud: - foo: bar - -You can access them in the app like so: - - $ jets console - > file = ActiveSupport::EncryptedFile.new(content_path: "./config/encrypted_file.yml.enc", key_path: "./config/master.key", env_key: Jets.env, raise_if_missing_key: true) - > YAML.load(file.read) - => {"cloud"=>{"foo"=>"bar"}} - > \ No newline at end of file diff --git a/lib/jets/commands/encrypted/encrypted_command.rb b/lib/jets/commands/encrypted/encrypted_command.rb deleted file mode 100644 index 1ff968b69..000000000 --- a/lib/jets/commands/encrypted/encrypted_command.rb +++ /dev/null @@ -1,87 +0,0 @@ -# frozen_string_literal: true - -require "pathname" -require "active_support" -require "jets/command/helpers/editor" - -module Jets - module Command - class EncryptedCommand < Jets::Command::Base # :nodoc: - include Helpers::Editor - - class_option :key, aliases: "-k", type: :string, - default: "config/master.key", desc: "The Jets.root relative path to the encryption key" - - no_commands do - def help - say "Usage:\n #{self.class.banner}" - say "" - say self.class.desc - end - end - - def edit(file_path) - require_application! - encrypted = Jets.application.encrypted(file_path, key_path: options[:key]) - - ensure_editor_available(command: "bin/jets encrypted:edit") || (return) - ensure_encryption_key_has_been_added(options[:key]) if encrypted.key.nil? - ensure_encrypted_file_has_been_added(file_path, options[:key]) - - catch_editing_exceptions do - change_encrypted_file_in_system_editor(file_path, options[:key]) - end - - say "File encrypted and saved." - rescue ActiveSupport::MessageEncryptor::InvalidMessage - say "Couldn't decrypt #{file_path}. Perhaps you passed the wrong key?" - end - - def show(file_path) - require_application! - encrypted = Jets.application.encrypted(file_path, key_path: options[:key]) - - say encrypted.read.presence || missing_encrypted_message(key: encrypted.key, key_path: options[:key], file_path: file_path) - end - - private - def ensure_encryption_key_has_been_added(key_path) - encryption_key_file_generator.add_key_file(key_path) - encryption_key_file_generator.ignore_key_file(key_path) - end - - def ensure_encrypted_file_has_been_added(file_path, key_path) - encrypted_file_generator.add_encrypted_file_silently(file_path, key_path) - end - - def change_encrypted_file_in_system_editor(file_path, key_path) - Jets.application.encrypted(file_path, key_path: key_path).change do |tmp_path| - system("#{ENV["EDITOR"]} #{tmp_path}") - end - end - - - def encryption_key_file_generator - require "rails/generators" - require "rails/generators/rails/encryption_key_file/encryption_key_file_generator" - - Rails::Generators::EncryptionKeyFileGenerator.new - end - - def encrypted_file_generator - require "rails/generators" - require "rails/generators/rails/encrypted_file/encrypted_file_generator" - - Rails::Generators::EncryptedFileGenerator.new - end - - def missing_encrypted_message(key:, key_path:, file_path:) - if key.nil? - "Missing '#{key_path}' to decrypt data. See `bin/jets encrypted:help`" - else - "File '#{file_path}' does not exist. Use `bin/jets encrypted:edit #{file_path}` to change that." - end - end - end - end -end diff --git a/lib/jets/commands/gems/gems_command.rb b/lib/jets/commands/gems/gems_command.rb deleted file mode 100644 index 489370beb..000000000 --- a/lib/jets/commands/gems/gems_command.rb +++ /dev/null @@ -1,14 +0,0 @@ -module Jets::Command - class GemsCommand < Base - desc "check", "Check if precompiled Lambda gems are available" - long_desc Help.text("gems:check") - option :verbose, type: :boolean, desc: "Verbose mode" - def check - Jets.boot - check = Jets::Api::Gems::Check.new(@options) - check.run! # exits early if missing gems found - # If reach here, means all gems are ok. - puts "Congrats! All gems are available in as precompiled Lambda gems 👍" - end - end -end diff --git a/lib/jets/commands/generate/generate_command.rb b/lib/jets/commands/generate/generate_command.rb deleted file mode 100644 index b4943a447..000000000 --- a/lib/jets/commands/generate/generate_command.rb +++ /dev/null @@ -1,95 +0,0 @@ -# frozen_string_literal: true - -module Jets::Command - class GenerateCommand < Base # :nodoc: - no_commands do - # jets generate -h - # Note: Other help flags like jets generate scaffold -h will be handled by - # invoking the generator and letting it handle the help flag. - def help - require_application_and_environment! - load_generators - all_commands_help self.class.command_name - end - end - - desc "Generate", "Generate code" - long_desc Help.text(:generate) - def perform(*) - # require lazily so that Rails constant is only defined within generators - require "jets/generators" - - generator = args.shift - return help unless generator - - require_application_and_environment! - load_generators # engine.rb load_generators => Jets.application.load_generators - - ARGV.replace(args) # set up ARGV for third-party libraries - - Jets::Generators.invoke generator, args, behavior: behavior, destination_root: Jets::Command.root - end - - private - def behavior - :invoke - end - - def all_commands_help(command = "generate") - puts "Usage: jets #{command} GENERATOR [args] [options]" - puts - puts "General options:" - puts " -h, [--help] # Print generator's options and usage" - puts " -p, [--pretend] # Run but do not make any changes" - puts " -f, [--force] # Overwrite files that already exist" - puts " -s, [--skip] # Skip files that already exist" - puts " -q, [--quiet] # Suppress status output" - puts - puts "Please choose a generator below." - puts - - Rails::Generators.print_generators - end - end -end - -# Decorate the Group#help method to replace rails with jets. -# -# Note: Initially tried only decorating when a help flag like `-h` is detected -# but some commands will trigger help output without the flag. For example: -# jets generate job -# The command requires a name -# jets generate job NAME -# -# The underlying Rails::Generators.invoke does this -# if klass = find_by_namespace(names.pop, names.any? && names.join(":")) -# args << "--help" if args.empty? && klass.arguments.any?(&:required?) -# and is able to detect that the name is missing and triggers the help output. -# -# Jets does not have this logic yet. So we'll decorate the help method to -# at the Thor::Group#help level. -require "thor" -class Thor::Group - module ReplaceHelpOutputWithJets - def help(shell) - out = capture_stdout_for_help do - super # invoke to get the help output - end - puts out.gsub('rails','jets').gsub('Rails','Jets') - end - - def capture_stdout_for_help - stdout_old = $stdout - io = StringIO.new - $stdout = io - yield - $stdout = stdout_old - io.string - end - end - - class << self - # The help method is defined in the Thor::Group class. We are decorating it - prepend ReplaceHelpOutputWithJets - end -end diff --git a/lib/jets/commands/help/USAGE b/lib/jets/commands/help/USAGE deleted file mode 100644 index c3e3f319b..000000000 --- a/lib/jets/commands/help/USAGE +++ /dev/null @@ -1,20 +0,0 @@ -Common Jets commands: - generate Generate new code (short-cut alias: "g") - console Start the Jets console (short-cut alias: "c") - server Start the Jets server (short-cut alias: "s") - deploy Deploy application to Lambda - logs View Lambda function logs -<% unless engine? -%> - new Create a new Jets application. "jets new demo" creates a - new application called Demo in "./demo" -<% end -%> - -Jets Pro commands: - projects List projects - stacks List stacks - releases List releases - rollback Rollback to a previous release - -All commands can be run with -h (or --help) for more information. -In addition to those commands, there are: - diff --git a/lib/jets/commands/help/help_command.rb b/lib/jets/commands/help/help_command.rb deleted file mode 100644 index 4aca3efce..000000000 --- a/lib/jets/commands/help/help_command.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -module Jets - module Command - class HelpCommand < Base # :nodoc: - hide_command! - - def help(*) - say self.class.desc - - Jets::Command.print_commands - end - end - end -end diff --git a/lib/jets/commands/initializers/initializers_command.rb b/lib/jets/commands/initializers/initializers_command.rb deleted file mode 100644 index 37e5fd0f2..000000000 --- a/lib/jets/commands/initializers/initializers_command.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -module Jets - module Command - class InitializersCommand < Base # :nodoc: - include EnvironmentArgument - - desc "initializers", "Print out all defined initializers in the order they are invoked by Jets." - def perform - extract_environment_option_from_argument - require_application_and_environment! - - Jets.application.initializers.tsort_each do |initializer| - say "#{initializer.context_class}.#{initializer.name}" - end - end - end - end -end diff --git a/lib/jets/commands/logs/logs_command.rb b/lib/jets/commands/logs/logs_command.rb deleted file mode 100644 index 070d3d24c..000000000 --- a/lib/jets/commands/logs/logs_command.rb +++ /dev/null @@ -1,45 +0,0 @@ -require "aws-logs" - -module Jets::Command - class LogsCommand < Base # :nodoc: - include Jets::AwsServices - - option :since, desc: "From what time to begin displaying logs. By default, logs will be displayed starting from 10m in the past. The value provided can be an ISO 8601 timestamp or a relative time. Examples: 10m 2d 2w" - option :follow, aliases: :f, default: false, type: :boolean, desc: " Whether to continuously poll for new logs. To exit from this mode, use Control-C." - option :format, default: "simple", desc: "The format to display the logs. IE: detailed or short. With detailed, the log stream name is also shown." - option :filter_pattern, desc: "The filter pattern to use. If not provided, all the events are matched" - option :log_group_name, aliases: :n, desc: "The log group name. By default, it is /aws/lambda/#{Jets.project_namespace}-controller" - option :refresh_rate, default: 1, type: :numeric, desc: "How often to refresh the logs in seconds." - option :wait_exists, default: true, type: :boolean, desc: "Whether to wait until the log group exists. By default, it will wait." - long_desc Help.text(:logs) - def perform - show_logs - end - - private - def show_logs - options = @options.dup # so it can be modified - options[:log_group_name] = log_group_name - options[:since] ||= "10m" # by default, start search 10m in the past - options[:wait_exists_retries] = 12 # 12 * 5 = 60 seconds - - verb = options[:follow] ? "Tailing" : "Showing" - $stderr.puts "#{verb} logs for #{options[:log_group_name]}" - - tail = AwsLogs::Tail.new(options) - tail.run - end - - def log_group_name - default = "/aws/lambda/#{Jets.project_namespace}-controller" - return default unless @options[:log_group_name] - - if @options[:log_group_name].include?("aws/lambda") || @options[:log_group_name].include?(Jets.project_namespace) - @options[:log_group_name] - else - # IE: hard_job-dig => /aws/lambda/demo-dev-hard_job-dig - "/aws/lambda/#{Jets.project_namespace}-#{@options[:log_group_name]}" - end - end - end -end diff --git a/lib/jets/commands/new/new_command.rb b/lib/jets/commands/new/new_command.rb deleted file mode 100644 index f8c5ee1b3..000000000 --- a/lib/jets/commands/new/new_command.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -module Jets - module Command - class NewCommand < Base # :nodoc: - no_commands do - def help - Jets::Command.invoke :application, [ "--help" ] - end - end - - def perform(*) - say "Can't initialize a new Jets application within the directory of another, please change to a non-Jets directory first.\n" - say "Type 'jets' for help." - exit 1 - end - end - end -end diff --git a/lib/jets/commands/notes/notes_command.rb b/lib/jets/commands/notes/notes_command.rb deleted file mode 100644 index 86eec2c51..000000000 --- a/lib/jets/commands/notes/notes_command.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -module Jets - module Command - class NotesCommand < Base # :nodoc: - class_option :annotations, aliases: "-a", desc: "Filter by specific annotations, e.g. Foobar TODO", type: :array - - def perform(*) - require_application_and_environment! - - # lazy require so Rails const is only defined when using notes - require "rails/source_annotation_extractor" - - display_annotations - end - - private - def display_annotations - annotations = options[:annotations] || Rails::SourceAnnotationExtractor::Annotation.tags - tag = (annotations.length > 1) - - Rails::SourceAnnotationExtractor.enumerate annotations.join("|"), tag: tag, dirs: directories - end - - def directories - Rails::SourceAnnotationExtractor::Annotation.directories - end - end - end -end diff --git a/lib/jets/commands/plugin/plugin_command.rb b/lib/jets/commands/plugin/plugin_command.rb deleted file mode 100644 index e93ed88c6..000000000 --- a/lib/jets/commands/plugin/plugin_command.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true - -module Jets - module Command - class PluginCommand < Base # :nodoc: - hide_command! - - def help - run_plugin_generator %w( --help ) - end - - def self.banner(*) # :nodoc: - "#{executable} new [options]" - end - - class_option :rc, type: :string, default: File.join("~", ".jetsrc"), - desc: "Initialize the plugin command with previous defaults. Uses .jetsrc in your home directory by default." - - class_option :no_rc, desc: "Skip evaluating .jetsrc." - - def perform(type = nil, *plugin_args) - plugin_args << "--help" unless type == "new" - - unless options.key?("no_rc") # Thor's not so indifferent access hash. - jetsrc = File.expand_path(options[:rc]) - - if File.exist?(jetsrc) - extra_args = File.read(jetsrc).split(/\n+/).flat_map(&:split) - say "Using #{extra_args.join(" ")} from #{jetsrc}" - plugin_args.insert(1, *extra_args) - end - end - - run_plugin_generator plugin_args - end - - private - def run_plugin_generator(plugin_args) - require "jets/generators" - require "jets/generators/jets/plugin/plugin_generator" - Jets::Generators::PluginGenerator.start plugin_args - end - end - end -end diff --git a/lib/jets/commands/projects/projects_command.rb b/lib/jets/commands/projects/projects_command.rb deleted file mode 100644 index c2acef19a..000000000 --- a/lib/jets/commands/projects/projects_command.rb +++ /dev/null @@ -1,30 +0,0 @@ -module Jets::Command - class ProjectsCommand < Base # :nodoc: - desc "projects", "List deployed projects" - long_desc Help.text(:projects) - paging_options.call - def perform - no_token_exit! - resp = Jets::Api::Project.list(paging_params) - check_for_error_message!(resp) - - data = resp["data"] - if data.empty? - $stderr.puts "No projects deployed yet." - else - show_items(data) - end - rescue Jets::Api::RequestError => e - puts "WARNING: Unable to list projects. #{e.class}: #{e.message}" - end - - private - def show_items(items) - presenter = CliFormat::Presenter.new - presenter.header = ["Name"] - items.each do |item| - puts item["name"] - end - end - end -end diff --git a/lib/jets/commands/rake/rake_command.rb b/lib/jets/commands/rake/rake_command.rb deleted file mode 100644 index ea1d20962..000000000 --- a/lib/jets/commands/rake/rake_command.rb +++ /dev/null @@ -1,59 +0,0 @@ -# frozen_string_literal: true - -module Jets - module Command - class RakeCommand < Base # :nodoc: - extend Jets::Command::Actions - - namespace "rake" - - class << self - def printing_commands - formatted_rake_tasks.map(&:first) - end - - def perform(task, args, config) - Jets.boot - require_rake - # Wonder if there's a better way to do this. - # We do not use the rask_task block to require dummy/rails - # because don't want Rails const to be defined for other rake tasks - # like jets:assets:precompile. We want the Rails const to be defined - # lazily at runtime only for db rake tasks. - require "jets/overrides/dummy/rails" if task.include?("db:") - - Rake.with_application do |rake| - rake.init("jets", [task, *args]) - rake.load_rakefile - if Jets.respond_to?(:root) - rake.options.suppress_backtrace_pattern = /\A(?!#{Regexp.quote(Jets.root.to_s)})/ - end - rake.standard_exception_handling { rake.top_level } - end - end - - private - def rake_tasks - require_rake - - return @rake_tasks if defined?(@rake_tasks) - - require_application! - - Rake::TaskManager.record_task_metadata = true - Rake.application.instance_variable_set(:@name, "jets") - load_tasks - @rake_tasks = Rake.application.tasks.select(&:comment) - end - - def formatted_rake_tasks - rake_tasks.map { |t| [ t.name_with_args, t.comment ] } - end - - def require_rake - require "rake" # Defer booting Rake until we know it's needed. - end - end - end - end -end diff --git a/lib/jets/commands/releases/releases_command.rb b/lib/jets/commands/releases/releases_command.rb deleted file mode 100644 index 56c5927bd..000000000 --- a/lib/jets/commands/releases/releases_command.rb +++ /dev/null @@ -1,124 +0,0 @@ -module Jets::Command - class ReleasesCommand < Base # :nodoc: - desc "releases", "List releases" - long_desc Help.text(:releases) - paging_options(order: 'desc').call - option :sha, desc: "Show release git sha" - def perform - Release.new(options.merge(paging_params)).list - end - - desc "releases:info", "View detailed information for a release" - long_desc Help.text(:info) - def info(version=nil) - if version.nil? - puts "ERROR: Must provide a version".color(:red) - puts <<~EOL - Example: - - jets releases:info 3 - EOL - exit 1 - end - Release.new(options.merge(version: version)).show - end - end - - class Release - include Jets::Command::ApiHelpers - def initialize(options={}) - @options = options - end - - def list - no_token_exit! - resp = Jets::Api::Stack.retrieve(:current) - if resp["error"] == "not_found" - puts "No release history. Stack not found: #{Jets.project_namespace}" - # Return early and avoid calling Jets::Api::Release.list if stack not found - # Note: Jets::Api::Release.list also checks for no stack found - return - end - - name = "#{resp['name']} #{resp['location']}" - resp = Jets::Api::Release.list(@options) - check_for_error_message!(resp) # can also return "Stack not found #{name}" message - data = resp["data"] - if data.empty? - $stderr.puts "No releases found for stack: #{name}" - else - $stderr.puts "Releases for stack: #{name}" - show_items(data) - end - rescue Jets::Api::RequestError => e - puts "ERROR: Unable to list releases. #{e.class}: #{e.message}" - end - - def show_items(items) - presenter = CliFormat::Presenter.new - header = ["Version", "Status", "Released At", "Message"] - header << "Git Sha" if @options[:sha] - presenter.header = header - items.each do |item| - version = item["version"] - status = item["stack_status"] - released_at = item["pretty_created_at"] || item["created_at"] - message = item["message"] || "Deployed" - message = message[0..50] - - row = [version, status, format_time(released_at), message] - if @options[:sha] - sha = item["git_sha"].to_s[0..7] if item["git_sha"] - row << sha - end - presenter.rows << row - end - presenter.show - end - - def format_time(string) - if string.include?("ago") # IE: 5 minutes ago - string - else - time = Time.parse(string) - time.in_time_zone(Time.zone) - end - end - - def get(version) - resp = Jets::Api::Release.retrieve(version) - check_for_error_message!(resp) - rescue Jets::Api::RequestError => e - puts "ERROR: Unable to get release. #{e.class}: #{e.message}" - end - - def show - release = get(@options[:version]) - release_at = release['pretty_created_at'] || release['created_at'] - - data = [ - ["Version", release['version']], - ["Status", release['stack_status']], - ["Released At", format_time(release_at)], - ["Message", release['message']], - ["User", release['deploy_user']], - ["Jets Env", release['jets_env']], - ["Jets Extra", release['jets_extra']], - ["Jets Version", release['jets_version']], - ["Ruby Version", release['ruby_version']], - ["Region", release['region']], - ["Git Branch", release['git_branch']], - ["Git Sha", release['git_sha']], - ["Git Url", release['git_url']], - ["Git Message", release['git_message']], - ] - column1_width = data.map { |row| row[1].nil? ? 0 : row[0].to_s.length }.max - column2_width = data.map { |row| row[1].nil? ? 0 : row[1].to_s.length }.max - - puts Jets.project_namespace.color(:green) - data.each do |row| - puts "#{row[0].ljust(column1_width)} #{row[1]}" unless row[1].nil? - end - end - end -end diff --git a/lib/jets/commands/rollback/rollback_command.rb b/lib/jets/commands/rollback/rollback_command.rb deleted file mode 100644 index fddb488b7..000000000 --- a/lib/jets/commands/rollback/rollback_command.rb +++ /dev/null @@ -1,33 +0,0 @@ -require "jets/commands/releases/releases_command" - -module Jets::Command - class RollbackCommand < Base # :nodoc: - desc "rollback", "Rollback to a previous release" - long_desc Help.text(:rollback) - def perform(version) - Rollback.new(options.merge(version: version)).run - end - end - - class Rollback - include Jets::Command::ApiHelpers - attr_reader :version - def initialize(options={}) - @options = options - @version = options[:version] - end - - def run - no_token_exit! - Jets.boot - payload = Release.new(@options).get(version) - check_for_error_message!(payload) - - puts "Rolling back to version #{version}" - # Download previous cfn templates - Jets::Cfn::Download.new.download_templates(version) - # Run cloudformation update - Jets::Cfn::Ship.new(@options.merge(rollback_version: version)).run # also creates the Jets Api deployment record - end - end -end diff --git a/lib/jets/commands/routes/routes_command.rb b/lib/jets/commands/routes/routes_command.rb deleted file mode 100644 index c2c26ce29..000000000 --- a/lib/jets/commands/routes/routes_command.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -require "cli-format" - -module Jets::Command - class RoutesCommand < Base # :nodoc: - option :controller, aliases: :c, desc: "Filter by a specific controller, e.g. PostsController or Admin::PostsController" - option :format, default: "table", desc: "Output formats: #{CliFormat.formats.join(', ')}" # csv, equal, json, space, table, tab - option :grep, aliases: :g, desc: "Grep routes by a specific pattern" - option :reject, aliases: :r, desc: "Reject filter routes by a specific pattern" - - desc "routes", "Print out your application routes" - long_desc Help.text(:routes) - def perform(*) - require_application_and_environment! - Jets::Router::Help.new(options).print - end - end -end diff --git a/lib/jets/commands/runner/runner_command.rb b/lib/jets/commands/runner/runner_command.rb deleted file mode 100644 index 40579e0a5..000000000 --- a/lib/jets/commands/runner/runner_command.rb +++ /dev/null @@ -1,60 +0,0 @@ -# frozen_string_literal: true - -module Jets - module Command - class RunnerCommand < Base # :nodoc: - include EnvironmentArgument - - self.environment_desc = "The environment for the runner to operate under (test/development/production)" - - no_commands do - def help - super - say self.class.desc - end - end - - def self.banner(*) - "#{super} [<'Some.ruby(code)'> | | -]" - end - - desc "runner", "Run Ruby code in the context of Jets app non-interactively" - long_desc Help.text(:runner) - def perform(code_or_file = nil, *command_argv) - extract_environment_option_from_argument - - unless code_or_file - help - exit 1 - end - - require_application_and_environment! - Jets.application.load_runner - - args = command_argv - - ARGV.replace(command_argv) - - if code_or_file == "-" - eval($stdin.read, TOPLEVEL_BINDING, "stdin") - elsif File.exist?(code_or_file) - expanded_file_path = File.expand_path code_or_file - $0 = expanded_file_path - Kernel.load expanded_file_path - else - begin - # Jets changed TOPLEVEL_BINDING to binding to have access to args - # To keep args working https://github.com/boltops-tools/jets/pull/669 - eval(code_or_file, binding, __FILE__, __LINE__) - rescue SyntaxError, NameError => e - error "Please specify a valid ruby command or the path of a script to run." - error "Run '#{self.class.executable} -h' for help." - error "" - error e - exit 1 - end - end - end - end - end -end diff --git a/lib/jets/commands/server/server_command.rb b/lib/jets/commands/server/server_command.rb deleted file mode 100644 index 626467326..000000000 --- a/lib/jets/commands/server/server_command.rb +++ /dev/null @@ -1,291 +0,0 @@ -# frozen_string_literal: true - -require "fileutils" -require "action_dispatch" -require "jets" -require "active_support/core_ext/string/filters" - -module Jets - class Server < ::Rack::Server - class Options - def parse!(args) - Jets::Command::ServerCommand.new([], args).server_options - end - end - - def initialize(options = nil) - @default_options = options || {} - super(@default_options) - set_environment - end - - def opt_parser - Options.new - end - - def set_environment - ENV["JETS_ENV"] ||= options[:environment] - end - - def start(after_stop_callback = nil) - trap(:INT) { exit } - create_tmp_directories - setup_dev_caching - # TODO: should this be commented out? Think it needs to be because Lambda will - # not run through the server... - # However, need to test puma and see if this will surface errors when there's an app error. - # log_to_stdout if options[:log_stdout] - - super() - ensure - after_stop_callback.call if after_stop_callback - end - - def serveable? # :nodoc: - server - true - rescue LoadError, NameError - false - end - - def middleware - Hash.new([]) - end - - def default_options - super.merge(@default_options) - end - - def served_url - "#{options[:SSLEnable] ? 'https' : 'http'}://#{options[:Host]}:#{options[:Port]}" unless use_puma? - end - - private - def setup_dev_caching - if options[:environment] == "development" - Jets::DevCaching.enable_by_argument(options[:caching]) - end - end - - def create_tmp_directories - %w(cache pids sockets).each do |dir_to_make| - FileUtils.mkdir_p(File.join(Jets.root, "tmp", dir_to_make)) - end - end - - def log_to_stdout - wrapped_app # touch the app so the logger is set up - - console = ActiveSupport::Logger.new(STDOUT) - console.formatter = Jets.logger.formatter - console.level = Jets.logger.level - - unless ActiveSupport::Logger.logger_outputs_to?(Jets.logger, STDOUT) - Jets.logger.extend(ActiveSupport::Logger.broadcast(console)) - end - end - - def use_puma? - server.to_s == "Rack::Handler::Puma" - end - end - - module Command - class ServerCommand < Base # :nodoc: - include EnvironmentArgument - - # Hard-coding a bunch of handlers here as we don't have a public way of - # querying them from the Rack::Handler registry. - RACK_SERVERS = %w(cgi fastcgi webrick lsws scgi thin puma unicorn falcon) - - DEFAULT_PORT = 8888 - DEFAULT_PIDFILE = "tmp/pids/server.pid" - - class_option :port, aliases: "-p", type: :numeric, - desc: "Runs Jets on the specified port - defaults to 8888.", banner: :port - class_option :binding, aliases: "-b", type: :string, - desc: "Binds Jets to the specified IP - defaults to 'localhost' in development and '0.0.0.0' in other environments'.", - banner: :IP - class_option :config, aliases: "-c", type: :string, default: "config.ru", - desc: "Uses a custom rackup configuration.", banner: :file - class_option :daemon, aliases: "-d", type: :boolean, default: false, - desc: "Runs server as a Daemon." - class_option :using, aliases: "-u", type: :string, - desc: "Specifies the Rack server used to run the application (thin/puma/webrick).", banner: :name - class_option :pid, aliases: "-P", type: :string, - desc: "Specifies the PID file - defaults to #{DEFAULT_PIDFILE}." - class_option :dev_caching, aliases: "-C", type: :boolean, default: nil, - desc: "Specifies whether to perform caching in development." - class_option :restart, type: :boolean, default: nil, hide: true - class_option :early_hints, type: :boolean, default: nil, desc: "Enables HTTP/2 early hints." - class_option :log_to_stdout, type: :boolean, default: nil, optional: true, - desc: "Whether to log to stdout. Enabled by default in development when not daemonized." - - def initialize(args, local_options, *) - super - - @original_options = local_options - %w( --restart ) - end - - desc "server", "Runs a local server that mimics API Gateway for development" - long_desc Help.text(:server) - def perform - extract_environment_option_from_argument - set_application_directory! - prepare_restart - - Jets::Server.new(server_options).tap do |server| - # Require application after server sets environment to propagate - # the --environment option. - # require APP_PATH - Jets.boot - Dir.chdir(Jets.application.root) - - if server.serveable? - print_boot_information(server.server, server.served_url) - after_stop_callback = -> { say "Exiting" unless options[:daemon] } - server.start(after_stop_callback) - else - say rack_server_suggestion(options[:using]) - end - end - end - - no_commands do - def server_options - { - user_supplied_options: user_supplied_options, - server: options[:using], - log_stdout: log_to_stdout?, - Port: port, - Host: host, - DoNotReverseLookup: true, - config: options[:config], - environment: environment, - daemonize: options[:daemon], - pid: pid, - caching: options[:dev_caching], - restart_cmd: restart_command, - early_hints: early_hints - } - end - end - - private - def user_supplied_options - @user_supplied_options ||= begin - # Convert incoming options array to a hash of flags - # ["-p3001", "-C", "--binding", "127.0.0.1"] # => {"-p"=>true, "-C"=>true, "--binding"=>true} - user_flag = {} - @original_options.each do |command| - if command.start_with?("--") - option = command.split("=")[0] - user_flag[option] = true - elsif command =~ /\A(-.)/ - user_flag[Regexp.last_match[0]] = true - end - end - - # Collect all options that the user has explicitly defined so we can - # differentiate them from defaults - user_supplied_options = [] - self.class.class_options.select do |key, option| - if option.aliases.any? { |name| user_flag[name] } || user_flag["--#{option.name}"] - name = option.name.to_sym - case name - when :port - name = :Port - when :binding - name = :Host - when :dev_caching - name = :caching - when :daemonize - name = :daemon - end - user_supplied_options << name - end - end - user_supplied_options << :Host if ENV["HOST"] || ENV["BINDING"] - user_supplied_options << :Port if ENV["PORT"] - user_supplied_options << :pid if ENV["PIDFILE"] - user_supplied_options.uniq - end - end - - def port - options[:port] || ENV.fetch("PORT", DEFAULT_PORT).to_i - end - - def host - if options[:binding] - options[:binding] - else - default_host = environment == "development" ? "localhost" : "0.0.0.0" - - ENV.fetch("BINDING", default_host) - end - end - - def environment - options[:environment] || Jets::Command.environment - end - - def restart_command - "jets server #{@original_options.join(" ")} --restart" - end - - def early_hints - options[:early_hints] - end - - def log_to_stdout? - options.fetch(:log_to_stdout) do - options[:daemon].blank? && environment == "development" - end - end - - def pid - File.expand_path(options[:pid] || ENV.fetch("PIDFILE", DEFAULT_PIDFILE)) - end - - def self.banner(*) - "jets server -u [thin/puma/webrick] [options]" - end - - def prepare_restart - FileUtils.rm_f(pid) if options[:restart] - end - - def rack_server_suggestion(server) - if server.in?(RACK_SERVERS) - <<~MSG - Could not load server "#{server}". Maybe you need to the add it to the Gemfile? - - gem "#{server}" - - Run `jets server --help` for more options. - MSG - else - error = CorrectableError.new("Could not find server '#{server}'.", server, RACK_SERVERS) - if error.respond_to?(:detailed_message) - formatted_message = error.detailed_message - else - formatted_message = error.message - end - <<~MSG - #{formatted_message} - Run `jets server --help` for more options. - MSG - end - end - - def print_boot_information(server, url) - say <<~MSG - => Booting #{ActiveSupport::Inflector.demodulize(server)} - => Jets #{Jets.version} application starting in #{Jets.env} #{url} - => Run `jets server --help` for more startup options - MSG - end - end - end -end diff --git a/lib/jets/commands/stacks/stacks_command.rb b/lib/jets/commands/stacks/stacks_command.rb deleted file mode 100644 index 86090291e..000000000 --- a/lib/jets/commands/stacks/stacks_command.rb +++ /dev/null @@ -1,39 +0,0 @@ -module Jets::Command - class StacksCommand < Base # :nodoc: - desc "stacks", "List deployed stacks" - long_desc Help.text(:stacks) - paging_options.call - option :all_projects, desc: "Show all stacks across all projects", type: :boolean, default: false - def perform - name = Jets.project_name - no_token_exit! - params = paging_params.merge(options) - resp = Jets::Api::Stack.list(params) - check_for_error_message!(resp) - - data = resp["data"] - if data.empty? - $stderr.puts "No stacks deployed yet: #{name}" - else - message = if options[:all_projects] - "Stacks for all projects:" - else - "Stacks for project: #{name}" - end - $stderr.puts message - show_items(data) - end - rescue Jets::Api::RequestError => e - puts "WARNING: Unable to list stacks. #{e.class}: #{e.message}" - end - - private - def show_items(items) - presenter = CliFormat::Presenter.new - presenter.header = ["Name"] - items.each do |item| - puts "#{item['name']} #{item['location']}" - end - end - end -end diff --git a/lib/jets/commands/status/status_command.rb b/lib/jets/commands/status/status_command.rb deleted file mode 100644 index 375b5eebe..000000000 --- a/lib/jets/commands/status/status_command.rb +++ /dev/null @@ -1,16 +0,0 @@ -module Jets::Command - class StatusCommand < Base - include Jets::AwsServices - - desc "status", "Shows the current status of the Jets app" - long_desc Help.text(:status) - def perform - Jets.boot - cfn_status = Jets::Cfn::Status.new - success = cfn_status.run - unless success - cfn_status.failure_message! - end - end - end -end diff --git a/lib/jets/commands/url/url_command.rb b/lib/jets/commands/url/url_command.rb deleted file mode 100644 index d76683242..000000000 --- a/lib/jets/commands/url/url_command.rb +++ /dev/null @@ -1,92 +0,0 @@ -module Jets::Command - class UrlCommand < Base # :nodoc: - option :format, aliases: :f, desc: "Output format: json or text", default: "text" - - desc "url", "App url if routes are defined" - long_desc Help.text(:url) - def url(*) - Url.new(options).display - end - end - - class Url - include Jets::AwsServices - - def initialize(options) - @options = options - end - - def display - Jets.boot - stack_name = Jets::Names.parent_stack_name - unless stack_exists?(stack_name) - $stderr.puts "Stack for #{Jets.project_name.color(:green)} project for environment #{Jets.env.color(:green)}. Couldn't find #{stack_name.color(:green)} stack." - exit 1 - end - - stack = cfn.describe_stacks(stack_name: stack_name).stacks.first - - data = {} - - api_gateway_stack_arn = lookup(stack[:outputs], "ApiGateway") - if api_gateway_stack_arn && endpoint_available? - data[:api_gateway_endpoint] = get_gateway_endpoint(api_gateway_stack_arn) - get_custom_domain!(data) - end - - if data.empty? - $stderr.puts "API Gateway not found. This jets app does have an API Gateway associated with it. Please double check your config/routes.rb if you were expecting to see a url for the app. Also check that #{stack_name.color(:green)} is a jets app." - exit 1 - end - - if @options[:format] == "json" - puts data.to_json - else - puts "API Gateway Endpoint: #{data[:api_gateway_endpoint]}" - puts "Custom Domain: #{data[:custom_domain]}" if data[:custom_domain] - puts "App Domain: #{data[:app_domain]}" if data[:app_domain] - end - end - - def get_gateway_endpoint(api_gateway_stack_arn) - stack = cfn.describe_stacks(stack_name: api_gateway_stack_arn).stacks.first - rest_api = lookup(stack[:outputs], "RestApi") - region_id = lookup(stack[:outputs], "Region") - stage_name = Jets::Cfn::Resource::ApiGateway::Deployment.stage_name - - # https://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-call-api.html - # https://my-api-id.execute-api.region-id.amazonaws.com/stage-name/{resourcePath} - "https://#{rest_api}.execute-api.#{region_id}.amazonaws.com/#{stage_name}" - end - - def get_custom_domain!(data) - return unless endpoint_available? && Jets.custom_domain? && Jets.config.domain.route53 - - domain_name = Jets::Cfn::Resource::ApiGateway::DomainName.new - # Looks funny but its right. - # domain_name is a method on the Jets::Cfn::Resource::ApiGateway::Domain instance - url = "https://#{domain_name.domain_name}" - data[:custom_domain] = url - data[:app_domain] = "https://#{Jets.config.app.domain}" if Jets.config.app.domain - end - - def endpoint_unavailable? - return false if Jets::Router.no_routes? - resp, status = stack_status - return false if status.include?("ROLLBACK") - end - - def endpoint_available? - !endpoint_unavailable? - end - - # All CloudFormation states listed here: - # http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-describing-stacks.html - def stack_status - resp = cfn.describe_stacks(stack_name: @parent_stack_name) - status = resp.stacks[0].stack_status - [resp, status] - end - - end -end diff --git a/lib/jets/commands/version/version_command.rb b/lib/jets/commands/version/version_command.rb deleted file mode 100644 index 62ca12af4..000000000 --- a/lib/jets/commands/version/version_command.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -module Jets::Command - class VersionCommand < Base # :nodoc: - desc "version", "Prints Jets version" - long_desc Help.text(:version) - def perform - puts Jets.version - end - end -end diff --git a/lib/jets/configuration.rb b/lib/jets/configuration.rb deleted file mode 100644 index 0700d9e39..000000000 --- a/lib/jets/configuration.rb +++ /dev/null @@ -1,166 +0,0 @@ -# frozen_string_literal: true - -require "active_support/ordered_options" -require "active_support/core_ext/object" - -module Jets - module Configuration - # MiddlewareStackProxy is a proxy for the Jets middleware stack that allows - # you to configure middlewares in your application. It works basically as a - # command recorder, saving each command to be applied after initialization - # over the default middleware stack, so you can add, swap, or remove any - # middleware in Jets. - # - # You can add your own middlewares by using the +config.middleware.use+ method: - # - # config.middleware.use Magical::Unicorns - # - # This will put the Magical::Unicorns middleware on the end of the stack. - # You can use +insert_before+ if you wish to add a middleware before another: - # - # config.middleware.insert_before Rack::Head, Magical::Unicorns - # - # There's also +insert_after+ which will insert a middleware after another: - # - # config.middleware.insert_after Rack::Head, Magical::Unicorns - # - # Middlewares can also be completely swapped out and replaced with others: - # - # config.middleware.swap ActionDispatch::Flash, Magical::Unicorns - # - # Middlewares can be moved from one place to another: - # - # config.middleware.move_before ActionDispatch::Flash, Magical::Unicorns - # - # This will move the Magical::Unicorns middleware before the - # ActionDispatch::Flash. You can also move it after: - # - # config.middleware.move_after ActionDispatch::Flash, Magical::Unicorns - # - # And finally they can also be removed from the stack completely: - # - # config.middleware.delete ActionDispatch::Flash - # - class MiddlewareStackProxy - def initialize(operations = [], delete_operations = []) - @operations = operations - @delete_operations = delete_operations - end - - def insert_before(...) - @operations << -> middleware { middleware.insert_before(...) } - end - - alias :insert :insert_before - - def insert_after(...) - @operations << -> middleware { middleware.insert_after(...) } - end - - def swap(...) - @operations << -> middleware { middleware.swap(...) } - end - - def use(...) - @operations << -> middleware { middleware.use(...) } - end - - def delete(...) - @delete_operations << -> middleware { middleware.delete(...) } - end - - def move_before(...) - @delete_operations << -> middleware { middleware.move_before(...) } - end - - alias :move :move_before - - def move_after(...) - @delete_operations << -> middleware { middleware.move_after(...) } - end - - def unshift(...) - @operations << -> middleware { middleware.unshift(...) } - end - - def merge_into(other) # :nodoc: - (@operations + @delete_operations).each do |operation| - operation.call(other) - end - - other - end - - def +(other) # :nodoc: - MiddlewareStackProxy.new(@operations + other.operations, @delete_operations + other.delete_operations) - end - - protected - attr_reader :operations, :delete_operations - end - - class Generators # :nodoc: - attr_accessor :aliases, :options, :templates, :fallbacks, :colorize_logging, :api_only - attr_reader :hidden_namespaces, :after_generate_callbacks - - def initialize - @aliases = Hash.new { |h, k| h[k] = {} } - @options = Hash.new { |h, k| h[k] = {} } - @fallbacks = {} - @templates = [] - @colorize_logging = true - @api_only = false - @hidden_namespaces = [] - @after_generate_callbacks = [] - end - - def initialize_copy(source) - @aliases = @aliases.deep_dup - @options = @options.deep_dup - @fallbacks = @fallbacks.deep_dup - @templates = @templates.dup - end - - def hide_namespace(namespace) - @hidden_namespaces << namespace - end - - def after_generate(&block) - @after_generate_callbacks << block - end - - def method_missing(method, *args) - method = method.to_s.delete_suffix("=").to_sym - - # Note: Using :rails because piggybacking off of rails generators - if args.empty? - if method == :rails - return @options[method] - else - return @options[:rails][method] - end - end - - if method == :rails || args.first.is_a?(Hash) - namespace, configuration = method, args.shift - else - # Examples: - # method :orm args [:active_record, {:migration=>true, :timestamps=>true}] @options {} - # namespace :active_record - # configuration {:migration=>true, :timestamps=>true} - namespace, configuration = args.shift, args.shift - namespace = namespace.to_sym if namespace.respond_to?(:to_sym) - @options[:rails][method] = namespace - end - - if configuration - aliases = configuration.delete(:aliases) - @aliases[namespace].merge!(aliases) if aliases - @options[namespace].merge!(configuration) - end - - # Example @options: {:rails=>{:orm=>:active_record}, :active_record=>{:migration=>true, :timestamps=>true}} - end - end - end -end diff --git a/lib/jets/controller/base.rb b/lib/jets/controller/base.rb deleted file mode 100644 index cae6c94e8..000000000 --- a/lib/jets/controller/base.rb +++ /dev/null @@ -1,117 +0,0 @@ -require "action_controller" -require "action_controller/log_subscriber" -require "active_support/concern" -require "active_support/callbacks" -require "abstract_controller/callbacks" -require "json" -require "rack/utils" # Rack::Utils.parse_nested_query - -# Controller public methods get turned into Lambda functions. -module Jets::Controller - DEFAULT_CONTENT_TYPE = "text/html; charset=utf-8" - - class RoutingError < StandardError; end - - class Base < Jets::Lambda::Functions - # Make Jets controller "compatible" with Rails - # Note: Rolling include into a single Compat module causes naming conflicts with ActionController and AbstractController. - # So we include each module individually. It's clearer this way anyway. - include Compat::AbstractController::Base - include Compat::ActionController::Metal - include Compat::RouteSet - include Compat::Caching - include Compat::Future - - # Order matters due to use of super and the module included chain. - include AbstractController::Rendering # at top to normalize render options asap - include AbstractController::Translation - include AbstractController::AssetPaths - - include ActionController::Helpers - include Jets::Router::Helpers::NamedRoutes - - include ActionController::UrlFor # includes ActionDispatch::Routing::UrlFor - include ActionController::Redirecting - include ActionView::Layouts # includes ActionView::Rendering - include ActionController::Rendering - include ActionController::Renderers::All # for use_renderers :json, :js, :xml - include ActionController::ConditionalGet - include ActionController::EtagWithTemplateDigest - include ActionController::EtagWithFlash - include ActionController::Caching - include ActionController::MimeResponds - include ActionController::ImplicitRender # includes BasicImplicitRender action_controller/metal/basic_implicit_render.rb - include ActionController::StrongParameters - include ActionController::ParameterEncoding - include ActionController::Cookies - include ActionController::Flash - include ActionController::FormBuilder - include ActionController::RequestForgeryProtection - include ActionController::ContentSecurityPolicy - include ActionController::PermissionsPolicy - # include ActionController::Streaming # not supported - # include ActionController::DataStreaming # not supported - include ActionController::HttpAuthentication::Basic::ControllerMethods - include ActionController::HttpAuthentication::Digest::ControllerMethods - include ActionController::HttpAuthentication::Token::ControllerMethods - # include ActionController::DefaultHeaders # not needed - include ActionController::Logging # log_at: ability to change log level - - # More Jets overrides and customizations - include Handler # Lambda Handler process! method. Runs on AWS only. - include RackAdapter::Action # action rack method - - # Before callbacks should also be executed as early as possible, so - # also include them at the bottom. - include AbstractController::Callbacks - - # Must near bottom because decorating Rails behavior - include Decorate::Authorization # APIGW Authorizers - include Decorate::UrlFor # add_apigw_stage - include Decorate::Redirecting # add_apigw_stage - include Decorate::Logging - - # Append rescue at the bottom to wrap as much as possible. - include ActionController::Rescue - - # Add instrumentations hooks at the bottom, to ensure they instrument - # all the methods properly. - include ActionController::Instrumentation # TODO: figure why notifications dont work - - # Params wrapper should come before instrumentation so they are - # properly showed in logs - include ActionController::ParamsWrapper - - def initialize(event, context, meth, rack_env) - # Passing in rack env so the same rack env (same object id) is used. - # This is important for: - # 1. Constraints lambda procs - # 2. Controller.action rack methods - @event = event - @context = context - @meth = meth - @rack_env = rack_env - @_request = Jets::Controller::Request.new(event: event, rack_env: @rack_env) - # Note: Rails sets request.route in the Rails::Engine#build_request instead. - # The Jets request class is built slightly differently, so set it here. - # The request.routes method is need to that url_helpers work in generally. - # It's just how Rails ActionView implements url_helpers. - @_request.routes = self.class._routes - @_response = Jets::Controller::Response.new - @_response.request = @request - # Jets::Controller::Base#initialize interface is different than ActionController::Controller::Base. - # The super call goes to ActionController modules that can decorate and call super again. - # At the end of the module chain is Jets::Controller::Compat::ActionController::Metal#initialize - # which goes back to the original Jets::Lambda::Functions#initialize(event, context, meth) interface. - super() # ActionController::Base#initialize() interface - end - - abstract! - - use_renderers :json, :js, :xml - end - - # See Jets::Controller::Compat::ActionController::Metal dispatch! method - - ActiveSupport.run_load_hooks(:jets_controller, Base) -end diff --git a/lib/jets/controller/compat/abstract_controller/base.rb b/lib/jets/controller/compat/abstract_controller/base.rb deleted file mode 100644 index 84b4d3add..000000000 --- a/lib/jets/controller/compat/abstract_controller/base.rb +++ /dev/null @@ -1,126 +0,0 @@ -require "abstract_controller/error" -require "active_support/configurable" -require "active_support/descendants_tracker" -require "active_support/core_ext/module/anonymous" -require "active_support/core_ext/module/attr_internal" - -module Jets::Controller::Compat::AbstractController - module Base - extend ActiveSupport::Concern - include ActiveSupport::Configurable - - delegate :action_methods, - :controller_name, - :controller_path, - to: :class - class_methods do - # A list of method names that should be considered actions. This - # includes all public instance methods on a controller, less - # any internal methods (see internal_methods), adding back in - # any methods that are internal, but still exist on the class - # itself. - # - # ==== Returns - # * Set - A set of all methods that should be considered actions. - def action_methods - @action_methods ||= begin - # All public instance methods of this class, including ancestors - methods = (public_instance_methods(true) - - # Except for public instance methods of Base and its ancestors - internal_methods + - # Be sure to include shadowed public instance methods of this class - public_instance_methods(false)) - - methods.map!(&:to_s) - - methods.to_set - end - end - - # A list of all internal methods for a controller. This finds the first - # abstract superclass of a controller, and gets a list of all public - # instance methods on that abstract class. Public instance methods of - # a controller would normally be considered action methods, so methods - # declared on abstract classes are being removed. - # (ActionController::Metal and ActionController::Base are defined as abstract) - def internal_methods - controller = self - - controller = controller.superclass until controller.abstract? - controller.public_instance_methods(true) - end - - def controller_name - @controller_name ||= name.demodulize.delete_suffix("Controller").underscore - end - - def controller_path - @controller_path ||= name.delete_suffix("Controller").underscore - end - - # Returns true if the given controller is capable of rendering - # a path. A subclass of +AbstractController::Base+ - # may return false. An Email controller for example does not - # support paths, only full URLs. - def supports_path? - true - end - end - - ## - # Returns the body of the HTTP response sent by the controller. - attr_internal :response_body - - ## - # Returns the name of the action this controller is processing. - def action_name - @meth - end - - def available_action?(action_name) - self.class.action_methods.include?(action_name.to_s) - end - - # Tests if a response body is set. Used to determine if the - # +process_action+ callback needs to be terminated in - # AbstractController::Callbacks. - def performed? - if response_body.respond_to?(:each) - !response_body.compact.empty? # [""] is considered true - else # nil - !response_body.nil? - end - end - - def inspect # :nodoc: - "#<#{self.class.name}:#{'%#016x' % (object_id << 1)}>" - end - - private - # Returns true if the name can be considered an action because - # it has a method defined in the controller. - # - # ==== Parameters - # * name - The name of an action to be tested - def action_method?(name) - self.class.action_methods.include?(name) - end - - # Call the action. Override this in a subclass to modify the - # behavior around processing an action. This, and not #process, - # is the intended way to override action dispatching. - # - # Notice that the first argument is the method to be dispatched - # which is *not* necessarily the same as the action name. - def process_action - send_action(action_name) # to BasicImplicitRender#send_action => super (posts#index) or default_render - end - - # Actually call the method associated with the action. Override - # this method if you wish to change how action methods are called, - # not to add additional behavior around it. For example, you would - # override #send_action if you want to inject arguments into the - # method. - alias send_action send - end -end diff --git a/lib/jets/controller/compat/action_controller/metal.rb b/lib/jets/controller/compat/action_controller/metal.rb deleted file mode 100644 index 06a8bd772..000000000 --- a/lib/jets/controller/compat/action_controller/metal.rb +++ /dev/null @@ -1,99 +0,0 @@ -module Jets::Controller::Compat::ActionController - module Metal - extend ActiveSupport::Concern - - delegate :commit_flash, - :filtered_parameters, - :parameter_filter, - :params, - :reset_session, - :session, - :session=, - :_routes, - to: :request - delegate :content_type, - :content_type=, - :get_header, - :headers, - :location, - :location=, - :media_type, - :response_code, - :set_header, - :status, - :status=, - :to_a, - to: :response - attr_internal :request, :response - - alias :response_code :status # :nodoc: - - # End of the module include chain. - # Go back from the ActionController::Base#initialize() interface - # to the original Jets::Lambda::Functions#initialize(event, context, meth) interface - def initialize - super(@event, @context, @meth) - end - - # One key difference between process! vs dispatch! - # - # process! - takes the request through the middleware stack - # dispatch! - does not - # - # dispatch! is useful for megamode or mounted applications - # - def dispatch! - # extend Jets.application.routes.url_helpers - # extend Blorgh::Engine.routes.url_helpers - - # ActionView::Base.send :include, Jets.application.routes.url_helpers - # ActionView::Base.send :include, Blorgh::Engine.routes.url_helpers - # extend Jets.application.routes.mounted_helpers - - method_override! - process_action - commit_flash - response.to_a - end - - # Override @meth when POST with _method=delete - # By the time processing reaches dispatch which calls method_override! - # The Rack::MethodOverride middleware has overriden env['REQUEST_METHOD'] with DELETE - # and set env['rack.methodoverride.original_method'] - def method_override! - env = request.env - if env['rack.methodoverride.original_method'] && env['REQUEST_METHOD'] == 'DELETE' - @original_meth = @meth - @meth = "destroy" - end - end - - # Not using rack.response.body directly because its value is wrapped in an Array, IE: [body] - # and ActionController components check response_body assuming it can be nil or a String. - # So we assign the String at the controller.response_body level and [body] at the - # the Response#body= level. - def response_body=(body) - body = [body] unless body.nil? || body.respond_to?(:each) - return unless body - response.body = body - super - end - - # Unsure how Rails defines this but this is the Rails behavior according to the Kingsman/Devise port - def response=(triplet) - if triplet.is_a?(Array) - status, headers, body = triplet - self.status = status - self.headers.merge!(headers) - self.response_body = body - else - self.response_body = triplet # string - end - end - - # Tests if render or redirect has already happened. - def performed? - response_body || response.committed? - end - end -end diff --git a/lib/jets/controller/compat/caching.rb b/lib/jets/controller/compat/caching.rb deleted file mode 100644 index 4e1228c75..000000000 --- a/lib/jets/controller/compat/caching.rb +++ /dev/null @@ -1,13 +0,0 @@ -module Jets::Controller::Compat - # Got most from AbstractController::Caching and ActionController::Caching - module Caching - extend ActiveSupport::Concern - - delegate :perform_caching, to: :class - class_methods do - def perform_caching - Jets.config.controller.perform_caching - end - end - end -end diff --git a/lib/jets/controller/compat/future.rb b/lib/jets/controller/compat/future.rb deleted file mode 100644 index 44fa73467..000000000 --- a/lib/jets/controller/compat/future.rb +++ /dev/null @@ -1,13 +0,0 @@ -module Jets::Controller::Compat - module Future - # raise_on_missing_callback_actions will be called in Rails 7.1 - def raise_on_missing_callback_actions - puts <<~EOL - raise_on_missing_callback_actions called, Jets defaults to true so - when we upgrade to Rails 7.1 components it'll trigger and will - fix any issues. Also will need to make it configurable. - EOL - true - end - end -end diff --git a/lib/jets/controller/compat/route_set.rb b/lib/jets/controller/compat/route_set.rb deleted file mode 100644 index 15ef19664..000000000 --- a/lib/jets/controller/compat/route_set.rb +++ /dev/null @@ -1,47 +0,0 @@ -module Jets::Controller::Compat - module RouteSet - # Override behavior from ActionDispatch::Routing::UrlFor - # - # When Jets include ActionDispatch::Routing::UrlFor is in the Jets::Controller::Base class - # It sets @_routes = nil - # - # We need to set @_routes to the Jets::Controller::Request routes object - # to provide a Jets custom routes object that's compatiable the Rails routes object. - # - # Why does @_routes have to be set? - # - # How this works is a bit hard to follow. Here's the current trace with actionpack 7.0.8 - # - # Here's where controller render goes from controller-land to view-land. - # - # action_controller/metal/rendering.rb:158:in `render_to_body' - # action_view/rendering.rb:114:in `render_to_body' - # - # In action_view/rendering.rb render_to_body => _render_template is called and - # a view_context is created. - # - # def view_context - # view_context_class.new(lookup_context, view_assigns, self) - # end - # - # self is the . The controller instance is the _routes is used. - # The view_context_class is somehow a wrapped anonymous class that has the - # ActionView::Base#initialize method. - # - # Usage: - # - # module Jets::Controller - # class Base < Jets::Lambda::Functions - # ... - # include Compat::RouteSet::ControllerPrepend - # ... - # include ActionController::UrlFor # includes ActionDispatch::Routing::UrlFor - # ... - # include Compat::RouteSet::ControllerAppend - # - def initialize(*) - @_routes = self.class._routes # set by inherited hook in JetsTurbines::RoutesHelpers.with - super - end - end -end diff --git a/lib/jets/controller/decorate/apigw_stage.rb b/lib/jets/controller/decorate/apigw_stage.rb deleted file mode 100644 index be118937e..000000000 --- a/lib/jets/controller/decorate/apigw_stage.rb +++ /dev/null @@ -1,39 +0,0 @@ -module Jets::Controller::Decorate - module ApigwStage - def add_apigw_stage(url) - return url unless add_apigw_stage? - stage_name = ENV['JETS_APIGW_STAGE'] || Jets::Cfn::Resource::ApiGateway::Deployment.stage_name - uri = URI.parse(url) - path = uri.path - original_ends_with_slash = path.ends_with?('/') - path = "/#{path}" unless path.starts_with?('/') - segments = path.split('/') - # unless to prevent stage name being added twice if url_for is called twice on the same string - segments.insert(1, stage_name) unless segments[1] == stage_name - new_path = segments.join('/') # modified path - new_path = "#{new_path}/" if !new_path.ends_with?('/') && original_ends_with_slash - uri.path = new_path - uri.to_s - end - - def add_apigw_stage? - return true if ENV['JETS_APIGW_STAGE'] - return false if ENV['JETS_TEST'] - return false unless request # nil for `importmap json` cli and actionmailer - - # Using request.host which might be different than event['headers']['Host'] when config.app.domain is set. - # This means that visiting the APIGW domain name directly will not prepend the stage name - # to the helper method urls. This is ok since the APIGW domain name is not used in production. - # It's a compromise since we cannot pass the CloudFront host to APIGW. - # Rather have the CloudFront user-friendly domain name work than APIGW domain name. - # Examples: - # https://djvojd3em5.execute-api.us-west-2.amazonaws.com/dev/ - # https://friendly.domain.com/ - host = request.host - on_cloud9 = !!(host =~ /cloud9\..*\.amazonaws\.com/) - return false if on_cloud9 - - host.include?('amazonaws.com') - end - end -end diff --git a/lib/jets/controller/decorate/authorization.rb b/lib/jets/controller/decorate/authorization.rb deleted file mode 100644 index 979197585..000000000 --- a/lib/jets/controller/decorate/authorization.rb +++ /dev/null @@ -1,91 +0,0 @@ -module Jets::Controller::Decorate - module Authorization - extend ActiveSupport::Concern - - included do - class_attribute :auth_type, - :auth_to, - :auth_options, # for only and except filters - :api_key_needed, - :authorization_scopes_value - end - - class_methods do - def controller_path - name.sub(/Controller$/, "".freeze).underscore - end - - def authorization_type(value=nil) - if !value.nil? - self.auth_type = value - else - self.auth_type - end - end - - def authorizer(value=nil, options={}) - if !value.nil? - self.auth_to = value # IE: main#protect - self.auth_options = options # IE: only: %w[index] or expect: [:show] - else - self.auth_to - end - end - - def authorization_scopes(value=nil) - if !value.nil? - self.authorization_scopes_value = value - else - self.authorization_scopes_value - end - end - - def authorizer_metadata - Jets::Router::Route::Authorizer.metadata(auth_to) - end - - # Used to add to parameter to controller templates - def authorizer_id - Jets::Router::Route::Authorizer.logical_id(auth_to) - end - - # Used to add to the API::Gateway Method selectively - def authorizer_logical_id_for(action_name) - return unless auth_to - - only = auth_options[:only].map(&:to_s) if auth_options && auth_options[:only] - except = auth_options[:except].map(&:to_s) if auth_options && auth_options[:except] - - if except and !except.include?(action_name) - logical_id = Jets::Router::Route::Authorizer.logical_id(auth_to) - end - - # only overrides except - if only and only.include?(action_name) - logical_id = Jets::Router::Route::Authorizer.logical_id(auth_to) - end - - # if both only and except are not set then always set the logical_id - if !only && !except - logical_id = Jets::Router::Route::Authorizer.logical_id(auth_to) - end - - logical_id - end - - # Autoamtically sets authorization_type for the specific route based on the controller authorizer - def infer_authorization_type_for(action_name) - return unless authorizer_logical_id_for(action_name) - Jets::Authorizer::Base.authorization_type(auth_to) - end - - def api_key_required(value=nil) - if !value.nil? - self.api_key_needed = value - else - self.api_key_needed - end - end - end - end -end diff --git a/lib/jets/controller/decorate/logging.rb b/lib/jets/controller/decorate/logging.rb deleted file mode 100644 index 51f7b8c98..000000000 --- a/lib/jets/controller/decorate/logging.rb +++ /dev/null @@ -1,90 +0,0 @@ -module Jets::Controller::Decorate - module Logging - extend ActiveSupport::Concern - - included do - # A little bit tricky. Jets::Controller::Base includes AbstractController::Logger - # via ActionController::Redirecting. - # AbstractController::Logger defines a config_accessor :logger - # This results in logger being nil. We set it to logger here to correct it. - # Unsure why this does not happen with ActionController::Base though. - delegate :logger, to: Jets # fixes logger but Rails doesnt need this - end - - def dispatch! - t1 = Time.now - log_start - triplet = super # Abstract#dispatch! - took = Time.now - t1 - log_finish status: status, took: took - triplet - end - - def processing_log - processing = "Processing #{self.class.name}##{@meth}" - processing << " (original method #{@original_meth})" if @original_meth - processing - end - - # Documented interface method, careful not to rename - def log_start - # Commented out Started because Jets::Rack::Logger middleware handles now - # ip = request.ip - # logger.info "Started (Jets) #{request.request_method} \"#{request.path}\" for #{ip} at #{Time.now}" - - logger.info processing_log - logger.info " Raw Event: #{@event}" if ENV['JETS_LOG_RAW_EVENT'] - logger.info " Event: #{event_log}" if log_event? - params = filtered_parameters.to_h.except('controller', 'action') - # JSON.dump makes logging look pretty in CloudWatch logs because it keeps it on 1 line - logger.info " Parameters: #{JSON.dump(params)}" unless params.empty? - end - - def log_event? - return false if ENV['JETS_LOG_EVENT'] == '0' - Jets.config.logging.event - end - - # Documented interface method, careful not to rename - def log_finish(options={}) - status, took = options[:status], options[:took] - logger.info "Completed Status Code #{status} in #{"%.3f" % took}s" - end - - def event_log - event = @event.dup # clone to display event - - if event['isBase64Encoded'] - event['body'] = '[BASE64_ENCODED]' - else - event['body'] = filter_json_log(event['body']) - end - - event["multiValueQueryStringParameters"] = parameter_filter.filter(event['multiValueQueryStringParameters']) if event['multiValueQueryStringParameters'] - event["pathParameters"] = parameter_filter.filter(event['pathParameters']) if event['pathParameters'] - event["queryStringParameters"] = parameter_filter.filter(event['queryStringParameters']) if event['queryStringParameters'] - json_dump_log(event) - end - - # Append _log to reduce chance of name collision with user defined methods - def filter_json_log(json_text) - return json_text if json_text.blank? - - begin - hash_params = JSON.parse(json_text) - filtered_params = parameter_filter.filter(hash_params) - JSON.dump(filtered_params) - rescue JSON::ParserError - '[FILTERED]' - end - end - - # Handles binary data safely - def json_dump_log(data) - JSON.dump(data) - rescue Encoding::UndefinedConversionError - data['body'] = '[BINARY]' - JSON.dump(data) - end - end -end diff --git a/lib/jets/controller/decorate/redirecting.rb b/lib/jets/controller/decorate/redirecting.rb deleted file mode 100644 index 33418edd6..000000000 --- a/lib/jets/controller/decorate/redirecting.rb +++ /dev/null @@ -1,16 +0,0 @@ -module Jets::Controller::Decorate - module Redirecting - include ApigwStage - - def _compute_redirect_to_location(request, options) # :nodoc: - adjust = options.respond_to?(:to_str) || options.is_a?(String) - options = add_apigw_stage(options) if adjust - super - end - - def redirect_back(fallback_location: '/') - location = request.headers["Referer"] || fallback_location - redirect_to location - end - end -end diff --git a/lib/jets/controller/decorate/url_for.rb b/lib/jets/controller/decorate/url_for.rb deleted file mode 100644 index 899222d18..000000000 --- a/lib/jets/controller/decorate/url_for.rb +++ /dev/null @@ -1,10 +0,0 @@ -module Jets::Controller::Decorate - module UrlFor - include ApigwStage - - def url_for(options = nil) - url = super - add_apigw_stage(url) - end - end -end diff --git a/lib/jets/controller/handler.rb b/lib/jets/controller/handler.rb deleted file mode 100644 index 415924d77..000000000 --- a/lib/jets/controller/handler.rb +++ /dev/null @@ -1,41 +0,0 @@ -module Jets::Controller - # Only called by AWS Lambda before it runs through the middlewares. - module Handler - extend ActiveSupport::Concern - - # One key difference between process! vs dispatch! - # - # process! - takes the request through the middleware stack - # dispatch! - does not - # - # Generally, we want to use process! so it goes through the middleware stacks. - # - # The last middleware stack is Jets::Controller::Middleware::Main - # Which comes back to dispatch! in this same Controller Base class. - # - # class Jets::Controller::Middleware::Main - # def call! - # setup - # @controller.dispatch! # Returns triplet - # end - # end - # - def process! - apigw = Jets::Controller::Handler::Apigw.new(event, context, self, @meth, @rack_env) - apigw.process_through_middlewares # Returns API Gateway hash structure - end - - class_methods do - def process(event, context={}, meth) - rack_env = Jets::Controller::RackAdapter::Env.new(event, context).convert # convert to Rack env - controller = new(event, context, meth, rack_env) - # Using send because process! was a private method in Jets::RackController (old) so - # it doesnt create a lambda function. It's doesnt matter what scope process! - # is in Controller::Base because Jets lambda functions inheritance doesnt - # include methods in Controller::Base. - controller.send(:process!) - end - end - end -end - diff --git a/lib/jets/controller/handler/apigw.rb b/lib/jets/controller/handler/apigw.rb deleted file mode 100644 index 608858e4f..000000000 --- a/lib/jets/controller/handler/apigw.rb +++ /dev/null @@ -1,93 +0,0 @@ -require 'base64' - -# Also, logic in the opposite direction: Jets::Controller::Middleware::Mimic::Apigw -# Only called by AWS Lambda before it runs through the middlewares. -module Jets::Controller::Handler - class Apigw - extend Memoist - - attr_reader :event, :context, :controller, :meth, :rack_env - def initialize(event, context, controller, meth, rack_env) - @event = event - @context = context - @controller = controller - @meth = meth - @rack_env = rack_env - end - - # 1. Convert API Gateway event event to Rack env - # 2. Process using full Rack middleware stack - # 3. Convert back to API gateway response structure payload - # - # Returns back API Gateway response hash structure - # Only called when running in AWS Lambda. - # - # Set the jets.* env variables so we have access to them in the middleware. - # The controller instance is called in the Main middleware. - # On AWS, will use original event and context. - # On local server, will use Middleware::Mimic event and context. - def process_through_middlewares - env = rack_env.merge( - 'jets.controller' => @controller, # original controller instance from handler - 'jets.context' => @context, # original AWS Lambda context - 'jets.event' => @event, # original AWS Lambda event - 'jets.meth' => @meth, - ) - status, headers, body = Jets.application.call(env) # goes through full middleware stack - # middleware can handle Array or BodyProxy, APIGW require Strings - case body - when Rack::Files::Iterator - str = File.read(body.path) - else - # join for Rack::BodyProxy or Array - str = body.join - end - convert_to_api_gateway(status, headers, str) - end - - # Transform the structure to AWS_PROXY compatible structure - # http://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-output-format - def convert_to_api_gateway(status, headers, body) - base64 = headers["x-jets-base64"] == 'yes' - body = body.respond_to?(:read) ? body.read : body - body = Base64.encode64(body) if base64 - - {}.tap do |resp| - resp['statusCode'] = status - resp['body'] = body - resp['isBase64Encoded'] = base64 - add_response_headers(resp, headers) - adjust_for_elb(resp) - end - end - - def add_response_headers(resp, headers) - resp['headers'] = headers.reject { |_, val| val.is_a?(Array) } - multi_value_headers = headers.select { |_, val| val.is_a?(Array) } - - resp['multiValueHeaders'] = multi_value_headers unless multi_value_headers.blank? - end - - # Note: ELB is not officially support. This is just in case users wish to manually - # connect ELBs to the functions created by Jets. - def adjust_for_elb(resp) - return resp unless from_elb? - - # ELB requires statusCode to be an Integer whereas API Gateway requires statusCode to be a String - status = resp["statusCode"] = resp["statusCode"].to_i - - # ELB also requires statusDescription attribute - status_desc = Rack::Utils::HTTP_STATUS_CODES[status] - status_desc = status_desc.nil? ? status.to_s : "#{status} #{status_desc}" - resp["statusDescription"] = status_desc - - resp - end - - def from_elb? - # NOTE: @event["requestContext"]["elb"] is set when the request is coming from an elb - # Can set JETS_ELB=1 for local testing - @event["requestContext"] && @event["requestContext"]["elb"] || ENV['JETS_ELB'] - end - end -end diff --git a/lib/jets/controller/middleware/main.rb b/lib/jets/controller/middleware/main.rb deleted file mode 100644 index 6cda0d04c..000000000 --- a/lib/jets/controller/middleware/main.rb +++ /dev/null @@ -1,61 +0,0 @@ -# All roads lead here -# -# 1. AWS Lambda: PostsController - Handler::Apigw - Jets.application.call -# 2. Local server: config.ru - run Jet.application - Jets.application.call -# -# Then eventually: -# -# Jets.application.call - Middleware stack - Jets::Controller::Middleware::Main -# -module Jets::Controller::Middleware - class Main - include Jets::ExceptionReporting - - def initialize(env) - @env = env - @controller = env['jets.controller'] # original controller instance from handler or mimic - @context = env['jets.context'] # original AWS Lambda event or mimic context - @event = env['jets.event'] # original AWS Lambda event or mimic event - @meth = env['jets.meth'] - end - - def call - dup.call! - end - - # With exception reporting here instead of Controller::Base#process so any - # exception in the middleware stack is caught. - # Also using with_exception_reporting instead of - # prepend Jets::ExceptionReporting::Process because the method is call! - # not process. The interface is different. - def call! - with_exception_reporting do - setup - @controller.dispatch! # Returns triplet - end - end - - # Common setup logical at this point of middleware processing right before - # calling any controller actions. - def setup - # We already recreated a mimic rack env earlier as part of the very first - # middleware layer. However, by the time the rack env reaches the main middleware - # it could had been updated by other middlewares. We update the env here again. - @controller.request.set_env!(@env) - # This allows sesison helpers to work. Sessions are managed by - # the Rack::Session::Cookie middleware by default. - @controller.session = @env['rack.session'] || {} - end - - def jets_host - protocol = @event.dig('headers', 'X-Forwarded-Proto') || @env['rack.url_scheme'] - default = "#{protocol}://#{@env['HTTP_HOST']}" - Jets.config.helpers.host || default - end - - def self.call(env) - instance = new(env) - instance.call - end - end -end diff --git a/lib/jets/controller/middleware/mimic.rb b/lib/jets/controller/middleware/mimic.rb deleted file mode 100644 index c14b82dfa..000000000 --- a/lib/jets/controller/middleware/mimic.rb +++ /dev/null @@ -1,105 +0,0 @@ -require 'kramdown' - -# Handles mimicing of API Gateway to Lambda function call locally -# Logic in the opposite direction: Jets::Controller::Handler::Apigw#rack_vars -module Jets::Controller::Middleware - class Mimic - extend Memoist - - def initialize(app) - @app = app - end - - def call(env) - matcher = Jets::Router::Matcher.new - route = matcher.find_by_env(env) # simpler version does not check constraints - return not_found(env) unless route - - # Make instance available so we dont not have to pass it around. - # Also important to assign to instance variables so values are memoized in variables - # So the mimic logic work because @env.merge! does not happened - # until end. We do not want apigw.controller to create a new controller instance - # with a different object_id - apigw = Apigw.new(route, env) # takes rack env and converts to API Gateway event structure - @controller = apigw.controller - @meth = apigw.meth - @event = apigw.event - @context = apigw.context - - route = matcher.find_by_request(@controller.request) # need request object to check constraints - return not_found(env) unless route - - if route.to == 'jets/rack#process' # megamode - # Bypass the Jets middlewares since it could interfere with the Rack - # application's middleware stack. - # - # Jets sends back a transfer-encoding=chunked. Curling Jets directly works, - # but passing the Jets response back through this middleware results in errors. - # Disable chunking responses by deleting the transfer-encoding response header. - # Would like to understand why this happens this better, if someone can explain please let me know. - status, headers, body = @controller.dispatch! # jets/rack_controller - headers.delete "transfer-encoding" - [status, headers, body] - elsif route.to == 'jets/mount#call' # mount route - status, headers, body = @controller.dispatch! # jets/mount_controller - [status, headers, body] - elsif polymorphic_function? - run_polymophic_function - else # Normal Jets request - unless on_aws?(env) - # Only set these variables when running locally and not on AWS. - # On AWS, these are set by the Jets::Controller::Handler::Apigw#rack_vars - env.merge!( - 'jets.controller' => @controller, # mimic controller instance - 'jets.context' => @context, # mimic context - 'jets.event' => @event, # mimic event - 'jets.meth' => @meth, - ) - end - @app.call(env) # goes to all middleware all the way to Jets::Controller::Middleware::Main - end - end - - # Never hit when calling polymorphic function on AWS Lambda. Can only get called with the local server. - def polymorphic_function? - return false if ENV['_HANDLER'] # slight speed improvement on Lambda - polymorphic_function.definition.lang != :ruby - end - - def polymorphic_function - # Abusing Poly to run polymorphic code, should call LambdaExecutor directly - # after reworking LambdaExecutor so it has a better interface. - Jets::Poly.new(@controller.class, @meth) - end - memoize :polymorphic_function - - def run_polymophic_function - resp = polymorphic_function.run(@event, @meth) # polymorphic code - status = resp['statusCode'] - headers = resp['headers'] - body = StringIO.new(resp['body']) - [status, headers, body] # triplet - end - - def on_aws?(env) - return true if ENV['ON_AWS'] - return false if Jets.env.test? # usually with test we're passing in full API Gateway fixtures with the HTTP_X_AMZN_TRACE_ID - return false if ENV['JETS_ELB'] # If we're using an ELB and Jets is inside a container running jets server, we don't want to pretend we're on AWS. - on_cloud9 = !!(env['HTTP_HOST'] =~ /cloud9\..*\.amazonaws\.com/) - !!env['HTTP_X_AMZN_TRACE_ID'] && !on_cloud9 - end - - def routes_error_message(env) - message = "

404 Error: Route #{env['PATH_INFO'].sub('/','')} not found

" - if Jets.env != "production" - message << "

Here are the routes defined in your application:

" - message << "#{routes_table}" - end - message - end - - def not_found(env) - raise Jets::Controller::RoutingError, "No route matches #{env['PATH_INFO'].inspect}" - end - end -end diff --git a/lib/jets/controller/middleware/mimic/apigw.rb b/lib/jets/controller/middleware/mimic/apigw.rb deleted file mode 100644 index c613c37ab..000000000 --- a/lib/jets/controller/middleware/mimic/apigw.rb +++ /dev/null @@ -1,130 +0,0 @@ -# Takes a Rack env and converts to ApiGateway event -# This runs only locally, when not on AWS Lambda -# Note: Logic in the opposite direction: Jets::Controller::Handler::Apigw#rack_vars -class Jets::Controller::Middleware::Mimic - class Apigw - extend Memoist - - def initialize(route, env) - @route, @env = route, env - end - - # Actual controller instance - def controller - controller_class = @route.controller_name.constantize - meth = @route.action_name - # must keep the same env as @env, rack env, else constraint lambda proc request will be different - controller_class.new(event, context, meth, @env) - end - memoize :controller # for same object_id in case apigw.controller is called twice - - def meth - @route.action_name - end - memoize :meth - - def context - LambdaContext.new - end - memoize :context - - def event - resource = @route.path(:api_gateway) # /posts/{id}/edit - path = @env['PATH_INFO'].sub('/','') # remove beginning slash - { - "resource" => resource, # "/posts/{id}/edit" - "path" => @env['PATH_INFO'], # /posts/tung/edit - "httpMethod" => @env['REQUEST_METHOD'], # GET - "headers" => request_headers, - "queryStringParameters" => query_string_parameters, - "multiValueQueryStringParameters" => multi_value_query_string_parameters, - "pathParameters" => @route.extract_parameters(path), - "stageVariables" => nil, - "requestContext" => {}, - "body" => get_body, - "isBase64Encoded" => false, - } - end - memoize :event - alias convert event - - protected - # Annoying. The headers part of the AWS Lambda proxy structure - # does not consisently use the same casing scheme for the header keys. - # Sometimes it looks like this: - # Accept-Encoding - # and sometimes it looks like this: - # cache-control - # Map for special cases when the casing doesn't match. - CASING_MAP = { - "Cache-Control" => "cache-control", - "Content-Type" => "content-type", - "Origin" => "origin", - "Upgrade-Insecure-Requests" => "upgrade-insecure-requests", - } - - # Map rack env headers to Api Gateway event headers. Most rack env headers are - # prepended by HTTP_. - # - # Some API Gateway Lambda Proxy are also in the rack env headers. Example: - # - # "X-Amz-Cf-Id": "W8DF6J-lx1bkV00eCiBwIq5dldTSGGiG4BinJlxvN_4o8fCZtbsVjw==", - # "X-Amzn-Trace-Id": "Root=1-5a0dc1ac-58a7db712a57d6aa4186c2ac", - # "X-Forwarded-For": "88.88.88.88, 54.239.203.117", - # "X-Forwarded-Port": "443", - # "X-Forwarded-Proto": "https", - # - # For sample dump of the event headers, check out: - # spec/fixtures/samples/event-headers-form-post.json - # - # We generally do add those API Gateway Lambda specific headers because - # they would be fake anyway and by not adding them we can distinguish a - # local request from a remote request on API Gateway. - def request_headers - headers = @env.select { |k,v| k =~ /^HTTP_/ }.inject({}) do |h,(k,v)| - # map things like HTTP_USER_AGENT to "User-Agent" - key = k.sub('HTTP_','').split('_').map(&:capitalize).join('-') - h[key] = v - h - end - # Content type is not prepended with HTTP_ - headers["Content-Type"] = @env["CONTENT_TYPE"] if @env["CONTENT_TYPE"] - - # Adjust the casing so it matches the Lambda AWS Proxy structure - CASING_MAP.each do |nice_casing, bad_casing| - if headers.key?(nice_casing) - headers[bad_casing] = headers.delete(nice_casing) - end - end - - headers - end - - def query_string_parameters - Rack::Utils.parse_nested_query(@env['QUERY_STRING']) - # @env['QUERY_STRING']&.split('&')&.each_with_object({}) do |parameter, hash| - # key, value = parameter.split('=') - # hash[key] = value - # end || {} - end - alias multi_value_query_string_parameters query_string_parameters - - # def multi_value_query_string_parameters - # @env['QUERY_STRING']&.split('&')&.each_with_object({}) do |parameter, hash| - # key, value = parameter.split('=') - # hash[key] = [] if hash[key].nil? - # hash[key] << value - # end || {} - # end - - # To get the post body: - # rack.input: # - def get_body - input = @env["rack.input"] || StringIO.new - body = input.read - input.rewind # IMPORTANT or else it screws up other middlewares that use the body - # return nil for blank string, because Lambda AWS_PROXY does this - body unless body.empty? - end - end -end diff --git a/lib/jets/controller/middleware/mimic/lambda_context.rb b/lib/jets/controller/middleware/mimic/lambda_context.rb deleted file mode 100644 index 1955522e2..000000000 --- a/lib/jets/controller/middleware/mimic/lambda_context.rb +++ /dev/null @@ -1,44 +0,0 @@ -class Jets::Controller::Middleware::Mimic - # This class uses method_missing to mimic for all possible future methods - # that AWS Lambda might add to the context object. - # - # The current methods available on the Lambda Context object: - # aws_request_id, invoked_function_arn, log_group_name, - # log_stream_name, function_name, memory_limit_in_mb, function_version, - # identity, client_context, deadline_ms - # - # Locally, example mimic values: - # - # context.aws_request_id "mimic lambda context: aws_request_id" - # context.invoked_function_arn "mimic lambda context: invoked_function_arn" - # context.log_group_name "mimic lambda context: log_group_name" - # context.log_stream_name "mimic lambda context: log_stream_name" - # context.function_name "mimic lambda context: function_name" - # context.memory_limit_in_mb "mimic lambda context: memory_limit_in_mb" - # context.function_version "mimic lambda context: function_version" - # context.identity "mimic lambda context: identity" - # context.client_context "mimic lambda context: client_context" - # context.deadline_ms "mimic lambda context: deadline_ms" - # - # On AWS, example real values: - # - # context.aws_request_id "b8357b1d-15f2-4197-9c6b-d6873be5eaba" - # context.invoked_function_arn "arn:aws:lambda:us-west-2:112233445566:function:demo-dev-controller" - # context.log_group_name "/aws/lambda/demo-dev-controller" - # context.log_stream_name "2023/09/10/[$LATEST]18323c481c674665b25496c329daa382" - # context.function_name "demo-dev-controller" - # context.memory_limit_in_mb "1536" - # context.function_version "$LATEST" - # context.identity nil - # context.client_context nil - # context.deadline_ms 1694353587386 - # - class LambdaContext - def initialize(*) - end - - def method_missing(method_name, *args) - "mimic lambda context: #{method_name}" - end - end -end diff --git a/lib/jets/controller/middleware/webpacker_setup.rb b/lib/jets/controller/middleware/webpacker_setup.rb deleted file mode 100644 index b0d42a91b..000000000 --- a/lib/jets/controller/middleware/webpacker_setup.rb +++ /dev/null @@ -1,6 +0,0 @@ -# Breaking all the rules to get the beautiful webpacker middleware working - -require "webpacker" -require "webpacker/dev_server_proxy" - -Webpacker.bootstrap # whenever jets server is runs should Webpacker.bootstrap diff --git a/lib/jets/controller/rack_adapter/action.rb b/lib/jets/controller/rack_adapter/action.rb deleted file mode 100644 index 9187c5a97..000000000 --- a/lib/jets/controller/rack_adapter/action.rb +++ /dev/null @@ -1,35 +0,0 @@ -module Jets::Controller::RackAdapter - module Action - extend ActiveSupport::Concern - - class_methods do - def action(name) - lambda do |env| - route = find_first_route_for_action(name) - env['PATH_INFO'] = route_path(route) - controller = new({}, {}, name, env) - controller.dispatch! # does not go through middleware stack - end - end - - # Find first route to be used for PATH_INFO. - # This allows engine_delegate to work properly. - def find_first_route_for_action(name) - Jets::Router.routes.find do |r| - route_controller_path, route_action_name = r.to.split('#') - route_controller_path == controller_path && route_action_name == name.to_s - end - end - - def route_path(route) - if route - path = route.path - path = "/#{path}" unless path.starts_with?('/') - else - path = "/#{controller_path}##{name} (action proc)" - end - path - end - end - end -end diff --git a/lib/jets/controller/rack_adapter/env.rb b/lib/jets/controller/rack_adapter/env.rb deleted file mode 100644 index 672506479..000000000 --- a/lib/jets/controller/rack_adapter/env.rb +++ /dev/null @@ -1,145 +0,0 @@ -require 'rack' -require 'base64' - -# Only called by AWS Lambda -# Takes an ApiGateway event and converts it to an Rack env that can be used for rack middleware -module Jets::Controller::RackAdapter - class Env - def initialize(event, context, options={}) - @event, @context, @options = event, context, options - end - - def convert - options = {} - options = add_top_level(options) - options = add_http_headers(options) - path = path_with_base_path || @event['path'] || '/' # always set by API Gateway but might not be when testing shim, so setting it to make testing easier - - # In case of non-ascii characters, CGI.escape will escape them - # IE: get '/ほげ' - # This just because MockRequest.env_for uses URI.parse which does not - # escape non-ascii characters and throws an error - unescaped_path = path - path = path.chars.map { |char| char.ascii_only? ? char : CGI.escape(char) }.join - env = Rack::MockRequest.env_for(path, options) - env['PATH_INFO'] = unescaped_path - # env['QUERY_STRING'] = query_string - env - end - - private - def path_with_base_path - resource = @event['resource'] - pathParameters = @event['pathParameters'] - - if(!pathParameters.nil? and !resource.nil?) - resource = pathParameters.reduce(resource) {|resource, parameter| - key, value = parameter - resource = resource.gsub("{#{key}+}", value) - resource = resource.gsub("{#{key}}", value) - resource - } - end - - resource - end - - def add_top_level(options) - map = { - 'CONTENT_TYPE' => content_type, - 'QUERY_STRING' => query_string, - 'REMOTE_ADDR' => headers['X-Forwarded-For'], - 'REMOTE_HOST' => headers_host, - 'REQUEST_METHOD' => @event['httpMethod'] || 'GET', # useful to default to GET when testing with Lambda console - 'REQUEST_PATH' => @event['path'], - 'REQUEST_URI' => request_uri, - 'SCRIPT_NAME' => "", - 'SERVER_NAME' => headers_host, - 'SERVER_PORT' => headers['X-Forwarded-Port'], - 'SERVER_PROTOCOL' => "HTTP/1.1", # unsure if this should be set - 'SERVER_SOFTWARE' => headers['User-Agent'], - } - - map['CONTENT_LENGTH'] = content_length if content_length - # Even if not set, Rack always assigns an StringIO to "rack.input" - map['rack.input'] = StringIO.new(body) if body - - options.merge(map) - end - - # Generally should use content_type method instead of headers['Content-Type'] because - # headers['Content-Type'] is not normally available for Rails. - # - # Jets makes it available and normalizes the casing by grabbing it from APIGW event['headers'] - # APIGW is inconsistent about the casing. - # In POST request it's content-type and in GET request it's Content-Type - # - # Rack (local) uses Content-Type and APIGW (remote) uses content-type - # Rack::MethodOverride relys on content-type to detect content type properly. - # - def content_type - headers['Content-Type'] || headers['content-type'] - end - - def content_length - bytesize = body.bytesize.to_s if body - headers['Content-Length'] || bytesize - end - - # Decoding base64 from API Gateaway if necessary - # Rack will be none the wiser - def body - if @event['isBase64Encoded'] - Base64.decode64(@event['body']) - else - @event['body'] - end - end - - def add_http_headers(options) - headers.each do |k,v| - # content-type => HTTP_CONTENT_TYPE - key = k.gsub('-','_').upcase - key = "HTTP_#{key}" - if k == "Host" - options[key] = headers_host - else - options[key] = v - end - end - options - end - - def headers_host - Jets.config.app.domain || headers['Host'] - end - - def request_uri - # IE: "http://localhost:8888/posts/tung/edit?foo=bar" - proto = headers['X-Forwarded-Proto'] - host = headers_host - port = headers['X-Forwarded-Port'] - - # Add port if needed - if proto == 'https' && port != '443' or - proto == 'http' && port != '80' - host = "#{host}:#{port}" - end - - path = @event['path'] - qs = "?#{query_string}" unless query_string.empty? - "#{proto}://#{host}#{path}#{qs}" - end - - def query_string - query = @event["queryStringParameters"] || @event["multiValueQueryStringParameters"] || {} # always set with API Gateway but when testing node shim might not be - Rack::Utils.build_nested_query(query) - # query.map { |k,v| "#{k}=#{v}" }.join('&') - end - - # request headers - def headers - @event['headers'] || {} - end - end -end diff --git a/lib/jets/controller/request.rb b/lib/jets/controller/request.rb deleted file mode 100644 index 5e5829b13..000000000 --- a/lib/jets/controller/request.rb +++ /dev/null @@ -1,46 +0,0 @@ -require 'rack/request' - -module Jets::Controller - class Request - include Rack::Request::Helpers - include Compat::Request - include Compat::Params - - include ActionDispatch::Flash::RequestMethods - include ActionDispatch::Http::Cache::Request - include ActionDispatch::Http::MimeNegotiation - include ActionDispatch::Http::FilterParameters # parameter_filter and filtered_parameters - include ActionDispatch::Http::URL - include ActionDispatch::RequestCookieMethods - include ActionDispatch::ContentSecurityPolicy::Request - include ActionDispatch::PermissionsPolicy::Request - include Rack::Request::Env - - # since jets delegates parameter_filter from controller to request - public :parameter_filter - - attr_reader :event, :env - attr_accessor :routes - def initialize(rack_env: nil, event: nil) - @rack_env = rack_env - @event = event - @env = normalize_env - super(@env) # Rack::Env module => super() - end - - def normalize_env - if @rack_env # already rack env - @rack_env # rack_env is from Controller.action => lambda { |env| .. } - else - Jets::Controller::RackAdapter::Env.new(@event, {}).convert # convert to Rack env - end - end - - # When request hits the middleware Controller::RackAdapter::Middleware::Main endpoint - # We updated env since it could had been mutated down the middleware stack - # from Mimic to Main. - def set_env!(env) - @env = env - end - end -end diff --git a/lib/jets/controller/request/compat/params.rb b/lib/jets/controller/request/compat/params.rb deleted file mode 100644 index 950e4e81c..000000000 --- a/lib/jets/controller/request/compat/params.rb +++ /dev/null @@ -1,144 +0,0 @@ -require "action_dispatch/request/utils" -require "rack" - -module Jets::Controller::Request::Compat - # Provides these 3 methods to make it Rails compatible: - # - # request_parameters - # query_parameters - # path_parameters - # - module Params - extend Memoist - - delegate :normalize_encode_params, to: ActionDispatch::Request::Utils - def set_binary_encoding(params) - controller = path_parameters[:controller] - action = path_parameters[:action] - ActionDispatch::Request::Utils.set_binary_encoding(self, params, controller, action) - end - - # Merge all the parameters together for convenience. - # Users still have access via events. - # - # Precedence: - # 1. path parameters have highest precdence - # 2. query string parameters - # 3. body parameters - def parameters(include_path_params: true, include_body_params: true) - params = {} - params = params.deep_merge(request_parameters) if include_body_params - params = params.deep_merge(unescape_recursively(query_parameters)) # always - params = params.deep_merge(path_parameters) if include_path_params - params = set_binary_encoding(params) - params = normalize_encode_params(params) - params - end - memoize :parameters - alias params parameters - - def path_parameters - path_params = event["pathParameters"] || {} - path_params.merge!(path_parameters_defaults) - path_params = path_params.map { |k, path| [k, CGI.unescape(path.to_s)] }.to_h - end - - def path_parameters_defaults - route = Jets::Router::Matcher.new.find_by_request(self) - if route - controller = route.controller_name.delete_suffix("Controller").underscore - { - controller: controller, - action: route.action_name, - }.merge(route.resolved_defaults) - else - {} - end - end - - # Based on ActionDispatch::Request#GET https://bit.ly/48hvDwe - # Override Rack's GET method to support indifferent access. - def GET - fetch_header("action_dispatch.request.query_parameters") do |k| - params = event["queryStringParameters"] || {} - set_binary_encoding(params) - set_header k, normalize_encode_params(params) - end - end - alias query_parameters GET - - # Based on ActionDispatch::Request#POST https://bit.ly/48fxh1A - # Override Rack's POST method to support indifferent access. - def POST - fetch_header("action_dispatch.request.request_parameters") do - params = get_request_parameters - self.request_parameters = normalize_encode_params(params) - end - end - alias request_parameters POST - - def request_parameters=(params) - raise if params.nil? - set_header("action_dispatch.request.request_parameters", params) - end - - def get_request_parameters - body = event['isBase64Encoded'] ? base64_decode(event["body"]) : event["body"] - return {} if body.nil? - - parsed_json = parse_json(body) - return parsed_json if parsed_json - - if content_type.to_s.include?("application/x-www-form-urlencoded") - return ::Rack::Utils.parse_nested_query(body) - elsif content_type.to_s.include?("multipart/form-data") - return parse_multipart(body) - end - - {} # fallback to empty Hash - end - memoize :get_request_parameters - - # jets specific - def request_method_from_hidden_method_field - get_request_parameters["_method"].to_s.upcase if get_request_parameters["_method"] - end - - private - - def parse_multipart(body) - boundary = ::Rack::Multipart::Parser.parse_boundary(content_type) - options = multipart_options(body, boundary) - env = ::Rack::MockRequest.env_for("/", options) - ::Rack::Multipart.parse_multipart(env) # params Hash - end - - def multipart_options(data, boundary = "AaB03x") - type = %(multipart/form-data; boundary=#{boundary}) - length = data.bytesize - - { "CONTENT_TYPE" => type, - "CONTENT_LENGTH" => length.to_s, - :input => StringIO.new(data) } - end - - def parse_json(text) - JSON.parse(text) - rescue JSON::ParserError - nil - end - - def base64_decode(body) - return nil if body.nil? - Base64.decode64(body) - end - - def unescape_recursively(obj) - case obj - when Hash then obj.map { |k, v| [k, unescape_recursively(v)] }.to_h - when Array then obj.map { |v| unescape_recursively(v) } - else CGI.unescape(obj.to_s) - end - end - end -end diff --git a/lib/jets/controller/request/compat/request.rb b/lib/jets/controller/request/compat/request.rb deleted file mode 100644 index be90e1a5a..000000000 --- a/lib/jets/controller/request/compat/request.rb +++ /dev/null @@ -1,125 +0,0 @@ -module Jets::Controller::Request::Compat - # Based on ActionDispatch::Request class - module Request - extend Memoist - - ENV_METHODS = %w[ AUTH_TYPE GATEWAY_INTERFACE - PATH_TRANSLATED REMOTE_HOST - REMOTE_IDENT REMOTE_USER REMOTE_ADDR - SERVER_NAME SERVER_PROTOCOL - ORIGINAL_SCRIPT_NAME - - HTTP_ACCEPT HTTP_ACCEPT_CHARSET HTTP_ACCEPT_ENCODING - HTTP_ACCEPT_LANGUAGE HTTP_CACHE_CONTROL HTTP_FROM - HTTP_NEGOTIATE HTTP_PRAGMA HTTP_CLIENT_IP - HTTP_X_FORWARDED_FOR HTTP_ORIGIN HTTP_VERSION - HTTP_X_CSRF_TOKEN HTTP_X_REQUEST_ID HTTP_X_FORWARDED_HOST - ].freeze - - ENV_METHODS.each do |env| - class_eval <<-METHOD, __FILE__, __LINE__ + 1 - # frozen_string_literal: true - def #{env.delete_prefix("HTTP_").downcase} # def accept_charset - get_header "#{env}" # get_header "HTTP_ACCEPT_CHARSET" - end # end - METHOD - end - - LOCALHOST = Regexp.union [/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, /^::1$/, /^0:0:0:0:0:0:0:1(%.*)?$/] - - def session - key = ActionDispatch::Request::Session::ENV_SESSION_KEY - session = env[key] - if session.nil? # controller dispatch! does not go through middleware - # mock session that is disabled - session = ActionDispatch::Request::Session.new(nil, self, enabled: false) - end - session - end - - def session=(session) # :nodoc: - ActionDispatch::Request::Session.set self, session - end - - def session_options=(options) - ActionDispatch::Request::Session::Options.set self, options - end - - def reset_session - session.destroy - end - - def headers - top_level_headers.merge(http_headers) - end - - def http_headers - Hash[*env.select {|k,v| k.start_with? 'HTTP_'} - .collect {|k,v| [k.sub(/^HTTP_/, ''), v]} - .collect {|k,v| [k.split('_').collect(&:capitalize).join('-'), v]} - .sort - .flatten] - end - - # These are not part of a Rails headers but believe - # they make sense to add as it feel like expected behavior. - def top_level_headers - hash = { - "Host" => env["HTTP_HOST"], - "Content-Type" => env["CONTENT_TYPE"], - "Content-Length" => env["CONTENT_LENGTH"], - } - hash.delete_if { |k, v| v.nil? } - hash - end - - # Returns the lowercase name of the HTTP server software. - def server_software - (get_header("SERVER_SOFTWARE") && /^([a-zA-Z]+)/ =~ get_header("SERVER_SOFTWARE")) ? $1.downcase : nil - end - - # Returns the authorization header regardless of whether it was specified directly or through one of the - # proxy alternatives. - def authorization - get_header("HTTP_AUTHORIZATION") || - get_header("X-HTTP_AUTHORIZATION") || - get_header("X_HTTP_AUTHORIZATION") || - get_header("REDIRECT_X_HTTP_AUTHORIZATION") - end - - # True if the request came from localhost, 127.0.0.1, or ::1. - def local? - LOCALHOST.match?(remote_addr) && LOCALHOST.match?(remote_ip) - end - - def controller_class_for(name) - controller_param = name.underscore - const_name = controller_param.camelize << "Controller" - const_name.constantize - end - - def request_method_symbol - request_method.downcase.to_sym - end - alias method_symbol request_method_symbol - - # Returns the IP address of client as a +String+, - # usually set by the RemoteIp middleware. - def remote_ip - @remote_ip ||= (get_header("action_dispatch.remote_ip") || ip).to_s - end - - def remote_ip=(remote_ip) - @remote_ip = nil - set_header "action_dispatch.remote_ip", remote_ip - end - - # Needed when action_view.preload_links_header = true - # Rails 6.1 defaults - def send_early_hints(links) - return unless env["rack.early_hints"] - - env["rack.early_hints"].call(links) - end - end -end diff --git a/lib/jets/controller/response.rb b/lib/jets/controller/response.rb deleted file mode 100644 index d7c72a105..000000000 --- a/lib/jets/controller/response.rb +++ /dev/null @@ -1,144 +0,0 @@ -require 'action_dispatch/http/cache' -require 'action_dispatch/http/filter_redirect' -require 'monitor' -require 'rack/response' -require 'rack/utils' - -module Jets::Controller - # The response object. See Rack::Response and Rack::Response::Helpers for - # more info: - # http://rubydoc.info/github/rack/rack/master/Rack/Response - # http://rubydoc.info/github/rack/rack/master/Rack/Response/Helpers - class Response < ::Rack::Response - include Compat::Response - - include ActionDispatch::Http::FilterRedirect - include ActionDispatch::Http::Cache::Response - include MonitorMixin - - attr_accessor :request - - def initialize(*) - super - prepare_cache_control! - end - - # What Rack::Response#initialize does. - def body=(body) - if body.nil? - @body = [] - @buffered = true - @length = 0 - elsif body.respond_to?(:to_str) - @body = [body] - @buffered = true - @length = body.to_str.bytesize - else - @body = body - @buffered = false - @length = 0 - end - @body - end - - # Sets the HTTP status code. - def status=(status) - @status = Rack::Utils.status_code(status) - end - - def to_a - commit! - rack_response status, headers.to_hash - end - - def committed?; synchronize { @committed }; end - # sending? and send? are for compatibility but not used for Jets - def sending?; synchronize { @sending }; end - def sent?; synchronize { @sent }; end - - def commit! - synchronize do - before_committed - @committed = true - end - end - - def before_committed - assign_default_content_type_and_charset! - merge_and_normalize_cache_control!(@cache_control) - handle_conditional_get! - handle_no_content! - end - - def assign_default_content_type_and_charset! - return if media_type - - ct = parsed_content_type_header - set_content_type(ct.mime_type || Mime[:html].to_s, - ct.charset || self.class.default_charset) - end - - def handle_no_content! - if NO_CONTENT_CODES.include?(@status) - headers.delete "Content-Length" - end - end - - NO_CONTENT_CODES = [100, 101, 102, 103, 204, 205, 304] - def rack_response(status, header) - if NO_CONTENT_CODES.include?(status) - [status, header, []] - else - [status, header, body] - end - end - - def each - block_given? ? super : enum_for(:each) - end - - def finish - result = body - - if drop_content_info? - headers.delete "Content-Length" - headers.delete "Content-Type" - end - - if drop_body? - close - result = [] - end - - if calculate_content_length? - # if some other code has already set Content-Length, don't muck with it - # currently, this would be the static file-handler - headers["Content-Length"] = body.inject(0) { |l, p| l + p.bytesize }.to_s - end - - [status.to_i, headers, result] - end - - # The response code of the request. - alias response_code status - - def media_type - headers["Content-Type"] - end - - private - - def calculate_content_length? - headers["Content-Type"] and not headers["Content-Length"] and Array === body - end - - def drop_content_info? - status.to_i / 100 == 1 or drop_body? - end - - DROP_BODY_RESPONSES = [204, 304] - def drop_body? - DROP_BODY_RESPONSES.include?(status.to_i) - end - end -end \ No newline at end of file diff --git a/lib/jets/controller/response/compat/response.rb b/lib/jets/controller/response/compat/response.rb deleted file mode 100644 index 408130c7c..000000000 --- a/lib/jets/controller/response/compat/response.rb +++ /dev/null @@ -1,67 +0,0 @@ -module Jets::Controller::Response::Compat - module Response - extend ActiveSupport::Concern - - included do - # Must come before include ActionDispatch::Http::Cache::Response - # Aliasing these off because AD::Http::Cache::Response defines them. - alias :_cache_control :cache_control - alias :_cache_control= :cache_control= - end - - class_methods do - cattr_accessor :default_charset, default: "utf-8" - end - - ContentTypeHeader = Struct.new :mime_type, :charset - NullContentTypeHeader = ContentTypeHeader.new nil, nil - - CONTENT_TYPE = "Content-Type" - CONTENT_TYPE_PARSER = / - \A - (?[^;\s]+\s*(?:;\s*(?:(?!charset)[^;\s])+)*)? - (?:;\s*charset=(?"?)(?[^;\s]+)\k)? - /x # :nodoc: - - def parse_content_type(content_type) - if content_type && match = CONTENT_TYPE_PARSER.match(content_type) - ContentTypeHeader.new(match[:mime_type], match[:charset]) - else - NullContentTypeHeader - end - end - - # Small internal convenience method to get the parsed version of the current - # content type header. - def parsed_content_type_header - parse_content_type(get_header("Content-Type")) - end - - def set_content_type(content_type, charset) - type = content_type || "" - type = "#{type}; charset=#{charset.to_s.downcase}" if charset - set_header CONTENT_TYPE, type - end - - # Sets the HTTP character set. In case of +nil+ parameter - # it sets the charset to +default_charset+. - # - # response.charset = 'utf-16' # => 'utf-16' - # response.charset = nil # => 'utf-8' - def charset=(charset) - content_type = parsed_content_type_header.mime_type - if false == charset - set_content_type content_type, nil - else - set_content_type content_type, charset || self.class.default_charset - end - end - - # The charset of the response. HTML wants to know the encoding of the - # content you're giving them, so we need to send that along. - def charset - header_info = parsed_content_type_header - header_info.charset || self.class.default_charset - end - end -end diff --git a/lib/jets/core.rb b/lib/jets/core.rb index 7cc742e0b..4b58ffed6 100644 --- a/lib/jets/core.rb +++ b/lib/jets/core.rb @@ -1,221 +1,66 @@ module Jets::Core extend Memoist - mattr_accessor :cache - - delegate :aws, :autoloaders, :config, to: :application - - @application = @app_class = nil - - attr_writer :application - attr_accessor :app_class - def application - @application ||= (app_class.instance if app_class) - end - - def backtrace_cleaner - @backtrace_cleaner ||= Jets::BacktraceCleaner.new - end - - # The Configuration instance used to configure the Rails environment - def configuration - application.config - end - def root - root = ENV['JETS_ROOT'].to_s - root = Dir.pwd if root == '' + root = ENV["JETS_ROOT"].to_s + root = Dir.pwd if root == "" Pathname.new(root) end + memoize :root def env - env = ENV['JETS_ENV'] || 'development' - ENV['RAILS_ENV'] = ENV['RACK_ENV'] = env + env = ENV["JETS_ENV"] || "dev" ActiveSupport::StringInquirer.new(env) end memoize :env - @@extra_warning_shown = false def extra - # Keep for backwards compatibility - unless ENV['JETS_ENV_EXTRA'].blank? - puts "DEPRECATION WARNING: JETS_ENV_EXTRA is deprecated. Use JETS_EXTRA instead.".color(:yellow) unless @@extra_warning_shown - @@extra_warning_shown = true - extra = ENV['JETS_ENV_EXTRA'] - end - extra = ENV['JETS_EXTRA'] unless ENV['JETS_EXTRA'].blank? - extra + ENV["JETS_EXTRA"] unless ENV["JETS_EXTRA"].blank? end - def project_name - path = "config/project_name" - if ENV['JETS_PROJECT_NAME'] && !ENV['JETS_PROJECT_NAME'].blank? - ENV['JETS_PROJECT_NAME'] - elsif File.exist?(path) - IO.read(path).strip - elsif parsed_project_name - parsed_project_name - else - Dir.pwd.split("/").last # conventionally infer app name from current directory - end + def project + Config::Project.instance end - def project_namespace - [project_name, short_env, extra].compact.join('-').gsub('_','-') + def bootstrap + Config::Bootstrap.instance end - def table_namespace - [project_name, short_env].compact.join('-') + def shim + Jets::Shim end - ENV_MAP = { - development: 'dev', - production: 'prod', - staging: 'stag', - } - def short_env - ENV_MAP[Jets.env.to_sym] || Jets.env + # Load project and app config files + def boot + Jets::Core::Booter.boot! end - # Double evaling config/application.rb causes subtle issues: - # * double loading of shared resources: Jets::Stack.subclasses will have the same - # class twice when config is called when declaring a function - # * forces us to rescue all exceptions, which is a big hammer - # - # Lets parse for the project name instead for now. - # - # Keep for backwards compatibility - def parsed_project_name - lines = IO.readlines("#{Jets.root}/config/application.rb") - project_name_line = lines.find { |l| l =~ /config\.project_name.*=/ && l !~ /^\s+#/ } - if project_name_line - parsed = project_name_line.gsub(/.*=/,'').strip - # The +? makes it non-greedy - # See: https://ruby-doc.org/core-2.5.1/Regexp.html#class-Regexp-label-Repetition - md = parsed.match(/['"](.+?)['"]/) - md ? md[1] : raise("Unable to parse project name from config/application.rb: #{project_name_line}") - end + # delegate :aws, :autoloaders, :config, to: :application + def aws + Jets::AwsServices::AwsInfo.new end - memoize :parsed_project_name + memoize :aws - # Load all application base classes and project classes - def boot - Jets::Booter.boot! + # Frameworks can set Jets.logger to use their own logger. + cattr_accessor :logger + def logger + @logger ||= Jets.bootstrap.config.logger end def build_root - "/tmp/jets/#{Jets.project_name}".freeze + root = ENV["JETS_BUILD_ROOT"] || "/tmp/jets" + "#{root}/#{Jets.project.namespace}".freeze end memoize :build_root - def logger - @logger - end - - def logger=(logger) - @logger = logger + def s3_bucket + Jets::Cfn::Resource::S3::JetsBucket.name end def deprecator # :nodoc: @deprecator ||= ActiveSupport::Deprecation.new end - def webpacker? - Gem.loaded_specs.keys.any?{|k| k.start_with?("webpacker")} - end - memoize :webpacker? - - def version - Jets::VERSION - end - - # NOTE: In development this will always be 1 because the app gets reloaded. - # On AWS Lambda, this will be ever increasing until the container gets replaced. - @@call_count = 0 - def increase_call_count - @@call_count += 1 - end - - def call_count - @@call_count - end - - @@prewarm_count = 0 - def increase_prewarm_count - @@prewarm_count += 1 - end - - def prewarm_count - @@prewarm_count - end - - def poly_only? - return true if ENV['JETS_POLY_ONLY'] # bypass to allow rapid development of handlers - Jets::Cfn::Builder.poly_only? - end - - def custom_domain? - Jets.config.domain.hosted_zone_name - end - - def s3_events? - !Jets::Job::Base._s3_events.empty? - end - - def process(event, context, handler) - if event['_prewarm'] - Jets.increase_prewarm_count - Jets.logger.info("Prewarm request") - {prewarmed_at: Time.now.to_s} - else - Jets::Processors::MainProcessor.new(event, context, handler).run - end - end - - def once - boot - override_lambda_ruby_runtime - # require "jets/overrides/puma" # leaving around as a comment in case needed in the future - tmp_load! - end - - def tmp_load! - Jets::TmpLoader.load! - end - - def override_lambda_ruby_runtime - require "jets/overrides/lambda" - end - - def ruby_folder - RUBY_VERSION.split('.')[0..1].join('.') + '.0' - end - - # used to configure internal lambda functions - # current ruby runtime that user is running - # IE: ruby2.5 ruby2.7 - def ruby_runtime - version = RUBY_VERSION.split('.')[0..1].join('.') - "ruby#{version}" - end - - def one_lambda_per_controller? - Jets.config.cfn.build.controllers == "one_lambda_per_controller" - end - - def one_lambda_for_all_controllers? - Jets.config.cfn.build.controllers == "one_lambda_for_all_controllers" - end - - def gem_layer? - !Jets.poly_only? || Jets.one_lambda_for_all_controllers? - end - - # Do not memoize here. The JetsBucket.name does it's own special memoization. - def s3_bucket - Jets::Cfn::Resource::S3::JetsBucket.name - end - def report_exception(exception) # See Jets::ExceptionReporting decorate_exception_with_exception_reported! if exception.respond_to?(:with_exception_reported?) && exception.with_exception_reported? @@ -240,25 +85,4 @@ def error application && application.executor.error_reporter # ActiveSupport.error_reporter end - - # Returns a Pathname object of the public folder of the current - # \Jets project, otherwise it returns +nil+ if there is no project: - # - # Jets.public_path - # # => # - def public_path - application && Pathname.new(application.paths["public"].first) - end - - def autoloaders - application.autoloaders - end - - # It's useful to eager load and find out any error within the jets code immediately. - # Leaving in place because think the layer of protection is good. - # Eager load outside of a jets project can error. IE: `jets -h` - # Eager load inside a jets project is fine. - def eager_load_gem? - File.exist?("config/application.rb") || ENV['JETS_TEST'] || defined?(ENGINE_ROOT) # jets project - end end diff --git a/lib/jets/core/booter.rb b/lib/jets/core/booter.rb new file mode 100644 index 000000000..df99331a9 --- /dev/null +++ b/lib/jets/core/booter.rb @@ -0,0 +1,58 @@ +require "securerandom" + +module Jets::Core + class Booter + class << self + extend Memoist + + attr_reader :boot_at, :gid + def boot! + return false if @boot_at + + Jets::Bundle.require # require all the gems in the Gemfile + require_config(:project) # for config.dotenv.overwrite + + if require_bootstrap? + Jets::Dotenv.load! + require_config(:bootstrap) + end + + initialize! + + @gid = SecureRandom.uuid[0..7] + @boot_at = Time.now.utc + end + + def initialize! + main = Jets::Autoloaders.main + main.configure(Jets.root) + main.setup + end + + # Essentially deployment commands require the bootstrap to be run + def require_bootstrap? + args = ARGV.reject { |arg| arg.start_with?("-") } + %w[ + bootstrap + build + delete + deploy + dockerfile + release:rollback + ].include?(args.last) + end + + def require_config(name) + files = [ + "config/jets/#{name}.rb", + "config/jets/#{name}/#{Jets.env}.rb" + ] + files.each do |file| + next unless File.exist?(file) + require "#{Jets.root}/#{file}" + end + end + memoize :require_config + end + end +end diff --git a/lib/jets/core/config/base.rb b/lib/jets/core/config/base.rb new file mode 100644 index 000000000..bff1c4c49 --- /dev/null +++ b/lib/jets/core/config/base.rb @@ -0,0 +1,17 @@ +require "singleton" + +module Jets::Core::Config + class Base + extend Memoist + include Jets::Util::Camelize + include Singleton + + def configure(&block) + instance_eval(&block) if block + end + + def config + self + end + end +end diff --git a/lib/jets/core/config/bootstrap.rb b/lib/jets/core/config/bootstrap.rb new file mode 100644 index 000000000..639128754 --- /dev/null +++ b/lib/jets/core/config/bootstrap.rb @@ -0,0 +1,24 @@ +module Jets::Core::Config + class Bootstrap < Base + include Helpers + # config settings + include Cfn + include Code + include Codebuild + include S3Bucket + + attr_accessor :infra, :logger + def initialize(*) + super + @infra = false + @logger = default_logger + end + + def default_logger + logger = ActiveSupport::Logger.new($stderr) + logger.formatter = ActiveSupport::Logger::SimpleFormatter.new # no timestamps + logger.level = ENV["JETS_LOG_LEVEL"] || :info + logger + end + end +end diff --git a/lib/jets/core/config/bootstrap/cfn.rb b/lib/jets/core/config/bootstrap/cfn.rb new file mode 100644 index 000000000..8ba38fc9b --- /dev/null +++ b/lib/jets/core/config/bootstrap/cfn.rb @@ -0,0 +1,12 @@ +class Jets::Core::Config::Bootstrap + module Cfn + attr_accessor :cfn + + def initialize(*) + super + + @cfn = ActiveSupport::OrderedOptions.new + @cfn.resource_tags = {} # tags to add to all resources + end + end +end diff --git a/lib/jets/core/config/bootstrap/code.rb b/lib/jets/core/config/bootstrap/code.rb new file mode 100644 index 000000000..bb1bc748d --- /dev/null +++ b/lib/jets/core/config/bootstrap/code.rb @@ -0,0 +1,16 @@ +class Jets::Core::Config::Bootstrap + module Code + attr_accessor :code + + def initialize(*) + super + + @code = ActiveSupport::OrderedOptions.new + @code.copy = ActiveSupport::OrderedOptions.new + @code.copy.always_keep = ["config/jets/env"] + @code.copy.always_remove = ["tmp"] + @code.copy.strategy = "auto" + @code.copy.warn_large = true + end + end +end diff --git a/lib/jets/core/config/bootstrap/codebuild.rb b/lib/jets/core/config/bootstrap/codebuild.rb new file mode 100644 index 000000000..1f5ee9aa0 --- /dev/null +++ b/lib/jets/core/config/bootstrap/codebuild.rb @@ -0,0 +1,97 @@ +class Jets::Core::Config::Bootstrap + module Codebuild + attr_accessor :codebuild + + def initialize(*) + super + + @codebuild = ActiveSupport::OrderedOptions.new + @codebuild.project = ActiveSupport::OrderedOptions.new + @codebuild.project.compute_type = { + ComputeType: "BUILD_GENERAL1_SMALL", + Image: "aws/codebuild/amazonlinux2-aarch64-standard:3.0", + Type: "ARM_CONTAINER" + } + @codebuild.project.env = ActiveSupport::OrderedOptions.new + @codebuild.project.env.vars = {} + @codebuild.project.env.pass = [] + # Be careful about default env.pass + # Jets::Dotenv will not load these variables as a part of the Lambda Function + # So we should be specific about what JETS_ vars we want to the codebuild remote runner + # Should not use regexp /JETS_/ + @codebuild.project.env.default_pass = [ + "JETS_API", + "JETS_DOCKER_IMAGE", + "JETS_ENV", + "JETS_EXTRA", + "JETS_GO_VERSION", + "JETS_PROJECT", + "JETS_REMOTE_VERSION", + "JETS_RESET" + ] + @codebuild.project.env.block = [] + @codebuild.project.environment = {} + # fleet arn + @codebuild.project.fleet_override = ENV["JETS_CODEBUILD_FLEET_OVERRIDE"] + @codebuild.project.timeout_in_minutes = 60 + + # CodebuildLambda + # The only setting that is used for CodeBuildLambda compute_type + # Using a separate compute_type config because images are required to be different. + # A shared setting will not work. + # + # All other settings are shared between the CodeBuild and CodeBuildLambda projects + # IE: @codebuild.project.env.vars etc + # Think this keeps things simpler and easy to understand. + # IE: Less room for config errors. + @codebuild.lambda = ActiveSupport::OrderedOptions.new + @codebuild.lambda.enable = false + @codebuild.lambda.project = ActiveSupport::OrderedOptions.new + @codebuild.lambda.project.compute_type = { + ComputeType: "BUILD_LAMBDA_1GB", + Image: "aws/codebuild/amazonlinux-aarch64-lambda-standard:ruby3.2", + Type: "ARM_LAMBDA_CONTAINER" + } + + @codebuild.fleet = ActiveSupport::OrderedOptions.new + @codebuild.fleet.base_capacity = 1 + @codebuild.fleet.enable = !!ENV["JETS_CODEBUILD_FLEET_ENABLE"] + + @codebuild.logging = ActiveSupport::OrderedOptions.new + @codebuild.logging.final_phases = false + @codebuild.logging.show = "filtered" # filtered or all + # Note: Do not use .display It's an ActiveSuppport method + + @codebuild.iam = ActiveSupport::OrderedOptions.new + @codebuild.iam.policy = [] + @codebuild.iam.managed_policy = [] + @codebuild.iam.default_policy = %w[ + apigateway + cloudformation + cloudfront + codebuild + dynamodb + ecr + ecr-public + events + iam + lambda + logs + route53 + s3 + sns + sqs + sts:GetServiceBearerToken + waf + wafv2 + ] + @codebuild.iam.default_managed_policy = %w[ + AmazonSSMReadOnlyAccess + AWSCertificateManagerReadOnly + ] + @codebuild.iam.default_vpc_policy = %w[ + ec2 + ] + end + end +end diff --git a/lib/jets/core/config/bootstrap/s3_bucket.rb b/lib/jets/core/config/bootstrap/s3_bucket.rb new file mode 100644 index 000000000..d0b86d56c --- /dev/null +++ b/lib/jets/core/config/bootstrap/s3_bucket.rb @@ -0,0 +1,19 @@ +class Jets::Core::Config::Bootstrap + module S3Bucket + attr_accessor :s3_bucket + + def initialize(*) + super + + @s3_bucket = ActiveSupport::OrderedOptions.new + @s3_bucket.cors_configuration = { + CorsRules: [{ + AllowedHeaders: ["*"], + AllowedMethods: ["GET"], + AllowedOrigins: ["*"], + ExposedHeaders: [] + }] + } + end + end +end diff --git a/lib/jets/core/config/helpers.rb b/lib/jets/core/config/helpers.rb new file mode 100644 index 000000000..f0095d478 --- /dev/null +++ b/lib/jets/core/config/helpers.rb @@ -0,0 +1,5 @@ +module Jets::Core::Config + module Helpers + include Ssm + end +end diff --git a/lib/jets/core/config/helpers/ssm.rb b/lib/jets/core/config/helpers/ssm.rb new file mode 100644 index 000000000..5d3d64a56 --- /dev/null +++ b/lib/jets/core/config/helpers/ssm.rb @@ -0,0 +1,9 @@ +require "aws-sdk-ssm" + +module Jets::Core::Config::Helpers + module Ssm + def ssm_env + Jets::Dotenv::Convention.ssm_env + end + end +end diff --git a/lib/jets/core/config/info.rb b/lib/jets/core/config/info.rb new file mode 100644 index 000000000..fbec92c68 --- /dev/null +++ b/lib/jets/core/config/info.rb @@ -0,0 +1,27 @@ +require "singleton" + +module Jets::Core::Config + class Info + extend Memoist + include Singleton + + def data + data = File.exist?(path) ? YAML.load_file(path) : {} + ActiveSupport::HashWithIndifferentAccess.new(data) + end + memoize :data + + def method_missing(name, *args) + data.key?(name.to_sym) ? data[name.to_sym] : super + end + + def respond_to_missing?(name, include_private = false) + data.key?(name.to_sym) || super + end + + # Do not use absolute path. This is because the path is written to the stage/code area + def path + "config/jets/info.yml" + end + end +end diff --git a/lib/jets/core/config/project.rb b/lib/jets/core/config/project.rb new file mode 100644 index 000000000..1483e4277 --- /dev/null +++ b/lib/jets/core/config/project.rb @@ -0,0 +1,103 @@ +module Jets::Core::Config + # Intentionally keep this class simple. + # Frameworks should do the heavy lifting. + class Project < Base + attr_accessor( + :autoload_paths, + :ignore_paths, + :base64_encode, + :dotenv, + :tips + ) + def initialize(*) + @autoload_paths = Set.new(%w[ + app/events + app/extensions + shared/resources + shared/extensions + ]) + @ignore_paths = Set.new(%w[ + app/functions + shared/functions + ]) + # Jets does not implement the concept of eager_load_paths like Rails for simplicity. + # Also Jets always does an eager load of app code that it manages above. + # This is because shared/extensions and shared/resources needed to be defined + # early to generate the CloudFormation templates. + + @dotenv = ActiveSupport::OrderedOptions.new + @dotenv.ssm = ActiveSupport::OrderedOptions.new + @dotenv.ssm.autoload = ActiveSupport::OrderedOptions.new + @dotenv.ssm.autoload.default_skip = ["BASIC_AUTH_USERNAME", "BASIC_AUTH_PASSWORD", "BASIC_AUTH_CREDENTIALS"] + @dotenv.ssm.autoload.enable = true # autoloads parameters by path IE: /demo/dev/ + @dotenv.ssm.autoload.skip = [] + @dotenv.ssm.convention_resolver = nil # proc receives ssm_leaf_name + @dotenv.ssm.envs = ActiveSupport::OrderedOptions.new + @dotenv.ssm.envs.fallback = "dev" + @dotenv.ssm.envs.unique = ["dev", "prod"] + @dotenv.ssm.long_env_helper = false # for completeness + @dotenv.ssm.long_env_name = false # helps with Jets 5 legacy + + @base64_encode = true + + @tips = ActiveSupport::OrderedOptions.new + @tips.enable = true + @tips.concurrency_change = true + @tips.env_change = true + @tips.faster_deploy = true + @tips.remote_run = true + @tips.ssm_change = true + end + + attr_writer :name + def name + if ENV["JETS_PROJECT"] && !ENV["JETS_PROJECT"].blank? + return ENV["JETS_PROJECT"] + end + + # Too easy to call a method that requires the project name before Jets.boot. + # Ran into this a few times. + # IE: Ran into this with Jets API ping. That's no longer requires Jets.project.name + # But will leave this here in case we miss Jets.boot in the future. + Jets::Core::Booter.require_config(:project) + return @name if @name + + if jets_info_project_name + jets_info_project_name + else + # when not set, conventionally infer app name from current directory + @name_inferred = true + Dir.pwd.split("/").last.gsub(/[^a-zA-Z0-9_]/, "-").squeeze("-") + end + end + memoize :name + + def jets_info_project_name + info = Jets::Core::Config::Info.instance + info.project_name if info.respond_to?(:project_name) + end + + def name_inferred? + name # trigger memoization + !!@name_inferred + end + + def namespace + [name, Jets.env, Jets.extra].compact.join("-").tr("_", "-") + end + + def s3_bucket + Jets::Cfn::Resource::S3::JetsBucket.name + end + + def extension_paths + @autoload_paths.select { |path| path.ends_with?("/extensions") } + end + + # Useful for jets rails engine + # Used to ignore all paths managed by Jets + def all_load_paths + @autoload_paths + @ignore_paths + end + end +end diff --git a/lib/jets/core_ext.rb b/lib/jets/core_ext.rb index 422f36516..12f4eafec 100644 --- a/lib/jets/core_ext.rb +++ b/lib/jets/core_ext.rb @@ -1,4 +1,3 @@ require "jets/core_ext/bundler" require "jets/core_ext/file" require "jets/core_ext/symbol" -# Do not require jets/core_ext/kernel because it is a special case \ No newline at end of file diff --git a/lib/jets/core_ext/bundler.rb b/lib/jets/core_ext/bundler.rb index 4b44a0834..648edda37 100644 --- a/lib/jets/core_ext/bundler.rb +++ b/lib/jets/core_ext/bundler.rb @@ -1,7 +1,9 @@ # Bundler 2.0 does yet not have with_unbundled_env -# Bundler 2.1 deprecates with_unbundled_env for with_unbundled_env +# Bundler 2.1 deprecates with_clean_env for with_unbundled_env require "bundler" -def Bundler.with_unbundled_env(&block) - with_clean_env(&block) -end unless Bundler.respond_to?(:with_unbundled_env) +unless Bundler.respond_to?(:with_unbundled_env) + def Bundler.with_unbundled_env(&block) + with_clean_env(&block) + end +end diff --git a/lib/jets/core_ext/kernel.rb b/lib/jets/core_ext/kernel.rb deleted file mode 100644 index 4315f9d2e..000000000 --- a/lib/jets/core_ext/kernel.rb +++ /dev/null @@ -1,13 +0,0 @@ -module Kernel - module_function - - alias_method :jets_original_require, :require - # @param path [String] - # @return [Boolean] - def require(path) - # Hack to prevent Jets const from being defined - # Actionview requires "jets-html-sanitizer" and that creates a Jets module - path = "jets-html-sanitizer" if path == "rails-html-sanitizer" && !ENV['JETS_RAILS_CONST'] - jets_original_require(path) - end -end diff --git a/lib/jets/dev_caching.rb b/lib/jets/dev_caching.rb deleted file mode 100644 index d77a3e0f0..000000000 --- a/lib/jets/dev_caching.rb +++ /dev/null @@ -1,44 +0,0 @@ -# frozen_string_literal: true - -require "fileutils" - -module Jets - module DevCaching # :nodoc: - class << self - FILE = "tmp/caching-dev.txt" - - def enable_by_file - FileUtils.mkdir_p("tmp") - - if File.exist?(FILE) - delete_cache_file - puts "Development mode is no longer being cached." - else - create_cache_file - puts "Development mode is now being cached." - end - - FileUtils.touch "tmp/restart.txt" - end - - def enable_by_argument(caching) - FileUtils.mkdir_p("tmp") - - if caching - create_cache_file - elsif caching == false && File.exist?(FILE) - delete_cache_file - end - end - - private - def create_cache_file - FileUtils.touch FILE - end - - def delete_cache_file - File.delete FILE - end - end - end -end diff --git a/lib/jets/dotenv.rb b/lib/jets/dotenv.rb index fd6716c5e..65cf313e4 100644 --- a/lib/jets/dotenv.rb +++ b/lib/jets/dotenv.rb @@ -1,54 +1,59 @@ require "dotenv" -class Jets::Dotenv - def self.load!(remote = false) - new(remote).load! - end - - def initialize(remote = false) - @remote = ENV["JETS_ENV_REMOTE"] || remote - end - - # @@vars cache to prevent multiple calls to Ssm - # Tricky note: The cache also prevents the second call to Dotenv.load from - # returning {} vars. Dotenv 3.0 will not return the vars if it has already been loaded - # in the ENV. We want this side-effect due to the new way Dotenv 3.0 works. - @@vars = nil - def load! - return @@vars if @@vars - return if on_aws? # this prevents ssm calls if used in dotenv files - vars = ::Dotenv.load(*dotenv_files) - @@vars = Ssm.new(vars).interpolate! - end - - def on_aws? - return true if ENV["ON_AWS"] - !!ENV["_HANDLER"] # https://docs.aws.amazon.com/lambda/latest/dg/lambda-environment-variables.html - end - - # dotenv files with the following precedence: - # - # - .env.development.jets_extra (highest) - # - .env.development.remote (2nd highest, only if JETS_ENV_REMOTE=1) - # - .env.development.local (unless JETS_ENV_REMOTE=1) - # - .env.development - # - .env.local - This file is loaded for all environments _except_ `test` or unless JETS_ENV_REMOTE=1 - # - .env - The original (lowest) - # - def dotenv_files - files = [] - - files << files << root.join(".env.#{Jets.env}.#{Jets.extra}") if Jets.extra - files << root.join(".env.#{Jets.env}.remote") if @remote - files << root.join(".env.#{Jets.env}.local") unless @remote - files << root.join(".env.#{Jets.env}") - files << root.join(".env.local") unless Jets.env.test? || @remote - files << root.join(".env") - - files.compact.map(&:to_s) - end - - def root - Jets.root || Pathname.new(ENV["JETS_ROOT"] || Dir.pwd) +module Jets + class Dotenv + include Jets::AwsServices + include Jets::Util::Logging + + def load! + return unless load? + variables = ::Dotenv.load(*dotenv_files) + Ssm.new(variables).interpolate! + end + + def parse + return {} unless load? + variables = ::Dotenv.parse(*dotenv_files) + Ssm.new(variables).interpolate! + end + + def load? + enabled = ENV["JETS_DOTENV"] != "0" # allow to disable with JETS_DOTENV=0 + # Prevent ssm calls when on AWS Lambda but will call if on AWS CodeBuild + on_aws = (ENV["ON_AWS"] || ENV["_HANDLER"]) && !ENV["CODEBUILD_CI"] + enabled && !on_aws + end + + # dotenv files with the following precedence: + # + # - config/jets/env/.env.dev.extra (highest) + # - config/jets/env/.env.dev + # - config/jets/env/.env - The original (lowest) + # + def dotenv_files + files = [] + + files << ".env.#{Jets.env}.#{Jets.extra}" if Jets.extra + files << ".env.#{Jets.env}" + files << ".env" + + files.map! { |f| Jets.root.join("config/jets/env", f) }.compact + files.map(&:to_s) + end + + class << self + extend Memoist + @@load = nil + def load! + @@load ||= new.load! + end + memoize :load! + + @@parse = nil + def parse + @@parse ||= new.parse + end + memoize :parse + end end end diff --git a/lib/jets/dotenv/convention.rb b/lib/jets/dotenv/convention.rb new file mode 100644 index 000000000..b9beb051b --- /dev/null +++ b/lib/jets/dotenv/convention.rb @@ -0,0 +1,86 @@ +class Jets::Dotenv + class Convention + extend Memoist + delegate :config, to: "Jets.project" + delegate :env, to: Jets + + # ssm leaf name is the name of the leaf in the SSM parameter store. + # It can come from the key or the value. + # + # DATABASE_URL=SSM => DATABASE_URL + # DATABASE_URL=SSM:DB_URL => DB_URL + # + # + # Full SSM name. Example: + # + # /project/dev/DATABASE_URL + # /project/dev/DB_URL + # + def ssm_name(ssm_leaf_name) + if config.dotenv.ssm.convention_resolver + config.dotenv.ssm.convention_resolver.call(ssm_leaf_name) + else + "#{self.class.ssm_path}#{ssm_leaf_name}" + end + end + + class << self + extend Memoist + delegate :config, to: "Jets.project" + delegate :env, to: Jets + + def ssm_path + project = config.dotenv.ssm.project_name || Jets.project.name + # consider long_env config in the ssm_name only + ssm_env = config.dotenv.ssm.long_env_name ? resolved_long_env : resolved_env + + "/#{project}/#{ssm_env}/" + end + + # Does not use the envs.fallbakc and envs.unique settings. + def jets_env_path + project = config.dotenv.ssm.project_name || Jets.project.name + ssm_env = config.dotenv.ssm.long_env_name ? (long_env_map[Jets.env.to_sym] || Jets.env) : Jets.env + + "/#{project}/#{ssm_env}/" + end + + def ssm_env + config.dotenv.ssm.long_env_helper ? resolved_long_env : resolved_env + end + + def resolved_env + if unique_env?(env) + env # IE: dev or prod + else + config.dotenv.ssm.envs.fallback # IE: dev + end + end + + def unique_env?(env) + config.dotenv.ssm.envs.unique.nil? || + config.dotenv.ssm.envs.unique == :all || + config.dotenv.ssm.envs.unique.include?(env) + end + + def resolved_long_env + short_env = resolved_env + long_env_map[short_env.to_sym] || short_env + end + + # Jets 5.0 legacy + def long_env + short_env = Jets.env.to_s + long_env_map[short_env.to_sym] || short_env + end + + def long_env_map + { + dev: "development", + prod: "production", + stag: "staging" + } + end + end + end +end diff --git a/lib/jets/dotenv/show.rb b/lib/jets/dotenv/show.rb deleted file mode 100644 index 593898a49..000000000 --- a/lib/jets/dotenv/show.rb +++ /dev/null @@ -1,11 +0,0 @@ -class Jets::Dotenv - class Show - def self.list - puts "# Env from evaluated dotenv files" - vars = Jets::Dotenv.load! - vars.each do |k,v| - puts "#{k}=#{v}" - end - end - end -end diff --git a/lib/jets/dotenv/ssm.rb b/lib/jets/dotenv/ssm.rb index 3814f1ea2..f1b9a4aca 100644 --- a/lib/jets/dotenv/ssm.rb +++ b/lib/jets/dotenv/ssm.rb @@ -1,67 +1,90 @@ -require 'aws-sdk-ssm' - class Jets::Dotenv class Ssm - SSM_VARIABLE_REGEXP = /^ssm:(.*)/i + include Jets::AwsServices + include Jets::Util::Logging - def initialize(variables={}) + def initialize(variables) @variables = variables @missing = [] end def interpolate! - interpolated_variables = @variables.map do |key, value| - if value[SSM_VARIABLE_REGEXP] - value = fetch_ssm_value(key, $1) - elsif value == "SSM" - value = fetch_ssm_value(key, "SSM") - end - - [key, value] + interpolated_vars = merged_variables.map do |key, value| + var = Var.new(key, value) + @missing << var if var.ssm_missing? + var end - interpolated_variables.each do |key, value| - ENV[key] = value + interpolated_vars.each do |var| + ENV[var.name] = var.value end if @missing.empty? - interpolated_variables.to_h.sort_by { |k,_| k }.to_h # success + interpolated_vars.map { |var| [var.name, var.value] }.sort.to_h # success else message = "Error loading .env variables. No matching SSM parameters found for:\n".color(:red) - message += @missing.map do |k,v,n| - value = v == "SSM" ? v : "ssm:#{v}" - " #{k}=#{value} # ssm name: #{n}" + message += @missing.map do |var| + " #{var.name}=#{var.raw_value} # ssm name: #{var.ssm_name}" end.join("\n") abort message end end - def fetch_ssm_value(key, value) - return "fake-ssm-value" if ENV['JETS_NO_INTERNET'] + # Merges the variables from the dotenv files with the conventional ssm variables + # inferred by path, IE: /demo/dev/ + # User defined config/jets/env files values win over inferred ssm variables. + def merged_variables + if Jets.project.dotenv.ssm.autoload.enable + ssm_vars = get_ssm_parameters_path(Convention.ssm_path) # IE: /demo/dev/ + if Convention.jets_env_path != Convention.ssm_path + jets_env_vars = get_ssm_parameters_path(Convention.jets_env_path) # IE: /demo/sbx/ + ssm_vars = ssm_vars.merge(jets_env_vars) + end + + # skip list + ssm_vars = ssm_vars.delete_if { |k, v| skip_list?(k) } - name = ssm_name(key, value) - response = ssm.get_parameter(name: name, with_decryption: true) - response.parameter.value - rescue Aws::SSM::Errors::ParameterNotFound - @missing << [key, value, name] - '' - rescue Aws::SSM::Errors::ValidationException - puts "ERROR: Invalid SSM parameter name: #{name.inspect}".color(:red) - raise + # optimization: no need to get vars with SSM values since they are already in ssm_vars + vars = @variables.dup.delete_if { |k, v| v == "SSM" && ssm_vars.key?(k) } + ssm_vars.merge(vars) + else + @variables + end end - def ssm_name(key, value) - if value == "SSM" - "/#{Jets.project_name}/#{Jets.env}/#{key}" - else - value.start_with?("/") ? - value : - "/#{Jets.project_name}/#{Jets.env}/#{value}" + def skip_list?(key) + key = key.to_s + a = Jets.project.dotenv.ssm.autoload + skip_list = a.default_skip + a.skip + skip_list.detect do |i| + if i.is_a?(Regexp) + key.match(i) + else + key == i + end end end - def ssm - @ssm ||= Aws::SSM::Client.new + def get_ssm_parameters_path(path) + if ARGV.include?("dotenv") && ARGV.include?("list") + warn "# Autoloading SSM parameters within path #{path}" + end + parameters = {} + next_token = nil + + loop do + resp = ssm.get_parameters_by_path(path: path, with_decryption: true, next_token: next_token) + + resp.parameters.each do |param| + key = param.name.sub(path, "") + parameters[key] = param.value + end + + next_token = resp.next_token + break unless next_token + end + + parameters end end end diff --git a/lib/jets/dotenv/var.rb b/lib/jets/dotenv/var.rb new file mode 100644 index 000000000..520ab1adf --- /dev/null +++ b/lib/jets/dotenv/var.rb @@ -0,0 +1,60 @@ +class Jets::Dotenv + class Var + extend Memoist + include Jets::AwsServices + include Jets::Util::Logging + + attr_reader :raw_key, :raw_value + def initialize(raw_key, raw_value) + @raw_key, @raw_value = raw_key, raw_value + end + + def name + @raw_key + end + + def value + ssm? ? ssm_value : @raw_value + end + memoize :value + + SSM_VARIABLE_REGEXP = /^SSM:(.*)/i + def ssm_name + if @raw_value == "SSM" + # "/#{Jets.project.name}/#{Jets.env}/#{@raw_key}" + Convention.new.ssm_name(@raw_key) + else + value = @raw_value.sub(/SSM:/i, "") + if value.start_with?("/") + value + else + # "/#{Jets.project.name}/#{Jets.env}/#{value}" + Convention.new.ssm_name(value) + end + end + end + + def ssm_value + return "fake-ssm-value" if ENV["JETS_NO_INTERNET"] + + name = ssm_name + resp = ssm.get_parameter(name: name, with_decryption: true) + resp.parameter.value + rescue Aws::SSM::Errors::ParameterNotFound + @ssm_missing = true + nil + rescue Aws::SSM::Errors::ValidationException + puts "ERROR: Invalid SSM parameter name: #{name.inspect}".color(:red) + raise + end + + def ssm_missing? + value # trigger memoization + !!@ssm_missing + end + + def ssm? + @raw_value&.start_with?("SSM") + end + end +end diff --git a/lib/jets/engine.rb b/lib/jets/engine.rb deleted file mode 100644 index b1ac023cd..000000000 --- a/lib/jets/engine.rb +++ /dev/null @@ -1,380 +0,0 @@ -# frozen_string_literal: true - -require "active_support/callbacks" -require "active_support/core_ext/module/delegation" -require "active_support/core_ext/object/try" -require "pathname" -require "thread" - -module Jets - class Engine < Turbine - class << self - attr_accessor :called_from, :isolated - - alias :isolated? :isolated - alias :engine_name :turbine_name - - delegate :eager_load!, to: :instance - - def inherited(base) - unless base.abstract_turbine? - Jets::Turbine::Configuration.eager_load_namespaces << base - - base.called_from = begin - call_stack = caller_locations.map { |l| l.absolute_path || l.path } - - File.dirname(call_stack.detect { |p| !p.match?(%r[turbines[\w.-]*/lib/jets|rack[\w.-]*/lib/rack]) }) - end - end - - super - end - - def find_root(from) - find_root_with_flag "lib", from - end - - def endpoint(endpoint = nil) - @endpoint ||= nil - @endpoint = endpoint if endpoint - @endpoint - end - - def isolate_namespace(mod) - engine_name(generate_turbine_name(mod.name)) - - routes.default_scope = { module: ActiveSupport::Inflector.underscore(mod.name) } - self.isolated = true - - unless mod.respond_to?(:turbine_namespace) - name, turbine = engine_name, self - - mod.singleton_class.instance_eval do - define_method(:turbine_namespace) { turbine } - - unless mod.respond_to?(:table_name_prefix) - define_method(:table_name_prefix) { "#{name}_" } - end - - unless mod.respond_to?(:use_relative_model_naming?) - class_eval "def use_relative_model_naming?; true; end", __FILE__, __LINE__ - end - - unless mod.respond_to?(:turbine_helpers_paths) - define_method(:turbine_helpers_paths) { turbine.helpers_paths } - end - - unless mod.respond_to?(:turbine_routes_url_helpers) - define_method(:turbine_routes_url_helpers) { |include_path_helpers = true| turbine.routes.url_helpers(include_path_helpers) } - end - end - end - end - - # Finds engine with given path. - def find(path) - expanded_path = File.expand_path path - Jets::Engine.subclasses.each do |klass| - engine = klass.instance - return engine if File.expand_path(engine.root) == expanded_path - end - nil - end - end - - include ActiveSupport::Callbacks - define_callbacks :load_seed - - delegate :middleware, :root, :paths, to: :config - delegate :engine_name, :isolated?, to: :class - - def initialize - @_all_autoload_paths = nil - @_all_load_paths = nil - @app = nil - @config = nil - @env_config = nil - @helpers = nil - @routes = nil - @app_build_lock = Mutex.new - super - end - - # Load console and invoke the registered hooks. - # Check Jets::Turbine.console for more info. - def load_console(app = self) - require "jets/console/app" - require "jets/console/helpers" - run_console_blocks(app) - self - end - - # Load Jets runner and invoke the registered hooks. - # Check Jets::Turbine.runner for more info. - def load_runner(app = self) - run_runner_blocks(app) - self - end - - # Load Rake and turbines tasks, and invoke the registered hooks. - # Check Jets::Turbine.rake_tasks for more info. - def load_tasks(app = self) - require "rake" - run_tasks_blocks(app) # loads tasks like db:migrate - self - end - - # Load Jets generators and invoke the registered hooks. - # Check Jets::Turbine.generators for more info. - def load_generators(app = self) - require "jets/generators" - run_generators_blocks(app) - Jets::Generators.configure!(app.config.generators) - self - end - - # Invoke the server registered hooks. - # Check Jets::Turbine.server for more info. - def load_server(app = self) - run_server_blocks(app) - self - end - - def eager_load! - # Already done by Zeitwerk::Loader.eager_load_all. By now, we leave the - # method as a no-op for backwards compatibility. - end - - def turbines - @turbines ||= Turbines.new - end - - # Returns a module with all the helpers defined for the engine. - def helpers - @helpers ||= begin - helpers = Module.new - all = ActionController::Base.all_helpers_from_path(helpers_paths) - ActionController::Base.modules_for_helpers(all).each do |mod| - helpers.include(mod) - end - helpers - end - end - - # Returns all registered helpers paths. - def helpers_paths - paths["app/helpers"].existent - end - - # Returns the underlying Rack application for this engine. - def app - @app || @app_build_lock.synchronize { - @app ||= begin - stack = default_middleware_stack - config.middleware = build_middleware.merge_into(stack) - config.middleware.build(endpoint) - end - } - end - - # Returns the endpoint for this engine. If none is registered, - # defaults to an ActionDispatch::Routing::RouteSet. - def endpoint - # self.class.endpoint || routes - self.class.endpoint || Jets::Controller::Middleware::Main - end - - # Define the Rack API for this engine. - def call(env) - env.merge!(env_config) - app.call(env) # to Jets::Middleware#app - end - - # Defines additional Rack env configuration that is added on each call. - def env_config - @env_config ||= {} - end - - # Defines the routes for this engine. If a block is given to - # routes, it is appended to the engine. - def routes(&block) - # @routes ||= ActionDispatch::Routing::RouteSet.new_with_config(config) - @routes ||= Jets::Router::RouteSet.new(self) - @routes.append(&block) if block_given? - @routes - end - - # Define the configuration object for the engine. - def config - @config ||= Engine::Configuration.new(self.class.find_root(self.class.called_from)) - end - - # Load data from db/seeds.rb file. It can be used in to load engines' - # seeds, e.g.: - # - # Blog::Engine.load_seed - def load_seed - seed_file = paths["db/seeds.rb"].existent.first - run_callbacks(:load_seed) { load(seed_file) } if seed_file - end - - initializer :load_environment_config, before: :load_environment_hook, group: :all do - paths["config/environments"].existent.each do |environment| - require environment - end - end - - initializer :set_load_path, before: :bootstrap_hook do |app| - _all_load_paths(app.config.add_autoload_paths_to_load_path).reverse_each do |path| - $LOAD_PATH.unshift(path) if File.directory?(path) - end - $LOAD_PATH.uniq! - end - - initializer :set_autoload_paths, before: :bootstrap_hook do - ActiveSupport::Dependencies.autoload_paths.unshift(*_all_autoload_paths) - ActiveSupport::Dependencies.autoload_once_paths.unshift(*_all_autoload_once_paths) - - config.autoload_paths.freeze - config.autoload_once_paths.freeze - end - - initializer :set_eager_load_paths, before: :bootstrap_hook do - ActiveSupport::Dependencies._eager_load_paths.merge(config.eager_load_paths) - config.eager_load_paths.freeze - end - - initializer :add_routing_paths do |app| - routing_paths = paths["config/routes.rb"].existent - external_paths = self.paths["config/routes"].paths - routes.draw_paths.concat(external_paths) - - if routes? || routing_paths.any? - app.routes_reloader.paths.unshift(*routing_paths) - app.routes_reloader.route_sets << routes - app.routes_reloader.external_routes.unshift(*external_paths) - end - end - - # I18n load paths are a special case since the ones added - # later have higher priority. - initializer :add_locales do - config.i18n.turbines_load_path << paths["config/locales"] - end - - initializer :add_view_paths do - views = paths["app/views"].existent - unless views.empty? - ActiveSupport.on_load(:jets_controller) { prepend_view_path(views) if respond_to?(:prepend_view_path) } - ActiveSupport.on_load(:action_mailer) { prepend_view_path(views) } - end - end - - initializer :prepend_helpers_path do |app| - if !isolated? || (app == self) - app.config.helpers_paths.unshift(*paths["app/helpers"].existent) - end - end - - initializer :load_config_initializers do - config.paths["config/initializers"].existent.sort.each do |initializer| - load_config_initializer(initializer) - end - end - - initializer :wrap_executor_around_load_seed do |app| - self.class.set_callback(:load_seed, :around) do |engine, seeds_block| - app.executor.wrap(&seeds_block) - end - end - - initializer :engines_blank_point do - # We need this initializer so all extra initializers added in engines are - # consistently executed after all the initializers above across all engines. - end - - rake_tasks do - next if is_a?(Jets::Application) - next unless has_migrations? - - namespace turbine_name do - namespace :install do - desc "Copy migrations from #{turbine_name} to application" - task :migrations do - ENV["FROM"] = turbine_name - if Rake::Task.task_defined?("turbines:install:migrations") - Rake::Task["turbines:install:migrations"].invoke - else - Rake::Task["app:turbines:install:migrations"].invoke - end - end - end - end - end - - def routes? # :nodoc: - @routes - end - - protected - def run_tasks_blocks(*) # :nodoc: - super - paths["lib/tasks"].existent.sort.each { |ext| load(ext) } - end - - private - def load_config_initializer(initializer) # :doc: - ActiveSupport::Notifications.instrument("load_config_initializer.turbines", initializer: initializer) do - load(initializer) - end - end - - def has_migrations? - paths["db/migrate"].existent.any? - end - - def self.find_root_with_flag(flag, root_path, default = nil) # :nodoc: - while root_path && File.directory?(root_path) && !File.exist?("#{root_path}/#{flag}") - parent = File.dirname(root_path) - root_path = parent != root_path && parent - end - - root = File.exist?("#{root_path}/#{flag}") ? root_path : default - raise "Could not find root path for #{self}" unless root - - Pathname.new File.realpath root - end - - def default_middleware_stack - ActionDispatch::MiddlewareStack.new - end - - def _all_autoload_once_paths - config.autoload_once_paths.uniq - end - - def _all_autoload_paths - @_all_autoload_paths ||= begin - autoload_paths = config.autoload_paths - autoload_paths += config.eager_load_paths - autoload_paths -= config.autoload_once_paths - autoload_paths.uniq - end - end - - def _all_load_paths(add_autoload_paths_to_load_path) - @_all_load_paths ||= begin - load_paths = config.paths.load_paths - if add_autoload_paths_to_load_path - load_paths += _all_autoload_paths - load_paths += _all_autoload_once_paths - end - load_paths.uniq - end - end - - def build_middleware - config.middleware - end - end -end diff --git a/lib/jets/engine/configuration.rb b/lib/jets/engine/configuration.rb deleted file mode 100644 index a31fc7303..000000000 --- a/lib/jets/engine/configuration.rb +++ /dev/null @@ -1,96 +0,0 @@ -# frozen_string_literal: true - -module Jets - class Engine - class Configuration < ::Jets::Turbine::Configuration - attr_reader :root - attr_accessor :middleware, :javascript_path - attr_writer :eager_load_paths, :autoload_once_paths, :autoload_paths - - def initialize(root = nil) - super() - @root = root - @generators = app_generators.dup - @middleware = Jets::Configuration::MiddlewareStackProxy.new - @javascript_path = "javascript" - end - - # Holds generators configuration: - # - # config.generators do |g| - # g.orm :data_mapper, migration: true - # g.template_engine :haml - # g.test_framework :rspec - # end - # - # If you want to disable color in console, do: - # - # config.generators.colorize_logging = false - # - def generators - @generators ||= Jets::Configuration::Generators.new - yield(@generators) if block_given? - @generators - end - - def paths - @paths ||= begin - paths = Jets::Paths::Root.new(@root) - - paths.add "app", eager_load: true, - glob: "{*,*/concerns}", - exclude: ["assets", javascript_path] - paths.add "app/assets", glob: "*" - paths.add "app/controllers", eager_load: true - paths.add "app/channels", eager_load: true - paths.add "app/helpers", eager_load: true - paths.add "app/models", eager_load: true - paths.add "app/mailers", eager_load: true - paths.add "app/views" - # Jets - paths.add "app/functions", ignore: true - paths.add "app/shared", eager_load: false - paths.add "app/shared/extensions", collapse: true - paths.add "app/shared/functions", ignore: true - paths.add "app/shared/resources", collapse: true - - paths.add "lib", load_path: true - paths.add "lib/assets", glob: "*" - paths.add "lib/tasks", glob: "**/*.rake" - - paths.add "config" - paths.add "config/environments", glob: -"#{Jets.env}.rb" - paths.add "config/initializers", glob: "**/*.rb" - paths.add "config/locales", glob: "**/*.{rb,yml}" - paths.add "config/routes.rb" - paths.add "config/routes", glob: "**/*.rb" - - paths.add "db" - paths.add "db/migrate" - paths.add "db/seeds.rb" - - paths.add "vendor", load_path: true - paths.add "vendor/assets", glob: "*" - - paths - end - end - - def root=(value) - @root = paths.path = Pathname.new(value).expand_path - end - - def eager_load_paths - @eager_load_paths ||= paths.eager_load - end - - def autoload_once_paths - @autoload_once_paths ||= paths.autoload_once - end - - def autoload_paths - @autoload_paths ||= paths.autoload_paths - end - end - end -end diff --git a/lib/jets/engine/turbines.rb b/lib/jets/engine/turbines.rb deleted file mode 100644 index ce7f011f2..000000000 --- a/lib/jets/engine/turbines.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -module Jets - class Engine < Turbine - class Turbines - include Enumerable - attr_reader :_all - - def initialize - @_all ||= ::Jets::Turbine.subclasses.map(&:instance) + - ::Jets::Engine.subclasses.map(&:instance) - end - - def each(*args, &block) - _all.each(*args, &block) - end - - def -(others) - _all - others - end - end - end -end diff --git a/lib/jets/erb.rb b/lib/jets/erb.rb deleted file mode 100644 index 99e147209..000000000 --- a/lib/jets/erb.rb +++ /dev/null @@ -1,51 +0,0 @@ -require 'erb' - -# Renders Erb and provide better backtrace where there's an error -# -# Usage: -# -# result = Jets::Erb.result(path, key1: "val1", key2: "val2") -# -class Jets::Erb - class << self - def result(path, variables={}) - set_template_variables(variables) - template = IO.read(path) - begin - ERB.new(template, trim_mode: '-').result(binding) - rescue Exception => e - puts e - puts e.backtrace if ENV['JETS_DEBUG'] - - # how to know where ERB stopped? - https://www.ruby-forum.com/topic/182051 - # syntax errors have the (erb):xxx info in e.message - # undefined variables have (erb):xxx info in e.backtrac - error_info = e.message.split("\n").grep(/\(erb\)/)[0] - error_info ||= e.backtrace.grep(/\(erb\)/)[0] - raise unless error_info # unable to find the (erb):xxx: error line - line = error_info.split(':')[1].to_i - puts "Error evaluating ERB template on line #{line.to_s.color(:red)} of: #{path.sub(/^\.\//, '').color(:green)}" - - template_lines = template.split("\n") - context = 5 # lines of context - top, bottom = [line-context-1, 0].max, line+context-1 - spacing = template_lines.size.to_s.size - template_lines[top..bottom].each_with_index do |line_content, index| - line_number = top+index+1 - if line_number == line - printf("%#{spacing}d %s\n".color(:red), line_number, line_content) - else - printf("%#{spacing}d %s\n", line_number, line_content) - end - end - exit 1 unless Jets.env.test? - end - end - - def set_template_variables(variables) - variables.each do |key, value| - instance_variable_set(:"@#{key}", value) - end - end - end -end diff --git a/lib/jets/event/base.rb b/lib/jets/event/base.rb new file mode 100644 index 000000000..9fc1c8f38 --- /dev/null +++ b/lib/jets/event/base.rb @@ -0,0 +1,55 @@ +require "json" + +# Event public methods get turned into Lambda functions. +# +# Jets::Event::Base < Jets::Lambda::Functions +# Both Jets::Event::Base and Jets::Lambda::Functions have Dsl modules included. +# So the Jets::Event::Dsl overrides some of the Jets::Lambda::Functions behavior. +module Jets::Event + class Base < Jets::Lambda::Functions + class Error < StandardError; end + + include Dsl + + # non-DSL methods + include Helpers::KinesisEvent + include Helpers::LogEvent + include Helpers::S3Event + include Helpers::SnsEvent + include Helpers::SqsEvent + prepend Jets::ExceptionReporting::Process + + class << self + include Jets::Util::Logging + + def handle(event, context, meth = :perform) + runner = new(event, context, meth) + runner.send(meth) + end + + def handle_now(meth = :handle, event = {}, context = {}) + handle(event, context, meth) + end + + def handle_later(meth = :handle, event = {}, context = {}) + function = "#{name.underscore}-#{meth}" # IE: "cool_event-handle" + call = Jets::CLI::Call.new( + function: function, + event: JSON.dump(event), + invocation_type: "Event" + ) + resp = begin + call.invoke + rescue Jets::CLI::Call::Error => e + puts "ERROR: #{e.message}".color(:red) + puts "The stack may not be full deployed yet. Please check the stack and try again." + return + end + unless resp.status_code == 202 + raise Error, "Error calling Lambda function #{function} with invocation_type Event. status code: #{resp.status_code}" + end + resp + end + end + end +end diff --git a/lib/jets/event/dsl.rb b/lib/jets/event/dsl.rb new file mode 100644 index 000000000..3372800a0 --- /dev/null +++ b/lib/jets/event/dsl.rb @@ -0,0 +1,35 @@ +require "active_support" +require "active_support/core_ext/class" + +# Jets::Event::Base < Jets::Lambda::Functions +# Both Jets::Event::Base and Jets::Lambda::Functions have Dsl modules included. +# So the Jets::Event::Dsl overrides some of the Jets::Lambda::Functions behavior. +# +# Implements: +# +# default_associated_resource_definition +# +module Jets::Event::Dsl + extend ActiveSupport::Concern + + included do + class << self + include Jets::AwsServices + + include DynamodbEvent + include IotEvent + include KinesisEvent + include LogEvent + include S3Event + include ScheduledEvent + include SnsEvent + include SqsEvent + + # TODO: Get rid of default_associated_resource_definition concept. + # Also gets rid of the need to keep track of running @associated_properties too. + def default_associated_resource_definition(meth) + events_rule_definition + end + end + end +end diff --git a/lib/jets/event/dsl/dynamodb_event.rb b/lib/jets/event/dsl/dynamodb_event.rb new file mode 100644 index 000000000..22b20fbb3 --- /dev/null +++ b/lib/jets/event/dsl/dynamodb_event.rb @@ -0,0 +1,8 @@ +module Jets::Event::Dsl + module DynamodbEvent + # interface method + def dynamodb_event(table_name_without_namespace, options = {}) + {} + end + end +end diff --git a/lib/jets/event/dsl/iot_event.rb b/lib/jets/event/dsl/iot_event.rb new file mode 100644 index 000000000..8a22f8446 --- /dev/null +++ b/lib/jets/event/dsl/iot_event.rb @@ -0,0 +1,17 @@ +module Jets::Event::Dsl + module IotEvent + # The user must at least pass in an SQL statement + # Returns topic_props + # interface method + def iot_event(props = {}) + if props.is_a?(String) # SQL Statement + props = {Sql: props} + {TopicRulePayload: props} + elsif props.key?(:TopicRulePayload) # full properties structure + props + else # just the TopicRulePayload + {TopicRulePayload: props} + end + end + end +end diff --git a/lib/jets/event/dsl/kinesis_event.rb b/lib/jets/event/dsl/kinesis_event.rb new file mode 100644 index 000000000..dc5891dc2 --- /dev/null +++ b/lib/jets/event/dsl/kinesis_event.rb @@ -0,0 +1,8 @@ +module Jets::Event::Dsl + module KinesisEvent + # interface method + def kinesis_event(stream_name, options = {}) + {} + end + end +end diff --git a/lib/jets/event/dsl/log_event.rb b/lib/jets/event/dsl/log_event.rb new file mode 100644 index 000000000..9875c15f5 --- /dev/null +++ b/lib/jets/event/dsl/log_event.rb @@ -0,0 +1,8 @@ +module Jets::Event::Dsl + module LogEvent + # interface method + def log_event(log_group_name, props = {}) + {} + end + end +end diff --git a/lib/jets/event/dsl/rate_expression.rb b/lib/jets/event/dsl/rate_expression.rb new file mode 100644 index 000000000..41563bffc --- /dev/null +++ b/lib/jets/event/dsl/rate_expression.rb @@ -0,0 +1,27 @@ +require "fugit" + +module Jets::Event::Dsl + module RateExpression + # normalizes the rate expression + def rate_expression(expr) + duration = Fugit::Duration.parse(expr) + map = { + sec: "second", + min: "minute", + hou: "hour", + day: "day", + wee: "week", + mon: "month", + yea: "year" + } + # duration.h has a hash like {:hou=>1}. unit is truncated to 3 characters + h = duration.h # IE: {:hou=>1} + value = h.values.first + unit = h.keys.first + unit = map[unit] + # Fix the unit to be singular or plural for user + unit = (value > 1) ? unit.pluralize : unit.singularize + "#{value} #{unit}" + end + end +end diff --git a/lib/jets/event/dsl/s3_event.rb b/lib/jets/event/dsl/s3_event.rb new file mode 100644 index 000000000..fecc0edc6 --- /dev/null +++ b/lib/jets/event/dsl/s3_event.rb @@ -0,0 +1,7 @@ +module Jets::Event::Dsl + module S3Event + def s3_event(bucket_name, props = {}) + {} + end + end +end diff --git a/lib/jets/event/dsl/scheduled_event.rb b/lib/jets/event/dsl/scheduled_event.rb new file mode 100644 index 000000000..2bf1b745d --- /dev/null +++ b/lib/jets/event/dsl/scheduled_event.rb @@ -0,0 +1,53 @@ +module Jets::Event::Dsl + module ScheduledEvent + include RateExpression + # Public: Creates CloudWatch Event Rule + # + # expression - The rate expression. + # + # Examples + # + # rate("10 minutes") + # rate("10 minutes", description: "Hard event") + # + def rate(expression, props = {}) + expression = rate_expression(expression) # normalize the rate expression + md = expression.match(/\d+\s+\w+/) + raise ArgumentError, "Invalid rate expression: #{expression}" unless md + + expression = "rate(#{expression})" + scheduled_event(expression, props) + end + + # Public: Creates CloudWatch Event Rule + # + # expression - The cron expression. + # + # Examples + # + # cron("0 */12 * * ? *") + # cron("0 */12 * * ? *", description: "Hard event") + # + def cron(expression, props = {}) + expression = normalize_cron_expression(expression) + scheduled_event("cron(#{expression})", props) + end + + def normalize_cron_expression(expr) + parts = expr.split(" ") + # AWS Cron expressions require ? for the day of the week field + parts[-2] = "?" if parts[-2] == "*" + parts.join(" ") + end + + def scheduled_event(expression, props = {}) + props = props.merge(ScheduleExpression: expression) + rule_event(props) + end + + # interface method + def rule_event(props = {}) + {} + end + end +end diff --git a/lib/jets/event/dsl/sns_event.rb b/lib/jets/event/dsl/sns_event.rb new file mode 100644 index 000000000..123ca9bdb --- /dev/null +++ b/lib/jets/event/dsl/sns_event.rb @@ -0,0 +1,8 @@ +module Jets::Event::Dsl + module SnsEvent + # interface method + def sns_event(topic_name, props = {}) + {} + end + end +end diff --git a/lib/jets/event/dsl/sqs_event.rb b/lib/jets/event/dsl/sqs_event.rb new file mode 100644 index 000000000..d83b57510 --- /dev/null +++ b/lib/jets/event/dsl/sqs_event.rb @@ -0,0 +1,8 @@ +module Jets::Event::Dsl + module SqsEvent + # interface method + def sqs_event(queue_name, options = {}) + {} + end + end +end diff --git a/lib/jets/event/helpers/kinesis_event.rb b/lib/jets/event/helpers/kinesis_event.rb new file mode 100644 index 000000000..6be9584fb --- /dev/null +++ b/lib/jets/event/helpers/kinesis_event.rb @@ -0,0 +1,17 @@ +require "base64" + +module Jets::Event::Helpers + module KinesisEvent + def kinesis_data + records = event["Records"] + records.map do |record| + encoded = record["kinesis"]["data"] + Base64.decode64(encoded) # data + end + end + + def kinesis_data? + event["Records"]&.any? { |r| r.dig("kinesis", "data") } + end + end +end diff --git a/lib/jets/event/helpers/log_event.rb b/lib/jets/event/helpers/log_event.rb new file mode 100644 index 000000000..d7291a297 --- /dev/null +++ b/lib/jets/event/helpers/log_event.rb @@ -0,0 +1,21 @@ +require "base64" +require "json" +require "stringio" +require "zlib" + +module Jets::Event::Helpers + module LogEvent + def log_event + encoded = event["awslogs"]["data"] + compressed_string = Base64.decode64(encoded) + gz = Zlib::GzipReader.new(StringIO.new(compressed_string)) + uncompressed_string = gz.read + data = JSON.load(uncompressed_string) + ActiveSupport::HashWithIndifferentAccess.new(data) + end + + def log_event? + !!event.dig("awslogs", "data") + end + end +end diff --git a/lib/jets/event/helpers/s3_event.rb b/lib/jets/event/helpers/s3_event.rb new file mode 100644 index 000000000..57269afcb --- /dev/null +++ b/lib/jets/event/helpers/s3_event.rb @@ -0,0 +1,76 @@ +module Jets::Event::Helpers + module S3Event + extend Memoist + + def s3_events + encoded_messages = event[:Records].map do |record| + record[:Sns][:Message] # SNS message is JSON + end + # Decode the JSON messages + messages = encoded_messages.map do |message| + data = JSON.load(message) + ActiveSupport::HashWithIndifferentAccess.new(data) + end + # Extract the S3 event records + messages.map do |message| + message[:Records].map do |record| + ActiveSupport::HashWithIndifferentAccess.new(record) + end + end.flatten + end + + def s3_events? + event[:Records]&.any? { |r| r.dig(:Sns, :Message) } + end + + def s3_objects + s3_events.map do |record| + record[:s3][:object] + end + end + + def s3_objects? + s3_events.any? { |r| r.dig(:s3, :object) } + end + + # Downloads the s3 object and returns a Ruby File-like handle to the object + def s3_files + s3_events.map do |event| + bucket = event[:s3][:bucket][:name] + object_key = event[:s3][:object][:key] + + s3 = Aws::S3::Resource.new + obj = s3.bucket(bucket).object(object_key) + + file_path = "/tmp/s3_files/#{object_key}" + FileUtils.mkdir_p(File.dirname(file_path)) + File.open(file_path, "w") do |file| + obj.get(response_target: file) + end + + file = File.open(file_path, "r") + NamedFile.new(file, object_key) + end + end + memoize :s3_files + + # The Ruby File handle does not store information about the filename. + # NamedFile includes the filename which is useful for downstream processing. + class NamedFile < ::File + extend Memoist + attr_reader :filename + alias_method :object_key, :filename + alias_method :key, :object_key + + def initialize(file_handle, filename) + @filename = filename + super(file_handle, "r") + end + + def content + read + end + memoize :content + end + end +end diff --git a/lib/jets/event/helpers/sns_event.rb b/lib/jets/event/helpers/sns_event.rb new file mode 100644 index 000000000..0508c98f4 --- /dev/null +++ b/lib/jets/event/helpers/sns_event.rb @@ -0,0 +1,16 @@ +module Jets::Event::Helpers + module SnsEvent + def sns_events + records = event["Records"] + return [] unless records + records.map do |record| + message = record["Sns"]["Message"] + ActiveSupport::HashWithIndifferentAccess.new(JSON.load(message)) + end + end + + def sns_events? + event["Records"]&.any? { |r| r.dig("Sns", "Message") } + end + end +end diff --git a/lib/jets/event/helpers/sqs_event.rb b/lib/jets/event/helpers/sqs_event.rb new file mode 100644 index 000000000..4e7025d81 --- /dev/null +++ b/lib/jets/event/helpers/sqs_event.rb @@ -0,0 +1,23 @@ +module Jets::Event::Helpers + module SqsEvent + extend Memoist + + def sqs_records + event[:Records].map { |record| record } + end + memoize :sqs_records + + def sqs_events + records = sqs_records + return [] unless records + records.map do |record| + JSON.parse(record[:body]) + end + end + memoize :sqs_events + + def sqs_events? + sqs_records&.any? { |r| r.dig(:body) } + end + end +end diff --git a/lib/jets/event/s3.rb b/lib/jets/event/s3.rb new file mode 100644 index 000000000..43b7a270c --- /dev/null +++ b/lib/jets/event/s3.rb @@ -0,0 +1,21 @@ +module Jets::Event + module S3 + extend self + + # The registry tracks bucket each time an s3_event is declared + # Map of bucket_name => stack_name (nested part) + cattr_accessor :registry + @@registry = {} + + def any? + !@@registry.empty? + end + + def create_s3_event_buckets + buckets = @@registry.keys + buckets.each do |bucket| + Jets::AwsServices::S3Bucket.ensure_exists(bucket) + end + end + end +end diff --git a/lib/jets/generators.rb b/lib/jets/generators.rb deleted file mode 100644 index 4c17e8ea3..000000000 --- a/lib/jets/generators.rb +++ /dev/null @@ -1,112 +0,0 @@ -# Piggy back off of Rails Generators. - -module Jets - # Original Rails::Generators is a module. - # It cannot be included or inherited given the way Rails module is defined. - # We use a class that delegates to the Rails::Generators.invoke method. - class Generators - include Jets::Command::Behavior - - def initialize(namespace, args = ARGV, config = {}) - @namespace, @args, @config = namespace, args, config - @args << '--pretend' if noop? - end - - # Used to delegate noop option to Rails generator pretend option. Both work: - # - # jets generate scaffold user title:string --noop - # jets generate scaffold user title:string --pretend - # - # Grabbing directly from the ARGV because think its cleaner than passing options from - # Thor all the way down. - def noop? - ARGV.include?('--noop') - end - - def run(behavior=:invoke) - if @namespace == "job" - run_job_generator - else - # Required by: - # jets generate migration create_articles user:references - # jets generate model article user:references - # Makes use of Rails.application - if %w[model migration].include?(@namespace) - require "jets/overrides/dummy/rails" - end - run_rails_generator - end - end - - # For job generators, use a more custom generator instead of Rails generators. - # We this for more control over the jobs and can add more features. - # IE: Different event based jobs. - def run_job_generator - require "jets/generators/job/job_generator" - JobGenerator.start(@args, @config) - end - - def run_rails_generator - # We lazy require so Rails const is only defined when using generators - # Using require at the top and configuring do_not_eager_load is not enough. - # This is because we call require "jets/generators" throughout the codebase. - require "rails/generators" - require "rails/configuration" - - # => Jets::Generators.configure! => Rails::Generators.configure!(config) - self.class.configure!(config) - - # Ultimately, Rails::Generator.invoke is called. - # Here are some examples to show how args are passed to Rails::Generator.invoke. - # - # jets generate kingsman:controllers users -c=sessions - # @namespace kingsman:controllers - # @args ["users", "-c=sessions"] - # - # jets generate scaffold post title:string body:text published:boolean --force - # @args ["post", "title:string", "body:text", "published:boolean", "--force"] - - Rails::Generators.invoke(@namespace, @args, @config) - end - - def config - config = Jets.application.config.generators - config.orm :active_record, migration: true, timestamps: true - config.templates = [jets_templates_path] # add jets_templates_path for customizations - if Jets.application.config.mode == 'api' - config.api_only = true - config.template_engine nil - else - config.template_engine :erb - end - config - end - - # Rails and Thor allows overriding the provided templates with source root. - # The structure is slightly different with source_paths. - # source_root railties-7.0.8/lib/rails/generators/erb/scaffold/templates - # source_paths lib/jets/generators/overrides/templates/erb/scaffold - # See how templates is prefix instead of a suffix. - # erb/scaffold/templates - # templates/erb/scaffold - # This is nice because everything is together in the overrides/template folder. - def jets_templates_path - File.expand_path("generators/overrides/templates", __dir__) - end - - class << self - def configure!(config) - require "rails/generators" - Rails::Generators.configure!(config) - end - - def invoke(generator, args = ARGV, config = {}) - new(generator, args, config).run(:invoke) - end - - def revoke(generator, args = ARGV, config = {}) - new(generator, args, config).run(:revoke) - end - end - end -end diff --git a/lib/jets/generators/job/job_generator.rb b/lib/jets/generators/job/job_generator.rb deleted file mode 100644 index 591d22434..000000000 --- a/lib/jets/generators/job/job_generator.rb +++ /dev/null @@ -1,41 +0,0 @@ -module Jets # :nodoc: - class Generators # :nodoc: - class JobGenerator < Rails::Generators::NamedBase # :nodoc: - desc "This generator creates an Jets job file at app/jobs" - - class_option :name, aliases: :n, default: "perform", desc: "The method name for job" - class_option :type, aliases: :t, default: "scheduled", desc: "The job event type: dynamodb iot kinesis log rule s3 scheduled sns sqs" - - def self.default_generator_root - __dir__ - end - - def self.banner - "jets generate job #{self.arguments.map(&:usage).join(' ')} [options]" - end - - def create_job_file - template "event_types/#{options[:type]}.rb", File.join("app/jobs", class_path, "#{file_name}_job.rb") - - in_root do - if behavior == :invoke && !File.exist?(application_job_file_name) - template "application_job.rb", application_job_file_name - end - end - end - - private - def file_name - @_file_name ||= super.sub(/_job\z/i, "") - end - - def application_job_file_name - @application_job_file_name ||= if mountable_engine? - "app/jobs/#{namespaced_path}/application_job.rb" - else - "app/jobs/application_job.rb" - end - end - end - end -end diff --git a/lib/jets/generators/job/templates/application_job.rb.tt b/lib/jets/generators/job/templates/application_job.rb.tt deleted file mode 100644 index ac2a6c6de..000000000 --- a/lib/jets/generators/job/templates/application_job.rb.tt +++ /dev/null @@ -1,6 +0,0 @@ -<% module_namespacing do -%> -class ApplicationJob < Jets::Job::Base - # Adjust to increase the default timeout for all Job classes - class_timeout 60 -end -<% end -%> diff --git a/lib/jets/generators/job/templates/event_types/dynamodb.rb.tt b/lib/jets/generators/job/templates/event_types/dynamodb.rb.tt deleted file mode 100644 index 62abb5900..000000000 --- a/lib/jets/generators/job/templates/event_types/dynamodb.rb.tt +++ /dev/null @@ -1,8 +0,0 @@ -<% module_namespacing do -%> -class <%= class_name %>Job < ApplicationJob - dynamodb_event "test-table" # existing table - def <%= options[:name] %> - puts "event #{JSON.dump(event)}" - end -end -<% end -%> diff --git a/lib/jets/generators/job/templates/event_types/iot.rb.tt b/lib/jets/generators/job/templates/event_types/iot.rb.tt deleted file mode 100644 index 5e3c7e635..000000000 --- a/lib/jets/generators/job/templates/event_types/iot.rb.tt +++ /dev/null @@ -1,8 +0,0 @@ -<% module_namespacing do -%> -class <%= class_name %>Job < ApplicationJob - iot_event "SELECT * FROM 'my/topic'" - def <%= options[:name] %> - puts "event #{JSON.dump(event)}" - end -end -<% end -%> diff --git a/lib/jets/generators/job/templates/event_types/kinesis.rb.tt b/lib/jets/generators/job/templates/event_types/kinesis.rb.tt deleted file mode 100644 index 71c56f0c6..000000000 --- a/lib/jets/generators/job/templates/event_types/kinesis.rb.tt +++ /dev/null @@ -1,9 +0,0 @@ -<% module_namespacing do -%> -class <%= class_name %>Job < ApplicationJob - kinesis_event "my-stream" # existing stream - def <%= options[:name] %> - puts "event #{JSON.dump(event)}" - puts "kinesis_data #{JSON.dump(kinesis_data)}" - end -end -<% end -%> diff --git a/lib/jets/generators/job/templates/event_types/log.rb.tt b/lib/jets/generators/job/templates/event_types/log.rb.tt deleted file mode 100644 index aa0ad5148..000000000 --- a/lib/jets/generators/job/templates/event_types/log.rb.tt +++ /dev/null @@ -1,9 +0,0 @@ -<% module_namespacing do -%> -class <%= class_name %>Job < ApplicationJob - log_event "/aws/lambda/hello" - def <%= options[:name] %> - puts "event #{JSON.dump(event)}" - puts "log_event #{JSON.dump(log_event)}" - end -end -<% end -%> diff --git a/lib/jets/generators/job/templates/event_types/s3.rb.tt b/lib/jets/generators/job/templates/event_types/s3.rb.tt deleted file mode 100644 index 2db862f56..000000000 --- a/lib/jets/generators/job/templates/event_types/s3.rb.tt +++ /dev/null @@ -1,11 +0,0 @@ -<% module_namespacing do -%> -class <%= class_name %>Job < ApplicationJob - # Please read the Considerations section before using s3_event - s3_event "my-bucket" # new or existing bucket - def <%= options[:name] %> - puts "event #{JSON.dump(event)}" - puts "s3_events #{JSON.dump(s3_events)}" - puts "s3_objects #{JSON.dump(s3_objects)}" - end -end -<% end -%> diff --git a/lib/jets/generators/job/templates/event_types/scheduled.rb.tt b/lib/jets/generators/job/templates/event_types/scheduled.rb.tt deleted file mode 100644 index a85b77cd8..000000000 --- a/lib/jets/generators/job/templates/event_types/scheduled.rb.tt +++ /dev/null @@ -1,8 +0,0 @@ -<% module_namespacing do -%> -class <%= class_name %>Job < ApplicationJob - rate "10 hours" - def <%= options[:name] %> - puts "Do something with event #{JSON.dump(event)}" - end -end -<% end -%> diff --git a/lib/jets/generators/job/templates/event_types/sns.rb.tt b/lib/jets/generators/job/templates/event_types/sns.rb.tt deleted file mode 100644 index 884e8829e..000000000 --- a/lib/jets/generators/job/templates/event_types/sns.rb.tt +++ /dev/null @@ -1,11 +0,0 @@ -<% module_namespacing do -%> -class <%= class_name %>Job < ApplicationJob - class_timeout 30 # must be less than or equal to the SNS Topic default timeout - sns_event "hello-topic" - def <%= options[:name] %> - puts "event #{JSON.dump(event)}" - puts "sns_events #{JSON.dump(sns_events)}" - puts "sns_events? #{JSON.dump(sns_events?)}" - end -end -<% end -%> diff --git a/lib/jets/generators/job/templates/event_types/sqs.rb.tt b/lib/jets/generators/job/templates/event_types/sqs.rb.tt deleted file mode 100644 index 88db265bb..000000000 --- a/lib/jets/generators/job/templates/event_types/sqs.rb.tt +++ /dev/null @@ -1,11 +0,0 @@ -<% module_namespacing do -%> -class <%= class_name %>Job < ApplicationJob - class_timeout 30 # must be less than or equal to the SNS Topic default timeout - sqs_event "hello-queue" - def <%= options[:name] %> - puts "event #{JSON.dump(event)}" - puts "sqs_events #{JSON.dump(sqs_events)}" - puts "sqs_events? #{JSON.dump(sqs_events?)}" - end -end -<% end -%> diff --git a/lib/jets/generators/overrides/app/USAGE b/lib/jets/generators/overrides/app/USAGE deleted file mode 100644 index 26618ebbd..000000000 --- a/lib/jets/generators/overrides/app/USAGE +++ /dev/null @@ -1,38 +0,0 @@ -## Examples - - $ jets new demo - Creating a new Jets project called demo. - create demo/app/controllers/application_controller.rb - create demo/app/helpers/application_helper.rb - create demo/app/jobs/application_job.rb - ... - ================================================================ - Congrats. You have successfully created a Jets project. - - Cd into the project directory: - - cd demo - - To start a server and test locally: - - jets server # localhost:8888 should have the Jets welcome page - - Scaffold example: - - jets generate scaffold post title:string body:text published:boolean - - To deploy to AWS Lambda: - - jets deploy - -## Mode Option - -The `--mode` is a notable option. With it, you can generate different starter Jets projects. Examples: - - jets new demo --mode html # default - jets new api --mode api - jets new cron --mode job - -* The html mode generates a starter app useful for html web application. -* The api mode is useful for building an API. -* The job mode creates a very lightweight project. It is useful when you just need to run a Lambda function. diff --git a/lib/jets/generators/overrides/app/app_generator.rb b/lib/jets/generators/overrides/app/app_generator.rb deleted file mode 100644 index 43342ebc8..000000000 --- a/lib/jets/generators/overrides/app/app_generator.rb +++ /dev/null @@ -1,148 +0,0 @@ -require "rails/generators" -require "rails/generators/rails/app/app_generator" - -module Jets::Generators::Overrides::App - # Allows overriding generator creation of the Gemfile, README, etc. - # See: Rails::AppBuilder for the full list of overridable methods. - class AppBuilder < Rails::AppBuilder - end - - module AppBaseOverrides - extend ActiveSupport::Concern - DATABASES = Rails::Generators::Database::DATABASES - VALID_MODES = %w[html api job] - - module ClassMethods - # Override to support jets options only - def add_shared_options_for(name) - class_option :name, type: :string, aliases: "-n", - desc: "Name of the app" - - class_option :template, type: :string, aliases: "-m", - desc: "Path to some #{name} template (can be a filesystem path or URL)" - - class_option :database, type: :string, aliases: "-d", default: "mysql", - desc: "Preconfigure for selected database (options: #{DATABASES.join('/')})" - - class_option :mode, default: 'html', - desc: "mode: #{VALID_MODES.join(',')}" - - class_option :javascript, type: :string, aliases: "-j", default: "importmap", - desc: "Choose JavaScript approach [options: importmap (default)]" - - end - - # Prepend jets_template_path to source_paths so that the jets templates - # can override the rails templates. - def source_paths - rails_templates_path = Rails::Generators::AppGenerator.source_root - [jets_templates_path, rails_templates_path] + super - end - - def jets_templates_path - File.join(jets_generator_root, "templates") - end - - def jets_generator_root - File.expand_path(__dir__) - end - - def banner # :doc: - "jets new #{arguments.map(&:usage).join(' ')} [options]" - end - - def usage_path - path = File.join(jets_generator_root, "USAGE") - if File.exist?(path) - path - else - super - end - end - - # We want to exit on failure to be kind to other libraries - # This is only when accessing via CLI - def exit_on_failure? - true - end - end - end - - class AppGenerator < Rails::Generators::AppBase - include AppBaseOverrides - include Helpers - - add_shared_options_for "application" - - public_task :set_default_accessors! - public_task :create_root - # public_task :target_rails_prerelease - - # The Rails way generates a project piecemeal like so: - # - # def create_root_files - # build(:readme) - # build(:rakefile) - # end - # - # The Jets way generates the entire project at once. - # - # We first create the entire project at once because it's easier to maintain. - # But we can also use the Rails way to generate piecemeal and leverage - # the existing Rails generators. - - public_task :set_initial_variables - public_task :copy_project # Note: Support for clone has been removed - - def create_root_files - build(:version_control) - end - - def create_bin_files - build(:bin) - end - - # def create_active_record_files - # return if options[:skip_active_record] - # build(:database_yml) - # end - - public_task :run_bundle - - # Custom version because Rails run_javascript calls rails importmap:install - def run_javascript - return unless options[:mode] == "html" - - if options[:javascript] == "importmap" - sh "bundle exec jets importmap:install" - else - puts "WARN: Only importmap is supported at this time." - end - end - - public_task :git_first_commit - - private - def gemfile_entries # :doc: - if options[:database].nil? # --no-database - return [] - end - - [ - database_gemfile_entry, - ].flatten.compact.select(&@gem_filter) - end - - def sh(command) - puts "=> #{command}" - system(command) - end - - def get_builder_class - AppBuilder - end - end -end - -# Keeps zeitwerk happy -Jets::Generators::AppGenerator = Jets::Generators::Overrides::App::AppGenerator diff --git a/lib/jets/generators/overrides/app/helpers.rb b/lib/jets/generators/overrides/app/helpers.rb deleted file mode 100644 index c6b58991e..000000000 --- a/lib/jets/generators/overrides/app/helpers.rb +++ /dev/null @@ -1,139 +0,0 @@ -module Jets::Generators::Overrides::App - module Helpers - private - def set_initial_variables - @project_folder = app_path - @project_name = app_path == '.' ? File.basename(Dir.pwd) : app_path - @app_namespace = @project_name.gsub('-','_').gsub(/[^a-zA-Z_0-9]/, '').camelize - @database_name = @project_name.gsub('-','_') - - # options is a frozen hash by Thor so cannot modify it. - # Also had trouble unfreezing it with .dup. So using instance variables instead - case options[:mode] - when 'html' - @bootstrap = options[:bootstrap] - @database = options[:database] - @javascript = options[:javascript] # importmap or webpacker - when 'api' - @bootstrap = false - @database = options[:database] - @javascript = 'none' - when 'job' - @bootstrap = false - @database = false - @javascript = 'none' - else - puts "Invalid mode provided: #{@options[:mode].color(:red)}. Please pass in an valid mode: #{VALID_MODES.join(',').color(:green)}." - exit 1 - end - end - - def jets_minor_version - md = Jets::VERSION.match(/(\d+)\.(\d+)\.\d+/) - major, minor = md[1], md[2] - [major, minor, '0'].join('.') - end - - def clone_project - unless git_installed? - abort "Unable to detect git installation on your system. Git needs to be installed in order to use the --repo option." - end - - if File.exist?(project_folder) - abort "The folder #{project_folder} already exists." - else - run "git clone https://github.com/#{options[:repo]} #{project_folder}" - end - confirm_jets_project - end - - def confirm_jets_project - jets_project = File.exist?("#{project_folder}/config/application.rb") - unless jets_project - puts "#{options[:repo]} does not look like a Jets project. Double check your repo!".color(:red) - exit 1 - end - end - - def copy_project - directory ".", ".", copy_options - end - - def copy_options - excludes = excludes() - default_excludes = %w[ - bin - ] - excludes += default_excludes - excludes.uniq! - - unless @database - excludes += %w[ - database.yml - db - models/application_record - ] - end - - if excludes.empty? - {} - else - pattern = Regexp.new(excludes.join('|')) - {exclude_pattern: pattern } - end - end - - def excludes - case @options[:mode] - when 'job' - # For job mode: list of words to include in the exclude pattern and will not be generated. - %w[ - app/views - assets - config.ru - config/database.yml - config/dynamodb.yml - config/initializers - controllers - db/ - helpers - javascript - models/application_ - Procfile - public - routes - spec - yarn - ] - when 'api' - %w[ - app/assets - app/views - ] - else # html - [] - end - end - - def git_first_commit - return unless git_installed? && git_credentials_set? - run("git add .") - run("git commit -m 'first commit'") - end - - def git_installed? - system("type git > /dev/null 2>&1") - end - - # In order to automatically create first commit - # the user needs to have their credentials set - def git_credentials_set? - configs = `git config --list`.split("\n") - configs.any? { |c| c.start_with? 'user.name=' } && configs.any? { |c| c.start_with? 'user.email=' } - end - - def yarn_installed? - system("type yarn > /dev/null 2>&1") - end - end -end diff --git a/lib/jets/generators/overrides/app/templates/.env b/lib/jets/generators/overrides/app/templates/.env deleted file mode 100644 index 3623e5a33..000000000 --- a/lib/jets/generators/overrides/app/templates/.env +++ /dev/null @@ -1,10 +0,0 @@ -# Variables in here are available and shared across all environments: development, production, etc -# Docs: https://docs.rubyonjets.com/docs/env-files/ - -# Examples: -# DATABASE_URL="mysql2://user:pass@host.com/db_name?pool=5" # raw value as-is -# SSM can be use to reference a parameter store value -# DATABASE_URL=SSM # conventionally references /PROJECT_NAME/JETS_ENV/DATABASE_URL -# DATABASE_URL=SSM # /demo/development/DATABASE_URL -# DATABASE_URL=SSM:database-url # /demo/development/database-url -# DATABASE_URL=SSM:/path/to/database-url # /path/to/database-url diff --git a/lib/jets/generators/overrides/app/templates/.gitignore.tt b/lib/jets/generators/overrides/app/templates/.gitignore.tt deleted file mode 100644 index 406b3d58e..000000000 --- a/lib/jets/generators/overrides/app/templates/.gitignore.tt +++ /dev/null @@ -1,13 +0,0 @@ -.bundle -.byebug_history -.DS_Store -.env* -*.gem -/node_modules -/public/assets -/public/packs -/public/packs-test -bundled -coverage -pkg -tmp diff --git a/lib/jets/generators/overrides/app/templates/.jetsignore b/lib/jets/generators/overrides/app/templates/.jetsignore deleted file mode 100644 index a2ddca784..000000000 --- a/lib/jets/generators/overrides/app/templates/.jetsignore +++ /dev/null @@ -1,6 +0,0 @@ -# Additional files not to add to the zip file that jets creates for code deploy. -# .gitignore files are also not added. -# This adds additional files. -# -# Ignoring /public/files since should be served by s3 when using asset_path helper -/public/files diff --git a/lib/jets/generators/overrides/app/templates/.rspec b/lib/jets/generators/overrides/app/templates/.rspec deleted file mode 100644 index b83d9b7aa..000000000 --- a/lib/jets/generators/overrides/app/templates/.rspec +++ /dev/null @@ -1,3 +0,0 @@ ---color ---format documentation ---require spec_helper diff --git a/lib/jets/generators/overrides/app/templates/Gemfile.tt b/lib/jets/generators/overrides/app/templates/Gemfile.tt deleted file mode 100644 index e28fb64a4..000000000 --- a/lib/jets/generators/overrides/app/templates/Gemfile.tt +++ /dev/null @@ -1,28 +0,0 @@ -source "https://rubygems.org" - -gem "jets", "~> <%= Jets::VERSION %>" - -<%- if options[:mode] == 'html'-%> -gem "sprockets-jets" -<% end -%> - -<%- if options[:mode] != 'job' && !options[:database].nil? -%> -<%= database_gemfile_entry %> -<%- end -%> -<%- if options[:mode] == 'html' -%> -gem "importmap-jets" -<%- end -%> -gem "zeitwerk", ">= 2.6.12" - -# development and test groups are not bundled as part of the deployment -group :development, :test do - gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] -<%- unless options[:mode] == 'job' -%> - gem 'rack' - gem 'puma' -<%- end -%> -end - -group :test do - gem 'rspec' -end diff --git a/lib/jets/generators/overrides/app/templates/README.md b/lib/jets/generators/overrides/app/templates/README.md deleted file mode 100644 index 25857366e..000000000 --- a/lib/jets/generators/overrides/app/templates/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Jets Project - -This README would normally document whatever steps are necessary to get the application up and running. - -Things you might want to cover: - -* Dependencies -* Configuration -* Database setup -* How to run the test suite -* Deployment instructions diff --git a/lib/jets/generators/overrides/app/templates/Rakefile b/lib/jets/generators/overrides/app/templates/Rakefile deleted file mode 100644 index 6e613d1a8..000000000 --- a/lib/jets/generators/overrides/app/templates/Rakefile +++ /dev/null @@ -1,2 +0,0 @@ -require 'jets' -Jets.application.load_tasks diff --git a/lib/jets/generators/overrides/app/templates/app/assets/config/manifest.js b/lib/jets/generators/overrides/app/templates/app/assets/config/manifest.js deleted file mode 100644 index 7fbd7f025..000000000 --- a/lib/jets/generators/overrides/app/templates/app/assets/config/manifest.js +++ /dev/null @@ -1,3 +0,0 @@ -//= link_tree ../images -//= link_directory ../stylesheets .css -//= link_tree ../javascripts .js diff --git a/lib/jets/generators/overrides/app/templates/app/assets/images/.keep b/lib/jets/generators/overrides/app/templates/app/assets/images/.keep deleted file mode 100644 index e69de29bb..000000000 diff --git a/lib/jets/generators/overrides/app/templates/app/assets/javascripts/.keep b/lib/jets/generators/overrides/app/templates/app/assets/javascripts/.keep deleted file mode 100644 index e69de29bb..000000000 diff --git a/lib/jets/generators/overrides/app/templates/app/assets/stylesheets/application.css b/lib/jets/generators/overrides/app/templates/app/assets/stylesheets/application.css deleted file mode 100644 index 288b9ab71..000000000 --- a/lib/jets/generators/overrides/app/templates/app/assets/stylesheets/application.css +++ /dev/null @@ -1,15 +0,0 @@ -/* - * This is a manifest file that'll be compiled into application.css, which will include all the files - * listed below. - * - * Any CSS (and SCSS, if configured) file within this directory, lib/assets/stylesheets, or any plugin's - * vendor/assets/stylesheets directory can be referenced here using a relative path. - * - * You're free to add application-wide styles to this file and they'll appear at the bottom of the - * compiled file so the styles you add here take precedence over styles defined in any other CSS - * files in this directory. Styles in this file should be added after the last require_* statement. - * It is generally better to create a new file per style scope. - * - *= require_tree . - *= require_self - */ diff --git a/lib/jets/generators/overrides/app/templates/app/controllers/application_controller.rb b/lib/jets/generators/overrides/app/templates/app/controllers/application_controller.rb deleted file mode 100644 index 6de3cf92b..000000000 --- a/lib/jets/generators/overrides/app/templates/app/controllers/application_controller.rb +++ /dev/null @@ -1,2 +0,0 @@ -class ApplicationController < Jets::Controller::Base -end diff --git a/lib/jets/generators/overrides/app/templates/app/helpers/application_helper.rb b/lib/jets/generators/overrides/app/templates/app/helpers/application_helper.rb deleted file mode 100644 index de6be7945..000000000 --- a/lib/jets/generators/overrides/app/templates/app/helpers/application_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module ApplicationHelper -end diff --git a/lib/jets/generators/overrides/app/templates/app/javascript/application.js b/lib/jets/generators/overrides/app/templates/app/javascript/application.js deleted file mode 100644 index a424aa087..000000000 --- a/lib/jets/generators/overrides/app/templates/app/javascript/application.js +++ /dev/null @@ -1,5 +0,0 @@ -// Configure your import map in config/importmap.rb. Read more: https://github.com/boltops-tools/importmap-jets -import jquery from 'jquery' -window.$ = jquery -import Jets from "@rubyonjets/ujs-compat" -Jets.start() diff --git a/lib/jets/generators/overrides/app/templates/app/jobs/application_job.rb b/lib/jets/generators/overrides/app/templates/app/jobs/application_job.rb deleted file mode 100644 index 0664a9e43..000000000 --- a/lib/jets/generators/overrides/app/templates/app/jobs/application_job.rb +++ /dev/null @@ -1,4 +0,0 @@ -class ApplicationJob < Jets::Job::Base - # Adjust to increase the default timeout for all Job classes - class_timeout 60 -end diff --git a/lib/jets/generators/overrides/app/templates/app/models/application_record.rb b/lib/jets/generators/overrides/app/templates/app/models/application_record.rb deleted file mode 100644 index b63caeb8a..000000000 --- a/lib/jets/generators/overrides/app/templates/app/models/application_record.rb +++ /dev/null @@ -1,3 +0,0 @@ -class ApplicationRecord < ActiveRecord::Base - primary_abstract_class -end diff --git a/lib/jets/generators/overrides/app/templates/app/views/layouts/application.html.erb.tt b/lib/jets/generators/overrides/app/templates/app/views/layouts/application.html.erb.tt deleted file mode 100644 index b5f8352be..000000000 --- a/lib/jets/generators/overrides/app/templates/app/views/layouts/application.html.erb.tt +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - <%%= csrf_meta_tags %> - Jets App - - <% if @webpacker %> - <%%= javascript_pack_tag "application" %> - <%%= stylesheet_pack_tag "theme" %> - <% else %> - <%%= stylesheet_link_tag "application" %><%# NOTE: javascript_importmap_tags is inserted by importmap:install %> - <% end %> - - -
-
-
-
- <%% if @title %>

<%%= @title %>

<%% end %> - <%%= yield %> -
-
-
-
- - diff --git a/lib/jets/generators/overrides/app/templates/bin/jets b/lib/jets/generators/overrides/app/templates/bin/jets deleted file mode 100755 index 6beba59de..000000000 --- a/lib/jets/generators/overrides/app/templates/bin/jets +++ /dev/null @@ -1,3 +0,0 @@ -APP_PATH = File.expand_path("../config/application", __dir__) -require "jets" -require "jets/commands" diff --git a/lib/jets/generators/overrides/app/templates/config.ru b/lib/jets/generators/overrides/app/templates/config.ru deleted file mode 100644 index ddd69de64..000000000 --- a/lib/jets/generators/overrides/app/templates/config.ru +++ /dev/null @@ -1,5 +0,0 @@ -# This file is used by Rack-based servers to start the application. - -require "jets" -Jets.boot -run Jets.application diff --git a/lib/jets/generators/overrides/app/templates/config/application.rb.tt b/lib/jets/generators/overrides/app/templates/config/application.rb.tt deleted file mode 100644 index 1c4d1a0de..000000000 --- a/lib/jets/generators/overrides/app/templates/config/application.rb.tt +++ /dev/null @@ -1,15 +0,0 @@ -module <%= @app_namespace %> - class Application < Jets::Application - config.load_defaults 5.0 - - config.project_name = "<%= @project_name %>" - config.mode = "<%= @options[:mode] %>" - -<%- if @options[:mode] == 'job' -%> - config.prewarm.enable = false -<%- end -%> - # Docs: - # https://rubyonjets.com/docs/config/ - # https://rubyonjets.com/docs/config/reference/ - end -end diff --git a/lib/jets/generators/overrides/app/templates/config/database.yml.tt b/lib/jets/generators/overrides/app/templates/config/database.yml.tt deleted file mode 100644 index cd0508c5f..000000000 --- a/lib/jets/generators/overrides/app/templates/config/database.yml.tt +++ /dev/null @@ -1,27 +0,0 @@ -default: &default - adapter: <%= @database == 'mysql' ? 'mysql2' : 'postgresql' %> - encoding: <%= @database == 'mysql' ? 'utf8mb4' : 'unicode' %> - pool: <%%= ENV["DB_POOL"] || 5 %> - database: <%%= ENV['DB_NAME'] || '<%= @database_name %>_development' %> -<% if @database == 'mysql' -%> - username: <%%= ENV['DB_USER'] || 'root' %> -<% else -%> - username: <%%= ENV['DB_USER'] || ENV['USER'] %> -<% end -%> - password: <%%= ENV['DB_PASS'] %> - host: <%%= ENV["DB_HOST"] %> - url: <%%= ENV['DATABASE_URL'] %> # takes higher precedence than other settings - # reconnect: true # reconnect option is deprecated with newer mysql database versions - -development: - <<: *default - database: <%%= ENV['DB_NAME'] || '<%= @database_name %>_development' %> - -test: - <<: *default - database: <%= @database_name %>_test - -production: - <<: *default - database: <%= @database_name %>_production - url: <%%= ENV['DATABASE_URL'] %> diff --git a/lib/jets/generators/overrides/app/templates/config/environments/development.rb.tt b/lib/jets/generators/overrides/app/templates/config/environments/development.rb.tt deleted file mode 100644 index e7dbadd60..000000000 --- a/lib/jets/generators/overrides/app/templates/config/environments/development.rb.tt +++ /dev/null @@ -1,29 +0,0 @@ -Jets.application.configure do - # Docs: https://rubyonjets.com/docs/config/reference/ - - config.cache_classes = false - config.eager_load = false - config.logging.event = false # can be useful for CloudWatch - -<%- unless options[:mode] == 'job' -%> - # Show full error reports. - config.consider_all_requests_local = true - - # Enable server timing - config.server_timing = true - - # Enable/disable caching. By default caching is disabled. - if Jets.root.join("tmp/caching-dev.txt").exist? - config.jets_controller.perform_caching = true - config.cache_store = :memory_store - else - config.jets_controller.perform_caching = false - config.cache_store = :null_store - end - - # Docs: http://rubyonjets.com/docs/email-sending/ - config.action_mailer.raise_delivery_errors = false - config.action_mailer.perform_caching = false - # config.action_mailer.default_url_options = { host: 'localhost', port: 8888 } -<%- end -%> -end diff --git a/lib/jets/generators/overrides/app/templates/config/environments/production.rb.tt b/lib/jets/generators/overrides/app/templates/config/environments/production.rb.tt deleted file mode 100644 index 59515d446..000000000 --- a/lib/jets/generators/overrides/app/templates/config/environments/production.rb.tt +++ /dev/null @@ -1,21 +0,0 @@ -Jets.application.configure do - config.cache_classes = true - config.eager_load = true - config.log_level = :info - config.logging.event = false # can be useful for CloudWatch - -<%- unless options[:mode] == 'job' -%> - config.consider_all_requests_local = false - # Do not fallback to assets pipeline if a precompiled asset is missed. -<%- if options[:mode] == 'html' -%> - config.assets.compile = false -<%- end -%> - - # Ignore bad email addresses and do not raise email delivery errors. - # Set this to true and configure the email server for immediate delivery to raise delivery errors. - # Docs: http://rubyonjets.com/docs/email-sending/ - config.action_mailer.raise_delivery_errors = true - config.action_mailer.perform_caching = false - # config.action_mailer.default_url_options = { host: 'localhost', port: 8888 } -<%- end -%> -end diff --git a/lib/jets/generators/overrides/app/templates/config/environments/test.rb.tt b/lib/jets/generators/overrides/app/templates/config/environments/test.rb.tt deleted file mode 100644 index 9ef5bcde7..000000000 --- a/lib/jets/generators/overrides/app/templates/config/environments/test.rb.tt +++ /dev/null @@ -1,23 +0,0 @@ -Jets.application.configure do - config.cache_classes = true - config.eager_load = ENV["CI"].present? - config.cache_store = :null_store - -<%- unless options[:mode] == 'job' -%> - config.consider_all_requests_local = true - config.server_timing = true - - config.jets_controller.perform_caching = false - - # Raise exceptions instead of rendering exception templates. - config.action_dispatch.show_exceptions = false - - # Tell Action Mailer not to deliver emails to the real world. - # The :test delivery method accumulates sent emails in the - # ActionMailer::Base.deliveries array. - # Docs: http://rubyonjets.com/docs/email-sending/ - config.action_mailer.delivery_method = :test - config.action_mailer.perform_caching = false - # config.action_mailer.default_url_options = { host: 'localhost', port: 8888 } -<%- end -%> -end diff --git a/lib/jets/generators/overrides/app/templates/config/initializers/content_security_policy.rb b/lib/jets/generators/overrides/app/templates/config/initializers/content_security_policy.rb deleted file mode 100644 index e2a2d4208..000000000 --- a/lib/jets/generators/overrides/app/templates/config/initializers/content_security_policy.rb +++ /dev/null @@ -1,31 +0,0 @@ -# Be sure to restart your server when you modify this file. -# -# Define an application-wide content security policy. -# The Rails docs cover how to use Content Security Policy headers, Jets -# uses the same interface. -# See the Securing Rails Applications Guide for more information: -# https://guides.rubyonrails.org/security.html#content-security-policy-header -# -# Note: Turning on the policy below will result in not being able to use -# inline tags unless their are secure. For example, inline style -# tags will not work. -# -# Jets.application.configure do -# config.content_security_policy do |policy| -# policy.default_src :self, :https -# policy.font_src :self, :https, :data -# policy.img_src :self, :https, :data -# policy.object_src :none -# policy.script_src :self, :https -# policy.style_src :self, :https -# # Specify URI for violation reports -# # policy.report_uri "/csp-violation-report-endpoint" -# end -# -# # Generate session nonces for permitted importmap and inline scripts -# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } -# config.content_security_policy_nonce_directives = %w(script-src) -# -# # Report violations without enforcing the policy. -# # config.content_security_policy_report_only = true -# end diff --git a/lib/jets/generators/overrides/app/templates/config/initializers/filter_parameter_logging.rb b/lib/jets/generators/overrides/app/templates/config/initializers/filter_parameter_logging.rb deleted file mode 100644 index 83243f021..000000000 --- a/lib/jets/generators/overrides/app/templates/config/initializers/filter_parameter_logging.rb +++ /dev/null @@ -1,8 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Configure parameters to be filtered from the log file. Use this to limit dissemination of -# sensitive information. See the ActiveSupport::ParameterFilter documentation for supported -# notations and behaviors. -Jets.application.config.filter_parameters += [ - :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn -] diff --git a/lib/jets/generators/overrides/app/templates/config/initializers/permissions_policy.rb b/lib/jets/generators/overrides/app/templates/config/initializers/permissions_policy.rb deleted file mode 100644 index 4d29d9b34..000000000 --- a/lib/jets/generators/overrides/app/templates/config/initializers/permissions_policy.rb +++ /dev/null @@ -1,11 +0,0 @@ -# Define an application-wide HTTP permissions policy. For further -# information see https://developers.google.com/web/updates/2018/06/feature-policy -# -# Jets.application.config.permissions_policy do |f| -# f.camera :none -# f.gyroscope :none -# f.microphone :none -# f.usb :none -# f.fullscreen :self -# f.payment :self, "https://secure.example.com" -# end diff --git a/lib/jets/generators/overrides/app/templates/config/routes.rb b/lib/jets/generators/overrides/app/templates/config/routes.rb deleted file mode 100644 index 9d3b3a3eb..000000000 --- a/lib/jets/generators/overrides/app/templates/config/routes.rb +++ /dev/null @@ -1,13 +0,0 @@ -Jets.application.routes.draw do - root "jets/welcome#index" - - # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. - # Can be used by load balancers and uptime monitors to verify that the app is live. - get "up" => "jets/health#show", as: :jets_health_check - - # The jets/public#show controller can serve static utf8 content out of the public folder. - # Note, as part of the deploy process Jets uploads files in the public folder to s3 - # and serves them out of s3 directly. S3 is well suited to serve static assets. - # More info here: https://rubyonjets.com/docs/extras/assets-serving/ - any "*catchall", to: "jets/public#show" -end diff --git a/lib/jets/generators/overrides/app/templates/db/.gitkeep b/lib/jets/generators/overrides/app/templates/db/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/lib/jets/generators/overrides/app/templates/public/404.html b/lib/jets/generators/overrides/app/templates/public/404.html deleted file mode 100644 index 4e02f2ee8..000000000 --- a/lib/jets/generators/overrides/app/templates/public/404.html +++ /dev/null @@ -1,67 +0,0 @@ - - - - The page you were looking for doesn't exist (404) - - - - - - -
-
-

The page you were looking for doesn't exist.

-

You may have mistyped the address or the page may have moved.

-
-

If you are the application owner check the logs for more information.

-
- - diff --git a/lib/jets/generators/overrides/app/templates/public/422.html b/lib/jets/generators/overrides/app/templates/public/422.html deleted file mode 100644 index 4a76c6af2..000000000 --- a/lib/jets/generators/overrides/app/templates/public/422.html +++ /dev/null @@ -1,67 +0,0 @@ - - - - The change you wanted was rejected (422) - - - - - - -
-
-

The change you wanted was rejected.

-

Maybe you tried to change something you didn't have access to.

-
-

If you are the application owner check the logs for more information.

-
- - diff --git a/lib/jets/generators/overrides/app/templates/public/500.html b/lib/jets/generators/overrides/app/templates/public/500.html deleted file mode 100644 index 130567847..000000000 --- a/lib/jets/generators/overrides/app/templates/public/500.html +++ /dev/null @@ -1,66 +0,0 @@ - - - - We're sorry, but something went wrong (500) - - - - - - -
-
-

We're sorry, but something went wrong.

-
-

If you are the application owner check the logs for more information.

-
- - diff --git a/lib/jets/generators/overrides/app/templates/public/favicon.ico b/lib/jets/generators/overrides/app/templates/public/favicon.ico deleted file mode 100644 index 1ae4055cf..000000000 Binary files a/lib/jets/generators/overrides/app/templates/public/favicon.ico and /dev/null differ diff --git a/lib/jets/generators/overrides/app/templates/spec/controllers/posts_controller_spec.rb b/lib/jets/generators/overrides/app/templates/spec/controllers/posts_controller_spec.rb deleted file mode 100644 index 733394dab..000000000 --- a/lib/jets/generators/overrides/app/templates/spec/controllers/posts_controller_spec.rb +++ /dev/null @@ -1,15 +0,0 @@ -# Example: -# describe PostsController, type: :controller do -# it "index returns a success response" do -# get '/posts' -# expect(response.status).to eq 200 -# pp response.body -# end -# -# it "show returns a success response" do -# Post.create(id: 1) unless Post.find_by(id: 1) # TODO: set up factory_bot -# get '/posts/:id', id: 1 -# expect(response.status).to eq 200 -# pp response.body -# end -# end diff --git a/lib/jets/generators/overrides/app/templates/spec/fixtures/payloads/posts-index.json b/lib/jets/generators/overrides/app/templates/spec/fixtures/payloads/posts-index.json deleted file mode 100644 index 346d39f83..000000000 --- a/lib/jets/generators/overrides/app/templates/spec/fixtures/payloads/posts-index.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "resource": "/posts", - "path": "/posts", - "httpMethod": "GET", - "headers": { - "Accept": "*/*", - "CloudFront-Forwarded-Proto": "https", - "CloudFront-Is-Desktop-Viewer": "true", - "CloudFront-Is-Mobile-Viewer": "false", - "CloudFront-Is-SmartTV-Viewer": "false", - "CloudFront-Is-Tablet-Viewer": "false", - "CloudFront-Viewer-Country": "US", - "Host": "qg45s7uvg2.execute-api.us-east-1.amazonaws.com", - "User-Agent": "curl/7.54.0", - "Via": "1.1 3d3d633d266d05d90a4eea7a6a59b514.cloudfront.net (CloudFront)", - "X-Amz-Cf-Id": "4mAgowukJJbA7lgTWITzgOPmdiDsXPCwy6vonS8VKPXCdEsmldVgdg==", - "X-Amzn-Trace-Id": "Root=1-59fb8ea5-38c5ad176dac130f3eb9ce97", - "X-Forwarded-For": "69.42.1.180, 54.239.203.118", - "X-Forwarded-Port": "443", - "X-Forwarded-Proto": "https" - }, - "queryStringParameters": null, - "pathParameters": null, - "stageVariables": null, - "requestContext": { - "path": "/prod/posts", - "accountId": "123456789012", - "resourceId": "ery965", - "stage": "prod", - "requestId": "292fbcc8-c015-11e7-94fa-cd109b693f3c", - "identity": { - "cognitoIdentityPoolId": null, - "accountId": null, - "cognitoIdentityId": null, - "caller": null, - "apiKey": "", - "sourceIp": "69.42.1.180", - "accessKey": null, - "cognitoAuthenticationType": null, - "cognitoAuthenticationProvider": null, - "userArn": null, - "userAgent": "curl/7.54.0", - "user": null - }, - "resourcePath": "/posts", - "httpMethod": "GET", - "apiId": "qg45s7uvg2" - }, - "body": null, - "isBase64Encoded": false -} diff --git a/lib/jets/generators/overrides/app/templates/spec/fixtures/payloads/posts-show.json b/lib/jets/generators/overrides/app/templates/spec/fixtures/payloads/posts-show.json deleted file mode 100644 index 1ea12a479..000000000 --- a/lib/jets/generators/overrides/app/templates/spec/fixtures/payloads/posts-show.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "resource": "/posts/{id}", - "path": "/posts/tung", - "httpMethod": "GET", - "headers": { - "Accept": "*/*", - "CloudFront-Forwarded-Proto": "https", - "CloudFront-Is-Desktop-Viewer": "true", - "CloudFront-Is-Mobile-Viewer": "false", - "CloudFront-Is-SmartTV-Viewer": "false", - "CloudFront-Is-Tablet-Viewer": "false", - "CloudFront-Viewer-Country": "US", - "Host": "qg45s7uvg2.execute-api.us-east-1.amazonaws.com", - "User-Agent": "curl/7.54.0", - "Via": "1.1 dc553909528b8b63475c922dc07d8ba6.cloudfront.net (CloudFront)", - "X-Amz-Cf-Id": "s9NB2f_Z1scd6ksA4fcXA2r4QhpSKQ6QcTLQHGfxDPfI-X7sXM3YLg==", - "X-Amzn-Trace-Id": "Root=1-59fb8ecb-586ab81d73e36910793d767c", - "X-Forwarded-For": "69.42.1.180, 54.239.203.97", - "X-Forwarded-Port": "443", - "X-Forwarded-Proto": "https" - }, - "queryStringParameters": null, - "pathParameters": { - "id": "tung" - }, - "stageVariables": null, - "requestContext": { - "path": "/prod/posts/tung", - "accountId": "123456789012", - "resourceId": "wf2gvu", - "stage": "prod", - "requestId": "3feafb4e-c015-11e7-85c9-194f38f4c414", - "identity": { - "cognitoIdentityPoolId": null, - "accountId": null, - "cognitoIdentityId": null, - "caller": null, - "apiKey": "", - "sourceIp": "69.42.1.180", - "accessKey": null, - "cognitoAuthenticationType": null, - "cognitoAuthenticationProvider": null, - "userArn": null, - "userAgent": "curl/7.54.0", - "user": null - }, - "resourcePath": "/posts/{id}", - "httpMethod": "GET", - "apiId": "qg45s7uvg2" - }, - "body": null, - "isBase64Encoded": false -} diff --git a/lib/jets/generators/overrides/app/templates/spec/spec_helper.rb.tt b/lib/jets/generators/overrides/app/templates/spec/spec_helper.rb.tt deleted file mode 100644 index c7eff93d3..000000000 --- a/lib/jets/generators/overrides/app/templates/spec/spec_helper.rb.tt +++ /dev/null @@ -1,30 +0,0 @@ -ENV['JETS_TEST'] = "1" -ENV['JETS_ENV'] ||= "test" -# Ensures aws api never called. Fixture home folder does not contain ~/.aws/credentails -ENV['HOME'] = "spec/fixtures/home" - -require "byebug" -require "fileutils" -require "jets" - -abort("The Jets environment is running in production mode!") if Jets.env == "production" -Jets.boot - -require "jets/spec_helpers" - -<% if @webpacker -%> -require 'capybara/rspec' -Capybara.app = Jets.application -# Capybara.current_driver = :selenium -# Capybara.app_host = 'http://localhost:8888' -<% end %> - -module Helpers - def payload(name) - JSON.load(IO.read("spec/fixtures/payloads/#{name}.json")) - end -end - -RSpec.configure do |c| - c.include Helpers -end diff --git a/lib/jets/generators/overrides/templates/erb/scaffold/_form.html.erb b/lib/jets/generators/overrides/templates/erb/scaffold/_form.html.erb deleted file mode 100644 index b80c1280c..000000000 --- a/lib/jets/generators/overrides/templates/erb/scaffold/_form.html.erb +++ /dev/null @@ -1,34 +0,0 @@ -<%%= form_with(model: <%= model_resource_name %>, local: true) do |form| %> - <%% if <%= singular_table_name %>.errors.any? %> -
-

<%%= pluralize(<%= singular_table_name %>.errors.count, "error") %> prohibited this <%= singular_table_name %> from being saved:

- -
    - <%% <%= singular_table_name %>.errors.full_messages.each do |message| %> -
  • <%%= message %>
  • - <%% end %> -
-
- <%% end %> - -<% attributes.each do |attribute| -%> -
-<% if attribute.password_digest? -%> - <%%= form.label :password %> - <%%= form.password_field :password %> -
- -
- <%%= form.label :password_confirmation %> - <%%= form.password_field :password_confirmation %> -<% else -%> - <%%= form.label :<%= attribute.column_name %> %> - <%%= form.<%= attribute.field_type %> :<%= attribute.column_name %> %> -<% end -%> -
- -<% end -%> -
- <%%= form.submit %> -
-<%% end %> diff --git a/lib/jets/generators/overrides/templates/erb/scaffold/edit.html.erb b/lib/jets/generators/overrides/templates/erb/scaffold/edit.html.erb deleted file mode 100644 index 81329473d..000000000 --- a/lib/jets/generators/overrides/templates/erb/scaffold/edit.html.erb +++ /dev/null @@ -1,6 +0,0 @@ -

Editing <%= singular_table_name.titleize %>

- -<%%= render 'form', <%= singular_table_name %>: @<%= singular_table_name %> %> - -<%%= link_to 'Show', @<%= singular_table_name %> %> | -<%%= link_to 'Back', <%= index_helper %>_path %> diff --git a/lib/jets/generators/overrides/templates/erb/scaffold/index.html.erb b/lib/jets/generators/overrides/templates/erb/scaffold/index.html.erb deleted file mode 100644 index 9578e4c29..000000000 --- a/lib/jets/generators/overrides/templates/erb/scaffold/index.html.erb +++ /dev/null @@ -1,22 +0,0 @@ -

<%= plural_table_name.titleize %>

- - - - -<% attributes.reject(&:password_digest?).each do |attribute| -%> - -<% end -%> - - - - - - <%% @<%= plural_table_name %>.each do |<%= singular_table_name %>| %> - <%%= render <%= singular_table_name %> %> - <%% end %> - -
<%= attribute.human_name %>
- -
- -<%%= link_to 'New <%= singular_table_name.titleize %>', new_<%= singular_route_name %>_path %> diff --git a/lib/jets/generators/overrides/templates/erb/scaffold/new.html.erb b/lib/jets/generators/overrides/templates/erb/scaffold/new.html.erb deleted file mode 100644 index 9b2b2f487..000000000 --- a/lib/jets/generators/overrides/templates/erb/scaffold/new.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -

New <%= singular_table_name.titleize %>

- -<%%= render 'form', <%= singular_table_name %>: @<%= singular_table_name %> %> - -<%%= link_to 'Back', <%= index_helper %>_path %> diff --git a/lib/jets/generators/overrides/templates/erb/scaffold/partial.html.erb b/lib/jets/generators/overrides/templates/erb/scaffold/partial.html.erb deleted file mode 100644 index cb34d63e5..000000000 --- a/lib/jets/generators/overrides/templates/erb/scaffold/partial.html.erb +++ /dev/null @@ -1,8 +0,0 @@ - -<% attributes.reject(&:password_digest?).each do |attribute| -%> - <%%= <%= singular_table_name %>.<%= attribute.column_name %> %> -<% end -%> - <%%= link_to 'Show', <%= model_resource_name %> %> - <%%= link_to 'Edit', edit_<%= singular_route_name %>_path(<%= singular_table_name %>) %> - <%%= link_to 'Destroy', <%= model_resource_name %>, method: :delete, data: { confirm: 'Are you sure?' } %> - diff --git a/lib/jets/generators/overrides/templates/erb/scaffold/show.html.erb b/lib/jets/generators/overrides/templates/erb/scaffold/show.html.erb deleted file mode 100644 index 032aeec6c..000000000 --- a/lib/jets/generators/overrides/templates/erb/scaffold/show.html.erb +++ /dev/null @@ -1,9 +0,0 @@ -<% attributes.reject(&:password_digest?).each do |attribute| -%> -

- <%= attribute.human_name %>: - <%%= @<%= singular_table_name %>.<%= attribute.column_name %> %> -

- -<% end -%> -<%%= link_to 'Edit', edit_<%= singular_table_name %>_path(@<%= singular_table_name %>) %> | -<%%= link_to 'Back', <%= index_helper %>_path %> diff --git a/lib/jets/generators/overrides/templates/rails/assets/javascript.js b/lib/jets/generators/overrides/templates/rails/assets/javascript.js deleted file mode 100644 index dee720fac..000000000 --- a/lib/jets/generators/overrides/templates/rails/assets/javascript.js +++ /dev/null @@ -1,2 +0,0 @@ -// Place all the behaviors and hooks related to the matching controller here. -// All this logic will automatically be available in application.js. diff --git a/lib/jets/generators/overrides/templates/rails/assets/stylesheet.css b/lib/jets/generators/overrides/templates/rails/assets/stylesheet.css deleted file mode 100644 index 7594abf26..000000000 --- a/lib/jets/generators/overrides/templates/rails/assets/stylesheet.css +++ /dev/null @@ -1,4 +0,0 @@ -/* - Place all the styles related to the matching controller here. - They will automatically be included in application.css. -*/ diff --git a/lib/jets/generators/overrides/templates/rails/scaffold/scaffold.css b/lib/jets/generators/overrides/templates/rails/scaffold/scaffold.css deleted file mode 100644 index cd4f3de38..000000000 --- a/lib/jets/generators/overrides/templates/rails/scaffold/scaffold.css +++ /dev/null @@ -1,80 +0,0 @@ -body { - background-color: #fff; - color: #333; - margin: 33px; -} - -body, p, ol, ul, td { - font-family: verdana, arial, helvetica, sans-serif; - font-size: 13px; - line-height: 18px; -} - -pre { - background-color: #eee; - padding: 10px; - font-size: 11px; -} - -a { - color: #000; -} - -a:visited { - color: #666; -} - -a:hover { - color: #fff; - background-color: #000; -} - -th { - padding-bottom: 5px; -} - -td { - padding: 0 5px 7px; -} - -div.field, -div.actions { - margin-bottom: 10px; -} - -#notice { - color: green; -} - -.field_with_errors { - padding: 2px; - background-color: red; - display: table; -} - -#error_explanation { - width: 450px; - border: 2px solid red; - padding: 7px 7px 0; - margin-bottom: 20px; - background-color: #f0f0f0; -} - -#error_explanation h2 { - text-align: left; - font-weight: bold; - padding: 5px 5px 5px 15px; - font-size: 12px; - margin: -7px -7px 0; - background-color: #c00; - color: #fff; -} - -#error_explanation ul li { - font-size: 12px; - list-style: square; -} - -label { - display: block; -} diff --git a/lib/jets/generators/overrides/templates/rails/scaffold_controller/api_controller.rb.tt b/lib/jets/generators/overrides/templates/rails/scaffold_controller/api_controller.rb.tt deleted file mode 100644 index f24c01dbd..000000000 --- a/lib/jets/generators/overrides/templates/rails/scaffold_controller/api_controller.rb.tt +++ /dev/null @@ -1,58 +0,0 @@ -<% module_namespacing do -%> -class <%= controller_class_name %>Controller < ApplicationController - before_action :set_<%= singular_table_name %>, only: %i[ show update destroy ] - - # GET <%= route_url %> - def index - @<%= plural_table_name %> = <%= orm_class.all(class_name) %> - - render json: <%= "@#{plural_table_name}" %> - end - - # GET <%= route_url %>/1 - def show - render json: <%= "@#{singular_table_name}" %> - end - - # POST <%= route_url %> - def create - @<%= singular_table_name %> = <%= orm_class.build(class_name, "#{singular_table_name}_params") %> - - if @<%= orm_instance.save %> - render json: <%= "@#{singular_table_name}" %>, status: :created, location: <%= "@#{singular_table_name}" %> - else - render json: <%= "@#{orm_instance.errors}" %>, status: :unprocessable_entity - end - end - - # PATCH/PUT <%= route_url %>/1 - def update - if @<%= orm_instance.update("#{singular_table_name}_params") %> - render json: <%= "@#{singular_table_name}" %> - else - render json: <%= "@#{orm_instance.errors}" %>, status: :unprocessable_entity - end - end - - # DELETE <%= route_url %>/1 - def destroy - @<%= orm_instance.destroy %> - render json: {deleted: true} - end - - private - # Use callbacks to share common setup or constraints between actions. - def set_<%= singular_table_name %> - @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %> - end - - # Only allow a list of trusted parameters through. - def <%= "#{singular_table_name}_params" %> - <%- if attributes_names.empty? -%> - params.fetch(:<%= singular_table_name %>, {}) - <%- else -%> - params.require(:<%= singular_table_name %>).permit(<%= permitted_params %>) - <%- end -%> - end -end -<% end -%> diff --git a/lib/jets/generators/overrides/templates/rails/scaffold_controller/controller.rb.tt b/lib/jets/generators/overrides/templates/rails/scaffold_controller/controller.rb.tt deleted file mode 100644 index 399d5a9c7..000000000 --- a/lib/jets/generators/overrides/templates/rails/scaffold_controller/controller.rb.tt +++ /dev/null @@ -1,67 +0,0 @@ -<% module_namespacing do -%> -class <%= controller_class_name %>Controller < ApplicationController - before_action :set_<%= singular_table_name %>, only: %i[ show edit update destroy ] - - # GET <%= route_url %> - def index - @<%= plural_table_name %> = <%= orm_class.all(class_name) %> - end - - # GET <%= route_url %>/1 - def show - end - - # GET <%= route_url %>/new - def new - @<%= singular_table_name %> = <%= orm_class.build(class_name) %> - end - - # GET <%= route_url %>/1/edit - def edit - end - - # POST <%= route_url %> - def create - @<%= singular_table_name %> = <%= orm_class.build(class_name, "#{singular_table_name}_params") %> - - if @<%= orm_instance.save %> - redirect_to <%= redirect_resource_name %>, notice: <%= %("#{human_name} was successfully created.") %> - else - render :new, status: :unprocessable_entity - end - end - - # PATCH/PUT <%= route_url %>/1 - def update - if @<%= orm_instance.update("#{singular_table_name}_params") %> - redirect_to <%= redirect_resource_name %>, notice: <%= %("#{human_name} was successfully updated.") %>, status: :see_other - else - render :edit, status: :unprocessable_entity - end - end - - # DELETE <%= route_url %>/1 - def destroy - @<%= orm_instance.destroy %> - respond_to do |format| - format.html { redirect_to <%= index_helper %>_url, notice: <%= %("#{human_name} was successfully destroyed.") %>, status: :see_other } - format.json { render json: {location: <%= index_helper %>_url} } - end - end - - private - # Use callbacks to share common setup or constraints between actions. - def set_<%= singular_table_name %> - @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %> - end - - # Only allow a list of trusted parameters through. - def <%= "#{singular_table_name}_params" %> - <%- if attributes_names.empty? -%> - params.fetch(:<%= singular_table_name %>, {}) - <%- else -%> - params.require(:<%= singular_table_name %>).permit(<%= permitted_params %>) - <%- end -%> - end -end -<% end -%> diff --git a/lib/jets/git.rb b/lib/jets/git.rb new file mode 100644 index 000000000..481adb1db --- /dev/null +++ b/lib/jets/git.rb @@ -0,0 +1,5 @@ +module Jets + module Git + class Error < StandardError; end + end +end diff --git a/lib/jets/git/azure.rb b/lib/jets/git/azure.rb new file mode 100644 index 000000000..f64bee298 --- /dev/null +++ b/lib/jets/git/azure.rb @@ -0,0 +1,64 @@ +module Jets::Git + class Azure < Base + def info + info = { + git_system: "azure", + git_branch: git_branch, + git_sha: git_sha, + git_dirty: false + # git_message: nil, + # git_version: nil, + } + info[:git_url] = git_url if git_url + info + end + + def git_branch + if pr_number + message = ENV["BUILD_SOURCEVERSIONMESSAGE"] + md = message.match(/Merge pull request \d+ from (.*) into (.*)/) + if md + # IE: BUILD_SOURCEVERSIONMESSAGE=Merge pull request 2 from feature into main + # Its a bit weird but with azure repos with check policy trigger + md[1] + else # GitHub and Bitbucket PR has actual branch though + # IE: SYSTEM_PULLREQUEST_SOURCEBRANCH=feature + message + end + else # push + ENV["BUILD_SOURCEBRANCHNAME"] + end + end + + def git_sha + ENV["BUILD_SOURCEVERSION"] + end + + def git_url + "#{host}/#{full_repo}" + end + + # IE: BUILD_REPOSITORY_URI=https://tongueroo@dev.azure.com/tongueroo/infra-project/_git/infra-ci + def host + uri = URI(ENV["BUILD_REPOSITORY_URI"]) + "#{uri.scheme}://#{uri.host}" + end + + # IE: BUILD_REPOSITORY_URI=https://tongueroo@dev.azure.com/tongueroo/infra-project/_git/infra-ci + def full_repo + uri = URI(ENV["BUILD_REPOSITORY_URI"]) + org = uri.path.split("/")[1] # since there's a leading / + repo = ENV["BUILD_REPOSITORY_NAME"] # tongueroo + "#{org}/#{repo}" + end + + # IE: SYSTEM_PULLREQUEST_PULLREQUESTID=2 + def pr_number + ENV["SYSTEM_PULLREQUEST_PULLREQUESTID"] + end + + def build_type + ENV["SYSTEM_PULLREQUEST_PULLREQUESTID"] ? "pull_request" : "push" + end + end +end diff --git a/lib/jets/git/base.rb b/lib/jets/git/base.rb new file mode 100644 index 000000000..879fbf61a --- /dev/null +++ b/lib/jets/git/base.rb @@ -0,0 +1,26 @@ +module Jets::Git + class Base + extend Memoist + + def params + base.merge(info) + end + memoize :params + + # interface method + def info + {} + end + + def base + { + git_user: user.name + } + end + + def user + User.new + end + memoize :user + end +end diff --git a/lib/jets/git/bitbucket.rb b/lib/jets/git/bitbucket.rb new file mode 100644 index 000000000..8049d0876 --- /dev/null +++ b/lib/jets/git/bitbucket.rb @@ -0,0 +1,30 @@ +module Jets::Git + class Bitbucket < Base + def info + info = { + git_system: "bitbucket", + git_branch: git_branch, + git_sha: git_sha, + git_dirty: false + # git_message: nil, + # git_version: nil, + } + info[:git_url] = git_url if git_url + info + end + + def git_branch + ENV["BITBUCKET_BRANCH"] + end + + def git_sha + ENV["BITBUCKET_COMMIT"] + end + + def git_url + host = ENV["BITBUCKET_HOST"] || "https://bitbucket.org" + full_repo = ENV["BITBUCKET_REPO_FULL_NAME"] + "#{host}/#{full_repo}" + end + end +end diff --git a/lib/jets/git/circleci.rb b/lib/jets/git/circleci.rb new file mode 100644 index 000000000..81b355fc5 --- /dev/null +++ b/lib/jets/git/circleci.rb @@ -0,0 +1,23 @@ +module Jets::Git + class Circleci < Base + def info + { + git_system: "circleci", + git_branch: git_branch, + git_sha: git_sha, + git_dirty: false + # git_message: nil, + # git_version: nil, + } + # info[:git_url] = git_url if git_url + end + + def git_branch + ENV["CIRCLE_BRANCH"] + end + + def git_sha + ENV["CIRCLE_SHA1"] + end + end +end diff --git a/lib/jets/git/codebuild.rb b/lib/jets/git/codebuild.rb new file mode 100644 index 000000000..a7bea8154 --- /dev/null +++ b/lib/jets/git/codebuild.rb @@ -0,0 +1,49 @@ +module Jets::Git + class Codebuild < Base + def info + info = { + git_system: "codebuild", + git_branch: git_branch, + git_sha: git_sha, + git_dirty: false, + git_url: git_url + # git_message: nil, + # git_version: nil, + } + info.delete_if { |k, v| v.nil? } + info + end + + def git_branch + ENV["CODEBUILD_SOURCE_VERSION"] + end + + def git_sha + ENV["CODEBUILD_RESOLVED_SOURCE_VERSION"] + end + + def git_url + "#{host}/#{full_repo}" + end + + def host + return unless ENV["CODEBUILD_SOURCE_REPO_URL"] + uri = URI(ENV["CODEBUILD_SOURCE_REPO_URL"]) # https://github.com/ORG/REPO + "#{uri.scheme}://#{uri.host}" + end + + # ORG/REPO + def full_repo + return unless repo_url + uri = URI(repo_url) + uri.path.sub(/^\//, "") + end + + # https://github.com/ORG/REPO + def repo_url + return unless ENV["CODEBUILD_SOURCE_REPO_URL"] + # https://github.com/ORG/REPO.git + ENV["CODEBUILD_SOURCE_REPO_URL"].sub(".git", "") + end + end +end diff --git a/lib/jets/git/custom.rb b/lib/jets/git/custom.rb new file mode 100644 index 000000000..e044a44c7 --- /dev/null +++ b/lib/jets/git/custom.rb @@ -0,0 +1,32 @@ +module Jets::Git + class Custom < Base + def info + info = { + git_system: "custom", + git_branch: git_branch, + git_sha: git_sha, + git_dirty: false, + git_message: git_message + # git_version: nil, + } + info[:git_url] = git_url if git_url + info + end + + def git_branch + ENV["JETS_GIT_CUSTOM_BRANCH"] + end + + def git_sha + ENV["JETS_GIT_CUSTOM_SHA"] + end + + def git_url + ENV["JETS_GIT_CUSTOM_URL"] + end + + def git_message + ENV["JETS_GIT_CUSTOM_MESSAGE"] + end + end +end diff --git a/lib/jets/git/git_cli.rb b/lib/jets/git/git_cli.rb new file mode 100644 index 000000000..eef8bc6ba --- /dev/null +++ b/lib/jets/git/git_cli.rb @@ -0,0 +1,28 @@ +module Jets::Git + module GitCli + def git? + git_folder? && git_installed? + end + + def git_folder? + File.exist?(".git") + end + + def git_installed? + system "type git > /dev/null 2>&1" + end + + def git(args, on_error: :nil) + out = `git #{args}`.strip + unless $?.success? + case on_error + when :raise + raise Jets::Git::Error, "ERROR: git #{args} failed".color(:red) + when :nil + return + end + end + out + end + end +end diff --git a/lib/jets/git/github.rb b/lib/jets/git/github.rb new file mode 100644 index 000000000..0e8c8bfd5 --- /dev/null +++ b/lib/jets/git/github.rb @@ -0,0 +1,48 @@ +module Jets::Git + class Github < Base + def info + info = { + git_system: "github", + git_branch: git_branch, + git_sha: git_sha, + git_dirty: false + # git_message: nil, + # git_version: nil, + } + info[:git_url] = git_url if git_url + info + end + + def git_branch + if build_type == "pull_request" + pr.dig("pull_request", "head", "ref") + else # push + ENV["GITHUB_REF_NAME"] + end + end + + def git_sha + if build_type == "pull_request" + pr.dig("pull_request", "head", "sha") + else # push + ENV["GITHUB_SHA"] + end + end + + def git_url + host = ENV["GITHUB_SERVER_URL"] || "https://github.com" + full_repo = ENV["GITHUB_REPOSITORY"] + "#{host}/#{full_repo}" + end + + # GitHub webhook JSON payload in file and path is set in GITHUB_EVENT_PATH + def pr + return {} unless ENV["GITHUB_EVENT_PATH"] + JSON.load(IO.read(ENV["GITHUB_EVENT_PATH"])) + end + + def build_type + ENV["GITHUB_EVENT_NAME"] + end + end +end diff --git a/lib/jets/git/gitlab.rb b/lib/jets/git/gitlab.rb new file mode 100644 index 000000000..daa2afe56 --- /dev/null +++ b/lib/jets/git/gitlab.rb @@ -0,0 +1,30 @@ +module Jets::Git + class Gitlab < Base + def info + info = { + git_system: "gitlab", + git_branch: git_branch, + git_sha: git_sha, + git_dirty: false + # git_message: nil, + # git_version: nil, + } + info[:git_url] = git_url if git_url + info + end + + def git_branch + ENV["CI_COMMIT_REF_NAME"] + end + + def git_sha + ENV["CI_COMMIT_SHA"] + end + + def git_url + host = ENV["CI_SERVER_URL"] || "https://gitlab.com" + full_repo = ENV["CI_PROJECT_PATH"] + "#{host}/#{full_repo}" + end + end +end diff --git a/lib/jets/git/info.rb b/lib/jets/git/info.rb new file mode 100644 index 000000000..766859b0f --- /dev/null +++ b/lib/jets/git/info.rb @@ -0,0 +1,39 @@ +module Jets::Git + class Info + extend Memoist + # Not using options but trying to future proof initialize + def initialize(options = {}) + @options = options + end + + def user + User.new + end + memoize :user + + # Best effort to get git info + def params + return {} if ENV["JETS_GIT_DISABLED"] + strategy_class.new.params + end + + def strategy_class + return Saved if File.exist?(".jets/gitinfo.yml") + + env_map = { + BITBUCKET_COMMIT: Bitbucket, + CIRCLECI: Circleci, + CODEBUILD_CI: Codebuild, + GITHUB_ACTIONS: Github, + GITLAB_CI: Gitlab, + JETS_GIT_CUSTOM: Custom, + SYSTEM_TEAMFOUNDATIONSERVERURI: Azure + } + found = env_map.find do |env_key, strategy_class| + ENV[env_key.to_s] + end + found ? found[1] : Local + end + memoize :strategy_class + end +end diff --git a/lib/jets/git/local.rb b/lib/jets/git/local.rb new file mode 100644 index 000000000..8c4451427 --- /dev/null +++ b/lib/jets/git/local.rb @@ -0,0 +1,69 @@ +module Jets::Git + class Local < Base + include GitCli + + def info + return {} unless git? && git_branch + info = { + git_system: "local", + git_branch: git_branch, + git_sha: git_sha, + git_dirty: git_dirty?, + git_message: git_message, + git_version: git_version, + git_default_branch: git_default_branch + } + info[:git_url] = git_url if git_url + info + end + + def git_message + git "log -1 --pretty=%B" + end + + def git_branch + git "rev-parse --abbrev-ref HEAD" + end + + def git_sha + git "rev-parse HEAD" + end + + def git_url + # IE: git "config --get remote.origin.url" + git "config --get remote.#{git_remote}.url" + end + + def git_dirty? + !git("status --porcelain").empty? + end + + def git_version + git "--version", on_error: :raise + end + + # discover git remote name. in case it's not origin + def git_remote + # 2>&1 to suppress error message + # "fatal: not a git repository (or any parent up to mount point /path)\nStopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).\n" + `git remote 2>&1` + return unless $?.success? + `git remote`.strip # IE: origin or blank string + end + memoize :git_remote + + def git_default_branch + default = ENV["JETS_GIT_DEFAULT_BRANCH"] || "master" + out = `git remote show origin 2>&1`.strip + return default unless $?.success? + + lines = out.split("\n") + lines.each do |line| + if line.include?("HEAD") + return line.split(" ").last + end + end + default + end + end +end diff --git a/lib/jets/git/saved.rb b/lib/jets/git/saved.rb new file mode 100644 index 000000000..b52734686 --- /dev/null +++ b/lib/jets/git/saved.rb @@ -0,0 +1,16 @@ +require "yaml" +require "active_support" +require "active_support/core_ext/hash" + +module Jets::Git + class Saved + extend Memoist + + # gitinfo.yml contains original git info from the project + def params + return {} unless File.exist?(".jets/gitinfo.yml") + data = YAML.load_file(".jets/gitinfo.yml") + ActiveSupport::HashWithIndifferentAccess.new(data) + end + end +end diff --git a/lib/jets/git/user.rb b/lib/jets/git/user.rb new file mode 100644 index 000000000..cdfb5ce30 --- /dev/null +++ b/lib/jets/git/user.rb @@ -0,0 +1,33 @@ +module Jets::Git + class User + extend Memoist + include GitCli + + def first_name + name.split(" ").first if name # name can be nil + end + + def name + saved[:git_user] || git_config["user.name"] + end + + def saved + return {} unless File.exist?(".jets/gitinfo.yml") + data = YAML.load_file(".jets/gitinfo.yml") + ActiveSupport::HashWithIndifferentAccess.new(data) + end + + def git_config + return {} if ENV["JETS_GIT_DISABLED"] + + return {} unless git? + list = git("config --list") + lines = list.split("\n") + # Other values in the git config are not needed. + # And can cause .to_h to bomb and throw an error. + lines.select! { |l| l =~ /^user\./ } + lines.map { |l| l.split("=") }.to_h + end + memoize :git_config + end +end diff --git a/lib/jets/inflections.rb b/lib/jets/inflections.rb deleted file mode 100644 index 2b2620fd1..000000000 --- a/lib/jets/inflections.rb +++ /dev/null @@ -1,32 +0,0 @@ -module Jets - class Inflections - class << self - def load! - ActiveSupport::Inflector.inflections(:en) do |inflect| - load(inflect, base) - load(inflect, custom) - end - end - - def load(inflect, inflections) - inflections.each do |k,v| - inflect.irregular k,v - end - end - - # base custom inflections - def base - { - sns: 'sns', - sqs: 'sqs' - } - end - - # User defined custom inflections - def custom - path = "#{Jets.root}/config/inflections.yml" - File.exist?(path) ? Jets::Util::Yamler.load_file(path) : {} - end - end - end -end diff --git a/lib/jets/info.rb b/lib/jets/info.rb deleted file mode 100644 index 6a1d2770f..000000000 --- a/lib/jets/info.rb +++ /dev/null @@ -1,116 +0,0 @@ -# frozen_string_literal: true - -require "cgi" -require "rack" - -module Jets - # This module helps build the runtime properties that are displayed in - # Jets::InfoController responses. These include the active Jets version, - # Ruby version, Rack version, and so on. - module Info - mattr_accessor :properties, default: [] - - class << @@properties - def names - map(&:first) - end - - def value_for(property_name) - if property = assoc(property_name) - property.last - end - end - end - - class << self # :nodoc: - def property(name, value = nil) - value ||= yield - properties << [name, value] if value - rescue Exception - end - - def to_s - column_width = properties.names.map(&:length).max - info = properties.map do |name, value| - value = value.join(", ") if value.is_a?(Array) - "%-#{column_width}s %s" % [name, value] - end - info.unshift "About your application's environment" - info * "\n" - end - - alias inspect to_s - - def to_html - (+"").tap do |table| - properties.each do |(name, value)| - table << %() - formatted_value = if value.kind_of?(Array) - "
    " + value.map { |v| "
  • #{CGI.escapeHTML(v.to_s)}
  • " }.join + "
" - else - CGI.escapeHTML(value.to_s) - end - table << %() - end - table << "
#{CGI.escapeHTML(name.to_s)}#{formatted_value}
" - end - end - end - - # The Jets version. - property "Jets version" do - Jets.version.to_s - end - - # The Ruby version and platform, e.g. "2.0.0-p247 (x86_64-darwin12.4.0)". - property "Ruby version" do - RUBY_DESCRIPTION - end - - # The RubyGems version, if it's installed. - property "RubyGems version" do - Gem::VERSION - end - - property "Rack version" do - ::Rack.release - end - - property "JavaScript Runtime" do - ExecJS.runtime.name - end - - # Note: The Jets.configuration.middleware is not available until Jets.boot finishes. - # It's important for the jets gem not to eager load because - # Jets.configuration.middleware is not available until after Jets.boot. - # - # Originally called Jets.boot here, but it results in a boot twice. - # That results in weird behavior like commenting out DebugException middleware - # breaks startup with ActiveSupport::Dependencies.autoload_paths freeze error. - # The side-effect errors are hard to debug. Jets::Info should be loaded lazily. - # - # In Jets::Autoloaders::Gem we do_not_eager_load Jets::Info to avoid this issue. - property "Middleware" do - Jets.configuration.middleware.map(&:inspect) - end - - # The application's location on the filesystem. - property "Application root" do - File.expand_path(Jets.root) - end - - # The current Jets environment (development, test, or production). - property "Environment" do - Jets.env - end - - # The name of the database adapter for the current environment. - property "Database adapter" do - ActiveRecord::Base.connection.pool.db_config.adapter - end - - property "Database schema version" do - ActiveRecord::Base.connection.migration_context.current_version rescue nil - end - end -end diff --git a/lib/jets/initializable.rb b/lib/jets/initializable.rb deleted file mode 100644 index 7accb14d7..000000000 --- a/lib/jets/initializable.rb +++ /dev/null @@ -1,95 +0,0 @@ -# frozen_string_literal: true - -require "tsort" - -module Jets - module Initializable - def self.included(base) # :nodoc: - base.extend ClassMethods - end - - class Initializer - attr_reader :name, :block - - def initialize(name, context, options, &block) - options[:group] ||= :default - @name, @context, @options, @block = name, context, options, block - end - - def before - @options[:before] - end - - def after - @options[:after] - end - - def belongs_to?(group) - @options[:group] == group || @options[:group] == :all - end - - def run(*args) - @context.instance_exec(*args, &block) - end - - def bind(context) - return self if @context - Initializer.new(@name, context, @options, &block) - end - - def context_class - @context.class - end - end - - class Collection < Array - include TSort - - alias :tsort_each_node :each - def tsort_each_child(initializer, &block) - select { |i| i.before == initializer.name || i.name == initializer.after }.each(&block) - end - - def +(other) - Collection.new(to_a + other.to_a) - end - end - - def run_initializers(group = :default, *args) - return if instance_variable_defined?(:@ran) - initializers.tsort_each do |initializer| - initializer.run(*args) if initializer.belongs_to?(group) - end - @ran = true - end - - def initializers - @initializers ||= self.class.initializers_for(self) - end - - module ClassMethods - def initializers - @initializers ||= Collection.new - end - - def initializers_chain - initializers = Collection.new - ancestors.reverse_each do |klass| - next unless klass.respond_to?(:initializers) - initializers = initializers + klass.initializers - end - initializers - end - - def initializers_for(binding) - Collection.new(initializers_chain.map { |i| i.bind(binding) }) - end - - def initializer(name, opts = {}, &blk) - raise ArgumentError, "A block must be passed when defining an initializer" unless blk - opts[:after] ||= initializers.last.name unless initializers.empty? || initializers.find { |i| i.name == opts[:before] } - initializers << Initializer.new(name, nil, opts, &blk) - end - end - end -end diff --git a/lib/jets/job/base.rb b/lib/jets/job/base.rb deleted file mode 100644 index d2079ea28..000000000 --- a/lib/jets/job/base.rb +++ /dev/null @@ -1,52 +0,0 @@ -require 'json' - -# Job public methods get turned into Lambda functions. -# -# Jets::Job::Base < Jets::Lambda::Functions -# Both Jets::Job::Base and Jets::Lambda::Functions have Dsl modules included. -# So the Jets::Job::Dsl overrides some of the Jets::Lambda::Functions behavior. -module Jets::Job - class Base < Jets::Lambda::Functions - include Dsl - - # non-DSL methods - include Helpers::KinesisEvent - include Helpers::LogEvent - include Helpers::S3Event - include Helpers::SnsEvent - include Helpers::SqsEvent - prepend Jets::ExceptionReporting::Process - - # Tracks bucket each time an s3_event is declared - # Map of bucket_name => stack_name (nested part) - cattr_accessor :_s3_events # dont want this to be inheritable intentionally - self._s3_events = {} - - class << self - def process(event, context, meth) - job = new(event, context, meth) - job.send(meth) - end - - def perform_now(meth, event={}, context={}) - process(event, context, meth) - end - - def perform_later(meth, event={}, context={}) - if on_lambda? - function_name = "#{self.to_s.underscore}-#{meth}" - call = Jets::Commands::Call::Caller.new(function_name, JSON.dump(event), invocation_type: "Event") - call.run - else - Jets.logger.info "INFO: Not on AWS Lambda. In local mode perform_later executes the job with perform_now instead." - perform_now(meth, event, context) - end - end - - private - def on_lambda? - !!ENV['AWS_LAMBDA_FUNCTION_NAME'] - end - end - end -end diff --git a/lib/jets/job/dsl.rb b/lib/jets/job/dsl.rb deleted file mode 100644 index dc8ef4bc1..000000000 --- a/lib/jets/job/dsl.rb +++ /dev/null @@ -1,40 +0,0 @@ -require 'active_support' -require 'active_support/core_ext/class' - -# Jets::Job::Base < Jets::Lambda::Functions -# Both Jets::Job::Base and Jets::Lambda::Functions have Dsl modules included. -# So the Jets::Job::Dsl overrides some of the Jets::Lambda::Functions behavior. -# -# Implements: -# -# default_associated_resource_definition -# -module Jets::Job::Dsl - extend ActiveSupport::Concern - - included do - class << self - include Jets::AwsServices - - include DynamodbEvent - include EventSourceMapping - include IotEvent - include KinesisEvent - include LogEvent - include RuleEvent - include S3Event - include SnsEvent - include SqsEvent - - # Used to provide a little more identifiable event rule auto-descriptions - class_attribute :rule_counter - self.rule_counter = 0 - - # TODO: Get rid of default_associated_resource_definition concept. - # Also gets rid of the need to keep track of running @associated_properties too. - def default_associated_resource_definition(meth) - events_rule_definition - end - end - end -end diff --git a/lib/jets/job/dsl/dynamodb_event.rb b/lib/jets/job/dsl/dynamodb_event.rb deleted file mode 100644 index 923a1234c..000000000 --- a/lib/jets/job/dsl/dynamodb_event.rb +++ /dev/null @@ -1,78 +0,0 @@ -module Jets::Job::Dsl - module DynamodbEvent - def dynamodb_event(table_name_without_namespace, options={}) - return if ENV['JETS_NO_INTERNET'] # Disable during build since jets build tries to init this - - table_name = add_dynamodb_table_namespace(table_name_without_namespace) - stream_arn = full_dynamodb_stream_arn(table_name) - default_iam_policy = default_dynamodb_stream_policy(stream_arn) - - # Create iam policy allows access to dynamodb - iam_policy_option = [options.delete(:iam_policy)].compact.flatten - user_iam_policy = [@iam_policy].compact.flatten - iam_policy_props = iam_policy_option + user_iam_policy + [default_iam_policy] - iam_policy(iam_policy_props) - - props = options # by this time options only has EventSourceMapping properties - default = { - EventSourceArn: stream_arn, - StartingPosition: "TRIM_HORIZON", - } - props = default.merge(props) - - event_source_mapping(props) - end - - def add_dynamodb_table_namespace(table_name_without_namespace) - ns = if Jets.config.events.dynamodb.table_namespace == true - Jets.table_namespace # does not include extra - elsif Jets.config.events.dynamodb.table_namespace - Jets.config.events.dynamodb.table_namespace # allow user to fully control namespace - end - ns_separator = Jets.config.events.dynamodb.table_namespace_separator - [ns, table_name_without_namespace].compact.join(ns_separator) - end - - # Expands table name to the full stream arn. Example: - # - # test-table - # To: - # arn:aws:dynamodb:us-west-2:112233445566:table/test-table/stream/2019-02-15T21:41:15.217 - # - # Note, this does not check if the stream has been disabled. - def full_dynamodb_stream_arn(table_name) - return table_name if table_name.include?("arn:aws:dynamodb") # assume full stream arn - - begin - resp = dynamodb.describe_table(table_name: table_name) - rescue Aws::DynamoDB::Errors::ResourceNotFoundException => e - puts e.message - puts "ERROR: Was not able to find the DynamoDB table: #{table_name}.".color(:red) - code_line = caller.grep(%r{/app/jobs}).first - puts "Please check: #{code_line}" - puts "Exiting" - exit 1 - end - stream_arn = resp.table.latest_stream_arn - return stream_arn if stream_arn - end - - def default_dynamodb_stream_policy(stream_name_arn='*') - stream = { - Action: ["dynamodb:GetRecords", - "dynamodb:GetShardIterator", - "dynamodb:DescribeStream", - "dynamodb:ListStreams"], - Effect: "Allow", - Resource: stream_name_arn, - } - table_name_arn = stream_name_arn.gsub(%r{/stream/20.*},'') - table = { - Action: ["dynamodb:DescribeTable"], - Effect: "Allow", - Resource: table_name_arn, - } - [stream, table] - end - end -end \ No newline at end of file diff --git a/lib/jets/job/dsl/event_source_mapping.rb b/lib/jets/job/dsl/event_source_mapping.rb deleted file mode 100644 index ef971fa2b..000000000 --- a/lib/jets/job/dsl/event_source_mapping.rb +++ /dev/null @@ -1,11 +0,0 @@ -# SqsEvent uses this module -module Jets::Job::Dsl - module EventSourceMapping - def event_source_mapping(props={}) - r = Jets::Cfn::Resource::Lambda::EventSourceMapping.new(props) - with_fresh_properties do - resource(r.definition) # add associated resource immediately - end - end - end -end diff --git a/lib/jets/job/dsl/iot_event.rb b/lib/jets/job/dsl/iot_event.rb deleted file mode 100644 index 14ae29b3e..000000000 --- a/lib/jets/job/dsl/iot_event.rb +++ /dev/null @@ -1,24 +0,0 @@ -module Jets::Job::Dsl - module IotEvent - # The user must at least pass in an SQL statement - def iot_event(props={}) - if props.is_a?(String) # SQL Statement - props = {Sql: props} - topic_props = {TopicRulePayload: props} - elsif props.key?(:TopicRulePayload) # full properties structure - topic_props = props - else # just the TopicRulePayload - topic_props = {TopicRulePayload: props} - end - - declare_iot_topic(topic_props) - end - - def declare_iot_topic(props={}) - r = Jets::Cfn::Resource::Iot::TopicRule.new(props) - with_fresh_properties do - resource(r.definition) # add associated resource immediately - end - end - end -end diff --git a/lib/jets/job/dsl/kinesis_event.rb b/lib/jets/job/dsl/kinesis_event.rb deleted file mode 100644 index e7e5f52e7..000000000 --- a/lib/jets/job/dsl/kinesis_event.rb +++ /dev/null @@ -1,47 +0,0 @@ -module Jets::Job::Dsl - module KinesisEvent - def kinesis_event(stream_name, options={}) - stream_arn = full_kinesis_stream_arn(stream_name) - default_iam_policy = default_kinesis_stream_policy(stream_arn) - - # Create iam policy allows access to kinesis - iam_policy_option = [options.delete(:iam_policy)].compact.flatten - user_iam_policy = [@iam_policy].compact.flatten - iam_policy_props = iam_policy_option + user_iam_policy + [default_iam_policy] - iam_policy(iam_policy_props) - - props = options # by this time options only has EventSourceMapping properties - default = { - EventSourceArn: stream_arn, - StartingPosition: "LATEST", - } - props = default.merge(props) - - event_source_mapping(props) - end - - # Expands table name to the full stream arn. Example: - # - # test-table - # To: - # arn:aws:kinesis:us-west-2:112233445566:table/test-table/stream/2019-02-15T21:41:15.217 - # - # Note, this does not check if the stream has been disabled. - def full_kinesis_stream_arn(stream_name) - return stream_name if stream_name.include?("arn:aws:kinesis") # assume full stream arn - - "arn:aws:kinesis:#{Jets.aws.region}:#{Jets.aws.account}:stream/#{stream_name}" - end - - def default_kinesis_stream_policy(stream_name_arn='*') - { - Action: ["kinesis:GetRecords", - "kinesis:GetShardIterator", - "kinesis:DescribeStream", - "kinesis:ListStreams"], - Effect: "Allow", - Resource: stream_name_arn, - } - end - end -end \ No newline at end of file diff --git a/lib/jets/job/dsl/log_event.rb b/lib/jets/job/dsl/log_event.rb deleted file mode 100644 index fa2bdd112..000000000 --- a/lib/jets/job/dsl/log_event.rb +++ /dev/null @@ -1,15 +0,0 @@ -module Jets::Job::Dsl - module LogEvent - def log_event(log_group_name, props={}) - props.merge!(LogGroupName: log_group_name) - declare_log_subscription_filter(props) - end - - def declare_log_subscription_filter(props={}) - r = Jets::Cfn::Resource::Logs::SubscriptionFilter.new(props) - with_fresh_properties do - resource(r.definition) # add associated resource immediately - end - end - end -end diff --git a/lib/jets/job/dsl/rule_event.rb b/lib/jets/job/dsl/rule_event.rb deleted file mode 100644 index 3045214ce..000000000 --- a/lib/jets/job/dsl/rule_event.rb +++ /dev/null @@ -1,78 +0,0 @@ -module Jets::Job::Dsl - module RuleEvent - # Public: Creates CloudWatch Event Rule - # - # expression - The rate expression. - # - # Examples - # - # rate("10 minutes") - # rate("10 minutes", description: "Hard job") - # - def rate(expression, props={}) - schedule_job("rate(#{expression})", props) - end - - # Public: Creates CloudWatch Event Rule - # - # expression - The cron expression. - # - # Examples - # - # cron("0 */12 * * ? *") - # cron("0 */12 * * ? *", description: "Hard job") - # - def cron(expression, props={}) - schedule_job("cron(#{expression})", props) - end - - def schedule_job(expression, props={}) - props = props.merge(ScheduleExpression: expression) - rule_event(props) - end - - def rule_event(props={}) - # detail should all be lowercase: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-events-rule.html - # source should also all be lowercase - if props.key?(:detail) - # :description should be lowercased. It's a convenience property, and belongs at a higher level - description = props.key?(:description) ? props.delete(:description) : rule_description - rule_props = { EventPattern: props, Description: description } - else # if props.key?(:event_pattern) - props[:Description] ||= rule_description - rule_props = props - end - - with_fresh_properties(multiple_resources: false) do - associated_properties(rule_props) # TODO: consider getting rid of @associated_properties concept - resource(events_rule_definition) # add associated resource immediately - end - end - - def rule_description - self.rule_counter += 1 - "#{self.name} event rule #{rule_counter}" - end - - def events_rule_definition - resource = Jets::Cfn::Resource::Events::Rule.new(associated_properties) - resource.definition # returns a definition to be added by associated_resources - end - - # Deprecated methods, will be removed in the future - def events_rule(props) - puts "DEPRECATED: events_rule. Instead use rule_event. The events_rule will be removed in the future. Pausing for 5 seconds".color(:yellow) - puts caller[0] - sleep 5 - rule_event(props) - end - # Deprecated methods, will be removed in the future - - def event_pattern(props) - puts "DEPRECATED: events_rule. Instead use rule_event. The events_rule will be removed in the future. Pausing for 5 seconds".color(:yellow) - puts caller[0] - sleep 5 - rule_event(props) - end - end -end diff --git a/lib/jets/job/dsl/s3_event.rb b/lib/jets/job/dsl/s3_event.rb deleted file mode 100644 index fbffbd1ec..000000000 --- a/lib/jets/job/dsl/s3_event.rb +++ /dev/null @@ -1,77 +0,0 @@ -module Jets::Job::Dsl - module S3Event - # Register an S3 event. - # Allow custom sns_subscription_properties to be passed in. - # - # Examples - # - # props = { - # sns_subscription_properties: { - # FilterPolicyScope: "MessageBody", - # FilterPolicy: { - # Records: { - # s3: { - # object: { - # key: [ - # { prefix: "test-prefix/" } - # ] - # } - # } - # } - # }.to_json - # } - # } - # - # s3_event("s3-bucket", props) - # def process_s3_event - # ... - # end - # - # The S3 event is set up with the following resources: - # - # - S3 Bucket - # - S3 Bucket Notification Configuration - # - SNS Topic - # - SNS Subscription - # - # @param [String] bucket_name - # @param [Hash] props - def s3_event(bucket_name, props={}) - stack_name = declare_s3_bucket_resources(bucket_name) # only set up once per bucket - sns_subscription_properties = { - **props[:sns_subscription_properties] || {}, - TopicArn: "!Ref #{stack_name}SnsTopic" - } - - declare_sns_subscription(sns_subscription_properties) # set up subscription every time - end - - # Returns stack_name - def declare_s3_bucket_resources(bucket_name) - # If shared s3 bucket resources have already been declared. - # We will not generate them again. However, we still need to always - # add the depends_on declaration to ensure that the shared stack parameters - # are properly passed to the nested child stack. - stack_name = _s3_events[bucket_name] # already registered - if stack_name - depends_on stack_name.underscore.to_sym, class_prefix: true # always add this - return stack_name - end - - # Create shared resources - one time - stack_name = declare_shared_s3_event_resources(bucket_name) - depends_on stack_name.underscore.to_sym, class_prefix: true # always add this - self._s3_events[bucket_name] = stack_name # tracks buckets already set up - end - - def declare_shared_s3_event_resources(bucket_name) - s3_stack = Jets::Stack::S3Event.new(bucket_name) - s3_stack.build_stack - s3_stack.stack_name - end - - def _s3_events - Jets::Job::Base._s3_events - end - end -end diff --git a/lib/jets/job/dsl/sns_event.rb b/lib/jets/job/dsl/sns_event.rb deleted file mode 100644 index bc62690b5..000000000 --- a/lib/jets/job/dsl/sns_event.rb +++ /dev/null @@ -1,54 +0,0 @@ -module Jets::Job::Dsl - module SnsEvent - def sns_event(topic_name, props={}) - if topic_name.to_s =~ /generate/ - declare_sns_topic(props.delete(:topic_properties)) - topic_arn = "!Ref {namespace}SnsTopic" - props.merge!(TopicArn: topic_arn) - declare_sns_subscription(props) - elsif topic_name.include?('!Ref') # reference shared resource - topic_arn = topic_name # contains !Ref - props.merge!(TopicArn: topic_arn) - declare_sns_subscription(props) - else # existing topic: short name or full arn - topic_arn = full_sns_topic_arn(topic_name) - props.merge!(TopicArn: topic_arn) - declare_sns_subscription(props) - end - end - - def declare_sns_topic(props={}) - props ||= {} # props.delete(:topic_properties) can be nil - r = Jets::Cfn::Resource::Sns::Topic.new(props) - with_fresh_properties do - resource(r.definition) # add associated resource immediately - end - end - - def declare_sns_topic_policy(props={}) - props ||= {} # options.delete(:topic_policy_properties) can be nil - r = Jets::Cfn::Resource::Sns::TopicPolicy.new(props) - with_fresh_properties do - resource(r.definition) # add associated resource immediately - end - end - - def declare_sns_subscription(props={}) - r = Jets::Cfn::Resource::Sns::Subscription.new(props) - with_fresh_properties do - resource(r.definition) # add associated resource immediately - end - end - - # Expands simple topic name to full arn. Example: - # - # hello-topic - # To: - # arn:aws:sns:us-west-2:112233445566:hello-topic - def full_sns_topic_arn(topic_name) - return topic_name if topic_name.include?("arn:aws:sns") - - "arn:aws:sns:#{Jets.aws.region}:#{Jets.aws.account}:#{topic_name}" - end - end -end diff --git a/lib/jets/job/dsl/sqs_event.rb b/lib/jets/job/dsl/sqs_event.rb deleted file mode 100644 index a5e89e7a9..000000000 --- a/lib/jets/job/dsl/sqs_event.rb +++ /dev/null @@ -1,97 +0,0 @@ -# ## Event Source Mapping -# -# Underneath the hood, the `sqs_event` method sets up a [Lambda::EventSourceMapping](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-eventsourcemapping.html). So: -# -# ```ruby -# class HardJob -# class_timeout 30 # must be less than or equal to the SQS queue default timeout -# -# sqs_event "hello-queue" -# def dig -# puts "dig event #{JSON.dump(event)}" -# end -# end -# ``` -# -# Cloud also be written with something like this: -# -# ```ruby -# class HardJob -# class_timeout 30 # must be less than or equal to the SQS queue default timeout -# -# event_source_mapping( -# EventSourceArn: "arn:aws:sqs:us-west-2:112233445566:hello-queue", -# ) -# iam_policy( -# Action: ["sqs:ReceiveMessage", -# "sqs:DeleteMessage", -# "sqs:GetQueueAttributes"], -# Effect: "Allow", -# Resource: "arn:aws:sqs:us-west-2:112233445566:hello-queue", -# ) -# def dig -# puts "dig event #{JSON.dump(event)}" -# end -# end -# ``` -# -module Jets::Job::Dsl - module SqsEvent - def sqs_event(queue_name, options={}) - if queue_name.to_s =~ /generate/ - queue_arn = "!GetAtt {namespace}SqsQueue.Arn" - default_iam_policy = default_sqs_iam_policy('*') # Dont have access to full ARN on initial creation - declare_sqs_queue(options.delete(:queue_properties)) # delete to avoid using them for event_source_mapping - elsif queue_name.include?('!Ref') # reference shared resource - queue_arn = queue_name - default_iam_policy = default_sqs_iam_policy('*') # Dont have access to full ARN on initial creation - else # existing queue: short name or full arn - queue_arn = full_sqs_queue_arn(queue_name) - default_iam_policy = default_sqs_iam_policy(queue_arn) - end - - # Create iam policy allows access to queue - iam_policy_option = [options.delete(:iam_policy)].compact.flatten - user_iam_policy = [@iam_policy].compact.flatten - iam_policy_props = iam_policy_option + user_iam_policy + [default_iam_policy] - iam_policy(iam_policy_props) - - props = options # by this time options only has EventSourceMapping properties - default = { - EventSourceArn: queue_arn - } - props = default.merge(props) - - event_source_mapping(props) - end - - def declare_sqs_queue(props) - props ||= {} # since options.delete(:queue_properties) can be nil - r = Jets::Cfn::Resource::Sqs::Queue.new(props) - with_fresh_properties do - resource(r.definition) # add associated resource immediately - end - end - - # Expands simple queue name to full arn. Example: - # - # hello-queue - # To: - # arn:aws:sqs:us-west-2:112233445566:hello-queue - def full_sqs_queue_arn(queue_name) - return queue_name if queue_name.include?("arn:aws:sqs") - - "arn:aws:sqs:#{Jets.aws.region}:#{Jets.aws.account}:#{queue_name}" - end - - def default_sqs_iam_policy(queue_name_arn='*') - { - Action: ["sqs:ReceiveMessage", - "sqs:DeleteMessage", - "sqs:GetQueueAttributes"], - Effect: "Allow", - Resource: queue_name_arn, - } - end - end -end \ No newline at end of file diff --git a/lib/jets/job/helpers/kinesis_event.rb b/lib/jets/job/helpers/kinesis_event.rb deleted file mode 100644 index df6942161..000000000 --- a/lib/jets/job/helpers/kinesis_event.rb +++ /dev/null @@ -1,17 +0,0 @@ -require 'base64' - -module Jets::Job::Helpers - module KinesisEvent - def kinesis_data - records = event["Records"] - records.map do |record| - encoded = record["kinesis"]["data"] - Base64.decode64(encoded) # data - end - end - - def kinesis_data? - event["Records"]&.any? { |r| r.dig("kinesis", "data") } - end - end -end diff --git a/lib/jets/job/helpers/log_event.rb b/lib/jets/job/helpers/log_event.rb deleted file mode 100644 index 8dc385b9d..000000000 --- a/lib/jets/job/helpers/log_event.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'base64' -require 'json' -require 'stringio' -require 'zlib' - -module Jets::Job::Helpers - module LogEvent - def log_event - encoded = event["awslogs"]["data"] - compressed_string = Base64.decode64(encoded) - gz = Zlib::GzipReader.new(StringIO.new(compressed_string)) - uncompressed_string = gz.read - data = JSON.load(uncompressed_string) - ActiveSupport::HashWithIndifferentAccess.new(data) - end - - def log_event? - !!event.dig("awslogs", "data") - end - end -end diff --git a/lib/jets/job/helpers/s3_event.rb b/lib/jets/job/helpers/s3_event.rb deleted file mode 100644 index 993d335c9..000000000 --- a/lib/jets/job/helpers/s3_event.rb +++ /dev/null @@ -1,50 +0,0 @@ -module Jets::Job::Helpers - module S3Event - def s3_events - encoded_messages = event["Records"].map do |record| - record["Sns"]["Message"] # SNS message is JSON - end - # Decode the JSON messages - messages = encoded_messages.map do |message| - JSON.load(message) - end - # Extract the S3 event records - messages.map do |message| - message["Records"].map do |record| - ActiveSupport::HashWithIndifferentAccess.new(record) - end - end.flatten - end - alias s3_event_payloads s3_events - - def s3_events? - event["Records"]&.any? { |r| r.dig("Sns", "Message") } - end - alias s3_event_payloads? s3_events? - - def s3_objects - s3_events.map do |record| - record["s3"]["object"] - end - end - - def s3_objects? - s3_events.any? { |r| r.dig("s3", "object") } - end - - # Deprecated methods below - def s3_event - puts "WARN: s3_event is deprecated".color(:yellow) - puts "It can possibly drop events when they come in extremely fast." - puts "Use s3_events instead" - s3_events.first - end - - def s3_object - puts "WARN: s3_object is deprecated".color(:yellow) - puts "It can possibly drop events when they come in extremely fast." - puts "Use s3_objects instead" - s3_objects.first - end - end -end diff --git a/lib/jets/job/helpers/sns_event.rb b/lib/jets/job/helpers/sns_event.rb deleted file mode 100644 index a460be09d..000000000 --- a/lib/jets/job/helpers/sns_event.rb +++ /dev/null @@ -1,26 +0,0 @@ -module Jets::Job::Helpers - module SnsEvent - def sns_events - records = event["Records"] - return [] unless records - records.map do |record| - message = record["Sns"]["Message"] - ActiveSupport::HashWithIndifferentAccess.new(JSON.load(message)) - end - end - alias sns_event_payloads sns_events - - def sns_events? - event["Records"]&.any? { |r| r.dig("Sns", "Message") } - end - alias sns_event_payloads? sns_events? - - # Deprecated methods below - def sns_event_payload - puts "WARN: sns_event_payload is deprecated".color(:yellow) - puts "It can possibly drop events when they come in extremely fast." - puts "Use sns_events instead" - sns_events.first - end - end -end \ No newline at end of file diff --git a/lib/jets/job/helpers/sqs_event.rb b/lib/jets/job/helpers/sqs_event.rb deleted file mode 100644 index a14a37ebe..000000000 --- a/lib/jets/job/helpers/sqs_event.rb +++ /dev/null @@ -1,26 +0,0 @@ -module Jets::Job::Helpers - module SqsEvent - def sqs_events - records = event["Records"] - return [] unless records - records.map do |record| - message = record["body"] - ActiveSupport::HashWithIndifferentAccess.new(JSON.load(message)) - end - end - alias sqs_event_payloads sqs_events - - def sqs_events? - event["Records"]&.any? { |r| r.dig("body") } - end - alias sqs_event_payloads? sqs_events? - - # Deprecated methods below - def sqs_event_payload - puts "WARN: sqs_event_payload is deprecated".color(:yellow) - puts "It can possibly drop events when they come in extremely fast." - puts "Use sqs_events instead" - sqs_events.first - end - end -end diff --git a/lib/jets/klass.rb b/lib/jets/klass.rb deleted file mode 100644 index 997e96474..000000000 --- a/lib/jets/klass.rb +++ /dev/null @@ -1,108 +0,0 @@ -# Loading a class can usually be loaded via .constantize. -# But app/functions files are anonymous ruby classes created with -# Class.new. Anonymous classes cannot be loaded via .constantize and -# go through standard autoloading. -# -# Jets::Klass provides a way to load app classes in app/controllers, app/jobs, -# app/functions in a consistent way without having to worry about the anonymous -# class loading quirk. Classes that are not anonymously defined like controllers -# and jobs are loaded via autoloading with .constantize. Anonymously defined -# classes like functions are loaded via Object.const_set. -# -# Examples: -# -# Jets::Klass.from_path("app/controllers/posts_controller.rb") -# Jets::Klass.from_path("app/jobs/hard_job.rb") -# Jets::Klass.from_path("app/functions/hello.rb") -# Jets::Klass.from_path("app/functions/hello_function.rb") -# Jets::Klass.from_path("app/shared/functions/whatever.rb") -# -# Jets::Klass.from_definition(definition) -# -# The from_definition method takes a Jets::Lambda::Definition as an argument and is useful -# for the CloudFormation child stack generation there the registered definition info -# is available but the path info is now. -class Jets::Klass - class << self - # from_path allows us to load any app classes in consistent way for - # app/controllers, app/jobs, and app/functions. - def from_path(path) - class_name = class_name(path) - if path.include?("/functions/") # simple function - class_name = load_anonymous_class(class_name, path) - class_name.constantize # removed :: for anonymous classes - else - class_name.constantize # autoload - end - end - - # app/controllers/posts_controller.rb => PostsController - def class_name(path) - if path.include?("/shared/") - path.sub(%r{.*app/shared/(.*?)/},'').sub(/\.rb$/,'').camelize - else - path.sub(%r{.*app/(.*?)/},'').sub(/\.rb$/,'').camelize - end - end - - APP_TYPES = %w[controller job rule authorizer] - def from_definition(definition) - class_name = definition.class_name - filename = class_name.underscore - - # Examples of filename: posts_controller, hard_job, security_rule, main_authorizer - # hello_function, hello - type = filename.split('_').last - type = "function" unless APP_TYPES.include?(type) - - path = "app/#{type.pluralize}/#{filename}.rb" - from_path(path) - end - - @@loaded_anonymous_classes = [] - def load_anonymous_class(class_name, path) - parent_mod = modularize(class_name) - - constructor = Jets::Lambda::FunctionConstructor.new(path) - # Dont load anonyomous class more than once to avoid these warnings: - # warning: already initialized constant Hello - # warning: previous definition of Hello was here - unless @@loaded_anonymous_classes.include?(class_name) - # use class_name as the variable name for prettier class name. - leaf_class_name = class_name.split('::').last - parent_mod.const_set(leaf_class_name, constructor.build) - @@loaded_anonymous_classes << class_name - end - - class_name - end - - # Ensures the parent namespace modules are defined. Example: - # - # modularize("Foo::Bar::Test") - # => Foo::Bar # is a now defined as a module if it wasnt before - # - # Also returns the parent module, so we can use it to do a const_set if needed. IE: - # - # parent_mod = modularize("Foo::Bar::Test") - # parent_mod.const_set("Test") - def modularize(class_name) - leaves = [] - mods = class_name.split('::')[0..-2] # drop the last word - return Object if mods.empty? - - leaves = [] - mods.each do |leaf_mod| - leaves += [leaf_mod] - namespace = leaves.join('::') - previous_namespace = leaves[0..-2].join('::') - previous_namespace = "Object" if previous_namespace.empty? - previous_namespace = previous_namespace.constantize - previous_namespace.const_set(leaf_mod, Module.new) unless Object.const_defined?(namespace) - end - - mods.join('::').constantize - end - - end -end diff --git a/lib/jets/lambda/definition.rb b/lib/jets/lambda/definition.rb index 90b7153a2..9a4eca96f 100644 --- a/lib/jets/lambda/definition.rb +++ b/lib/jets/lambda/definition.rb @@ -3,13 +3,17 @@ class Definition include Jets::Util::Camelize attr_accessor :class_name, :type - attr_reader :meth, :properties, :iam_policy, :managed_iam_policy, :lang, :associated_resources - def initialize(class_name, meth, options={}) + attr_reader( + :meth, :properties, :provisioned_concurrency, :iam_policy, :managed_iam_policy, + :lang, :associated_resources + ) + def initialize(class_name, meth, options = {}) @class_name = class_name.to_s @meth = meth @options = options - @type = options[:type] || get_type # controller, job, or function + @type = options[:type] || get_type # controller, event, or function @properties = camelize(options[:properties]) || {} + @provisioned_concurrency = options[:provisioned_concurrency] @iam_policy = options[:iam_policy] @managed_iam_policy = options[:managed_iam_policy] @lang = options[:lang] || :ruby @@ -25,7 +29,7 @@ def public_meth? # For anonymous classes (app/functions/hello.rb) the class name will be blank. # These types of classes are treated specially and has only one handler method # that is registered. So we know it is public. - return true if @class_name.nil? || @class_name == '' + return true if @class_name.nil? || @class_name == "" # Consider all polymorphic methods public for now return true if @lang != :ruby @@ -40,20 +44,20 @@ def build_function_iam? end @@lang_exts = { - ruby: '.rb', - python: '.py', - node: '.js', + ruby: ".rb", + python: ".py", + node: ".js" } def lang_ext @@lang_exts[@lang] end - # The get_type method works for controller and job classes. + # The get_type method works for controller and event classes. # # Usually able to get the type from the class name. Examples: # # PostsController => controller - # HardJob => job + # CoolEvent => event # # However, for function types, we are not able to get the type for multiple of # reasons. First, function types are allowed to be named with or without @@ -69,16 +73,16 @@ def lang_ext # We add the class_type to the task later on as we are constructing the class # as part of the Class.new logic. # - # For controller and job standard ruby classes though it can easily be + # For controller and event standard ruby classes though it can easily be # determinated as part of initialization. So we get the type for convenience then. # # For anonymous function classes, we just set to nil and will later fix in # FunctionConstructor. # - # Returns: "controller", "job" or nil + # Returns: "controller", "event" or nil def get_type unless @class_name.empty? # when anonymous class is created with Class.new - @class_name.underscore.split('_').last # controller, job or rule + @class_name.underscore.split("_").last # controller, event or rule end end @@ -106,13 +110,15 @@ def replacements end def base_replacements - { namespace: namespace } + {namespace: namespace} end def namespace - ns = @class_name.gsub('::','').camelize - ns += @meth.to_s.camelize if @meth - ns + if @meth + @meth.to_s.camelize + else + @class_name.gsub("::", "").camelize + end end end end diff --git a/lib/jets/lambda/dsl.rb b/lib/jets/lambda/dsl.rb index e1955570e..7deb0c8af 100644 --- a/lib/jets/lambda/dsl.rb +++ b/lib/jets/lambda/dsl.rb @@ -11,411 +11,411 @@ def lambda_functions self.class.lambda_functions end - included do - class << self - extend Memoist - include Warnings - - def class_properties(options=nil) - if options - @class_properties ||= {} - @class_properties.deep_merge!(options) - else - @class_properties || {} + module ClassMethods + extend Memoist + + def class_properties(options = nil) + if options + @class_properties ||= {} + @class_properties.deep_merge!(options) + else + @class_properties || {} + end + end + alias_method :class_props, :class_properties + + def properties(options = {}) + @properties ||= {} + @properties.deep_merge!(options) + end + alias_method :props, :properties + + def class_environment(hash) + environment = standardize_env(hash) + class_properties(Environment: environment) + end + alias_method :class_env, :class_environment + + def environment(hash) + environment = standardize_env(hash) + properties(Environment: environment) + end + alias_method :env, :environment + + # Allows user to pass in hash with or without the :variables key. + def standardize_env(hash) + return hash if hash.key?(:Variables) + + environment = {} + environment[:Variables] ||= {} + environment[:Variables].merge!(hash) + environment + end + + # Convenience method that set properties. List based on https://amzn.to/2oSph1P + # Not all properites are included because some properties are not meant to be set + # directly. For example, function_name is a calculated setting by Jets. + PROPERTIES = %W[ + dead_letter_config + description + ephemeral_storage + handler + kms_key_arn + logging_config + memory_size + reserved_concurrent_executions + role + runtime + tags + timeout + tracing_config + vpc_config + ] + PROPERTIES.each do |property| + # Example: + # def timeout(value) + # properties(timeout: value) + # end + # + # def class_timeout(value) + # class_properties(timeout: value) + # end + class_eval <<~CODE, __FILE__, __LINE__ + 1 + def #{property}(value) + properties(#{property}: value) end - end - alias_method :class_props, :class_properties - - def properties(options={}) - @properties ||= {} - @properties.deep_merge!(options) - end - alias_method :props, :properties - - def class_environment(hash) - environment = standardize_env(hash) - class_properties(Environment: environment) - end - alias_method :class_env, :class_environment - def environment(hash) - environment = standardize_env(hash) - properties(Environment: environment) - end - alias_method :env, :environment - - # Allows user to pass in hash with or without the :variables key. - def standardize_env(hash) - return hash if hash.key?(:Variables) - - environment = {} - environment[:Variables] ||= {} - environment[:Variables].merge!(hash) - environment - end - - # Convenience method that set properties. List based on https://amzn.to/2oSph1P - # Not all properites are included because some properties are not meant to be set - # directly. For example, function_name is a calculated setting by Jets. - PROPERTIES = %W[ - dead_letter_config - description - ephemeral_storage - handler - kms_key_arn - memory_size - reserved_concurrent_executions - role - runtime - timeout - tracing_config - vpc_config - tags - ] - PROPERTIES.each do |property| + def class_#{property}(value) + class_properties(#{property}: value) + end + CODE + end + + # Expose the PROPERTIES list so we can access it + def properties_list + PROPERTIES + end + + # More convenience aliases + alias_method :memory, :memory_size + alias_method :class_memory, :class_memory_size + alias_method :desc, :description + alias_method :class_desc, :class_description + alias_method :reserved_concurrency, :reserved_concurrent_executions + alias_method :class_reserved_concurrency, :class_reserved_concurrent_executions + + attr_writer :provisioned_concurrency + + # definitions: one or more definitions + def iam_policy(*definitions) + if definitions.empty? + @iam_policy + else + @iam_policy ||= [] + @iam_policy += definitions.flatten + end + end + + # definitions: one or more definitions + def class_iam_policy(*definitions) + if definitions.empty? + @class_iam_policy + else + @class_iam_policy ||= [] + @class_iam_policy += definitions.flatten + end + end + + # definitions: one or more definitions + def managed_iam_policy(*definitions) + if definitions.empty? + @managed_iam_policy + else + @managed_iam_policy ||= [] + @managed_iam_policy += definitions.flatten + end + end + + # definitions: one or more definitions + def class_managed_iam_policy(*definitions) + if definitions.empty? + @class_managed_iam_policy + else + @class_managed_iam_policy ||= [] + @class_managed_iam_policy += definitions.flatten + end + end + + def build_class_iam? + !!(class_iam_policy || class_managed_iam_policy) + end + + ############################# + # Main method that registers resources associated with the Lambda function. + # All resources methods lead here. + def associated_resources(*definitions) + if definitions == [nil] # when associated_resources called with no arguments + @associated_resources || [] + else + @associated_resources ||= [] + associated_resource = Jets::Cfn::Resource::Associated.new(definitions) + associated_resource.multiple_resources = @multiple_resources + @associated_resources << associated_resource + @associated_resources.flatten! + end + end + alias_method :associated_resource, :associated_resources + # Allow user to use `resource` instead of `associated_resource` for a more natural feel + # User-friendly short resource method. Users will use this. + alias_method :resource, :associated_resources + + # Using this odd way of setting these properties so we can keep the + # resource(*definitions) signature simple. Using keyword arguments at the end + # interfere with being able to pass in any keys for the properties hash at the end. + # + # TODO: If there's a cleaner way of doing this, let me know. + def with_fresh_properties(fresh_properties: true, multiple_resources: true) + @associated_properties = nil if fresh_properties # dont use any current associated_properties + @multiple_resources = multiple_resources + + yield + + @multiple_resources = false + @associated_properties = nil if fresh_properties # reset for next definition, since we're defining eagerly + end + + # Properties belonging to the associated resource + def associated_properties(options = {}) + @associated_properties ||= {} + @associated_properties.deep_merge!(options) + end + alias_method :associated_props, :associated_properties + + # meta definition + def self.define_associated_properties(associated_properties) + associated_properties.each do |property| # Example: - # def timeout(value) - # properties(timeout: value) - # end - # - # def class_timeout(value) - # class_properties(timeout: value) + # def config_rule_name(value) + # associated_properties(config_rule_name: value) # end - class_eval <<~CODE + class_eval <<~CODE, __FILE__, __LINE__ + 1 def #{property}(value) - properties(#{property}: value) - end - - def class_#{property}(value) - class_properties(#{property}: value) + associated_properties(#{property}: value) end CODE end - # More convenience aliases - alias_method :memory, :memory_size - alias_method :class_memory, :class_memory_size - alias_method :desc, :description - alias_method :class_desc, :class_description - - # definitions: one or more definitions - def iam_policy(*definitions) - if definitions.empty? - @iam_policy - else - iam_policy_unused_warning(managed=false) - @iam_policy ||= [] - @iam_policy += definitions.flatten - end - end - - # definitions: one or more definitions - def class_iam_policy(*definitions) - if definitions.empty? - @class_iam_policy - else - class_iam_policy_unused_warning(managed=false) - @class_iam_policy ||= [] - @class_iam_policy += definitions.flatten - end - end - - # definitions: one or more definitions - def managed_iam_policy(*definitions) - if definitions.empty? - @managed_iam_policy - else - iam_policy_unused_warning(managed=true) - @managed_iam_policy ||= [] - @managed_iam_policy += definitions.flatten - end - end - - # definitions: one or more definitions - def class_managed_iam_policy(*definitions) - if definitions.empty? - @class_managed_iam_policy - else - class_iam_policy_unused_warning(managed=true) - @class_managed_iam_policy ||= [] - @class_managed_iam_policy += definitions.flatten - end - end - - def build_class_iam? - !!(class_iam_policy || class_managed_iam_policy) - end - - ############################# - # Main method that registers resources associated with the Lambda function. - # All resources methods lead here. - def associated_resources(*definitions) - if definitions == [nil] # when associated_resources called with no arguments - @associated_resources || [] - else - @associated_resources ||= [] - associated_resource = Jets::Cfn::Resource::Associated.new(definitions) - associated_resource.multiple_resources = @multiple_resources - @associated_resources << associated_resource - @associated_resources.flatten! - end - end - # User-friendly short resource method. Users will use this. - alias_method :resource, :associated_resources - - # Using this odd way of setting these properties so we can keep the - # resource(*definitions) signature simple. Using keyword arguments at the end - # interfere with being able to pass in any keys for the properties hash at the end. + end + + def add_logical_id_counter? + return false unless @associated_resources + # Only takes one associated resource with multiple set to true to return false of this check + return false if @associated_resources.detect { |associated| associated.multiple_resources } + # Otherwise check if there is more than 1 @associated_resources + @associated_resources.size > 1 + end + + # Loop back through the resources and add a counter to the end of the id + # to handle multiple events. + # Then replace @associated_resources entirely + def add_logical_id_counter + numbered_resources = [] + n = 1 + @associated_resources.map do |associated| + logical_id = associated.logical_id + attributes = associated.attributes + + logical_id = logical_id.to_s.sub(/\d+$/, "") + new_definition = {"#{logical_id}#{n}" => attributes} + numbered_resources << Jets::Cfn::Resource::Associated.new(new_definition) + n += 1 + end + @associated_resources = numbered_resources + end + + # Examples: + # + # depends_on :custom + # depends_on :custom, :alert + # depends_on :custom, class_prefix: true + # depends_on :custom, :alert, class_prefix: true + # + # interface method + def depends_on(*stacks) + end + + def ref(name) + "!Ref #{name.to_s.camelize}" + end + + def sub(value) + "!Sub #{value.to_s.camelize}" + end + + # meth is a Symbol + def method_added(meth) + return if %w[initialize method_missing].include?(meth.to_s) + return unless public_method_defined?(meth) + + register_definition(meth) + end + + def register_definition(meth, lang = :ruby) + # Note: for anonymous classes like for app/functions self.name is "" + # We adjust the class name when we build the functions later in + # FunctionContstructor#adjust_definitions. + + # At this point we can use the current associated_properties and defined the + # associated resource with the Lambda function. + unless associated_properties.empty? + associated_resources(default_associated_resource_definition(meth)) + end + + # Unsure why but we have to use @associated_resources vs associated_resources + # associated_resources is always nil + if add_logical_id_counter? + add_logical_id_counter + end + + all_definitions[meth] = Jets::Lambda::Definition.new(name, meth, + properties: @properties, # lambda function properties + provisioned_concurrency: @provisioned_concurrency, + iam_policy: @iam_policy, + managed_iam_policy: @managed_iam_policy, + associated_resources: @associated_resources, + lang: lang, + replacements: replacements(meth)) + + # Done storing options, clear out for the next added method. + clear_properties + # Important to clear @properties at the end of registering outside of + # register_definition because register_definition is overridden in Jets::Event::Dsl # - # TODO: If there's a cleaner way of doing this, let me know. - def with_fresh_properties(fresh_properties: true, multiple_resources: true) - @associated_properties = nil if fresh_properties # dont use any current associated_properties - @multiple_resources = multiple_resources - - yield - - @multiple_resources = false - @associated_properties = nil if fresh_properties # reset for next definition, since we're defining eagerly - end - - # Properties belonging to the associated resource - def associated_properties(options={}) - @associated_properties ||= {} - @associated_properties.deep_merge!(options) - end - alias_method :associated_props, :associated_properties - - # meta definition - def self.define_associated_properties(associated_properties) - associated_properties.each do |property| - # Example: - # def config_rule_name(value) - # associated_properties(config_rule_name: value) - # end - class_eval <<~CODE - def #{property}(value) - associated_properties(#{property}: value) - end - CODE - end - end - - def add_logical_id_counter? - return false unless @associated_resources - # Only takes one associated resource with multiple set to true to return false of this check - return false if @associated_resources.detect { |associated| associated.multiple_resources } - # Otherwise check if there is more than 1 @associated_resources - @associated_resources.size > 1 - end - - # Loop back through the resources and add a counter to the end of the id - # to handle multiple events. - # Then replace @associated_resources entirely - def add_logical_id_counter - numbered_resources = [] - n = 1 - @associated_resources.map do |associated| - logical_id = associated.logical_id - attributes = associated.attributes - - logical_id = logical_id.to_s.sub(/\d+$/,'') - new_definition = { "#{logical_id}#{n}" => attributes } - numbered_resources << Jets::Cfn::Resource::Associated.new(new_definition) - n += 1 - end - @associated_resources = numbered_resources - end - - # Examples: + # Jets::Event::Base < Jets::Lambda::Functions # - # depends_on :custom - # depends_on :custom, :alert - # depends_on :custom, class_prefix: true - # depends_on :custom, :alert, class_prefix: true - # - def depends_on(*stacks) - if stacks == [] - @depends_on + # Both Jets::Event::Base and Jets::Lambda::Functions have Dsl modules included. + # So the Jets::Event::Dsl overrides some of the Jets::Lambda::Dsl behavior. + + true + end + + # Meant to be overridden to add more custom replacements based on the app class type + def replacements(meth) + {} + end + + def clear_properties + @properties = nil + @provisioned_concurrency = nil + @iam_policy = nil + @managed_iam_policy = nil + @associated_resources = nil + @associated_properties = nil + end + + # Returns the all definitions for this class with their method names as keys. + # + # ==== Returns + # OrderedHash:: An ordered hash with definitions names as keys and definition + # objects as values. + # + def all_definitions + @all_definitions ||= ActiveSupport::OrderedHash.new + end + # Do not call all definitions outside this class, instead use: definitions or lambda functions + private :all_definitions + + # Goes up the class inheritance chain to build the definitions. + # + # Example heirarchy: + # + # Jets::Lambda::Functions > Jets::Controller::Base > ApplicationController ... + # > PostsController > ChildPostsController + # + # Do not include definitions from the direct subclasses of Jets::Lambda::Functions + # because those classes are abstract. Dont want those methods to be included. + def find_all_definitions(options = {}) + public = options[:public].nil? ? true : options[:public] + klass = self + direct_subclasses = Jets::Lambda::Functions.subclasses + lookup = [] + + # Go up class inheritance and builds lookup structure in memory + until direct_subclasses.include?(klass) + lookup << klass.send(:all_definitions) # one place we want to call private all_definitions method + klass = klass.superclass + end + merged_definitions = ActiveSupport::OrderedHash.new + # Go back down the class inheritance chain in reverse order and merge the definitions + lookup.reverse_each do |definitions_hash| + # definitions_hash is a result of all_definitions. Example: PostsController.all_definitions + merged_definitions.merge!(definitions_hash) + end + + # The cfn builders required the right final child class to build the lambda functions correctly. + merged_definitions.each do |meth, definition| + # Override the class name for the cfn builders + definition = definition.clone # do not stomp over current definitions since things are usually looked by reference + definition.instance_variable_set(:@class_name, name) + merged_definitions[meth] = definition + end + + # Methods can be made private with the :private keyword after the method has been defined. + # To account for this, loop back thorugh all the methods and check if the method is indeed public. + definitions = ActiveSupport::OrderedHash.new + merged_definitions.each do |meth, definition| + if public + definitions[meth] = definition if definition.public_meth? else - @depends_on ||= [] - options = stacks.last.is_a?(Hash) ? stacks.pop : {} - stacks.each do |stack| - @depends_on << Jets::Stack::Depends::Item.new(stack, options) - end + definitions[meth] = definition unless definition.public_meth? end end - - def ref(name) - "!Ref #{name.to_s.camelize}" - end - - def sub(value) - "!Sub #{value.to_s.camelize}" - end - - # meth is a Symbol - def method_added(meth) - return if %w[initialize method_missing].include?(meth.to_s) - return unless public_method_defined?(meth) - - register_definition(meth) - end - - def register_definition(meth, lang=:ruby) - # Note: for anonymous classes like for app/functions self.name is "" - # We adjust the class name when we build the functions later in - # FunctionContstructor#adjust_definitions. - - # At this point we can use the current associated_properties and defined the - # associated resource with the Lambda function. - unless associated_properties.empty? - associated_resources(default_associated_resource_definition(meth)) - end - - # Unsure why but we have to use @associated_resources vs associated_resources - # associated_resources is always nil - if add_logical_id_counter? - add_logical_id_counter - end - - all_definitions[meth] = Jets::Lambda::Definition.new(self.name, meth, - properties: @properties, # lambda function properties - iam_policy: @iam_policy, - managed_iam_policy: @managed_iam_policy, - associated_resources: @associated_resources, - lang: lang, - replacements: replacements(meth)) - - # Done storing options, clear out for the next added method. - clear_properties - # Important to clear @properties at the end of registering outside of - # register_definition because register_definition is overridden in Jets::Job::Dsl - # - # Jets::Job::Base < Jets::Lambda::Functions - # - # Both Jets::Job::Base and Jets::Lambda::Functions have Dsl modules included. - # So the Jets::Job::Dsl overrides some of the Jets::Lambda::Dsl behavior. - - true - end - - # Meant to be overridden to add more custom replacements based on the app class type - def replacements(meth) - {} - end - - def clear_properties - @properties = nil - @iam_policy = nil - @managed_iam_policy = nil - @associated_resources = nil - @associated_properties = nil - end - - # Returns the all definitions for this class with their method names as keys. - # - # ==== Returns - # OrderedHash:: An ordered hash with definitions names as keys and definition - # objects as values. - # - def all_definitions - @all_definitions ||= ActiveSupport::OrderedHash.new - end - # Do not call all definitions outside this class, instead use: definitions or lambda functions - private :all_definitions - - # Goes up the class inheritance chain to build the definitions. - # - # Example heirarchy: - # - # Jets::Lambda::Functions > Jets::Controller::Base > ApplicationController ... - # > PostsController > ChildPostsController - # - # Do not include definitions from the direct subclasses of Jets::Lambda::Functions - # because those classes are abstract. Dont want those methods to be included. - def find_all_definitions(options={}) - public = options[:public].nil? ? true : options[:public] - klass = self - direct_subclasses = Jets::Lambda::Functions.subclasses - lookup = [] - - # Go up class inheritance and builds lookup structure in memory - until direct_subclasses.include?(klass) - lookup << klass.send(:all_definitions) # one place we want to call private all_definitions method - klass = klass.superclass - end - merged_definitions = ActiveSupport::OrderedHash.new - # Go back down the class inheritance chain in reverse order and merge the definitions - lookup.reverse.each do |definitions_hash| - # definitions_hash is a result of all_definitions. Example: PostsController.all_definitions - merged_definitions.merge!(definitions_hash) - end - - # The cfn builders required the right final child class to build the lambda functions correctly. - merged_definitions.each do |meth, definition| - # Override the class name for the cfn builders - definition = definition.clone # do not stomp over current definitions since things are usually looked by reference - definition.instance_variable_set(:@class_name, self.name) - merged_definitions[meth] = definition - end - - # Methods can be made private with the :private keyword after the method has been defined. - # To account for this, loop back thorugh all the methods and check if the method is indeed public. - definitions = ActiveSupport::OrderedHash.new - merged_definitions.each do |meth, definition| - if public - definitions[meth] = definition if definition.public_meth? - else - definitions[meth] = definition unless definition.public_meth? - end - end - definitions - end - memoize :find_all_definitions - - def all_public_definitions - find_all_definitions(public: true) - end - - def all_private_definitions - find_all_definitions(public: false) - end - - # Returns the definitions for this class in Array form. - # - # ==== Returns - # Array of definition objects - # - def definitions - all_public_definitions.values - end - - # The public methods defined in the project app class ulimately become - # lambda functions. - # - # Example return value: - # [:index, :new, :create, :show] - def lambda_functions - all_public_definitions.keys - end - - # Used in Jets::Cfn::Builder::Interface#build - # Overridden in rule/dsl.rb - def build? - !definitions.empty? - end - - # Polymorphic support - def defpoly(lang, meth) - register_definition(meth, lang) - end - - def python(meth) - defpoly(:python, meth) - end - - def node(meth) - defpoly(:node, meth) - end - end # end of class << self - end # end of included + definitions + end + memoize :find_all_definitions + + def all_public_definitions + find_all_definitions(public: true) + end + + def all_private_definitions + find_all_definitions(public: false) + end + + # Returns the definitions for this class in Array form. + # + # ==== Returns + # Array of definition objects + # + def definitions + all_public_definitions.values + end + + # The public methods defined in the project app class ulimately become + # lambda functions. + # + # Example return value: + # [:index, :new, :create, :show] + def lambda_functions + all_public_definitions.keys + end + + # Used in Jets::Cfn::Builder::Interface#build + # Overridden in rule/dsl.rb + def build? + !definitions.empty? + end + + # Polymorphic support + def defpoly(lang, meth) + register_definition(meth, lang) + end + + def python(meth) + defpoly(:python, meth) + end + + def node(meth) + defpoly(:node, meth) + end + end end diff --git a/lib/jets/lambda/dsl/warnings.rb b/lib/jets/lambda/dsl/warnings.rb deleted file mode 100644 index fa0a6115f..000000000 --- a/lib/jets/lambda/dsl/warnings.rb +++ /dev/null @@ -1,57 +0,0 @@ -module Jets::Lambda::Dsl - module Warnings - def iam_policy_unused_warning(managed=false) - return unless show_iam_policy_unused_warning? - - managed_prefix = managed ? "managed_" : "" - use_instead = case Jets.config.cfn.build.controllers - when "one_lambda_per_contoller" - "class_#{managed_prefix}iam_policy" - when "one_lambda_for_all_controllers" - "class_#{managed_prefix}iam_policy in the ApplicationController" - end - puts <<~EOL.color(:yellow) - WARNING: #{managed_prefix}iam_policy is not respected when config.cfn.build.controllers is not set to "one_lambda_per_controller" - Current setting: config.cfn.build.controllers = "#{Jets.config.cfn.build.controllers}" - Please use #{use_instead} instead. - Docs: #{iam_docs_url(managed)} - EOL - - DslEvaluator.print_code(app_call_line) if app_call_line - end - - def class_iam_policy_unused_warning(managed=false) - return unless Jets.config.cfn.build.controllers == "one_lambda_for_all_controllers" - return if self.to_s == "ApplicationController" # ApplicationController not defined in job mode - return if self.ancestors.include?(Jets::Job::Base) - - managed_prefix = managed ? "managed_" : "" - puts <<~EOL.color(:yellow) - WARNING: class_#{managed_prefix}iam_policy is not respected when - config.cfn.build.controllers is not set to "one_lambda_per_controller" or "one_lambda_per_method" - Current setting: config.cfn.build.controllers = "#{Jets.config.cfn.build.controllers}" - Please use class_#{managed_prefix}iam_policy in the ApplicationController instead. - Docs: #{iam_docs_url(managed)} - EOL - - DslEvaluator.print_code(app_call_line) if app_call_line - end - - def show_iam_policy_unused_warning? - Jets.config.cfn.build.controllers != "one_lambda_per_method" && - self.name.include?("Controller") - end - - def iam_docs_url(managed) - if managed - "http://rubyonjets.com/docs/managed-iam-policies/" - else - "http://rubyonjets.com/docs/iam-policies/" - end - end - - def app_call_line - caller.find { |l| l.include?('app/controllers') } - end - end -end diff --git a/lib/jets/lambda/function_constructor.rb b/lib/jets/lambda/function_constructor.rb index b38a74eb1..890ca3a8b 100644 --- a/lib/jets/lambda/function_constructor.rb +++ b/lib/jets/lambda/function_constructor.rb @@ -43,7 +43,7 @@ def build # For anonymous classes method_added during definition registration contains "" # for the class name. We adjust it here. def adjust_definitions(klass) - class_name = @code_path.to_s.sub(/.*\/functions\//,'').sub(/\.rb$/, '') + class_name = @code_path.to_s.sub(/.*\/functions\//, "").sub(/\.rb$/, "") class_name = class_name.camelize klass.definitions.each do |definition| definition.class_name = class_name @@ -52,4 +52,3 @@ def adjust_definitions(klass) end end end - diff --git a/lib/jets/lambda/functions.rb b/lib/jets/lambda/functions.rb index 051259684..2d0e5783c 100644 --- a/lib/jets/lambda/functions.rb +++ b/lib/jets/lambda/functions.rb @@ -1,29 +1,23 @@ -require 'json' +require "json" # Jets::Lambda::Functions represents a collection of Lambda functions. # # Jets::Lambda::Functions is the superclass of: -# Jets::Controller::Base -# Jets::Job::Base +# Jets::Event::Base module Jets::Lambda class Functions include Jets::ExceptionReporting + include Jets::Util::Logging attr_reader :event, :context, :meth def initialize(event, context, meth) @event = HashWithIndifferentAccess.new(event) # Hash, JSON.parse(event) ran BaseProcessor @context = context # Hash. JSON.parse(context) ran in BaseProcessor - @meth = meth - # store meth because it is useful to for identifying the which template - # to use later. - end - - def logger - Jets.logger + @meth = meth # useful to identify which template to use later. end include Dsl # At the end so methods like event, context and method - # do not trigger method_added + # do not trigger method_added # Pretty hacky since action_view/rendering.rb _normalize_options calls super def _normalize_options(options) # :doc: @@ -31,19 +25,15 @@ def _normalize_options(options) # :doc: end class << self + include Jets::Util::Logging + attr_reader :abstract alias_method :abstract?, :abstract @abstract = true - class << self - def inherited(klass) # :nodoc: - # Define the abstract ivar on subclasses so that we don't get - # uninitialized ivar warnings - unless klass.instance_variable_defined?(:@abstract) - klass.instance_variable_set(:@abstract, false) - end - super - end + def inherited(base) + super + subclasses << base if base.name end # Define a controller as abstract. See internal_methods for more details. @@ -60,11 +50,6 @@ def subclasses @subclasses ||= [] end - def inherited(base) - super - self.subclasses << base if base.name - end - # Needed for depends_on. Got added due to stagger logic. def output_keys [] diff --git a/lib/jets/mega/hash_converter.rb b/lib/jets/mega/hash_converter.rb deleted file mode 100644 index 64d1930e1..000000000 --- a/lib/jets/mega/hash_converter.rb +++ /dev/null @@ -1,25 +0,0 @@ -# Thanks https://mensfeld.pl/2012/01/converting-nested-hash-into-http-url-params-hash-version-in-ruby/ -module Jets::Mega - module HashConverter - def self.encode(value, key = nil, out_hash = {}) - case value - when Hash then - value.each { |k,v| encode(v, append_key(key,k), out_hash) } - out_hash - when Array then - value.each { |v| encode(v, "#{key}", out_hash) } - out_hash - when nil then '' - else - out_hash[key] = value - out_hash - end - end - - private - - def self.append_key(root_key, key) - root_key.nil? ? :"#{key}" : :"#{root_key}[#{key.to_s}]" - end - end -end diff --git a/lib/jets/mega/request.rb b/lib/jets/mega/request.rb deleted file mode 100644 index 731672aa5..000000000 --- a/lib/jets/mega/request.rb +++ /dev/null @@ -1,163 +0,0 @@ -require 'net/http' -require 'rack' - -module Jets::Mega - class Request - JETS_OUTPUT = "/tmp/jets-output.log" - - extend Memoist - - def initialize(event, controller) - @event = event - @controller = controller # Jets::Controller instance - end - - def proxy - http_method = @event['httpMethod'] # GET, POST, PUT, DELETE, etc - params = @controller.params(include_path_params: false) - - uri = get_uri - - http = Net::HTTP.new(uri.host, uri.port) - http.open_timeout = http.read_timeout = 60 - - # Jets sets _method=patch or _method=put as workaround - # Falls back to GET when testing in lambda console - http_class = params['_method'] || http_method || 'GET' - http_class.capitalize! - - request_class = "Net::HTTP::#{http_class}".constantize # IE: Net::HTTP::Get - request = request_class.new(uri) - - # Set form data - if %w[Post Patch Put].include?(http_class) - params = HashConverter.encode(params) - request.set_form_data(params) - end - - # Set body info - request.body = source.body - request.content_length = source.content_length - - # Need to set headers after body and form_data for some reason - request = set_headers!(request) - - # Make request - response = send_request(http, request) - - puts_rack_output - - status = response.code.to_i - headers = response.each_header.to_h - encoding = get_encoding(headers['content-type']) - body = response.body&.force_encoding(encoding) - { - status: status, - headers: headers, - body: body, - } - end - - # Adds error handling in case the rack server has gone down. Will try to start the server back up if needed. - # Search CloudWatch logs for 'Unable to send request' to find for indication of this. - def send_request(http, request) - retries = 0 - max_retries = 30 # 15 seconds at a delay of 0.5s - delay = 0.5 - - begin - http.request(request) # response - rescue Errno::ECONNREFUSED, Errno::EAFNOSUPPORT - puts "Unable to send request to localhost:9292. Will try to start the server with a delay of #{delay} and try again." - Jets.start_rack_server - - sleep(delay) - retries += 1 - if retries < max_retries - retry - else - puts "Giving up on trying to send request to localhost:9292" - raise # re-raise error - end - end - end - - def get_encoding(content_type) - default = Jets.config.encoding.default - return default unless content_type - - md = content_type.match(/charset=(.+)/) - return default unless md - - md[1] - end - - # Grab the rack output from the /tmp/jets-output.log and puts it back in the - # main process' stdout - def puts_rack_output - return unless File.exist?(JETS_OUTPUT) - puts IO.readlines(JETS_OUTPUT) - File.truncate(JETS_OUTPUT, 0) - end - - def get_uri - url = "http://localhost:9292#{@controller.request.path}" # local rack server - unless @controller.query_parameters.empty? - # Thanks: https://stackoverflow.com/questions/798710/ruby-how-to-turn-a-hash-into-http-parameters - query_string = Rack::Utils.build_nested_query(@controller.query_parameters) - url += "?#{query_string}" - end - URI(url) - end - - def source - Source.new(@event) - end - memoize :source - - # Jets sets _method=patch or _method=put as workaround - # Falls back to GET when testing in lambda console - # @event['httpMethod'] is GET, POST, PUT, DELETE, etc - def http_class - http_class = params['_method'] || @event['httpMethod'] || 'GET' - http_class.capitalize! - http_class - end - - def params - @controller.params(include_path_params: false, include_body_params: true) - end - memoize :params - - # Set request headers. Forwards original request info from remote API gateway. - # By this time, the server/api_gateway.rb middleware. - def set_headers!(request) - headers = @event['headers'] # from api gateway - if headers # remote API Gateway - # Forward headers from API Gateway over to the sub http request. - # It's important to forward the headers. Here are some examples: - # - # "Turbolinks-Referrer"=>"http://localhost:8888/posts/122", - # "Referer"=>"http://localhost:8888/posts/122", - # "Accept-Encoding"=>"gzip, deflate", - # "Accept-Language"=>"en-US,en;q=0.9,pt;q=0.8", - # "Cookie"=>"_demo_session=...", - # "If-None-Match"=>"W/\"9fa479205fc6d24ca826d46f1f6cf461\"", - headers.each do |k,v| - request[k] = v - end - - # Note by the time headers get to rack later in the they get changed to: - # - # request['X-Forwarded-Host'] vs env['HTTP_X_FORWARDED_HOST'] - # - request['X-Forwarded-For'] = headers['X-Forwarded-For'] # "1.1.1.1, 2.2.2.2" # can be comma separated list - request['X-Forwarded-Host'] = headers['Host'] # uhghn8z6t1.execute-api.us-east-1.amazonaws.com - request['X-Forwarded-Port'] = headers['X-Forwarded-Port'] # 443 - request['X-Forwarded-Proto'] = headers['X-Forwarded-Proto'] # https # scheme - end - - request - end - end -end diff --git a/lib/jets/mega/request/source.rb b/lib/jets/mega/request/source.rb deleted file mode 100644 index ce1217b9e..000000000 --- a/lib/jets/mega/request/source.rb +++ /dev/null @@ -1,20 +0,0 @@ -class Jets::Mega::Request - class Source - def initialize(event) - @event = event - @source_request = Rack::Request.new(event: event) - end - - def body - if @source_request.body.respond_to?(:read) - body = @source_request.body.read - @source_request.body.rewind - end - body - end - - def content_length - @source_request.content_length.to_i - end - end -end diff --git a/lib/jets/names.rb b/lib/jets/names.rb index 1a40196d0..7d1e0a157 100644 --- a/lib/jets/names.rb +++ b/lib/jets/names.rb @@ -11,7 +11,7 @@ def templates_folder end def one_controller_template_path - "#{templates_folder}/jets-controller.yml" + "#{templates_folder}/controller.yml" end def app_template_path(app_class) @@ -34,18 +34,6 @@ def api_gateway_template_path "#{templates_folder}/api-gateway.yml" end - def api_resources_template_path(page_number) - "#{templates_folder}/api-resources-#{page_number}.yml" - end - - def api_cors_template_path(page_number) - "#{templates_folder}/api-cors-#{page_number}.yml" - end - - def api_methods_template_path(page_number) - "#{templates_folder}/api-methods-#{page_number}.yml" - end - def api_deployment_template_path "#{templates_folder}/api-deployment.yml" end @@ -59,21 +47,21 @@ def shared_resources_template_path end def parent_stack_name - Jets.project_namespace + Jets.project.namespace end def gateway_api_name - Jets.project_namespace + Jets.project.namespace end def authorizer_template_path(path) underscored = underscore(path) - underscored.sub!(/^app-/, '') + underscored.sub!(/^app-/, "") "#{templates_folder}/#{underscored}.yml" end def underscore(s) - s.to_s.underscore.sub(/\.rb$/,'').gsub('/','-') + s.to_s.underscore.sub(/\.rb$/, "").tr("/", "-") end end end diff --git a/lib/jets/overrides/dummy/rails.rb b/lib/jets/overrides/dummy/rails.rb deleted file mode 100644 index 64f37289a..000000000 --- a/lib/jets/overrides/dummy/rails.rb +++ /dev/null @@ -1,11 +0,0 @@ -# This is a dummy Rails class that delegates to Jets. -# It's only used for jets generate and jets db:migrate. -# It's easier to mock out the Rails module then to get them working. - -module Rails - cattr_accessor :application - self.application = Jets.application - class << self - delegate :env, :root, to: Jets - end -end diff --git a/lib/jets/overrides/lambda.rb b/lib/jets/overrides/lambda.rb deleted file mode 100644 index fed8597b5..000000000 --- a/lib/jets/overrides/lambda.rb +++ /dev/null @@ -1 +0,0 @@ -require "jets/overrides/lambda/marshaller" \ No newline at end of file diff --git a/lib/jets/overrides/lambda/marshaller.rb b/lib/jets/overrides/lambda/marshaller.rb deleted file mode 100644 index edf9cd8fd..000000000 --- a/lib/jets/overrides/lambda/marshaller.rb +++ /dev/null @@ -1,31 +0,0 @@ -require "json" - -# Hack AwsLambda Ruby Runtime to fix .to_json issue collision with ActiveSupport. -# To reproduce: -# Create a shared resource from the docs and call sns.publish -# -# Causes an infinite loop when calling sns.publish somehow. -# Overriding with JSON.dump and follow up with AWS ticket. -module AwsLambda - class Marshaller - class << self - # By default, just runs #to_json on the method's response value. - # This can be overwritten by users who know what they are doing. - # The response is an array of response, content-type. - # If returned without a content-type, it is assumed to be application/json - # Finally, StringIO/IO is used to signal a response that shouldn't be - # formatted as JSON, and should get a different content-type header. - def marshall_response(method_response) - case method_response - when StringIO, IO - [method_response, "application/unknown"] - else - # Note: Removed previous code which did force_encoding("ISO-8859-1").encode("UTF-8") - # It caused issues with international characters. - # It does not seem like we need the force_encoding anymore. - JSON.dump(method_response) - end - end - end - end -end diff --git a/lib/jets/overrides/puma.rb b/lib/jets/overrides/puma.rb deleted file mode 100644 index 266374699..000000000 --- a/lib/jets/overrides/puma.rb +++ /dev/null @@ -1,10 +0,0 @@ -require "puma/error_logger" -class Puma::ErrorLogger - alias_method :original_title, :title - def title(options={}) - string = original_title(options) - error = options[:error] - string << "\n#{error.backtrace.join("\n")}" if error - string - end -end diff --git a/lib/jets/paths.rb b/lib/jets/paths.rb deleted file mode 100644 index f289474a9..000000000 --- a/lib/jets/paths.rb +++ /dev/null @@ -1,243 +0,0 @@ -# frozen_string_literal: true - -require "pathname" - -module Jets - module Paths - # This object is an extended hash that behaves as root of the Jets::Paths system. - # It allows you to collect information about how you want to structure your application - # paths through a Hash-like API. It requires you to give a physical path on initialization. - # - # root = Root.new "/jets" - # root.add "app/controllers", eager_load: true - # - # The above command creates a new root object and adds "app/controllers" as a path. - # This means we can get a Jets::Paths::Path object back like below: - # - # path = root["app/controllers"] - # path.eager_load? # => true - # path.is_a?(Jets::Paths::Path) # => true - # - # The +Path+ object is simply an enumerable and allows you to easily add extra paths: - # - # path.is_a?(Enumerable) # => true - # path.to_ary.inspect # => ["app/controllers"] - # - # path << "lib/controllers" - # path.to_ary.inspect # => ["app/controllers", "lib/controllers"] - # - # Notice that when you add a path using +add+, the path object created already - # contains the path with the same path value given to +add+. In some situations, - # you may not want this behavior, so you can give :with as option. - # - # root.add "config/routes", with: "config/routes.rb" - # root["config/routes"].inspect # => ["config/routes.rb"] - # - # The +add+ method accepts the following options as arguments: - # eager_load, autoload, autoload_once, and glob. - # - # Finally, the +Path+ object also provides a few helpers: - # - # root = Root.new "/jets" - # root.add "app/controllers" - # - # root["app/controllers"].expanded # => ["/jets/app/controllers"] - # root["app/controllers"].existent # => ["/jets/app/controllers"] - # - # Check the Jets::Paths::Path documentation for more information. - class Root - attr_accessor :path - - def initialize(path) - @path = path - @root = {} - end - - def []=(path, value) - glob = self[path] ? self[path].glob : nil - add(path, with: value, glob: glob) - end - - def add(path, options = {}) - with = Array(options.fetch(:with, path)) - @root[path] = Path.new(self, path, with, options) - end - - def [](path) - @root[path] - end - - def values - @root.values - end - - def keys - @root.keys - end - - def values_at(*list) - @root.values_at(*list) - end - - def all_paths - values.tap(&:uniq!) - end - - def autoload_once - filter_by(&:autoload_once?) - end - - def eager_load - filter_by(&:eager_load?) - end - - def autoload_paths - filter_by(&:autoload?) - end - - def load_paths - filter_by(&:load_path?) - end - - private - def filter_by(&block) - all_paths.find_all(&block).flat_map { |path| - paths = path.existent - paths - path.children.flat_map { |p| yield(p) ? [] : p.existent } - }.uniq - end - end - - class Path - include Enumerable - - attr_accessor :glob - - def initialize(root, current, paths, options = {}) - @paths = paths - @current = current - @root = root - @glob = options[:glob] - @exclude = options[:exclude] - - options[:autoload_once] ? autoload_once! : skip_autoload_once! - options[:eager_load] ? eager_load! : skip_eager_load! - options[:autoload] ? autoload! : skip_autoload! - options[:load_path] ? load_path! : skip_load_path! - end - - def absolute_current # :nodoc: - File.expand_path(@current, @root.path) - end - - def children - keys = @root.keys.find_all { |k| - k.start_with?(@current) && k != @current - } - @root.values_at(*keys.sort) - end - - def first - expanded.first - end - - def last - expanded.last - end - - %w(autoload_once eager_load autoload load_path).each do |m| - class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def #{m}! # def eager_load! - @#{m} = true # @eager_load = true - end # end - # - def skip_#{m}! # def skip_eager_load! - @#{m} = false # @eager_load = false - end # end - # - def #{m}? # def eager_load? - @#{m} # @eager_load - end # end - RUBY - end - - def each(&block) - @paths.each(&block) - end - - def <<(path) - @paths << path - end - alias :push :<< - - def concat(paths) - @paths.concat paths - end - - def unshift(*paths) - @paths.unshift(*paths) - end - - def to_ary - @paths - end - - def paths - raise "You need to set a path root" unless @root.path - - map do |p| - Pathname.new(@root.path).join(p) - end - end - - def extensions # :nodoc: - $1.split(",") if @glob =~ /\{([\S]+)\}/ - end - - # Expands all paths against the root and return all unique values. - def expanded - raise "You need to set a path root" unless @root.path - result = [] - - each do |path| - path = File.expand_path(path, @root.path) - - if @glob && File.directory?(path) - result.concat files_in(path) - else - result << path - end - end - - result.uniq! - result - end - - # Returns all expanded paths but only if they exist in the filesystem. - def existent - expanded.select do |f| - does_exist = File.exist?(f) - - if !does_exist && File.symlink?(f) - raise "File #{f.inspect} is a symlink that does not point to a valid file" - end - does_exist - end - end - - def existent_directories - expanded.select { |d| File.directory?(d) } - end - - alias to_a expanded - - private - def files_in(path) - files = Dir.glob(@glob, base: path) - files -= @exclude if @exclude - files.map! { |file| File.join(path, file) } - files.sort - end - end - end -end diff --git a/lib/jets/poly.rb b/lib/jets/poly.rb deleted file mode 100644 index 584e7174d..000000000 --- a/lib/jets/poly.rb +++ /dev/null @@ -1,73 +0,0 @@ -module Jets - class Poly - extend Memoist - - def initialize(app_class, app_meth) - @app_class = app_class # already a Constant, IE: PostController - @app_meth = app_meth.to_sym - end - - def run(event, context={}) - check_private_method! - - if definition.lang == :ruby - # controller = PostsController.new(event, content) - # resp = controller.edit - run_ruby_code(event, context) - else - executor = LambdaExecutor.new(definition) - resp = executor.run(event, context) - if resp["errorMessage"] - raise_error(resp) - end - resp - end - end - - def run_ruby_code(event, context) - @app_class.process(event, context, @app_meth) - rescue Exception => e - Jets.report_exception(e) - raise(e) - end - - def raise_error(resp) - backtrace = resp["stackTrace"] + caller - backtrace = backtrace.map { |l| l.sub(/^\s+/,'') } - # Adjust the paths from the tmp path to the app path to improve user debugging - # experience. Example: - # From: - # File "/tmp/jets/lite/executor/20180917-16777-43a9e48/app/controllers/jets/public_controller/python/show.py", line 32 - # To: - # File "app/controllers/jets/public_controller/python/show.py", line 32 backtrace = - backtrace = backtrace.map do |l| - if l.include?(Jets.build_root) && !l.include?("lambda_executor.") - l.sub(/\/tmp\/jets.*executor\/\d{8}-+.*?\//, '') - else - l - end - end - - # IE: Jets::Poly::PythonError - error_class = "Jets::Poly::#{definition.lang.to_s.camelize}Error".constantize - raise error_class.new(resp["errorMessage"], backtrace) - end - - def definition - definition = @app_class.all_public_definitions[@app_meth] - # Provider user a better error message to user than a nil failure. - unless definition - raise "Unable to find #{@app_class}##{@app_meth}" - end - definition - end - memoize :definition - - def check_private_method! - private_detected = @app_class.all_private_definitions.keys.include?(@app_meth) - return unless private_detected # Ok to continue - - raise "The #{@app_class}##{@app_meth} is a private method. Unable to call it unless it is public" - end - end -end diff --git a/lib/jets/poly/base_executor.rb b/lib/jets/poly/base_executor.rb deleted file mode 100644 index bde9b9d14..000000000 --- a/lib/jets/poly/base_executor.rb +++ /dev/null @@ -1,125 +0,0 @@ -require 'open3' -require 'tmpdir' - -class Jets::Poly - class BaseExecutor - extend Memoist - - def initialize(definition) - @definition = definition - end - - # Handler is in properties: - # 1. copy lambda function into tmp folder - # 2. generate Lang wrapper script - # 3. call wrapper script from ruby. Handle stdout and stderr and result. Pass info back to ruby - def run(event, context) - @temp_dir = create_tmpdir - copy_src_to_temp - write(code) - result = run_lambda_executor(event, context) - cleanup - result - end - - def write(code) - puts "lambda_executor_script #{lambda_executor_script}" if ENV['KEEP_LAMBDA_WRAPPER'] - IO.write(lambda_executor_script, code) - end - - # Mimic Dir.mktmpdir randomness, not using Dir.mktmpdir because that generates - # the folder at the /tmp level only. - def create_tmpdir - random = "#{Time.now.strftime("%Y%d%H")}-#{Process.pid}-#{SecureRandom.hex[0..6]}" - tmpdir = "#{Jets.build_root}/executor/#{random}" - FileUtils.mkdir_p(tmpdir) - tmpdir - end - - def copy_src_to_temp - app_class = @definition.class_name.constantize - internal = app_class.respond_to?(:internal) && app_class.internal - src = internal ? - "#{File.expand_path("../internal", File.dirname(__FILE__))}/#{@definition.poly_src_path}" : - "#{Jets.root}/#{@definition.poly_src_path}" - dest = "#{@temp_dir}/#{@definition.poly_src_path}" - - FileUtils.mkdir_p(File.dirname(dest)) - FileUtils.cp(src, dest) - end - - def lambda_executor_script - File.dirname("#{@temp_dir}/#{@definition.poly_src_path}") + "/lambda_executor" + @definition.lang_ext - end - - # When polymorphic method errors, this method reproduces an error in the lambda format - # Here's some examples to help example: - # - # Example of what the raw python prints out to stderr: - # - # Traceback (most recent call last): - # File "/tmp/jets/lite/executor/20180804-10727-mcs6qk/lambda_executor.py", line 6, in - # resp = handle(event, context) - # File "/tmp/jets/lite/executor/20180804-10727-mcs6qk/index.py", line 22, in handle - # return response({'message': e.message}, 400) - # File "/tmp/jets/lite/executor/20180804-10727-mcs6qk/index.py", line 5, in response - # badcode - # NameError: global name 'badcode' is not defined - # - # So last line has the error summary info. Other lines have stack trace after the Traceback indicator. - # - # Example of the reproduced lambda error format: - # - # { - # "errorMessage": "'NameError' object has no attribute 'message'", - # "errorType": "AttributeError", - # "stackTrace": [ - # [ - # "/var/definition/handlers/controllers/posts_controller/python/index.py", - # 22, - # "handle", - # "return response({'message': e.message}, 400)" - # ] - # ] - # } - # - def run_lambda_executor(event, context) - interpreter = @definition.lang - command = %Q|#{interpreter} #{lambda_executor_script} '#{JSON.dump(event)}' '#{JSON.dump(context)}'| - stdout, stderr, status = Open3.capture3(command) - # puts "=> #{command}".color(:green) - # puts "stdout #{stdout}" - # puts "stderr #{stderr}" - # puts "status #{status}" - if status.success? - stdout - else - # We'll mimic the way lambda displays an error. - # $stderr.puts(stderr) # uncomment to debug - error_lines = stderr.split("\n") - error_message = error_lines.pop - error_type = error_message.split(':').first - error_lines.shift # remove first line that has the Traceback - stack_trace = error_lines.reverse # python shows stack trace in opposite order from ruby - JSON.dump( - "errorMessage" => error_message, - "errorType" => error_type, - "stackTrace" => stack_trace - ) - end - end - - def cleanup - FileUtils.rm_rf(@temp_dir) unless ENV['KEEP_LAMBDA_WRAPPER'] - end - - def handler - # Must use the generated CloudFormation template to get the handler because - # the handler is derived from mutiple sources. - resource = Jets::Cfn::Resource::Lambda::Function.new(@definition) - full_handler = resource.properties[:Handler] # full handler here - File.extname(full_handler).sub(/^./,'') # the extension of the full handler is the handler - end - memoize :handler - end -end diff --git a/lib/jets/poly/lambda_executor.rb b/lib/jets/poly/lambda_executor.rb deleted file mode 100644 index 9702cc637..000000000 --- a/lib/jets/poly/lambda_executor.rb +++ /dev/null @@ -1,16 +0,0 @@ -require 'json' - -class Jets::Poly - class LambdaExecutor - def initialize(task) - @task = task - end - - def run(event, context) - executor_class = "Jets::Poly::#{@task.lang.capitalize}Executor".constantize - executor = executor_class.new(@task) - text = executor.run(event, context) - JSON.load(text) - end - end -end \ No newline at end of file diff --git a/lib/jets/poly/node_error.rb b/lib/jets/poly/node_error.rb deleted file mode 100644 index 332da84de..000000000 --- a/lib/jets/poly/node_error.rb +++ /dev/null @@ -1,8 +0,0 @@ -class Jets::Poly - class NodeError < StandardError - def initialize(message, backtrace) - super(message) - set_backtrace(backtrace) - end - end -end \ No newline at end of file diff --git a/lib/jets/poly/node_executor.rb b/lib/jets/poly/node_executor.rb deleted file mode 100644 index 12c719f92..000000000 --- a/lib/jets/poly/node_executor.rb +++ /dev/null @@ -1,54 +0,0 @@ -class Jets::Poly - class NodeExecutor < BaseExecutor - # Code for wrapper script that mimics lambda execution. Wrapper script usage: - # - # node WRAPPER_SCRIPT EVENT - # - # Example: - # - # node /tmp/jets/demo/executor/20180804-12816-imqb9/lambda_executor.js '{}' - # - def code - if async_syntax? - async_code - else - callback_code - end - end - - # https://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-handler.html - def async_syntax? - app_path = "#{Jets.root}/" + @definition.handler_path.sub('handlers/', 'app/') - source_code = IO.read(app_path) - source_code.match(/=\s*async.*\(/) - end - - def async_code - <<-EOL -var event = process.argv[2] -event = JSON.parse(event) -var context = {} - -var app = require("./#{@definition.meth}.js") -app.#{handler}(event, context).then(resp => console.log(JSON.stringify(resp))) -EOL - end - - - def callback_code - <<-EOL -function callback(error, response) { - var text = JSON.stringify(response) - console.log(text) -} - -var event = process.argv[2] -event = JSON.parse(event) -var context = {} - -var app = require("./#{@definition.meth}.js") -var resp = app.#{handler}(event, context, callback) -EOL - end - end -end diff --git a/lib/jets/poly/python_error.rb b/lib/jets/poly/python_error.rb deleted file mode 100644 index 3318d3231..000000000 --- a/lib/jets/poly/python_error.rb +++ /dev/null @@ -1,8 +0,0 @@ -class Jets::Poly - class PythonError < StandardError - def initialize(message, backtrace) - super(message) - set_backtrace(backtrace) - end - end -end \ No newline at end of file diff --git a/lib/jets/poly/python_executor.rb b/lib/jets/poly/python_executor.rb deleted file mode 100644 index bf81bce66..000000000 --- a/lib/jets/poly/python_executor.rb +++ /dev/null @@ -1,23 +0,0 @@ -class Jets::Poly - class PythonExecutor < BaseExecutor - # Code for wrapper script that mimics lambda execution. Wrapper script usage: - # - # python WRAPPER_SCRIPT EVENT - # - # Example: - # - # python /tmp/jets/demo/executor/20180804-12816-imqb9/lambda_executor.py '{}' - def code - <<-EOL -import sys -import json -from #{@definition.meth} import #{handler} -event = json.loads(sys.argv[1]) -context = {} -resp = #{handler}(event, context) -result = json.dumps(resp) -print(result) -EOL - end - end -end \ No newline at end of file diff --git a/lib/jets/preheat.rb b/lib/jets/preheat.rb deleted file mode 100644 index 92cb2deef..000000000 --- a/lib/jets/preheat.rb +++ /dev/null @@ -1,110 +0,0 @@ -module Jets - class Preheat - extend Memoist - include Jets::AwsServices - - # Examples: - # - # Jets::Preheat.warm("posts_controller-index") - # Jets::Preheat.warm("jets-preheat_job-warm") - # - def self.warm(function_name, options={}) - Preheat.new(options).warm(function_name) - end - - def self.warm_all(options={}) - Preheat.new(options).warm_all - end - - def initialize(options) - @options = options # passed to Call.new options - @options[:mute_output] = true if @options[:mute_output].nil? - @options[:lambda_proxy] = false # do not transform controller event from {"event": "1"} to {"queryStringParameters":{"_prewarm":"1"}} - end - - # Makes remote call to the Lambda function. - def warm(function_name) - Jets::Commands::Call::Caller.new(function_name, '{"_prewarm": "1"}', @options).run unless Jets.env.test? - end - - # Loop through all methods for each class and makes special prewarm call to each method. - def warm_all - threads = [] - all_functions.each do |function_name| - next if function_name.include?('jets-public_controller') # handled by warm_public_controller_more - threads << Thread.new do - warm(function_name) - end - end - threads.each { |t| t.join } - - # Warm the these controllers more since they can be hit more often - warm_public_controller_more - - # return the funciton names so we can see in the Lambda console - # the functions being prewarmed - all_functions - end - - def warm_public_controller_more - function_name = 'jets-public_controller-show' # key function name - return unless all_functions.include?(function_name) - - public_ratio = Jets.config.prewarm.public_ratio - return if public_ratio == 0 - - puts "Prewarming the public controller extra at a ratio of #{public_ratio}" unless @options[:mute] - - threads = [] - public_ratio.times do - threads << Thread.new do - warm(function_name) - end - end - threads.each { |t| t.join } - end - - # Returns: - # [ - # "demo-posts_controller-index", - # "demo-posts_controller-show", - # ... - # ] - # - # or (for one lambda per controller) - # - # [ - # "demo-posts_controller", - # "demo-up_controller", - # ... - # ] - def all_functions - parent_stack = cfn.describe_stack_resources(stack_name: Jets::Names.parent_stack_name) - parent_resources = parent_stack.stack_resources.select do |resource| - resource.logical_resource_id =~ /Controller$/ # only controller functions - end - physical_resource_ids = parent_resources.map(&:physical_resource_id) - resources = physical_resource_ids.inject([]) do |acc, physical_resource_id| - stack_resources = cfn.describe_stack_resources(stack_name: physical_resource_id).stack_resources - stack_resources.each do |stack_resource| - acc << stack_resource if stack_resource.logical_resource_id.ends_with?('LambdaFunction') # only functions - end - acc - end - resources.map(&:physical_resource_id) # function names - end - memoize :all_functions - - def classes - Jets::Cfn::Builder.app_files.map do |path| - next if path.include?("preheat_job.rb") # dont want to cause an infinite loop, just in case - next unless path =~ %r{app/controllers} # only prewarm controllers - - class_path = path.sub(%r{.*app/\w+/},'').sub(/\.rb$/,'') - class_name = class_path.camelize - # IE: PostsController - class_name.constantize # load app/**/* class definition - end.compact - end - end -end diff --git a/lib/jets/prewarm.rb b/lib/jets/prewarm.rb new file mode 100644 index 000000000..ae0388674 --- /dev/null +++ b/lib/jets/prewarm.rb @@ -0,0 +1,27 @@ +module Jets + class Prewarm + class << self + include Jets::Util::Logging + + # Can use to prewarm post deploy + # Jets::Prewarm.handle + # Jets::Prewarm.handle(verbose: true, invocation_type: "RequestResponse") + # Note: verbose is only useful when invocation_type is "RequestResponse" + def handle(options = {}) + defaults = { + function_name: "controller", + event: '{"_prewarm": 1}' + } + options = defaults.merge(options.symbolize_keys) + # Always calls Lambda, not local + # Use invoke so messages don't get printed + Jets::CLI::Call.new(options).invoke + rescue Jets::CLI::Call::Error => e + puts "ERROR: #{e.message}".color(:red) + puts "The stack may not be full deployed yet. Please check the stack and try again." + end + + delegate :stats, to: Jets::Shim::Adapter::Prewarm + end + end +end diff --git a/lib/jets/processors/deducer.rb b/lib/jets/processors/deducer.rb deleted file mode 100644 index e23acf11e..000000000 --- a/lib/jets/processors/deducer.rb +++ /dev/null @@ -1,65 +0,0 @@ -# Jets::Processors::Deducer class figures out information that allows the -# controller or job to be called. Sme key methods for deducer: -# -# code - code to instance eval. IE: PostsController.new(event, context).index -# path - full path to the app code. IE: #{Jets.root}app/controllers/posts_controller.rb -# -class Jets::Processors::Deducer - def initialize(handler) - @handler = handler # handlers/controllers/posts.show - # @handler_path: "handlers/controllers/posts" - # @handler_method: "show" - @handler_path, @handler_method = @handler.split('.') - end - - def code - # code: "PostsController.process(event, context, "show")" - # code: "HardJob.process(event, context, "dig")" - %|#{class_name}.process(event, context, "#{@handler_method}")| - end - - # Input: @handler_path: handlers/jobs/hard_job.rb - # Output: #{Jets.root/app/jobs/hard_job.rb - def path - @handler_path.sub("handlers", "app") + ".rb" - end - - # process_type is key. It can be either "controller" or "job". It is used to - # deduce the rest of the methods: code, path. - def process_type - if shared? - "function" # all app/shared/functions are always function process_type - else - @handler.split('/')[1].singularize # controller, job, rule, etc - end - end - - def shared? - @handler.include?("/shared/functions") - end - - # Example underscored_class_name: - # class_name = underscored_class_name - # class_name = class_name # PostsController - def class_name - regexp = shared? ? - Regexp.new(".*handlers/shared/functions/") : - Regexp.new(".*handlers/#{process_type.pluralize}/") - - # Example regexp: - # /.*handlers\/controllers\// - # /.*handlers\/jobs\// - class_name = @handler_path.sub(regexp, "") - # Example class names: - # posts_controller - # hard_job - # hello - # hello_function - - class_name.camelize - end - - def load_class - Jets::Klass.from_path(path) - end -end diff --git a/lib/jets/processors/main_processor.rb b/lib/jets/processors/main_processor.rb deleted file mode 100644 index 3135ec72a..000000000 --- a/lib/jets/processors/main_processor.rb +++ /dev/null @@ -1,54 +0,0 @@ -require 'json' -require 'stringio' - -# Node shim calls this class to process both controllers and jobs -class Jets::Processors::MainProcessor - attr_reader :event, :context, :handler - def initialize(event, context, handler) - @event = event - @context = context - @handler = handler - end - - def run - # Use the handler to deduce app code to run. - # Example handlers: handlers/controllers/posts.create, handlers/jobs/sleep.perform - # - # deducer = Jets::Processors::Deducer.new("handlers/controllers/posts.create") - # - deducer = Jets::Processors::Deducer.new(handler) - begin - # Examples: - # deducer.code => PostsController.process(event, context, "show") - # deducer.path => app/controllers/posts_controller.rb - # - # deducer.code => HardJob.process(event, context, "dig") - # deducer.path => app/jobs/hard_job.rb - # - # deducer.code => HelloFunction.process(event, context, "world") - # deducer.path => app/functions/hello.rb - deducer.load_class - # result = PostsController.process(event, context, "create") - result = instance_eval(deducer.code, deducer.path) - result = HashWithIndifferentAccess.new(result) if result.is_a?(Hash) - - Jets.increase_call_count - - if result.is_a?(Hash) && result["headers"] - # API Gateway is okay with the header values as Integers but - # ELBs are more strict about this and require the header values to be Strings - result["headers"]["x-jets-call-count"] = Jets.call_count.to_s - result["headers"]["x-jets-prewarm-count"] = Jets.prewarm_count.to_s - end - - result - - # Additional rescue Exception as a paranoid measure. We want to make sure - # that we always report the exception. This is the last line of defense. - # Note: This only happens when the code is running in Lambda. - rescue => exception - Jets.report_exception(exception) - raise(exception) - end - end -end diff --git a/lib/jets/rack/logger.rb b/lib/jets/rack/logger.rb deleted file mode 100644 index 5bc759d38..000000000 --- a/lib/jets/rack/logger.rb +++ /dev/null @@ -1,77 +0,0 @@ -# frozen_string_literal: true - -require "active_support/core_ext/time/conversions" -require "active_support/core_ext/object/blank" -require "active_support/log_subscriber" -require "rack/body_proxy" - -module Jets - module Rack - # Sets log tags, logs the request, calls the app, and flushes the logs. - # - # Log tags (+taggers+) can be an Array containing: methods that the +request+ - # object responds to, objects that respond to +to_s+ or Proc objects that accept - # an instance of the +request+ object. - class Logger < ActiveSupport::LogSubscriber - def initialize(app, taggers = nil) - @app = app - @taggers = taggers || [] - end - - def call(env) - request = ActionDispatch::Request.new(env) - - if logger.respond_to?(:tagged) - logger.tagged(compute_tags(request)) { call_app(request, env) } - else - call_app(request, env) - end - end - - private - def call_app(request, env) # :doc: - instrumenter = ActiveSupport::Notifications.instrumenter - instrumenter_state = instrumenter.start "request.action_dispatch", request: request - instrumenter_finish = -> () { - instrumenter.finish_with_state(instrumenter_state, "request.action_dispatch", request: request) - } - - logger.info { started_request_message(request) } - status, headers, body = @app.call(env) - body = ::Rack::BodyProxy.new(body, &instrumenter_finish) - [status, headers, body] - rescue Exception - instrumenter_finish.call - raise - ensure - ActiveSupport::LogSubscriber.flush_all! - end - - # Started GET "/session/new" for 127.0.0.1 at 2012-09-26 14:51:42 -0700 - def started_request_message(request) # :doc: - 'Started %s "%s" for %s at %s' % [ - request.raw_request_method, - request.filtered_path, - request.remote_ip, - Time.now.to_default_s ] - end - - def compute_tags(request) # :doc: - @taggers.collect do |tag| - case tag - when Proc - tag.call(request) - when Symbol - request.send(tag) - else - tag - end - end - end - - def logger - Jets.logger - end - end - end -end diff --git a/lib/jets/rdoc.rb b/lib/jets/rdoc.rb index a91c588a8..24cd4888a 100644 --- a/lib/jets/rdoc.rb +++ b/lib/jets/rdoc.rb @@ -22,9 +22,9 @@ def options Rakefile bin ] - exclude = exclude.map { |word| ['-x', word] }.flatten + exclude = exclude.map { |word| ["-x", word] }.flatten ["-m", "README.md", "--markup", "tomdoc"] + exclude end extend self end -end \ No newline at end of file +end diff --git a/lib/jets/remote/base.rb b/lib/jets/remote/base.rb new file mode 100644 index 000000000..a5c245cbc --- /dev/null +++ b/lib/jets/remote/base.rb @@ -0,0 +1,11 @@ +module Jets::Remote + class Base + extend Memoist + include Jets::AwsServices + include Jets::Util::Logging + + def initialize(options) + @options = options + end + end +end diff --git a/lib/jets/remote/download.rb b/lib/jets/remote/download.rb new file mode 100644 index 000000000..35c8de2f6 --- /dev/null +++ b/lib/jets/remote/download.rb @@ -0,0 +1,65 @@ +module Jets::Remote + class Download < Base + delegate :s3_bucket, to: "Jets.project" + + def initialize(command) + @command = command + end + + def download_built + # build deploy ci:build ci:deploy waf:build waf:deploy + subcommand = @command.split(":").last + case subcommand + when "build", "deploy", "dockerfile" + download_templates + download_dockerfile + end + end + + def download_dockerfile + s3_key = "jets/sigs/#{sig}/docker/Dockerfile" + object = s3_resource.bucket(s3_bucket).object(s3_key) + # Can not exists for: jets build --templates + if object.exists? + dest = "#{Jets.build_root}/docker/Dockerfile" + FileUtils.mkdir_p(File.dirname(dest)) + object.download_file(dest) + if @command.split(":").last == "dockerfile" + log.info "Dockerfile at: #{dest}" + else + log.debug "Download: #{dest}" + end + end + end + + def download_templates + clean_all_templates + download_all_templates + end + + def clean_all_templates + FileUtils.rm_rf("#{Jets.build_root}/templates") + end + + def download_all_templates + s3.list_objects(bucket: s3_bucket, prefix: "jets/sigs/#{sig}/templates/").each do |resp| + resp.contents.each do |object| + download_template(object.key) + end + end + end + + def download_template(s3_key) + object = s3_resource.bucket(s3_bucket).object(s3_key) + path = object.key.sub("jets/sigs/#{sig}/templates/", "") + dest = "#{Jets.build_root}/templates/#{path}" + log.debug "Download: #{dest}" + FileUtils.mkdir_p(File.dirname(dest)) + object.download_file(dest) + end + + def sig + Jets::Remote::Runner.sig + end + end +end diff --git a/lib/jets/remote/runner.rb b/lib/jets/remote/runner.rb new file mode 100644 index 000000000..3e1cf608c --- /dev/null +++ b/lib/jets/remote/runner.rb @@ -0,0 +1,101 @@ +module Jets::Remote + class Runner < Base + cattr_reader :sig + def run + code_zip_and_upload + + command = @options[:command] || "deploy" + command_args = if @options[:version] # jets rollback 8 + @options[:version] + elsif @options[:templates] # jets build --templates or jets deploy --templates + "--templates" + end + + sig = Jets::Api::Sig.create(command: command, command_args: command_args, architecture: architecture) + @@sig = sig[:token] + @jets_go = sig[:jets_go] + + params = { + buildspec_override: sig[:buildspec], + environment_variables_override: environment_variables_override, + project_name: project_name + } + fleet_override = Jets.bootstrap.config.codebuild.project.fleet_override + if fleet_override + params[:fleet_override] = {fleet_arn: fleet_override} + end + + resp = codebuild.start_build(params) + Jets::Api::Sig.update(sig[:id], build_id: resp.build.id) + + logger.info "Started remote run for #{command}" + Jets::CLI::Tip.show(:remote_run, disable_howto: false) + logger.info "Console Log Url:" + logger.info codebuild_log_url(resp.build.id) + + tail_logs(resp.build.id) + + Download.new(command).download_built + + resp = codebuild.batch_get_builds(ids: [resp.build.id]) + success = resp.builds.first.build_status == "SUCCEEDED" + exit 1 unless success + success + end + + def code_zip_and_upload + code = Jets::Code.new(dummy: @options[:dummy]) + @app_src = code.zip_and_upload # s3_location + end + + # Get architecture of codebuild image being used + def architecture + project = codebuild.batch_get_projects(names: [project_name]).projects.first + image = project.environment.image # IE: aws/codebuild/amazonlinux-aarch64-lambda-standard:ruby3.2 + if image.include?("aarch64") || image.include?("arm64") + "arm64" + else + "x86_64" + end + end + + def project_name + stack_name = Jets::Names.parent_stack_name + stack = cfn.describe_stacks(stack_name: stack_name).stacks.first + logical_id = use_lambda_compute_type? ? "CodebuildLambda" : "Codebuild" + stack.outputs.find { |o| o.output_key == logical_id }.output_value + end + memoize :project_name + + def use_lambda_compute_type? + return true if Jets.bootstrap.config.infra + return false unless Jets.bootstrap.config.codebuild.lambda.enable + + command = ARGV.first + command == "ci" || + command == "waf" || + command == "dockerfile" || + ARGV.include?("--templates") + end + + def environment_variables_override + Jets::Cfn::Resource::Codebuild::Project::Env.new.pass_vars( + JETS_APP_SRC: @app_src, + JETS_GO: @jets_go, + JETS_SIG: @@sig + ) + end + + def tail_logs(build_id) + Tailer.new(@options, build_id).run + end + + private + + def codebuild_log_url(build_id) + build_id = build_id.split(":").last + region = Jets.aws.region + "https://#{region}.console.aws.amazon.com/codesuite/codebuild/projects/#{project_name}/build/#{project_name}%3A#{build_id}/log" + end + end +end diff --git a/lib/jets/remote/tailer.rb b/lib/jets/remote/tailer.rb new file mode 100644 index 000000000..10831a029 --- /dev/null +++ b/lib/jets/remote/tailer.rb @@ -0,0 +1,272 @@ +require "aws-logs" + +module Jets::Remote + class Tailer < Base + include Jets::Util::Pretty + + def initialize(options, build_id) + @options, @build_id = options, build_id + + @output = [] # for specs + @shown_phases = [] + @thread = nil + end + + @@delay = 2 # initial delay + def run + complete = false + until complete + build = find_build + unless build + puts "ERROR: Build id not found: #{@build_id}".color(:red) + return + end + + # CodeBuild AWS Lambda Compute Type take slightly a few milliseconds to report phases. + # But return the build already. So we have to wait until the phases are available. + while build.phases.nil? + build = find_build # refresh + sleep 1 + end + + print_phases(build) unless @stop_printing_phases # once cloudwatch logs starts, stop printing phases + set_log_group_name(build) + + complete = build.build_complete + + next if ENV["JETS_TEST"] + if build_phase_started?(build) + start_cloudwatch_tail + @stop_printing_phases = true + @@delay = 5 # increase @@delay also + end + + sleep @@delay + end + + stop_cloudwatch_tail(build) + if Jets.bootstrap.config.codebuild.logging.final_phases + print_phases(build) # print final phases + end + display_failed_phases(build) + end + + def build_phase_started?(build) + @build_phase_started ||= build.phases.any? do |phase| + phase.phase_type == "BUILD" + end + end + + def display_failed_phases(build) + status = build.build_status.to_s # in case nil + status = (status != "SUCCEEDED") ? status.color(:red) : status.color(:green) + return if status == "SUCCEEDED" + + failed_phases = build.phases.select do |phase| + phase.phase_status != "SUCCEEDED" && phase.phase_status.to_s != "" + end + return if failed_phases.empty? + + puts "Failed Phases:" + failed_phases.each do |phase| + puts "#{phase.phase_type}: #{phase.phase_status.color(:red)}" + next unless phase.contexts # can be nil + context = phase.contexts.last + if context # show error details: Unable to pull customer's container image https://gist.github.com/tongueroo/22e4ca3d4cde002108ff506eba9062f6 + message = context.message + puts message + if message.include?("CannotPullContainerError") && message.include?("access denied") + puts "See: https://docs.aws.amazon.com/codebuild/latest/userguide/sample-ecr.html" + end + end + end + end + + def display_time_took(build) + puts "Remote runner took #{build_time(build)} to complete" + end + + def find_build + resp = codebuild.batch_get_builds(ids: [@build_id]) + resp.builds.first + rescue Aws::CodeBuild::Errors::ThrottlingException => e + log_arch "WARN: find_build codebuild.batch_get_builds ThrottlingException: #{e.message}" + @@delay = 10 # increase global @@delay also + + # Also exponential backoff delay + # 2, 4, 8, 16, 32 + @retries ||= 1 + raise if @retries > 5 # give up after 5 retries + delay = 2**@retries + log_arch "Retrying in #{delay}s..." + sleep delay + @retries += 1 + retry + end + + def start_cloudwatch_tail + return if @cloudwatch_tail_started + return unless @log_group_name && @log_stream_name + + @thread = Thread.new do + @cw_tail = cloudwatch_tail + @cw_tail.run + end + @cloudwatch_tail_started = true + end + + def cloudwatch_tail + since = @options[:since] || "24h" # by default, search only 24h in the past + AwsLogs::Tail.new( + log_group_name: @log_group_name, + log_stream_names: [@log_stream_name], + since: since, + follow: true, + format: "plain", + show_if: show_if + ) + end + + def show_if + return true unless Jets.bootstrap.config.codebuild.logging.show == "filtered" + + start_marker = "./jets" + end_marker = "Phase complete: BUILD" + proc do |event| + @display_showing ||= event.message.include?(start_marker) + if @display_showing && event.message.include?(end_marker) + @display_showing = false + end + @display_showing + end + end + memoize :show_if + + def stop_cloudwatch_tail(build) + return if ENV["JETS_TEST"] + @cw_tail&.stop_follow! + @thread&.join + end + + def logs_command? + ARGV.join(" ").include?("logs") + end + + # build.build_status : The current status of the build. Valid values include: + # + # FAILED : The build failed. + # FAULT : The build faulted. + # IN_PROGRESS : The build is still in progress. + # STOPPED : The build stopped. + # SUCCEEDED : The build succeeded. + # TIMED_OUT : The build timed out. + # + def complete_failed?(build) + return if ENV["JETS_TEST"] + build.build_complete && build.build_status != "SUCCEEDED" + end + + # Setting enables start_cloudwatch_tail + def set_log_group_name(build) + logs = build.logs + @log_group_name = logs.group_name if logs.group_name + @log_stream_name = logs.stream_name if logs.stream_name + end + + def print_phases(build) + build.phases.each do |phase| + details = { + phase_type: phase.phase_type, + phase_status: phase.phase_status, + start_time: phase.start_time, + duration_in_seconds: phase.duration_in_seconds + } + display_phase(details) + @shown_phases << details + end + end + + def display_phase(details) + already_shown = @shown_phases.detect do |p| + p[:phase_type] == details[:phase_type] && + p[:phase_status] == details[:phase_status] && + p[:start_time] == details[:start_time] && + p[:duration_in_seconds] == details[:duration_in_seconds] + end + return if already_shown + + return if filter_phase_type?(details) + + phases = [ + "Phase:", details[:phase_type] + ] + + if details[:phase_type] == "COMPLETED" + # nothing to add + elsif details[:phase_status].nil? + phases << "Pending" + else + phases += [ + phase_status(details[:phase_status]), + phase_duration(details[:duration_in_seconds]) + ] + end + + phases = phases.flatten.compact.join(" ") + + say phases + end + + def filter_phase_type?(details) + return false unless Jets.bootstrap.config.codebuild.logging.show == "filtered" + + # Phase: SUBMITTED Status: SUCCEEDED Duration: 0s + # Phase: QUEUED Pending + # Phase: QUEUED Status: SUCCEEDED Duration: 0s + # Phase: PROVISIONING Pending + # Phase: PROVISIONING Status: SUCCEEDED Duration: 5s + # Phase: DOWNLOAD_SOURCE Status: SUCCEEDED Duration: 0s + # Phase: INSTALL Status: SUCCEEDED Duration: 0s + # Phase: PRE_BUILD Status: SUCCEEDED Duration: 0s + # Phase: BUILD Pending + # [Container] 2024/04/06 15:51:12.262049 Running command ... + # + # Becomes + # + # Phase: SUBMITTED Status: SUCCEEDED Duration: 0s + # Phase: QUEUED Pending + # Phase: QUEUED Status: SUCCEEDED Duration: 0s + # Phase: PROVISIONING Pending + # Phase: PROVISIONING Status: SUCCEEDED Duration: 5s + # [Container] 2024/04/06 15:51:12.262049 Running command ... + filtered_phase_types = %w[DOWNLOAD_SOURCE INSTALL PRE_BUILD BUILD] + filtered_phase_types.include?(details[:phase_type]) + end + + def phase_status(status) + return unless status # can be nil + + text = (status == "SUCCEEDED") ? status : status.color(:red) + ["Status:", text] + end + + def phase_duration(duration) + return unless duration # can be nil + + ["Duration:", pretty_time(duration)] + end + + def say(text) + ENV["JETS_TEST"] ? @output << text : puts(text) + end + + def output + @output.join("\n") + "\n" + end + + def build_time(build) + duration = build.phases.inject(0) { |sum, p| sum + p.duration_in_seconds.to_i } + pretty_time(duration) + end + end +end diff --git a/lib/jets/router.rb b/lib/jets/router.rb deleted file mode 100644 index 34ee00caa..000000000 --- a/lib/jets/router.rb +++ /dev/null @@ -1,94 +0,0 @@ -module Jets - module Router - class Error < RuntimeError ; end - - NAMED_ROUTES_METHODS = %w[index new create show edit update destroy].freeze - - def has_controller?(name) - routes.detect { |r| r.controller_name == name } - end - - def draw - drawn_route_set - end - - @@drawn_route_set = nil - def drawn_route_set - return @@drawn_route_set if @@drawn_route_set - - route_set = Jets.application.routes - @@drawn_route_set = route_set - end - - def clear! - @@drawn_route_set = nil - end - - def routes - drawn_route_set.routes - end - - # So we can save state in s3 post deploy. Example of structure. - # - # [ - # {"scope"=>{"options"=>{"as"=>"posts", "path"=>"posts", "param"=>nil, "from"=>"resources"}, "parent"=>{"options"=>{}, "parent"=>nil, "level"=>1}, "level"=>2}, "options"=>{"to"=>"posts#index", "path"=>"posts", "method"=>"get"}, "path"=>"posts", "to"=>"posts#index", "as"=>"posts"}, - # {"scope"=>{"options"=>{"as"=>"posts", "path"=>"posts", "param"=>nil, "from"=>"resources"}, "parent"=>{"options"=>{}, "parent"=>nil, "level"=>1}, "level"=>2}, "options"=>{"to"=>"posts#new", "path"=>"posts/new", "method"=>"get"}, "path"=>"posts/new", "to"=>"posts#new", "as"=>"new_post"}, - # ... - # ] - # - def to_json - JSON.dump(routes.map(&:to_h)) - end - - # Returns all paths including subpaths. - # Example: - # Input: ["posts/:id/edit"] - # Output: ["posts", "posts/:id", "posts/:id/edit"] - def all_paths - drawn_route_set.all_paths - end - - def all_routes_valid? - invalid_routes.empty? - end - - def invalid_routes - routes.select { |r| !r.valid? } - end - - def validate_routes! - return true if Jets.config.cfn.build.routes == "one_apigw_method_for_all_routes" - check_route_connected_functions - end - - # Checks that all routes are validate and have corresponding lambda functions - def check_route_connected_functions - return true if all_routes_valid? - - puts "Please double check the routes below map to valid controllers:".color(:red) - invalid_routes.each do |route| - puts " /#{route.path} => #{route.controller_name}##{route.action_name}" - end - false - end - - def find_route_by_event(event) - request = Jets::Controller::Request.new(event: event) - Jets::Router::Matcher.new.find_by_request(request) - end - - def find_by_definition(definition) - routes.find do |route| - route.controller_name == definition.class_name && - route.action_name == definition.meth.to_s - end - end - - # Filters out internal routes - def no_routes? - routes.reject(&:internal?).empty? - end - - extend self - end -end \ No newline at end of file diff --git a/lib/jets/router/compat/route_set.rb b/lib/jets/router/compat/route_set.rb deleted file mode 100644 index a2aaa0676..000000000 --- a/lib/jets/router/compat/route_set.rb +++ /dev/null @@ -1,177 +0,0 @@ -require "action_dispatch" -require "action_dispatch/routing/route_set" - -module Jets::Router::Compat - # Related: - # - # Jets::Controller::Compat::ActionDispatch::Routing::UrlFor - # Jets::Controller::Request::Compat::RouteSet - # Jets::Router::Compat::RouteSet - # - # Compat::RouteSet makes the Jets::Router::RouteSet "compatible" with the Rails RouteSet. - # Importantly, it provides a custom url_for method that mimics the Rails url_for method. - # - module RouteSet - # Original: - # RESERVED_OPTIONS = [:host, :protocol, :port, :subdomain, :domain, :tld_length, - # :trailing_slash, :anchor, :params, :only_path, :script_name, - # :original_script_name, :relative_url_root] - # UNKNOWN = ->(options) { ActionDispatch::Http::URL.url_for(options) } - RESERVED_OPTIONS = ActionDispatch::Routing::RouteSet::RESERVED_OPTIONS - UNKNOWN = ActionDispatch::Routing::RouteSet::UNKNOWN - - # This method is a version of the Rails RouteSet#url_for method. - # It only resolves simple routes for simple cases: - # - # when nil - # when Hash, ActionController::Parameters - # - # This is because ActionDispatch::Routing::UrlFor#url_for gets called before - # and then calls out to this method. Only have to account for 2 cases to - # mimic Jets behavior. - # - # Based on Jets ActionDispatch::Routing::RouteSet#url_for - def url_for(options, route_name = nil, url_strategy = UNKNOWN, method_name = nil, reserved = RESERVED_OPTIONS) - user = password = nil - - if options[:user] && options[:password] - user = options.delete :user - password = options.delete :password - end - - # save recall_controller for url_options when controller not specified by user - # IE: redirect_to action: :routes - recall_controller = options[:_recall][:controller] - recall = options.delete(:_recall) { {} } - - original_script_name = options.delete(:original_script_name) - # script_name = find_script_name options (original) - script_name = options.delete(:script_name) - - if original_script_name - script_name = original_script_name + script_name - end - - path_options = options.dup - reserved.each { |ro| path_options.delete ro } - path_options[:controller] ||= recall_controller - - route_with_params = generate(route_name, path_options, recall) - path = route_with_params.path(method_name) - - if options[:trailing_slash] && !options[:format] && !path.end_with?("/") - path += "/" - end - - params = route_with_params.params - - if options.key? :params - if options[:params]&.respond_to?(:to_hash) - params.merge! options[:params] - else - params[:params] = options[:params] - end - end - - options[:path] = path - options[:script_name] = script_name - options[:params] = params - options[:user] = user - options[:password] = password - - url_strategy.call options - end - - def generate(route_name, options, recall = {}, method_name = nil) - Generator.new(route_name, options, recall, self).generate - end - private :generate - - # This is only used for Custom resolve polymporphic path support - # Stub out. Jets does not currently support this. - # interface method - def polymorphic_mappings - {} - end - - # ActionView::Rendering ClassMethods build_view_context_class includes these modues. Something like this: - # - # def build_view_context_class(klass, supports_path, routes, helpers) - # ... - # if routes - # include routes.url_helpers(supports_path) - # include routes.mounted_helpers - # end - # - # interface method - # def url_helpers(supports_path=true) - # ActionDispatch::Routing::UrlFor - # end - - # interface method - def mounted_helpers - Module.new - end - - # Based on Rails ActionDispatch::Routing::RouteSet::Generator - class Generator - attr_reader :options, :recall, :set, :named_route - - def initialize(named_route, options, recall, set) - @named_route = named_route - @options = options.dup - # Fix kaminari pagination links. Looks like Rails can handle nil params - @options.delete_if { |_, v| v.to_param.nil? } - @recall = recall - @set = set - end - - # Generates a path from routes, returns a RouteWithParams or MissingRoute. - # MissingRoute will raise ActionController::UrlGenerationError. - def generate - RouteWithParams.new(named_route, recall, options) - end - end - - class RouteWithParams - def initialize(route, parameterized_parts, params) - @route = route - @parameterized_parts = parameterized_parts - @params = params - end - - def params - params = @params.dup - # So controller and action do not show up in the query string - params.delete(:controller) - params.delete(:action) - params - end - - def path(_) - # Match with Jets router instead of Rails router - matcher = Jets::Router::Matcher.new - route = matcher.find_by_controller_action(@parameterized_parts[:controller], @parameterized_parts[:action]) - if route - path = replace_placeholders(route.path, @parameterized_parts) - path.starts_with?('/') ? path : "/#{path}" - else - # Mimic Rails MissingRoute error - constraints = @params - missing_keys = [] - unmatched_keys = [] - routes = Jets.application.routes - name = nil - ActionDispatch::Journey::Formatter::MissingRoute.new(constraints, missing_keys, unmatched_keys, routes, name).path(name) - end - end - - def replace_placeholders(path, params) - params.each do |key, value| - path = path.gsub(":#{key}", value.to_param) - end - path - end - end - end -end diff --git a/lib/jets/router/dsl.rb b/lib/jets/router/dsl.rb deleted file mode 100644 index c4d6fd88c..000000000 --- a/lib/jets/router/dsl.rb +++ /dev/null @@ -1,164 +0,0 @@ -module Jets::Router - module Dsl - include Mount - - # Methods supported by API Gateway - %w[any delete get head options patch post put].each do |method_name| - define_method method_name do |*args| - options = args.extract_options! - normalize_path_to_controller_map_option!(args, options) - options = options.clone - path_arg = args.first - if path_arg.is_a?(Symbol) - options[:action] = path_arg # Info#action uses - end - options[:path] ||= path_arg - options[:http_method] = __method__ - create_route(options) - end - end - - # Normally first args is a String that is the path - # get "/posts", to: "posts#index" - # But it can also be a Hash that maps the path to the controller/action - # get "/jets/info" => "jets/info#index" - # This logic normalize options to support both cases. - def normalize_path_to_controller_map_option!(args, options) - if !options[:to] && !args.first.is_a?(String) && !args.first.is_a?(Symbol) - map = options.find { |k,v| k.is_a?(String) } - path, to = map[0], map[1] - options[:to] = to - options[:path] = path - options.delete(path) - end - end - - def match(path, options={}) - via = options.delete(:via) || :any - Array(via).each do |http_method| - http_method = :any if via == :all - send http_method, path, options - end - end - - def create_route(options) - one_apigw_method_for_all_routes_warning(options) - route = Route.new(options, @scope) - @routes << route - end - - def constraints(constraints, &block) - scope(from: :constraints, constraints: constraints, &block) - end - - def member(&block) - scope(from: :member, &block) - end - - def collection(&block) - scope(from: :collection, &block) - end - - def defaults(data={}, &block) - scope(from: :defaults, defaults: data, &block) - end - - def path(path, &block) - scope(from: :path, path: path, &block) - end - - def namespace(ns, &block) - scope(from: :namespace, path: ns, module: ns, as: ns, &block) - end - - def shallow(&block) - scope(from: :shallow, &block) - end - - # Examples - # scope :admin - # scope path: :admin - # scope 'admin', as: 'admin' - def scope(*args) - options = args.extract_options! - path = args.first - options[:path] = path.to_s if path - - root_level = @scope.nil? - @scope = root_level ? Scope.new(options) : @scope.new(options) - yield - ensure - @scope = @scope.parent if @scope - end - - # resources macro expands to all the routes - def resources(*args) - options = args.extract_options! - resource_names = args - resource_names.each do |resource_name| - scope(options.merge(from: :resources, resource_name: resource_name)) do - each_resource(resource_name, options) - yield if block_given? - end - end - end - - def resource(*args) - options = args.extract_options! - resource_names = args - resource_names.each do |resource_name| - scope(options.merge(from: :resource, resource_name: resource_name)) do - each_resource(resource_name, options.merge(singular_resource: true)) - yield if block_given? - end - end - end - - # Important: Options as, module, etc are handled by scope and should not be passed to the route - HANDLED_BY_SCOPE = [:as, :module, :path, :shallow].freeze - def each_resource(resource_name, options={}) - HANDLED_BY_SCOPE.each do |opt| - options.delete(opt) - end - o = Resources::Options.new(resource_name, options) - f = Resources::Filter.new(resource_name, options) - - # Looks a little weird with '' but the path is handled by the scope - get '', o.build(:index) if f.yes?(:index) && !options[:singular_resource] - post '', o.build(:create) if f.yes?(:create) - get 'new', o.build(:new) if f.yes?(:new) && !api_mode? - get 'edit', o.build(:edit) if f.yes?(:edit) && !api_mode? - get '', o.build(:show) if f.yes?(:show) - put '', o.build(:update) if f.yes?(:update) - # post to update wont work with singular_resource because it's a route collision - # Also makes it so that route.to is always changed - # Leaving in case need it for some reason. Will remove later. - # post '', o.build(:update) if f.yes?(:update) # for binary uploads - patch '', o.build(:update) if f.yes?(:update) - delete '', o.build(:destroy) if f.yes?(:destroy) - end - - # root "posts#index" - # root to: "posts#index" - def root(*args) - if args.size == 1 - options = args.first - if options.is_a?(String) # root "posts#index" - to = options - options = {} - elsif options.is_a?(Hash) # root to: "posts#index" - to = options.delete(:to) - end - else - to = args[0] - options = args[1] || {} - end - - http_method = options.delete(:via) - http_method ||= Jets.config.api.cors ? :any : :get - default = {path: '/', to: to, http_method: http_method, root: true} - options = default.merge(options) - create_route(options) - end - end -end diff --git a/lib/jets/router/dsl/mount.rb b/lib/jets/router/dsl/mount.rb deleted file mode 100644 index 23d361ec0..000000000 --- a/lib/jets/router/dsl/mount.rb +++ /dev/null @@ -1,47 +0,0 @@ -module Jets::Router::Dsl - module Mount - # support these notations: - # mount Blorgh::Engine, at: "/blog" - # mount sprockets_env => "/assets" - # mount sprockets_env => "/assets", internal: true - def mount(*args) - options = args.extract_options! - - if args.empty? - mount_option = options.find { |k,v| !k.is_a?(String) && !v.is_a?(Symbol) } - mount_class, at = mount_option[0], mount_option[1] - else - mount_class = args.first - at = options[:at] - end - - at = "/#{at}" unless at.starts_with?("/") - mount_class_at(mount_class, options.merge(at: at)) - end - - # The mounted class must be a Rack compatiable class - def mount_class_at(klass, options={}) - if klass.is_a?(Class) && klass < Jets::Engine - mount_engine(klass, options) - else - # Handles mount like Sprockets::Environment.new - at = options.delete(:at) - at = at[1..-1] if at.starts_with?('/') # be more forgiving if / accidentally included - at_wildcard = at.blank? ? "*path" : "#{at}/*path" - options.merge!(to: "jets/mount#call", mount_class: klass) - - any at, options - any at_wildcard, options - end - end - - mattr_accessor :mounted_engines - self.mounted_engines = {} - def mount_engine(klass, options={}) - at = options.delete(:at) - options.merge!(engine: klass, path: at) - create_route(options) - @@mounted_engines[at] = klass - end - end -end diff --git a/lib/jets/router/engine_mount.rb b/lib/jets/router/engine_mount.rb deleted file mode 100644 index a27e17a2d..000000000 --- a/lib/jets/router/engine_mount.rb +++ /dev/null @@ -1,36 +0,0 @@ -module Jets::Router - class EngineMount - def self.find_by(request_path: nil, engine: nil) - new.find_by(request_path: request_path, engine: engine) - end - - def find_by(request_path: nil, engine: nil) - mount = if request_path - find_by_request_path(request_path) - elsif engine - find_by_engine(engine) - end - - return unless mount - - OpenStruct.new(at: mount[0], engine: mount[1]) - end - - def find_by_request_path(request_path) - mounted_engines.find do |at, engine| - request_path.starts_with?(at) - end - end - - def find_by_engine(engine) - mounted = mounted_engines.find do |at, mounted_engine| - mounted_engine == engine - end - end - - # Example mounted_engines: {"/blog"=>Blorgh::Engine} - def mounted_engines - Jets::Router::Dsl::Mount.mounted_engines - end - end -end diff --git a/lib/jets/router/help.rb b/lib/jets/router/help.rb deleted file mode 100644 index 753b84139..000000000 --- a/lib/jets/router/help.rb +++ /dev/null @@ -1,130 +0,0 @@ -require 'cli-format' - -module Jets::Router - class Help - extend Memoist - - def initialize(options={}) - @options = options - @engines = {} - end - - def all_routes - Jets::Router.routes - end - - def routes - routes = filter(all_routes) - collect_engine_routes(routes) - if ENV['JETS_ROUTES_INTERNAL'] - routes = routes.select { |route| route.internal } - else - routes = routes.reject { |route| route.internal } - end - routes - end - - def collect_engine_routes(routes) - routes.each do |route| - collect_engine_routes_per(route) if route.engine? - end - end - - def collect_engine_routes_per(route) - name = route.endpoint - return unless route.engine? - return if @engines[name] - - route_set = route.rack_app.route_set - if route_set.is_a?(Jets::Router::RouteSet) - @engines[name] = route_set.routes - end - end - - def filter(routes) - if @options[:controller] - routes.select do |route| - route.controller.include?(@options[:controller]) - end - elsif @options[:grep] - grep_pattern = Regexp.new(@options[:grep], 'i') - proc = filter_proc(grep_pattern) - routes.select(&proc) - elsif @options[:reject] - reject_pattern = Regexp.new(@options[:reject], 'i') - proc = filter_proc(reject_pattern) - routes.reject(&proc) - else - routes - end - end - - def filter_proc(pattern) - Proc.new do |route| - route.as =~ pattern || - route.http_method =~ pattern || - route.path =~ pattern || - route.controller =~ pattern || - route.mount_class_name =~ pattern - end - end - - def header - header = ["As (Prefix)", "Verb", "Path (URI Pattern)", "Controller#action"] - # any_mount = routes.any?(&:mount_class_name) - # header << "Mount" if any_mount - header - end - - def any_mount? - routes.any?(&:mount_class_name) - end - - def print - puts text - end - - def text - text = presenter_text("Routes for app:", routes) - @engines.each do |name, routes| - text += presenter_text("Routes for #{name}:", routes) - end - text - end - - def presenter_text(summary_line, routes) - text = format_with_newlines(summary_line) - if routes.empty? - text += "The routes table is empty.\n" - return text - end - - presenter = CliFormat::Presenter.new(@options) - presenter.header = header unless @options[:header] == false - routes.each do |route| - row = [route.as, route.http_method, route.path, route.to] - # row << route.mount_class_name if any_mount? - presenter.rows << row - end - text += presenter.text.to_s # => String - text += "\n" unless text.ends_with?("\n") - text += "\n" if @options[:format] == "space" # add another newline for space format - text - end - - def format_with_newlines(line) - return '' if @options[:header] == false - - case @options[:format] - when "markdown" - # need leading newline for routes_table jets prints when route not found - # Unsure why it needs to be here and not at the routes_table method. - "\n#{line}\n\n" - when "space" - "#{line}\n\n" - else - "#{line}\n" - end - end - end -end diff --git a/lib/jets/router/helpers/named_routes/add_full_url.rb b/lib/jets/router/helpers/named_routes/add_full_url.rb deleted file mode 100644 index 4b3a89791..000000000 --- a/lib/jets/router/helpers/named_routes/add_full_url.rb +++ /dev/null @@ -1,13 +0,0 @@ -module Jets::Router::Helpers::NamedRoutes - module AddFullUrl - # Named add_full_url to avoid mental conflicts with Jets full_url_for - # Leverage Rails URL.url_for to avoid duplicating the logic - # Rails sets url_strategy to: - # => ActionDispatch::Routing::RouteSet::UNKNOWN - # => ActionDispatch::Http::URL.url_for(options) - def add_full_url(options) - options = url_options.merge(options) - ActionDispatch::Http::URL.url_for(options) - end - end -end diff --git a/lib/jets/router/helpers/named_routes/generated.rb b/lib/jets/router/helpers/named_routes/generated.rb deleted file mode 100644 index ac3dfe919..000000000 --- a/lib/jets/router/helpers/named_routes/generated.rb +++ /dev/null @@ -1,197 +0,0 @@ -require 'active_support/core_ext/module/remove_method' - -module Jets::Router::Helpers::NamedRoutes - module Generated - def create_helper_module(route_set) - module_name = helper_module_name(route_set) - module_name.constantize rescue create_helper_module!(module_name, route_set) - end - - def helper_module_name(route_set) - engine_class = route_set.engine.class - name = engine_class == Jets.app_class ? "main_app" : engine_class.engine_name - name = name.gsub('::','') - name = "#{name.camelize}Helpers" # MainAppHelpers or BlorghHelpers - "Jets::Router::Helpers::NamedRoutes::Generated::#{name}" - end - - def create_helper_module!(module_name, route_set) - mod = Module.new do - extend ActiveSupport::Concern # Needed for _url_for_modules to work since or UrlHelper ClassMethods not included - mattr_accessor :path_helpers, default: Set.new - mattr_accessor :url_helpers, default: Set.new - - include AddFullUrl - - # For compatibility with Rails url_helpers - include ActionDispatch::Routing::UrlFor - define_singleton_method(:_routes) { route_set } # needed for controller class context - define_method(:_routes) { route_set } # needed for view context - # Jets convenience method - define_singleton_method(:route_set) { route_set } - - define_singleton_method(:_url_for_modules) { ActionView::RoutingUrlFor } - define_method(:_url_for_modules) { ActionView::RoutingUrlFor } - - # Mimic Rails behavior - # Precdence: - # 1. config.action_mailer.default_url_options - # 2. Jets.application.default_url_options - def default_url_options - self.class.default_url_options.empty? ? Jets.application.default_url_options : self.class.default_url_options - end - - def add_methods! - route_set.routes.each do |route| - next unless route.as - next if route.engine? - - name = route.as - path_name = "#{name}_path" - url_name = "#{name}_url" - engine_class = route_set.engine.class - - # Normally, self is or View context instance - define_method(path_name) do |*args| - NamedRouteMethod.new(route, args, self, engine_class).path - end - self.path_helpers.add(path_name) - - define_method(url_name) do |*args| - path = send(path_name, *args) - options = args.extract_options! - add_full_url(options.merge(path: path)) - end - self.url_helpers.add(url_name) - end - end - - def remove_methods! - remove = Proc.new { |method| remove_method(method) } - path_helpers.each(&:remove) - url_helpers.each(&:remove) - self.path_helpers.clear - self.url_helpers.clear - end - - # Wonder if extend self is may idea since it can allow context to be module itself - # instead of the controller or view instance. However, it allows - # Jets.application.routes.url_helpers.posts_path style calls in jets console - extend self - - add_methods! - end - - # Name it instead of using anonymous module to help debugging - # IE: Jets::Router::Helpers::NamedRoutes::Generated::MainAppHelpers - basename = module_name.split('::').last.to_sym - Jets::Router::Helpers::NamedRoutes::Generated.const_set(basename, mod) - - define_mounted_helper(mod, route_set) - - mod - end - - def define_mounted_helper(mod, route_set) - name = mod.name.split('::').last.sub('Helpers','').underscore.to_sym - return if MountedHelpers.method_defined?(name) - - helpers_module = route_set.url_helpers - - MountedHelpers.class_eval do - define_method "_#{name}" do - Jets::Router::Helpers::NamedRoutes::Proxy.new(self, helpers_module) - end - end - - MountedHelpers.class_eval(<<-RUBY, __FILE__, __LINE__ + 1) - def #{name} - @_#{name} ||= _#{name} - end - RUBY - end - - module MountedHelpers - extend ActiveSupport::Concern - end - - # Contains all the mounted helpers across different - # engines and the `main_app` helper for the application. - # You can include this in your classes if you want to - # access routes for other engines. - def mounted_helpers - MountedHelpers - end - - extend self - end - - class NamedRouteMethod - include Jets::Controller::Decorate::ApigwStage - - attr_reader :options - def initialize(route, args, context, engine_class) - @route, @args, @context, @engine_class = route, args, context, engine_class - @path = route.path # With placeholders still IE: /posts/:id - @options = normalize_options(@path, args) - end - - # Normalized with format in options - # Helps to mimic Rails behavior. - # Processes extra arguments to support format and query string. - # - # post_path(post, "json", {foo: 'bar'}) => "/posts/1.json?foo=bar" - # post_path(post, {foo: 'bar'}) => "/posts/1?foo=bar" - # - def normalize_options(path, args) - args = args.dup - options = args.extract_options! - # If there is still an argument in rest, it's the format. Rails behavior. - rest = args[placeholders.size..-1] - format = rest.last - options[:format] = format if format - options - end - - def path - query_string = @options.dup # make copy to modify - format = query_string.delete(:format) - query_string.delete(:action) - query_string.delete(:controller) - query_string.delete(:only_path) - - path = path_with_replaced_placeholders - path = prepend_engine_mounted_at(path) - path = "#{path}.#{format}" if format - path = "#{path}?#{query_string.to_query}" unless query_string.empty? - path = add_apigw_stage(path) - path - end - - # Needed for add_apigw_stage - delegate :event, :request, to: :@context - - # IE: /posts/1 - def path_with_replaced_placeholders - path = @path - placeholders.each_with_index do |placeholder, index| - path = path.gsub(placeholder, @args[index].to_param) - end - path - end - - def placeholders - @path.scan(/:\w+/) - end - - def prepend_engine_mounted_at(path) - mount = Jets::Router::EngineMount.find_by(engine: @engine_class) - if mount - path = path.delete_prefix('/') - [mount.at, path].compact.join('/') - else - path - end - end - end -end \ No newline at end of file diff --git a/lib/jets/router/helpers/named_routes/proxy.rb b/lib/jets/router/helpers/named_routes/proxy.rb deleted file mode 100644 index a3b8fbde8..000000000 --- a/lib/jets/router/helpers/named_routes/proxy.rb +++ /dev/null @@ -1,66 +0,0 @@ -module Jets::Router::Helpers::NamedRoutes - class Proxy - include AddFullUrl - - def initialize(view_context, helpers_module) - @helpers = helpers_class(helpers_module).new(view_context) - end - - # * The helpers_module is passed as a argument to give the anonymous class - # access via closure at include time. - # * view_context is passed at initialize to provide access at instantiation time. - def helpers_class(helpers_module) - Class.new do - include helpers_module # IE: Generated::MainAppHelpers - def initialize(view_context) - @view_context = view_context - end - - # Important: - # - # * The add_full_url method needs url_options. - # Without url_options the proxy methods are unavailable and creates - # an infinite loop with method_missing. - # * request is needed for add_apigw_stage? to check request correctly - # Otherwise, gems like kingsman, that use the named routes proxy methods, - # won't get able to get the correct url with the stage name prepended. - # - # The request is the main reason an anonymous class is used since the - # view_context has included modules like ApigwStage that assume a - # request method is available. - delegate :url_options, :request, :event, to: :@view_context - end - end - - # No need to check respond_to? and call raise NoMethodError again because anonymous - # class will already return a undefined method error. - # - # It's annoying that we need to add_full_url here because - # the proxy calls the url methods with a module view_context instead - # of the controller or view_context instance. So we don't have access to - # the request and url_options. The url_options are only available in the controller - # or view_context instance. - # - # So we call the path method instead and then add_full_url here. - # Not calling the url method directly because could add the full url twice, - # when the user is setting the default_url_options. - # Also calling the url method would cause an error because request and url_options - # is not available. - # - # The logic is a bit convoluted but it works. Rails does something similar - # with their RoutesProxy class and view_context.url_options. - def method_missing(method_name, *args) - path_name = method_name.to_s.sub(/_url$/, '_path').to_sym - path = @helpers.send(path_name, *args) - if method_name.to_s.end_with?('_url') - options = args.extract_options! - options.merge!(path: path) - options = url_options.merge(options) - add_full_url(options) - else - path - end - end - end -end - diff --git a/lib/jets/router/matcher.rb b/lib/jets/router/matcher.rb deleted file mode 100644 index 59db9d74b..000000000 --- a/lib/jets/router/matcher.rb +++ /dev/null @@ -1,204 +0,0 @@ -module Jets::Router - class Matcher - extend Memoist - - attr_reader :routes - def initialize(route_set=Jets.application.routes) - @route_set = route_set - @routes = route_set.ordered_routes - end - - def request_path - # Ensure leading slash - # Be more forgiving and allow the request_path to be passed in without a leading slash. - # Note: request PATH_INFO will always have a leading slash, but just in case. - # This is also covered in specs. - return unless @request_path - @request_path.starts_with?('/') ? @request_path : "/#{@request_path}" - end - - def request_method - @request_method.to_s.upcase if @request_method - end - - # Precedence: - # 1. Routes with no captures get highest precedence: posts/new - # 2. Then we consider the routes with captures: post/:id - # - # Within these 2 groups we consider the routes with the longest path first - # since posts/:id and posts/:id/edit can both match. - def find_by_request(request, request_method=nil) - @request = request # @request is used to check constraints - # Checking the request_method_from_hidden_method_field so that handler can find the right route - # super early in the process. Otherwise lambda routes to the wrong controller action. - @request_method = request_method || @request.request_method_from_hidden_method_field || @request.request_method.to_s.upcase - @request_path = strip_format(@request.path) - route = find_route - match_constraints(route) if route - end - - # Simpler version of find_by_request that does not check constraints. - # Used by Jets::Controller::Middleware::Mimic and called super-early on. - # Does not have access to @request object and path_params - def find_by_env(env) - @env = env - @request_method = env["REQUEST_METHOD"] || "GET" - @request_path = strip_format(env["PATH_INFO"]) - find_route - end - - def find_by_controller_action(controller, action) - controller = "#{controller.camelize}Controller" - routes.find do |r| - r.controller_name == controller.to_s && - r.action_name == action.to_s - end - end - - def find_route - routes.each do |r| - if r.engine - route = find_engine_route(r) - return route if route - else - found = match?(r) - return r if found - end - end - nil - end - - def find_engine_route(route) - return unless mount - - engine_matcher = self.class.new(route.engine.route_set) - engine_route = if @request - engine_matcher.find_by_request(@request, @request_method) - else - engine_matcher.find_by_env(@env) - end - # save original engine for route.extract_parameters later - engine_route.original_engine = route.engine if engine_route - engine_route - end - - def mount - EngineMount.find_by(request_path: request_path) - end - memoize :mount - - def strip_format(path) - path.sub(/\..+$/, '') # Remove format from the end of the path - end - - def match?(route) - # Immediately stop checking when the request http_method: GET, POST, ANY, etc - # doesnt match. - return false if request_method != route.http_method && route.http_method != "ANY" - - route_path = strip_format(route.path) - route_path = "#{mount.at}#{route_path}" if mount - - if request_path == route_path - # regular string match detection - return true # exact route matches are highest precedence - end - - # Check path for route capture and wildcard matches: - # A colon (:) means the variable has a variable - if route_path.include?(':') # 2nd highest precedence - capture_detection(route_path, request_path) # early return true or false - # A star (*) means the variable has a glob - elsif route_path.include?('*') # lowest precedence - proxy_detection(route_path, request_path) # early return true or false - else - false # reach here, means no route matched - end - end - - def match_constraints(route) - return unless route - constraints_matches?(route) ? route : nil - end - - # Matcher called in mimic.rb before mimic event is available - # We need to extract the parameters from the possible matching route directly - # since event is unavailable. - def constraints_matches?(route) - # Matcher is used super-early when request not yet available. - # Cannot check constraints because we dont have the request object. - # To build a request object, would need to build a mimic event and it's not yet available. - return true if @request.nil? - - constraints = route.constraints - return true if constraints.blank? - - if constraints.is_a?(Hash) - parameters = route.extract_parameters(request_path) # extract directly - constraints.any? do |key, value| - key = key.to_s - if value.is_a?(Regexp) - value.match(parameters[key]) - else # String - if parameters[key] - value == parameters[key] - elsif @request.respond_to?(key) - value == @request.send(key) - end - end - end - elsif constraints.respond_to?(:call) - constraints.call(@request) - elsif constraints.respond_to?(:matches?) - constraints.matches?(@request) - end - end - - private - - # catchall/globbing/wildcard/proxy routes. Examples: - # - # get "files/*path", to: "files#show" - # get "others/*rest", to: "others#show" - # get "*catchall", to: "public_files#show" # last catchall route for Jets - # - def proxy_detection(route_path, request_path) - # drop the proxy_segment - leading_path = route_path.split('/')[0..-2].join('/') - - # get "*catchall", to: "public_files#show" - if leading_path.empty? # This is the last catchall route "*catchall" - return true # always return true here because the entire path - # will always match - end - - # Other types of wildcard route: - # - # get "files/*path", to: "files#show" - # get "others/*rest", to: "others#show" - unless leading_path.ends_with?('/') - # Ensure trailing slash to make pattern matching stricter - leading_path = "#{leading_path}/" - end - - pattern = "^#{leading_path}" - regexp = Regexp.new(pattern) - - !!regexp.match(request_path) # could be true or false - end - - def capture_detection(route_path, request_path) - # changes path to a string used for a regexp - # posts/:id/edit => posts\/(.*)\/edit - regexp_string = route_path.split('/').map do |s| - s.include?(':') ? Jets::Router::Route::CAPTURE_REGEX : s - end.join('\/') - # make sure beginning and end of the string matches - regexp_string = "^#{regexp_string}$" - - regexp = Regexp.new(regexp_string) - - !!regexp.match(request_path) # could be true or false - end - end -end diff --git a/lib/jets/router/resources/base.rb b/lib/jets/router/resources/base.rb deleted file mode 100644 index 4215864f8..000000000 --- a/lib/jets/router/resources/base.rb +++ /dev/null @@ -1,7 +0,0 @@ -module Jets::Router::Resources - class Base - def initialize(name, options) - @name, @options = name, options - end - end -end diff --git a/lib/jets/router/resources/filter.rb b/lib/jets/router/resources/filter.rb deleted file mode 100644 index 65dadd364..000000000 --- a/lib/jets/router/resources/filter.rb +++ /dev/null @@ -1,15 +0,0 @@ -module Jets::Router::Resources - class Filter < Base - def yes?(action) - return true unless @options[:only] || @options[:except] - - if @options[:only] - only = [@options[:only]].flatten.map(&:to_s) - only.include?(action.to_s) - else # except - except = [@options[:except]].flatten.map(&:to_s) - !except.include?(action.to_s) - end - end - end -end diff --git a/lib/jets/router/resources/options.rb b/lib/jets/router/resources/options.rb deleted file mode 100644 index 7fc16d7ec..000000000 --- a/lib/jets/router/resources/options.rb +++ /dev/null @@ -1,15 +0,0 @@ -module Jets::Router::Resources - class Options < Base - def build(action) - # Important to create a copy of the options since we are mutating it - # The original options are used for resources and resource scope - options = @options.dup - # Remove special options from getting to create_route. For some reason .slice! doesnt work - options.delete(:only) - options.delete(:except) - controller = options[:singular_resource] ? @name.to_s.pluralize : @name - options[:to] = "#{controller}##{action}" - options - end - end -end diff --git a/lib/jets/router/route.rb b/lib/jets/router/route.rb deleted file mode 100644 index 69a0c4066..000000000 --- a/lib/jets/router/route.rb +++ /dev/null @@ -1,249 +0,0 @@ -# route = Jets::Router::Route.new( -# path: "posts", -# http_method: :get, -# to: "posts#index", -# ) -module Jets::Router - class Route - extend Memoist - include Compat - include AfterInitialize - include As - include Authorizer - include Path - include Util - - CAPTURE_REGEX = "([^/]*)" # as string - - attr_reader :options, :scope, :info, :defaults - attr_accessor :original_engine - def initialize(options, scope=Scope.new) - @options = options - @scope = scope - @info = Info.new(@options, @scope) # @info.action and @info.controller - after_initialize - @path_names = {} - end - - def to - engine || "#{@info.controller}##{@info.action}" # IE: posts#index - end - - def engine - @options[:engine] - end - alias rack_app engine - - def engine? - !!engine - end - - def endpoint - engine.to_s if engine - end - - def resolved_defaults - defaults = @options[:defaults] || {} - @scope.resolved_defaults.merge(defaults) - end - - def http_method - @options[:http_method].to_s.upcase - end - - def constraints - @options[:constraints] || @scope.resolved_constraints - end - - def internal? - !!@options[:internal] - end - - def homepage? - path == '/' - end - - # IE: PostsController - # IE: index - delegate :action, :controller, :is_collection?, :is_member?, to: :@info - alias action_name action - - # IE: PostsController - # Different from @info.action - def controller_name - "#{controller.camelize}Controller" if controller - end - - # Checks to see if the corresponding controller exists. Useful to validate routes - # before deploying to CloudFormation and then rolling back. - def valid? - controller_class = begin - controller_name.constantize - rescue NameError => e - return false - end - controller_class.lambda_functions.include?(action_name.to_sym) - end - - # For Jets.config.cfn.build.routes == "one_apigw_method_for_all_routes" - # Need to build the pathParameters for the API Gateway event. - def rebuild_path_parameters(event) - extracted = extract_parameters(event["path"]) - if extracted - params = event["pathParameters"] || {} - params.merge(extracted) - else - event["pathParameters"] # pass through - end - end - - # Extracts the path parameters from the actual path - # Only supports extracting 1 parameter. So: - # - # request_path: posts/tung/edit - # route.path: posts/:id/edit - # - # Returns: - # { id: "tung" } - def extract_parameters(request_path) - request_path = "/#{request_path}" unless request_path.starts_with?('/') # be more forgiving if / accidentally not included - request_path = remove_engine_mount_at_path(request_path) - if path.include?(':') - extract_parameters_capture(request_path) - elsif path.include?('*') - extract_parameters_proxy(request_path) - else - # Lambda AWS_PROXY sets null to the input request when there are no path parmeters - nil - end - end - - def remove_engine_mount_at_path(request_path) - return request_path unless original_engine - - mount = Jets::Router::EngineMount.find_by(engine: original_engine) - return request_path unless mount - - request_path.sub(mount.at, '') - end - - def extract_parameters_proxy(request_path) - # changes path to a string used for a regexp - # others/*proxy => others\/(.*) - # nested/others/*proxy => nested/others\/(.*) - if path.include?('/') - leading_path = path.split('/')[0..-2].join('/') # drop last segment - # leading_path: nested/others - # capture everything after the leading_path as the value - regexp = Regexp.new("#{leading_path}/(.*)") - value = request_path.match(regexp)[1] - else - value = request_path - end - - # the last segment without the '*' is the key - proxy_segment = path.split('/').last # last segment is the proxy segment - # proxy_segment: *proxy - key = proxy_segment.sub('*','') - - { key => value } - end - - def extract_parameters_capture(request_path) - # changes path to a string used for a regexp - # posts/:id/edit => posts\/(.*)\/edit - labels = [] - regexp_string = path.split('/').map do |s| - if s.start_with?(':') - labels << s.delete_prefix(':') - CAPTURE_REGEX - else - s - end - end.join('\/') - # make sure beginning and end of the string matches - regexp_string = "^#{regexp_string}$" - regexp = Regexp.new(regexp_string) - - values = regexp.match(request_path).captures - labels.map do |next_label| - [next_label, values.delete_at(0)] - end.to_h - end - - # Prevents infinite loop when calling route.to_json for state.save("routes", ...) - def as_json(options= nil) - data = { - path: path, - http_method: http_method, - to: to, - } - data[:engine] = engine if engine - data[:internal] = internal if internal - data - end - - # To keep "self #{self}" more concise and helpful - # Use "self #{self.inspect}" more verbose info - def to_s - "#" - end - - # Old notes: - # For Grape apps, calling ActiveSupport to_json on a Grape class used to cause an infinite loop. - # Believe Grape fixed this issue. A GrapeApp.to_json now produces a string. - # No longer need to coerce to a string and back to a class. - # - # This is important because Sprocket::Environment.new cannot be coerced into a string or mounting wont work. - # This is used in sprockets-jets/lib/sprockets/jets/engine.rb - # - # Related PR: smarter apigw routes paging calculation #635 - # https://github.com/boltops-tools/jets/pull/635 - # Debugging notes: https://gist.github.com/tongueroo/c9baa7e98d5ad68bbdd770fde4651963 - def mount_class - @options[:mount_class] - end - - # For jets routes help table of routes - def mount_class_name - return unless mount_class - mount_class.class == Class ? mount_class : "#{mount_class.class}.new" - end - - private - def ensure_jets_format(path) - path.split('/').map do |s| - if s =~ /^\{/ and s =~ /\+\}$/ - s.sub(/^\{/, '*').sub(/\+\}$/,'') # {proxy+} => *proxy - elsif s =~ /^\{/ and s =~ /\}$/ - s.sub('{',':').sub(/\}$/,'') # {id} => :id - else - s - end - end.join('/') - end - - def api_gateway_format(path) - path.split('/') - .map {|s| transform_capture(s) } - .map {|s| transform_proxy(s) } - .join('/') - end - - def transform_capture(text) - if text.starts_with?(':') - text = text.sub(':','') - text = "{#{text}}" - end - text - end - - def transform_proxy(text) - if text.starts_with?('*') - text = text.sub('*','') - text = "{#{text}+}" - end - text - end - end -end diff --git a/lib/jets/router/route/after_initialize.rb b/lib/jets/router/route/after_initialize.rb deleted file mode 100644 index c15b778cc..000000000 --- a/lib/jets/router/route/after_initialize.rb +++ /dev/null @@ -1,64 +0,0 @@ -class Jets::Router::Route - module AfterInitialize - def after_initialize - normalize_to_option! - normalize_path_option! - normalize_id_constraint! - check_on_option! - end - - # Possibly infer to option from the path. Example: - # - # get 'posts/index' - # get 'posts', to: 'posts#index' - # - # get 'posts/show' - # get 'posts', to: 'posts#show' - # - def normalize_to_option! - return if @options[:to] - - path = @options[:path].to_s - return unless path.include?('/') - - path = path.delete_prefix('/') # remove leading slash - items = path.split('/') - if items.size == 2 - @options[:to] = items.join('#') - end - end - - def normalize_path_option! - path = @options[:path] - resource_name = @scope.resource_name - if path.is_a?(Symbol) and resource_name # Treat as controller action - # Only supported with used within scope, resource, resources block - action = path # IE: get :list => action = :list - controller = @scope.virtual_controller - # infer and set to option if not set - @options[:to] ||= "#{controller}##{action}" - end - - # override - @options.merge!(path: escape_path(path)) if path - end - - def normalize_id_constraint! - if @options[:id] && !@options[:constraints] - @options[:constraints] = { id: @options[:id] } - end - end - - def check_on_option! - if @options[:on] && !%w[resources resource].include?(@scope.from.to_s) - raise Jets::Router::Error.new("ERROR: The `on:` option can only be used within a resource or resources block") - end - end - - def escape_path(path) - path.to_s.split('/').map { |s| s =~ /\A[:\*]/ ? s : CGI.escape(s) }.join('/') - path = "/#{path}" unless path.starts_with?('/') - path - end - end -end diff --git a/lib/jets/router/route/as.rb b/lib/jets/router/route/as.rb deleted file mode 100644 index b7dee379d..000000000 --- a/lib/jets/router/route/as.rb +++ /dev/null @@ -1,93 +0,0 @@ -class Jets::Router::Route - module As - def as - return nil if @options[:as] == :disabled - return @options[:engine].engine_name if @options[:engine] - return unless @options[:root] || [:get, :post, :patch, :put, :delete].include?(@options[:http_method]) - - as = scoped_as - as = add_underscore_path(as) - as = add_action(as) - as = add_root(as) - as - end - - def scoped_as - list = [] - @scope.from_top.each do |scope| - node = Node.new(self, scope) - - as = node.resolved_as - as = as.to_s.singularize if node.has_param? - list << as if as && node.append_as? - end - list.reject!(&:blank?) # IE: list [:posts, :comments] - list.unshift(@options[:as]) if @options[:as] - list.join('_').gsub('/','_') unless list.empty? - end - - # IE: posts/:id/edit => posts - # Using this convention so that regular routes under namespace and path - # can be reassemble as if they were under resources - def add_underscore_path(as) - if as.nil? && @scope.needs_controller_path? # only consider leaf scope - @options[:as] || underscore_path_before_param - else - as - end - end - - # IE: posts/:id/edit => posts - # Using this convention so that regular routes under namespace and path - # can be reassemble as if they were under resources - def underscore_path_before_param - action_suffixes = %w[new edit] # edit in case of singular resource - parts = [] - path = @options[:path].to_s - return nil if path.include?('*') || !path.ascii_only? # IE: get '*catchall', to: 'public_files#show' - - path = path.delete_prefix('/') - path.split('/').each do |part| - if part.starts_with?(':') || part.starts_with?('*') || action_suffixes.include?(part) - break - end - parts << part - end - parts.map! { |p| p.gsub(/[^a-zA-Z0-9]/,'_') } - path = parts.join('_').squeeze('_') - if action_suffixes.include?(@info.action) - path.singularize - else - path - end - end - - def add_action(resource) - resource = resource.to_s - return resource if resource.blank? - - as_name = @options[:as] || @info.action - as_name = as_name.to_s.delete_prefix('/') if as_name - - if %w[new edit].include?(@info.action) && @options[:as].nil? - "#{as_name}_#{resource.singularize}" # IE: new_post - elsif %[index create].include?(@info.action) - resource # IE: posts - elsif %w[show edit update destroy].include?(as_name) - "#{resource.singularize}" # IE: post - elsif is_collection?(@scope) && @options[:as].nil? - "#{as_name}_#{resource}" # IE: list_post - elsif (is_member?(@scope) || @scope.from == :resource) && @options[:as].nil? - "#{as_name}_#{resource.singularize}" # IE: rate_post - elsif @scope.resource_name && @options[:as].nil? - "#{resource.singularize}_#{as_name}" # IE: post_list - else - resource # IE: post - end - end - - def add_root(as) - @options[:root] ? 'root' : as - end - end -end diff --git a/lib/jets/router/route/authorizer.rb b/lib/jets/router/route/authorizer.rb deleted file mode 100644 index 8995a8f56..000000000 --- a/lib/jets/router/route/authorizer.rb +++ /dev/null @@ -1,58 +0,0 @@ -class Jets::Router::Route - module Authorizer - # IE: main#protect => MainProtectAuthorizer - def authorizer_id(prefix_class: true) - return unless authorizer - logical_id(authorizer, prefix_class: prefix_class) - end - - # Metadata about the authorizer class that can be used later. Stored in the Authorizer template parameters. - # In app_class.rb `def controller_params` it is used to build the input parameters for controller templates. - def authorizer_metadata - metadata(authorizer) - end - - def authorizer - @options[:authorizer] - end - - def authorization_scopes - @options[:authorization_scopes] - end - - def authorization_type - @options[:authorization_type] || inferred_authorization_type - end - - def api_key_required - @options[:api_key_required] - end - - module ModuleMethods - def logical_id(authorizer, prefix_class: true) - klass, meth = authorizer.split("#") - klass += "_authorizer" - words = [meth, "authorizer"] - words.unshift(klass) if prefix_class - words.join('_').camelize # logical_id - end - - def metadata(authorizer) - klass = authorizer.split("#").first - authorizer_class = "#{klass}_authorizer".camelize - logical_id = logical_id(authorizer, prefix_class: false) - # IE: MainAuthorizer.ProtectAuthorizer - "#{authorizer_class}.#{logical_id}" - end - end - include ModuleMethods # so available as instance methods - extend ModuleMethods # so also available as module method. IE: Jets::Router::Route::Authorizer.metadata(auth_to) - - private - def inferred_authorization_type - return unless authorizer - Jets::Authorizer::Base.authorization_type(authorizer) - end - - end -end diff --git a/lib/jets/router/route/compat.rb b/lib/jets/router/route/compat.rb deleted file mode 100644 index 35a070a1d..000000000 --- a/lib/jets/router/route/compat.rb +++ /dev/null @@ -1,45 +0,0 @@ -class Jets::Router::Route - # Work in progress. Considering making Jets Route more compatiable with Rails Route. - # Then will be able to leverage Rails RouteWrapper for presenting the routes for /jets/info/routes. - # Ran into Route#path method that is a bit of work to make compatiable. - # Leaving this here for the future. - module Compat - extend Memoist - - # Interface based on Rails ActionDispatch::Routing::Endpoint - class App # :nodoc: - def initialize(route) - @route = route - end - - def dispatcher?; false; end - def redirect?; false; end - def matches?(req); true; end - def app; self; end - def rack_app; app; end - - def engine? - @route.engine? - end - end - - def app - App.new(self) - end - memoize :app - - # Rails interface method for ActionDispatch::Routing::RouteWrapper - def name - as || '' - end - - # Rails interface method for ActionDispatch::Routing::RouteWrapper - def verb - http_method - end - - def internal - !!@options[:internal] - end - end -end \ No newline at end of file diff --git a/lib/jets/router/route/info.rb b/lib/jets/router/route/info.rb deleted file mode 100644 index db639551c..000000000 --- a/lib/jets/router/route/info.rb +++ /dev/null @@ -1,74 +0,0 @@ -class Jets::Router::Route - # Organize to.controller and to.action into a class - # so don't have to keep repeating the logic. - class Info - def initialize(options, scope) - @options, @scope = options, scope - end - - # IE: posts - def controller - return if @options[:engine] # IE: { engine: Blorgh::Engine, path: "/blog" } - - controller = if @options[:controller] - @options[:controller] - elsif @options[:to] - @options[:to].split('#').first # IE: posts#index => posts - elsif @scope.virtual_controller - @scope.virtual_controller - else # {"/jets/info/properties"=>"jets/info#properties"} - map = @options.find { |k,v| k.is_a?(String) } - path, to = map[0], map[1] - to.split('#').first - end - - if controller.starts_with?('/') - # absolute controller path specified. use without scope adjustments - return controller.delete_prefix('/') # remove leading slash - end - - # no controller found yet, imply from the scope - segments = scoped_module_segments - segments << controller if controller - segments.compact.join('/') # add module - end - - # IE: index - def action - if @options.key?(:action) - @options[:action].to_s - elsif is_collection?(@scope) || is_member?(@scope) - @options[:path].to_s.delete_prefix('/') # action - elsif @options[:on] && @options[:on] != :member && @options[:on] != :collection - @options[:on].to_s - elsif @options[:to] - @options[:to].split('#').last - else # {"/"=>"jets/welcome#index", :internal=>true} - map = @options.find { |k,v| k.is_a?(String) } - path, to = map[0], map[1] - to.split('#').last - end - end - - def scoped_module_segments - segments = [] - @scope.from_top.each do |scope| - segments << scope.module if scope.module - if @scope == scope # last scope node - last_segment = @options[:module] - segments << last_segment if last_segment - end - end - segments - end - - def is_collection?(scope) - scope.from == :collection || @options[:on] == :collection - end - - def is_member?(scope) - scope.from == :member || @options[:on] == :member - end - end -end - diff --git a/lib/jets/router/route/node.rb b/lib/jets/router/route/node.rb deleted file mode 100644 index 37e5f37dc..000000000 --- a/lib/jets/router/route/node.rb +++ /dev/null @@ -1,137 +0,0 @@ -class Jets::Router::Route - # The Node class groups logic that requires both of route and scope info. - # It does not really belong in either the Route or Scope class and - # requires both to do its job. So it gets its own class. - # This seems to help keep the Route and Scope classes simpler. - # - # An instance of this class is made available with each - # iteration of loop that processes the DSL scopes. - class Node - attr_reader :route, :scope, :leaf_scope - def initialize(route, scope) - @route, @scope = route, scope # scope is current scope of iteration - @info = route.info - @options = route.options - @leaf_scope = @route.scope # the route scope is the leaf scope - end - - def append_path? - has_path? && add_path_segment? - end - - def append_param? - has_param? && add_path_segment? - end - - def append_as? - has_path? && add_path_segment? || - scope.from.nil? && scope.as - end - - # Accounts for shallow scope. Example: - # - # resources :posts, shallow: true do - # resources :comments do - # resources :likes - # end - # end - # - # add_path_segment? = add_path_segment_considering_shallow? - # - def add_path_segment? - if param_action? && shallow_param_action? - leaf? # only add path segments for leaf - elsif paramless_action? && shallow_paramless_action? - leaf? || leaf_scope.real_parent?(scope) - else - true # default is to always path preceding path segments - end - end - - def shallow_param_action? - lookahead_shallow? || leaf_scope.any_parent_shallow? - end - - def shallow_paramless_action? - lookahead_shallow?(parent: true) || scope.any_parent_shallow? - end - - def lookahead_shallow?(parent: false) - next_scope = scope - while next_scope - check_scope = parent ? next_scope.parent : next_scope - # Important to check next_scope == leaf_scope - # Otherwise it'll collapse path segments for higher scopes also. - return true if check_scope.shallow? && next_scope == leaf_scope - next_scope = next_scope.next - end - false - end - - def has_path? - scope.resolved_path && !scope.virtual? - end - - def has_param? - return false if scope.root? - - resolved_param && param_action? || - scope.resource_descendent? && !leaf? || - scope.resource_sibling? && !leaf? - end - - def resolved_as - as = scope.as || scope.resource_name - # as = scope.as || scope.resource_name - if scope.resource_name - controller = nil # dont need controller. can infer from resources, namespace, etc - else # direct route method without scope or module only etc - controller = @info.controller.split('/').last if leaf? #&& as.nil? - end - [as, controller].compact.join('_') - end - - def param_action? - @options[:on] == :member || scope.from == :member || - %w[edit show update destroy].include?(@info.action) - end - - def paramless_action? - @options[:on] == :collection || scope.from == :collection || - %w[index new create].include?(@info.action) - end - - def leaf? - scope == leaf_scope - end - - def resolved_path - scope.resolved_path - end - - def resolved_param - case scope.from - when :resources, :member - ":#{param}" - else # :resource, :namespace, :path, :collection - nil - end - end - - # If a block has pass then we assume the resources will be nested and then prefix - # the param name with the resource. IE: post_id instead of id - # This avoids an API Gateway parent sibling variable collision. - def param - return scope.options[:param] if scope.options[:param] - - if leaf? && Jets.config.routes.allow_sibling_conflicts - "id" - elsif scope.resource_descendent? || - scope.resource_sibling? && scope.colliding_resource_sibling? - "#{scope.resource_name.to_s.singularize}_id" - else - "id" - end - end - end -end diff --git a/lib/jets/router/route/path.rb b/lib/jets/router/route/path.rb deleted file mode 100644 index c4e2bbae4..000000000 --- a/lib/jets/router/route/path.rb +++ /dev/null @@ -1,93 +0,0 @@ -class Jets::Router::Route - module Path - # Note: The @options[:path] is missing prefix and is not support via direct create_route. - # This is because it can be added directly to the path. IE: - # - # get "myprefix/posts", to: "posts#index" - # - # Also, this helps to keep the method creator logic simpler. - # - def path(format=:jets) - segments = path_prefixes + [path_option] - segments = add_path_suffix(segments) - path = segments.reject(&:blank?).join('/') - format_path(format, path) - end - - # When scope used directly. IE: not coming from namespace or resources - # When coming from namespace or resources, the path is already accounted for. - def path_prefixes - list = [] - @scope.from_top.each do |scope| - node = Node.new(self, scope) - # Update @path_names as we walk down - @path_names.merge!(scope.options[:path_names] || {}) - - list << scope.resolved_path if node.append_path? - list << node.resolved_param if node.append_param? - end - list.reject!(&:blank?) # allows for path: '' to remove resource name from path - list - end - - def path_option - node = Node.new(self, @scope) - path = @options[:path].to_s.delete_prefix('/') # IE: new or edit - path.sub!(@scope.param_placeholder, ":#{node.resolved_param}") - path - end - - # Accounts for path names options map. Example: - # - # {path_names: {new: "sign_up", edit: "edit"}} - # - # new posts/new => posts/sign_up - # edit posts/:id/edit => posts/:id/edit - # - def add_path_suffix(all_segments) - # rip apart last path as segments - # work with and modify segments to be returned - segments = all_segments.last.to_s.split('/') - current_suffix = segments.last - - new_or_edit = %w[new edit].include?(current_suffix) - additional_action = @options[:on].is_a?(Symbol) && @options[:on] != :member && @options[:on] != :collection - will_replace = new_or_edit || additional_action - - if will_replace - segments_without_last = all_segments[0..-2] - # reassemble path with additional on action - segments_without_last << @options[:on].to_s if additional_action - # reassemble with new suffix from path_names map - new_suffix = @path_names[current_suffix.to_sym] || current_suffix - segments_without_last << new_suffix - new_last = segments_without_last.join('/') - segments[-1] = new_last # replace - segments - else - all_segments # original - end - end - - def path_suffixes - list = [] - list << action_suffix if action_suffix - list - end - - # IE: standard: posts/:id/edit - # api_gateway: posts/{id}/edit - def format_path(format, path) - path = case format - when :api_gateway - api_gateway_format(path) - when :raw - path - else # jets format - ensure_jets_format(path) - end - path = "/#{path}" unless path.starts_with?('/') # ensure starts with / be forgiving if / accidentally not included - path - end - end -end diff --git a/lib/jets/router/route_set.rb b/lib/jets/router/route_set.rb deleted file mode 100644 index c445cd111..000000000 --- a/lib/jets/router/route_set.rb +++ /dev/null @@ -1,214 +0,0 @@ -require 'dsl_evaluator' - -# The Jets::Router::RouteSet mimics the Rails interface. -# Jets works in a similar way to Rails. -# For example the url_helpers: -# -# Jets.application.routes.url_helpers -# => Jets.application.routes.named_routes.path_helpers_module -# => Jets::Router::Helpers::NamedRoutes::Generated::MainAppHelpers -# -# Jets makes less use of anonymous modules. It uses generated named modules -# to help debugging. Whereas Rails: -# -# Rails.application.routes.url_helpers => -# Rails.application.routes.named_routes.path_helpers_module => -# Rails.application.routes.named_routes.url_helpers_module => -# -# Rails generates an anonymous module for url_helpers that wraps -# and includes the path_helpers_module and url_helpers_module. -# -# Jets tries to exhibit enough of the same as Rails interface to make it -# compatible with with ActionController and ActionView components. -# -module Jets::Router - class RouteSet - extend Memoist - include Compat::RouteSet - include Dsl - - # With Jets, this class is not a really a Collection. It's a wrapper - # around Helpers::NamedRoutes::Generated which does the real work. - # Naming it NamedRouteCollection to be consistent with Rails and possibly - # in the future to make them more similar. - class NamedRouteCollection - attr_reader :url_helpers_module - def initialize(route_set) - @url_helpers_module = Helpers::NamedRoutes::Generated.create_helper_module(route_set) - end - - # interface method - # Rails AbstractController::UrlFor calls this method - # Reason why this wrapper is needed. - def helper_names - url_helpers_module.path_helpers.map(&:to_s) + - url_helpers_module.url_helpers.map(&:to_s) - end - - delegate :remove_methods!, :add_methods!, to: :url_helpers_module - end - - attr_reader :routes, :engine - attr_accessor :draw_paths, :default_scope, :default_url_options, - :disable_clear_and_finalize - def initialize(engine=Jets.application) - @engine = engine - @engine_class = engine.class - @default_url_options = {} # {host: "localhost"} - @routes = [] - @draw_paths = [] # not part of clear! addtional paths: config/routes/admin.rb - @prepend = [] # not part of clear! Otherwise breaks engines like sprockets-jets - @append = [] # not part of clear! - @finalized = false - @disable_clear_and_finalize = false - @default_scope = {} - end - - # draw is called from config/routes.rb - # load! is what we just internally to trigger it - def draw(&block) - clear! unless @disable_clear_and_finalize - scope(default_scope) do - instance_eval(&block) - end - finalize! unless @disable_clear_and_finalize - check_collision! - end - - # Be selective about what to clear. Keep other initialized instance variables - # like @prepend, @append, @default_scope - # Otherwise can break engines like sprockets-jets that use prepend. - def clear! - @finalized = false - @routes = [] - @scope = Scope.new - named_routes.remove_methods! - @prepend.each { |blk| instance_eval(&blk) } - end - - # Interface method for plugins. - # Plugins like kingsman use to hook into Jets after routes are loaded. - def finalize! - return if @finalized - @append.each { |blk| instance_eval(&blk) } - load_external_paths - named_routes.add_methods! - @finalized = true - end - - # additonal paths: config/routes/admin.rb - def load_external_paths - @draw_paths.each do |draw_path| - Dir.glob("#{draw_path}/*.rb").each do |route_path| - instance_eval(File.read(route_path), route_path.to_s) - end - end - end - - def append(&block) - @append << block - end - - def prepend(&block) - @prepend << block - end - - # Rails interface method - def eager_load! - # noop for Jets - end - - def url_helpers(supports_path = true) - @url_helpers ||= named_routes.url_helpers_module - end - - def mounted_helpers - # Calling url_helpers to create the mounted_helpers proxy methods - # IE: main_app and blorgh - url_helpers - @mounted_helpers ||= Helpers::NamedRoutes::Generated::MountedHelpers - end - - def define_mounted_helper(name) - Helpers::NamedRoutes::Generated.define_mounted_helper(name, self) - end - - # Note: Jets assigns this to a instance varialbe in the initialize method - # But Jets cannot do this because we currently eager load internally and - # that routes draw would not be called in proper order. - def named_routes - NamedRouteCollection.new(self) - end - memoize :named_routes - - # Validate routes that deployable - def check_collision! - return if Jets.config.cfn.build.routes == "one_apigw_method_for_all_routes" - paths = self.routes.map(&:path) - collision = Jets::Cfn::Resource::ApiGateway::RestApi::Routes::Collision.new - collide = collision.collision?(paths) - raise collision.exception if collide - end - - def one_apigw_method_for_all_routes_warning(options) - return unless options[:authorizer] - return unless Jets.config.cfn.build.routes == "one_apigw_method_for_all_routes" - return if options[:path].starts_with?('*') # *catchall - return if options[:path] == '' # root - - puts <<~EOL.color(:yellow) - WARNING: Authorizer should not be set at individual routes when using - - config.cfn.build.routes == "one_apigw_method_for_all_routes" - - You should only set authorizer for the root route and *catchall route. - The root route uses the root authorizer. - And all other routes uses the *catchall authorizer. - Docs: http://rubyonjets.com/docs/routing/authorizers/one-method/ - EOL - DslEvaluator.print_code(routes_call_line) if routes_call_line - end - - def routes_call_line - caller.find { |l| l.include?('config/routes.rb') } - end - - def api_mode? - Jets.config.mode == 'api' || Jets.config.api_only - end - - # Useful for creating API Gateway Resources - def all_paths - results = [] - paths = routes.map(&:path) - paths.each do |p| - sub_paths = [] - parts = p.split('/') - until parts.empty? - parts.pop - sub_path = parts.join('/') - sub_paths << sub_path unless sub_path == '' - end - results += sub_paths - end - @all_paths = (results + paths).sort.uniq - end - - # Useful for RouterMatcher - # - # Precedence: - # Routes with wildcards are considered after routes without wildcards - # - # Routes with fewer captures are ordered first since both - # /posts/:post_id/comments/new and /posts/:post_id/comments/:id are equally - # long - # - # Routes with the same amount of captures and wildcards are orderd so that - # the longest path is considered first since posts/:id and posts/:id/edit - # can both match. - def ordered_routes - length = Proc.new { |r| [r.path.count("*"), r.path.count(":"), r.path.length * -1] } - routes.sort_by(&length) - end - end -end diff --git a/lib/jets/router/scope.rb b/lib/jets/router/scope.rb deleted file mode 100644 index 528b56b36..000000000 --- a/lib/jets/router/scope.rb +++ /dev/null @@ -1,230 +0,0 @@ -module Jets::Router - class Scope - include Util - - module Macros - OPTION_ACCESSORS = Set.new - extend ActiveSupport::Concern - - class_methods do - def option_accessor(*names) - names.each do |name| - define_method(name) do - @options[name] - end - define_method("#{name}=") do |value| - if value.nil? - @options.delete(name) - else - @options[name] = value - end - end - define_method("#{name}?") do - !!@options[name] - end - OPTION_ACCESSORS.add(name) - end - end - end - - # Allow [] and []= notation. Though Jets does not use this internally. - # This is what Rails ActionDispatch::Routing::Mapper::Scope does. - # Helps with compatibility in case plugins use this notation. - def [](key) - if OPTION_ACCESSORS.include?(key) - send(key) - else - raise NoMethodError, "undefined method `#{key}' for #{self}" - end - end - - def []=(key, value) - if OPTION_ACCESSORS.include?(key) - send("#{key}=", value) - else - raise NoMethodError, "undefined method `#{key}' for #{self}" - end - end - end - include Macros - - attr_reader :options, :parent, :level, :children - attr_accessor :next - option_accessor :from, :path, :as, :resource_name, :module, :singular_resource, - :controller, :defaults, :constraints, :shallow - def initialize(options = {}, parent = nil, level = 1) - @options = options - @parent = parent - @level = level - @children = [] - if parent - parent.children << self - end - end - - def new(options={}) - self.class.new(options, self, level + 1) - end - - def virtual_controller - case from - when :member, :collection - parent.virtual_controller - when :resource - controller || resource_name.to_s.pluralize - else - controller || resource_name - end - end - - def resolved_defaults - result = {} - from_top.each do |scope| - result.merge!(scope.defaults) if scope.defaults - end - result - end - - def resolved_constraints - from_bottom.each do |scope| - return scope.constraints if scope.constraints - end - nil - end - - def resolved_module - items = from_top.map(&:module).compact - items.join('/') unless items.empty? - end - - def needs_controller_path? - return false if resource_name # no adjustments if within resource or resources scope - - from.nil? - end - - def virtual? - from == :member || from == :collection - end - - def resolved_path - case from - when :resource, :resources - path || resource_name - when :namespace, :path, nil - path - when :member, :collection - parent.resolved_path - end - end - - def parent_or_higher?(other_scope) - current_scope = self - - while current_scope.parent && !current_scope.parent.virtual? - return true if current_scope.parent == other_scope - current_scope = current_scope.parent - end - - false - end - - def real_parent?(scope) - if scope.virtual? - scope.parent == parent - else - parent == scope - end - end - - def resource_descendent?(scope=self) - # return false - scope.children.each do |child| - return true if child.from == :resource || child.from == :resources - return true if resource_descendent?(child) - end - false - end - - def resource_sibling? - return false - !resource_siblings.empty? - end - - def colliding_resource_sibling? - return false - # Don't think need to check for path because it doesn't really make sense - # to point 2 different resources to the same path. - resource_names = resource_siblings.map(&:resource_name) - resource_names.uniq.size != resource_names.size - end - - def resource_siblings - return [] unless parent - parent.children.select do |c| - c != self && - (c.from == :resource || c.from == :resources) - end - end - - # At time of each_resource DSL evaluation, the routes file has not fully evaluated - # and the scope context is not fully available. IE: info on the children. - # - # Placeholder param allows the values to lazily replaced later with full context. - # We only have to replace the last segment with the placeholder. - # This is because previous segments are already replaced with Route::Path#path_prefixes - # scopes_from_top logic. - def param_placeholder - if resource_name - "#{resource_name.upcase}_PARAM" - else - prefix = path.to_s.gsub('/','_').upcase - "#{prefix}_PARAM" - end - end - - def from_top - from_bottom.reverse - end - alias all_scopes from_top - - def from_bottom - current_scope = self - scopes = [] - previous_scope = nil # child - # do not include the root scope - while current_scope.parent - current_scope.next = previous_scope if previous_scope - scopes << current_scope - previous_scope = current_scope - current_scope = current_scope.parent - end - scopes - end - - def any_parent_shallow? - from_bottom.any? do |scope| - scope.shallow? || scope.from == :shallow - end - end - - # singularize all except last item - def singularize_leading(items) - result = [] - items.each_with_index do |item, index| - item = item.to_s - r = index == items.size - 1 ? item : item.singularize - result << r - end - result - end - - def root? - @parent.nil? - end - - def to_s - "" - end - end -end diff --git a/lib/jets/router/state.rb b/lib/jets/router/state.rb deleted file mode 100644 index 5b5ff7a9f..000000000 --- a/lib/jets/router/state.rb +++ /dev/null @@ -1,67 +0,0 @@ -module Jets::Router - class State - extend Memoist - include Jets::AwsServices - - def self.save_apigw_state - state = Jets::Router::State.new - state.save("methods", Jets::Cfn::Builder::Api::Pages::Methods.pages) - state.save("resources", Jets::Cfn::Builder::Api::Pages::Resources.pages) - # To avoid API limits for calculating changed routes in next deploy - state.save("routes", Jets::Router.routes) - end - - def load(filename) - resp = nil - begin - resp = s3.get_object( - bucket: Jets.aws.s3_bucket, - key: s3_storage_path(filename), - ) - rescue Aws::S3::Errors::NoSuchBucket - # Allow JETS_TEMPLATES=1 jets build to work with no-bucket-yet - return nil - end - text = resp.body.read - JSON.load(text) - rescue Aws::S3::Errors::NoSuchKey, Aws::S3::Errors::PermanentRedirect - end - memoize :load - - # Save previously deployed APIGW routes state - def save(filename, data) - body = data.respond_to?(:to_json) ? data.to_json : JSON.generate(data) - body = JSON.pretty_generate(JSON.parse(body)) # pretty generate - if ENV['JETS_API_STATE_DEBUG'] # useful with jets build --templates - puts "WARN: JETS_API_STATE_DEBUG=1 detected. Saving to tmp/#{filename}.json instead of S3.".color(:yellow) - IO.write("tmp/#{filename}.json", body) - else - s3.put_object( - body: body, - bucket: Jets.aws.s3_bucket, - key: s3_storage_path(filename), - ) - end - end - - # Examples: - # - # jets/state/apigw/resources.json - # jets/state/apigw/methods.json - # jets/state/apigw/routes.json - # - # Fetch or loaded in: - # - # resources.json: Jets::Cfn::Builder::Api::Pages::Resources#old_pages - # methods.json: Jets::Cfn::Builder::Api::Pages::Methods#old_pages - # routes.json: Jets::Cfn::Resource::ApiGateway::RestApi::Routes::Change::Base#deployed_routes - # - # Saved in: - # - # Jets::Cfn::Ship#save_apigw_state - # - def s3_storage_path(filename) - "jets/state/apigw/#{filename}.json" - end - end -end diff --git a/lib/jets/router/util.rb b/lib/jets/router/util.rb deleted file mode 100644 index e13dbca71..000000000 --- a/lib/jets/router/util.rb +++ /dev/null @@ -1,8 +0,0 @@ -module Jets::Router - module Util - def underscore(str) - return unless str - str.to_s.gsub(/[^a-zA-Z0-9]/,'_') - end - end -end \ No newline at end of file diff --git a/lib/jets/ruby_version_check.rb b/lib/jets/ruby_version_check.rb deleted file mode 100644 index 5c95d957c..000000000 --- a/lib/jets/ruby_version_check.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2.7.0") && RUBY_ENGINE == "ruby" - desc = defined?(RUBY_DESCRIPTION) ? RUBY_DESCRIPTION : "ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE})" - abort <<-end_message - - Jets 5 requires Ruby 2.7.0 or newer. - - You're running - #{desc} - - Please upgrade to Ruby 2.7.0 or newer to continue. - - end_message -end diff --git a/lib/jets/rule/base.rb b/lib/jets/rule/base.rb deleted file mode 100644 index 2180597ba..000000000 --- a/lib/jets/rule/base.rb +++ /dev/null @@ -1,20 +0,0 @@ -require 'json' - -# Base public methods get turned into Lambda functions. -# -# Jets::Rule::Base < Jets::Lambda::Functions -# Both Jets::Rule::Base and Jets::Lambda::Functions have Dsl modules included. -# So the Jets::Rule::Dsl overrides some of the Jets::Lambda::Functions behavior. -module Jets::Rule - class Base < Jets::Lambda::Functions - include Dsl - prepend Jets::ExceptionReporting::Process - - class << self - def process(event, context, meth) - job = new(event, context, meth) - job.send(meth) - end - end - end -end diff --git a/lib/jets/rule/dsl.rb b/lib/jets/rule/dsl.rb deleted file mode 100644 index 73ad00405..000000000 --- a/lib/jets/rule/dsl.rb +++ /dev/null @@ -1,109 +0,0 @@ -# Jets::Rule::Base < Jets::Lambda::Functions -# Both Jets::Rule::Base and Jets::Lambda::Functions have Dsl modules included. -# So the Jets::Rule::Dsl overrides some of the Jets::Lambda::Functions behavior. -# -# Implements: -# -# default_associated_resource_definition -# -module Jets::Rule::Dsl - extend ActiveSupport::Concern - - included do - class << self - def rule_namespace(value=nil) - if value.nil? - @rule_namespace # getter - else - @rule_namespace = value # # setter - end - end - - # Allows for different types of values. Examples: - # - # String: scope "AWS::EC2::SecurityGroup" - # Array: scope ["AWS::EC2::SecurityGroup"] - # Hash: scope {"ComplianceResourceTypes" => ["AWS::EC2::SecurityGroup"]} - def scope(value) - scope = case value - when String - {ComplianceResourceTypes: [value]} - when Array - {ComplianceResourceTypes: value} - else # default to hash - value - end - associated_properties(Scope: scope) - end - - # Convenience method that set properties. List based on https://amzn.to/2oSph1P - # Not all properties are included because some properties are not meant to be set - # directly. For example, function_name is a calculated setting by Jets. - ASSOCIATED_PROPERTIES = %W[ - config_rule_name - description - input_parameters - maximum_execution_frequency - ] - define_associated_properties(ASSOCIATED_PROPERTIES) - alias_method :desc, :description - - def default_associated_resource_definition(meth) - config_rule_definition(meth) - end - - def config_rule_definition(meth) - resource = Jets::Cfn::Resource::Config::ConfigRule.new(self, meth, associated_properties) - resource.definition # returns a definition to be added by associated_resources - end - - def managed_rule(name) - name = name.to_s - managed_rule = Jets::Cfn::Resource::Config::ManagedRule.new(self, name, associated_properties) - resource(managed_rule.definition) # Sets @associated_resources - - # The key to not register the task to all_tasks to avoid creating a Lambda function. - # Instead we store it in all_managed_rules. - register_managed_rule(name, managed_rule.definition) - end - - # Creates a task but registers it to all_managed_rules instead of all_tasks - # because we do not want Lambda functions to be created. - def register_managed_rule(name, definition) - # Mimic task to grab base_replacements, namely namespace. - # Do not actually use the task to create a Lambda function for managed rules. - # Only using the task for base_replacements. - resources = [definition] - meth = name - task = Jets::Lambda::Definition.new(self.name, meth, - resources: resources, - replacements: replacements(meth)) - all_managed_rules[name] = { definition: definition, replacements: task.replacements } - clear_properties - end - - # Override lambda/dsl.rb to add config_rule_name also - def replacements(meth) - name_without_rule = self.name.underscore.gsub(/_rule$/,'') - config_rule_name = "#{name_without_rule}_#{meth}".dasherize - { - config_rule_name: config_rule_name - } - end - - # AWS managed rules are not actual Lambda functions and require their own storage. - def all_managed_rules - @all_managed_rules ||= ActiveSupport::OrderedHash.new - end - - def managed_rules - all_managed_rules.values - end - - # Override Lambda::Dsl.build? to account for possible managed_rules - def build? - !definitions.empty? || !managed_rules.empty? - end - end - end -end diff --git a/lib/jets/secrets.rb b/lib/jets/secrets.rb deleted file mode 100644 index 88bbc666e..000000000 --- a/lib/jets/secrets.rb +++ /dev/null @@ -1,110 +0,0 @@ -# frozen_string_literal: true - -require "yaml" -require "tempfile" -require "active_support/message_encryptor" - -module Jets - # Greatly inspired by Ara T. Howard's magnificent sekrets gem. 😘 - class Secrets # :nodoc: - class MissingKeyError < RuntimeError - def initialize - super(<<-end_of_message.squish) - Missing encryption key to decrypt secrets with. - Ask your team for your master key and put it in ENV["JETS_MASTER_KEY"] - end_of_message - end - end - - @cipher = "aes-128-gcm" - @root = File # Wonky, but ensures `join` uses the current directory. - - class << self - attr_writer :root - - def parse(paths, env:) - paths.each_with_object(Hash.new) do |path, all_secrets| - require "erb" - - source = ERB.new(preprocess(path)).result - secrets = YAML.respond_to?(:unsafe_load) ? YAML.unsafe_load(source) : YAML.load(source) - secrets ||= {} - - all_secrets.merge!(secrets["shared"].deep_symbolize_keys) if secrets["shared"] - all_secrets.merge!(secrets[env].deep_symbolize_keys) if secrets[env] - end - end - - def key - ENV["JETS_MASTER_KEY"] || read_key_file || handle_missing_key - end - - def encrypt(data) - encryptor.encrypt_and_sign(data) - end - - def decrypt(data) - encryptor.decrypt_and_verify(data) - end - - def read - decrypt(IO.binread(path)) - end - - def write(contents) - IO.binwrite("#{path}.tmp", encrypt(contents)) - FileUtils.mv("#{path}.tmp", path) - end - - def read_for_editing(&block) - writing(read, &block) - end - - private - def handle_missing_key - raise MissingKeyError - end - - def read_key_file - if File.exist?(key_path) - IO.binread(key_path).strip - end - end - - def key_path - @root.join("config", "secrets.yml.key") - end - - def path - @root.join("config", "secrets.yml.enc").to_s - end - - def preprocess(path) - if path.end_with?(".enc") - decrypt(IO.binread(path)) - else - IO.read(path) - end - end - - def writing(contents) - file_name = "#{File.basename(path)}.#{Process.pid}" - - Tempfile.create(["", "-" + file_name]) do |tmp_file| - tmp_path = Pathname.new(tmp_file) - tmp_path.binwrite contents - - yield tmp_path - - updated_contents = tmp_path.binread - - write(updated_contents) if updated_contents != contents - end - end - - def encryptor - @encryptor ||= ActiveSupport::MessageEncryptor.new([ key ].pack("H*"), cipher: @cipher) - end - end - end -end diff --git a/lib/jets/shim.rb b/lib/jets/shim.rb new file mode 100644 index 000000000..db9de9067 --- /dev/null +++ b/lib/jets/shim.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Jets + module Shim + extend Memoist + + def handler(event, context, route = nil) + Handler.new(event, context, route).handle + end + + def to_rack_env(event, context) + Handler.new(event, context).to_rack_env + end + + def boot + # Don't boot Jets in maintenance mode. Makes cold start much faster. + return if Maintenance.enabled? + + paths = %w[ + config/jets/shim.rb + ] + paths.map! do |path| + path.starts_with?(".") ? path : "./#{path}" + end + found = paths.find { |p| File.exist?(p) } + if found + require found # calls Jets.shim.configure + else + config.rack_app # all settings are inferred + end + + # Boot Jets to add additional features + Jets.boot + end + + def configure + yield config + end + + def config + Config.instance + end + + extend self + end +end diff --git a/lib/jets/shim/adapter/alb.rb b/lib/jets/shim/adapter/alb.rb new file mode 100644 index 000000000..6ad3f96c8 --- /dev/null +++ b/lib/jets/shim/adapter/alb.rb @@ -0,0 +1,16 @@ +module Jets::Shim::Adapter + class Alb < Apigw + def env + super.merge( + "HTTP_PORT" => headers["x-forwarded-port"], + "SERVER_PORT" => headers["x-forwarded-port"], + "SERVER_PROTOCOL" => event.dig("requestContext", "protocol") || "HTTP/1.1" + ) + end + + def handle? + host =~ /elb\.amazonaws\.com/ || + event.dig("requestContext", "elb") + end + end +end diff --git a/lib/jets/shim/adapter/apigw.rb b/lib/jets/shim/adapter/apigw.rb new file mode 100644 index 000000000..00c527a69 --- /dev/null +++ b/lib/jets/shim/adapter/apigw.rb @@ -0,0 +1,37 @@ +module Jets::Shim::Adapter + class Apigw < Web + # See: https://github.com/rack/rack/blob/main/lib/rack/constants.rb + def env + { + # Request env keys + "HTTP_HOST" => host, + "HTTP_PORT" => headers["X-Forwarded-Port"], + "HTTPS" => https, + "PATH_INFO" => path_info, + "QUERY_STRING" => query_string, + "REQUEST_METHOD" => event["httpMethod"] || "GET", # useful to default to GET when testing with Lambda console + "REQUEST_PATH" => path_info, + "SCRIPT_NAME" => "", + "SERVER_NAME" => host, + "SERVER_PORT" => headers["X-Forwarded-Port"], + "SERVER_PROTOCOL" => event.dig("requestContext", "protocol") || "HTTP/1.1" + } + end + + def path_info + event["path"] || "/" # always set by API Gateway, but setting to make shim testing easier + end + + def handle? + host =~ /execute-api/ || + event["resource"] && event.dig("requestContext", "stage") + end + + private + + def query_string + query = event["queryStringParameters"] || {} # always set with API Gateway but when testing shim might not be + Rack::Utils.build_nested_query(query) + end + end +end diff --git a/lib/jets/shim/adapter/base.rb b/lib/jets/shim/adapter/base.rb new file mode 100644 index 000000000..672574360 --- /dev/null +++ b/lib/jets/shim/adapter/base.rb @@ -0,0 +1,16 @@ +require "active_support/core_ext/hash" +require "base64" + +module Jets::Shim::Adapter + class Base + extend Memoist + include Jets::Util::Logging + + attr_reader :event, :context, :target + def initialize(event, context = nil, target = nil) + @event = ActiveSupport::HashWithIndifferentAccess.new(event) + @context = context + @target = target # IE: cool_event.party + end + end +end diff --git a/lib/jets/shim/adapter/command.rb b/lib/jets/shim/adapter/command.rb new file mode 100644 index 000000000..b478a906e --- /dev/null +++ b/lib/jets/shim/adapter/command.rb @@ -0,0 +1,21 @@ +require "open3" + +module Jets::Shim::Adapter + class Command < Base + def handle + cmd = event[:command] + result = {stdout: "", stderr: ""} + # splat works for both String and Array + Open3.popen3(*cmd) do |stdin, stdout, stderr, wait_thread| + result[:stdout] << stdout.read + result[:stderr] << stderr.read + result[:status] = wait_thread.value.exitstatus + end + result + end + + def handle? + event[:command] + end + end +end diff --git a/lib/jets/shim/adapter/event.rb b/lib/jets/shim/adapter/event.rb new file mode 100644 index 000000000..1e0eba2c4 --- /dev/null +++ b/lib/jets/shim/adapter/event.rb @@ -0,0 +1,27 @@ +module Jets::Shim::Adapter + class Event < Base + def handle + target_class.handle(event, context, target_method) + end + + def handle? + target && target_class && target_method? + end + + def target_class + class_name, _ = target.split(".") + class_name.camelize.constantize + rescue NameError + end + + def target_method + _, method_name = target.split(".") + method_name ||= "perform" + method_name.to_sym + end + + def target_method? + target_class.public_instance_methods.include?(target_method) + end + end +end diff --git a/lib/jets/shim/adapter/fallback.rb b/lib/jets/shim/adapter/fallback.rb new file mode 100644 index 000000000..930a7c7b9 --- /dev/null +++ b/lib/jets/shim/adapter/fallback.rb @@ -0,0 +1,64 @@ +module Jets::Shim::Adapter + class Fallback < Base + def handle + fallback_handler.handler(event, context, target) + end + + def fallback_handler + Jets::Shim.config.fallback_handler || self.class + end + memoize :fallback_handler + + class << self + def handler(event, context = nil, target = nil) + puts "The default fallback handler is being called because a handler for it could not be found" + puts "For debugging, here is the" + if ENV["_HANDLER"] # on AWS Lambda + puts "event: #{JSON.dump(event)}" + else + puts "event:" + puts JSON.pretty_generate(event) + end + puts "Please double check the event payload.\n\n" + + if target + puts "ERROR: event handler target not found: #{target}".color(:red) + target_class, target_method = target.split(".") + target_class = target_class.camelize + target_method ||= "perform" + puts <<~EOL + You can configure define an app/events handler in your application. Example: + + app/events/#{target_class.underscore}.rb + + class #{target_class} < ApplicationEvent + def #{target_method} + puts "event #{JSON.dump(event)}" + end + end + EOL + + else + puts <<~EOL + You can also configure a custom fallback handler in the config/jets/shim.rb file. + Example: + + config/jets/shim.rb + + Jets.shim.configure do |config| + config.fallback_handler = FallbackHandler + end + + FallbackHandler should implement handler(event, context) + + EOL + end + # Custom Jets error message + { + errorMessage: "event handler not found. Look at logs for details", + errorType: "JetsEventHandlerNotFound" + } + end + end + end +end diff --git a/lib/jets/shim/adapter/lambda.rb b/lib/jets/shim/adapter/lambda.rb new file mode 100644 index 000000000..fc290ca37 --- /dev/null +++ b/lib/jets/shim/adapter/lambda.rb @@ -0,0 +1,35 @@ +module Jets::Shim::Adapter + class Lambda < Web + def env + { + # Request env keys + "HTTP_HOST" => host, + "HTTP_PORT" => headers["x-forwarded-port"], + "HTTPS" => https, + "PATH_INFO" => path_info, + "QUERY_STRING" => query_string, + "REQUEST_METHOD" => event.dig("requestContext", "http", "method") || "GET", # useful to default to GET when testing with Lambda console + "REQUEST_PATH" => path_info, + "SCRIPT_NAME" => "", + "SERVER_NAME" => host, + "SERVER_PORT" => headers["x-forwarded-proto"], + "SERVER_PROTOCOL" => event.dig("requestContext", "http", "protocol") || "HTTP/1.1" + } + end + + def handle? + host =~ /lambda-url/ || + event["version"] && event["routeKey"] + end + + private + + def path_info + event["rawPath"] || "/" + end + + def query_string + event["rawQueryString"] || "" + end + end +end diff --git a/lib/jets/shim/adapter/prewarm.rb b/lib/jets/shim/adapter/prewarm.rb new file mode 100644 index 000000000..ae5f412d8 --- /dev/null +++ b/lib/jets/shim/adapter/prewarm.rb @@ -0,0 +1,27 @@ +module Jets::Shim::Adapter + class Prewarm < Base + @@prewarm_count = 0 + @@prewarm_at = nil + + def handle + @@prewarm_count += 1 + @@prewarm_at = Time.now.utc + result = self.class.stats + log.info "Prewarm request: #{JSON.dump(result)}" if ENV["JETS_PREWARM_LOG"] + result + end + + def handle? + event["_prewarm"] + end + + def self.stats + { + boot_at: Jets::Core::Booter.boot_at, + gid: Jets::Core::Booter.gid, + prewarm_at: @@prewarm_at, + prewarm_count: @@prewarm_count + } + end + end +end diff --git a/lib/jets/shim/adapter/web.rb b/lib/jets/shim/adapter/web.rb new file mode 100644 index 000000000..41e18ad36 --- /dev/null +++ b/lib/jets/shim/adapter/web.rb @@ -0,0 +1,145 @@ +require "rack" +require "uri" + +module Jets::Shim::Adapter + # Not named Rack to avoid confusion with the Rack gem + class Web < Base + def handle + env = to_rack_env + app = Jets::Shim.config.app + triplet = app.call(env) + translate_response(triplet) + end + + def to_rack_env + rack_env = base_env.merge(env).merge(headers_env) + rack_env.delete_if { |k, v| v.nil? } + rack_env + end + + # Translate rack triplet to compatible response for the service + def translate_response(triplet) + adapter_name = self.class.name.split("::").last + response_class = Jets::Shim::Response.const_get(adapter_name) + response = response_class.new(triplet) + response.translate + end + + # env should be implemented by subclass + def env + {} + end + private :env + + def headers_env + env = {} + headers.each do |k, v| + key = k.tr("-", "_").upcase + http_key = "HTTP_#{key}" # IE: User-Agent => HTTP_USER_AGENT + + # specially handle host since it can be overridden with JETS_SHIM_HOST + next if http_key == "HTTP_HOST" + + if special_headers.include?(key) + env[special_headers[key]] = v + else + env[http_key] ||= v + end + end + env + end + + def special_headers + # Note: apigw does not have content-length in headers. + # See: https://stackoverflow.com/questions/56693981/how-do-i-get-http-header-content-length-in-api-gateway-lambda-proxy-integratio + # + # Instead content-length must be calculated from body. Done in base_env. + # Adding content-length here for other adapters. + # Headers have highest precedence since they get merged last. + { + "CONTENT_TYPE" => "CONTENT_TYPE", + "CONTENT_LENGTH" => "CONTENT_LENGTH" + } + end + + def base_env + # 'rack.input' - Even if not set, Rack always assigns an StringIO. + { + "CONTENT_LENGTH" => content_length, + "HTTP_COOKIE" => http_cookie, + "lambda.context" => context, + "lambda.event" => event, + "rack.input" => StringIO.new(body || ""), + "REMOTE_ADDR" => remote_addr, + "REMOTE_HOST" => host, + "REQUEST_URI" => request_uri + } + end + + # https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html + # https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html#http-api-develop-integrations-lambda.proxy-format + def http_cookie + event["cookies"]&.join("; ") + end + + def remote_addr + # X-Forwarded-For: client, proxy1, proxy2 + # X-Forwarded-For: + # The originating IP address of the client connecting to the ALB. + # This is used to capture the client IP address for requests that are sent to a proxy chain or a load balancer + # before they reach your server. + # If the X-Forwarded-For header is not present in the request, the remote IP address from the transport layer, + # such as the TCP connection, is used instead. + # https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-access-logs.html#access-log-entry-format + addr = headers["X-Forwarded-For"] || headers["x-forwarded-for"] || headers["REMOTE_ADDR"] + addr.split(",").first.strip if addr + end + + def content_length + bytesize = body.bytesize.to_s if body + headers["Content-Length"] || bytesize + end + + def request_uri + # IE: /foo?bar=1 + "#{path_info}?#{query_string}" + end + + # Decoding base64 from API Gateaway if necessary + # Rack will be none the wiser + def body + if event["isBase64Encoded"] + Base64.decode64(event["body"]) + else + event["body"] + end + end + + def headers + event["headers"] || {} + end + + def host + # Host: apigw + # host: lambda and alb + ENV["JETS_SHIM_HOST"] || + shim_host || + headers["Host"] || + headers["host"] + end + + # Can be added by CloudFront function + # Also, POST requests have an origin header. IE: Updating a record with CRUD. + def shim_host + return unless headers["jets-shim-host"] + URI.parse(headers["jets-shim-host"]).host + end + + def https + # X-Forwarded-Proto: apigw + # x-forwarded-proto: lambda and alb + proto = headers["X-Forwarded-Proto"] || headers["x-forwarded-proto"] + (proto == "https") ? "on" : "off" + end + end +end diff --git a/lib/jets/shim/config.rb b/lib/jets/shim/config.rb new file mode 100644 index 000000000..e4901244b --- /dev/null +++ b/lib/jets/shim/config.rb @@ -0,0 +1,71 @@ +require "singleton" + +module Jets::Shim + class Config + extend Memoist + include Singleton + + attr_reader :boot_path + attr_accessor :fallback_handler, :adapter + attr_writer :rack_app + + def boot_path=(value) + @boot_path = value # dont include ./ in @boot_path. @boot_path can be used to infer the rack_app class name + # Immediately require the boot_path so that rack_app is available for reference + require_boot_path(value) + end + + def rack_app + if Maintenance.enabled? + Maintenance.app + elsif @rack_app + @rack_app + else + boot_path = @boot_path || infer_boot_path + # Possible that boot_path is nil. + # Case: Jets events app only. + if boot_path + require_boot_path(boot_path) if boot_path # in case boot_path is not set + infer_rack_app(boot_path) # IE: Rails.application or App + end + end + end + alias_method :app, :rack_app + alias_method :app=, :rack_app= + + def infer_boot_path + if rails? + "config/environment" + elsif File.exist?("app.rb") + "app" + end + end + + def require_boot_path(path) + path = path.starts_with?(".") ? path : "./#{path}" + path = path.ends_with?(".rb") ? path : "#{path}.rb" + require path # IE: config/environment.rb (Rails) or app.rb (generic rack app) + end + + def infer_rack_app(boot_path) + if rails? + Rails.application + else + boot_path.sub!(/\.rb$/, "") # remove .rb extension + boot_path.camelize.constantize + end + end + + def rails? + framework?(:rails) + end + + def framework?(name) + if File.exist?("config.ru") + # IE: Jets.application or Rails.application + IO.readlines("config.ru").any? { |l| l.include?("#{name.to_s.camelize}.application") } + end + end + memoize :framework? + end +end diff --git a/lib/jets/shim/handler.rb b/lib/jets/shim/handler.rb new file mode 100644 index 000000000..b167ae30a --- /dev/null +++ b/lib/jets/shim/handler.rb @@ -0,0 +1,60 @@ +module Jets::Shim + class Handler + include Jets::Util::Logging + + attr_reader :event, :context, :target + def initialize(event, context = nil, target = nil) + @event = event.deep_stringify_keys + @context = context + @target = target # IE: cool_event.party + end + + def handle + show_debug_shim_event + adapter.handle + end + + def adapter + adapter_class = Adapter.const_get(adapter_name.to_s.camelize) + log.info "jets shim adapter: #{adapter_name}" if ENV["JETS_DEBUG_SHIM"] + adapter_class.new(event, context, target) # IE: Adapter::Apigw + end + + protected + + def adapter_name + Jets::Shim.config.adapter || infer_adapter + end + + def infer_adapter + adapters = %w[lambda apigw alb prewarm command event] + adapters.each do |adapter_name| + adapter_class = Adapter.const_get(adapter_name.to_s.camelize) + return adapter_name if adapter_class.new(event, context, target).handle? + end + :fallback + end + + def show_debug_shim_event + self.class.show_debug_shim("jets shim event:", event) + end + + class << self + include Jets::Util::Logging + + # interface method used by Shim::Response::Base + def show_debug_shim(message, payload) + return unless ENV["JETS_DEBUG_SHIM"] + + log.info message + # pretty mode is not useful on CloudWatch since it strips the surrounding spaces on each line + # It's only useful for testing handlers locally + if ENV["JETS_DEBUG_SHIM"] == "pretty" + log.info JSON.pretty_generate(payload) + else + log.info JSON.dump(payload) # json one line + end + end + end + end +end diff --git a/lib/jets/shim/maintenance.rb b/lib/jets/shim/maintenance.rb new file mode 100644 index 000000000..366beba4e --- /dev/null +++ b/lib/jets/shim/maintenance.rb @@ -0,0 +1,37 @@ +module Jets::Shim + class Maintenance + class << self + extend Memoist + include Jets::Util::Truthy + + def app + self + end + + def call(env) + [503, {"Content-Type" => content_type}, [body]] + end + + # IE: application/json; charset=utf-8 + # IE: text/html + def content_type + maintenance_file.end_with?("json") ? "application/json" : "text/html" + end + + def body + IO.read(maintenance_file) + end + + def maintenance_file + default_path = "#{__dir__}/maintenance/maintenance.html" + paths = %w[public/maintenance.html public/maintenance.json] + paths.find { |path| File.exist?(path) } || default_path + end + memoize :maintenance_file + + def enabled? + truthy?(ENV["JETS_MAINTENANCE"]) + end + end + end +end diff --git a/lib/jets/shim/maintenance/maintenance.html b/lib/jets/shim/maintenance/maintenance.html new file mode 100644 index 000000000..882f7c9ac --- /dev/null +++ b/lib/jets/shim/maintenance/maintenance.html @@ -0,0 +1,45 @@ + + + + + + + Maintenance Page + + + + +
+

We'll be back soon!

+

We're currently undergoing some maintenance. Thank you for your patience. Please check back later!

+
+ + + \ No newline at end of file diff --git a/lib/jets/shim/response/alb.rb b/lib/jets/shim/response/alb.rb new file mode 100644 index 000000000..be6f8b98c --- /dev/null +++ b/lib/jets/shim/response/alb.rb @@ -0,0 +1,10 @@ +module Jets::Shim::Response + class Alb < Apigw + def translate + hash = super + desc = Rack::Utils::HTTP_STATUS_CODES[hash[:statusCode]] + hash[:statusDescription] = "#{hash[:statusCode]} #{desc}" + hash + end + end +end diff --git a/lib/jets/shim/response/apigw.rb b/lib/jets/shim/response/apigw.rb new file mode 100644 index 000000000..94eff6954 --- /dev/null +++ b/lib/jets/shim/response/apigw.rb @@ -0,0 +1,4 @@ +module Jets::Shim::Response + class Apigw < Base + end +end diff --git a/lib/jets/shim/response/base.rb b/lib/jets/shim/response/base.rb new file mode 100644 index 000000000..1da2e384e --- /dev/null +++ b/lib/jets/shim/response/base.rb @@ -0,0 +1,124 @@ +require "active_support" +require "active_support/core_ext/string" +require "base64" +require "json" +require "mime/types" +require "rack" + +module Jets::Shim::Response + class Base + include Jets::Util::Logging + include Jets::Util::Truthy + + def initialize(triplet) + @triplet = triplet + end + + # AWS Lambda proxy integrations 2.0 + # https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html + # https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html#http-api-develop-integrations-lambda.proxy-format + def translate + status, headers, rack_body = @triplet + headers.merge!(prewarm_headers) + cookies = handle_set_cookie!(headers) + stringify_values!(headers) + body = translate_body(rack_body) + base64 = base64_encode?(headers) + if base64 + body = Base64.strict_encode64(body) + end + resp = { + statusCode: status, + headers: headers, # response headers + body: body, + cookies: cookies, + isBase64Encoded: base64 + }.delete_if { |k, v| v.nil? } + show_debug_shim_resp(resp) + resp + end + + private + + # Example headers: + # { + # "x-jets-prewarm-count"=>1, + # "x-jets-prewarm-at"=>"2024-04-18 12:11:37 UTC", + # "x-jets-gid"=>"1bd5d993" + # } + def prewarm_headers + Jets::Shim::Adapter::Prewarm.stats.transform_keys { |k| "x-jets-#{k.to_s.dasherize}" } + end + + def handle_set_cookie!(headers) + # Interesting: Both headers['Set-Cookie'] and headers['set-cookie'] work. + # Rack is smart enough to handle both. Also, Rack either returns a String + # for a single cookie or Array for multiple cookies. + # Must handle both Set-Cookie and set-cookie Rails seems to be inconsistent + # Locally: Set-Cookie + # AWS Lambda: set-cookie + set_cookie = headers.delete("Set-Cookie") || headers.delete("set-cookie") + return unless set_cookie + + # AWS Lambda proxy integrations 2.0 requires cookies to be an array + # https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html#http-api-develop-integrations-lambda.proxy-format + if set_cookie.is_a?(String) + # set_cookie is a single cookie String + # IE: "yummy_cookie=choco" + set_cookie.split("\n").map(&:strip) + else + # set_cookie is already cookies Array + # IE: ["yummy_cookie=choco", "tasty_cookie=strawberry"] + set_cookie + end + end + + def stringify_values!(headers) + headers.each do |k, v| + headers[k] = v.to_s + end + headers + end + + # Rack middleware handles Array or BodyProxy normally, but APIGW require String + def translate_body(rack_body) + case rack_body + when Rack::Files::Iterator + # Sinatra does not always send the right content-type header so we check the file extension + # ActiveStorage does not always have file extensions so will have to check the content-type header also + @encode_binary = binary?(rack_body.path) # set flag for base64_encode? + File.read(rack_body.path) + else # Rack::BodyProxy or Array + body = "" # String + rack_body.each { |part| body << part.to_s } + body + end + end + + def binary?(file_path) + mime_type = MIME::Types.type_for(file_path).first + mime_type&.binary? + end + + # Headers or @encode_binary flag is determine if body should be base64 encoded. + # Note: mime_type.binary? is not always accurate. + # Example: render json: {ok: true} is not binary but mime_type.binary? is true + def base64_encode?(headers) + return @encode_binary if @encode_binary + if headers.key?("x-jets-base64") + return truthy?(headers["x-jets-base64"]) + end + return true if Jets.project.config.base64_encode + + content_type = headers["content-type"] + if content_type + mime_type = MIME::Types[content_type].first + mime_type.encoding == "base64" + end + end + + def show_debug_shim_resp(resp) + Jets::Shim::Handler.show_debug_shim("jets shim response:", resp) + end + end +end diff --git a/lib/jets/shim/response/lambda.rb b/lib/jets/shim/response/lambda.rb new file mode 100644 index 000000000..4189c83de --- /dev/null +++ b/lib/jets/shim/response/lambda.rb @@ -0,0 +1,4 @@ +module Jets::Shim::Response + class Lambda < Base + end +end diff --git a/lib/jets/shim/response/web.rb b/lib/jets/shim/response/web.rb new file mode 100644 index 000000000..d5edecc61 --- /dev/null +++ b/lib/jets/shim/response/web.rb @@ -0,0 +1,4 @@ +module Jets::Shim::Response + class Web < Base + end +end diff --git a/lib/jets/spec_helpers.rb b/lib/jets/spec_helpers.rb deleted file mode 100644 index 9345f3c10..000000000 --- a/lib/jets/spec_helpers.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -require 'base64' - -module Jets - module SpecHelpers - include Fixtures - include Controllers - end -end - -require "rspec" -RSpec.configure do |c| - c.include Jets::SpecHelpers -end diff --git a/lib/jets/spec_helpers/controllers.rb b/lib/jets/spec_helpers/controllers.rb deleted file mode 100644 index cb1ce8c8d..000000000 --- a/lib/jets/spec_helpers/controllers.rb +++ /dev/null @@ -1,54 +0,0 @@ -module Jets::SpecHelpers - module Controllers - include Jets::Router::Helpers # must be at the top because response is overridden later - - rest_methods = %w[get post put patch delete] - rest_methods.each do |meth| - define_method(meth) do |path, **params| - http_call(method: meth, path: path, **params) - end - # Example: - # def get(path, **params) - # http_call(method: :get, path: path, **params) - # end - end - - attr_reader :request, :response - def http_call(method:, path:, **options) - headers = options.delete(:headers) || {} - params_hash = options.delete(:params) || options - md = path.match(/\?(.*)/) - path = path.gsub("?#{md[1]}", '') if md - query = md ? Rack::Utils.parse_nested_query(md[1]).merge(params_hash) : params_hash - - params = Params.new - if method.to_sym == :get - params.body_params = {} - params.query_params = query || {} - else - params.body_params = options.delete(:body) || query - end - - # Note: Do not cache the request object. Otherwise, it cannot be reused between specs. - # See: https://community.rubyonjets.com/t/is-jets-spechelpers-controllers-request-being-cached/244/2 - @request = Request.new(method, path, headers, params) - - @request.method = method.to_sym - @request.path = path - @request.headers.deep_merge!(headers) - - suppress_logging do - @response = @request.dispatch! - end - end - - def suppress_logging - old_logger = Jets.logger - unless ENV['JETS_TEST_LOGGING'] - Jets.logger = ActionView::Base.logger = Logger.new("/dev/null") - end - yield - Jets.logger = old_logger - end - end -end \ No newline at end of file diff --git a/lib/jets/spec_helpers/controllers/params.rb b/lib/jets/spec_helpers/controllers/params.rb deleted file mode 100644 index e064a3660..000000000 --- a/lib/jets/spec_helpers/controllers/params.rb +++ /dev/null @@ -1,8 +0,0 @@ -module Jets::SpecHelpers::Controllers - class Params - attr_accessor :path_params, :body_params, :query_params - def initialize(path_params={}, body_params={}, query_params={}) - @path_params, @body_params, @query_params = path_params, body_params, query_params - end - end -end diff --git a/lib/jets/spec_helpers/controllers/request.rb b/lib/jets/spec_helpers/controllers/request.rb deleted file mode 100644 index f2374f259..000000000 --- a/lib/jets/spec_helpers/controllers/request.rb +++ /dev/null @@ -1,107 +0,0 @@ -module Jets::SpecHelpers::Controllers - class Request - extend Memoist - - attr_accessor :method, :path, :headers, :params - def initialize(method, path, headers={}, params={}) - @method, @path, @headers, @params = method, path, headers, params - end - - def event - json = {} - id_params = route.path.scan(%r{:([^/]+)}).flatten - expanded_path = route.escape_path(path) - path_parameters = {} - - id_params.each do |id_param| - raise "missing param: :#{id_param}" unless path_params.include? id_param.to_sym - - path_param_value = path_params[id_param.to_sym] - raise "Path param :#{id_param} value cannot be blank" if path_param_value.blank? - - escaped_path_param_value = CGI.escape(path_param_value.to_s) - expanded_path = expanded_path.gsub(":#{id_param}", escaped_path_param_value) - path_parameters.deep_merge!(id_param => escaped_path_param_value) - end - - json['resource'] = strip_query_string(route.path.gsub(/:(\w+)/, '{\1}')) - json['path'] = strip_query_string(expanded_path) - json['httpMethod'] = method.to_s.upcase - json['pathParameters'] = path_parameters - json['headers'] = (headers || {}).stringify_keys - - if method != :get - json['headers']['Content-Type'] ||= 'application/x-www-form-urlencoded' - - if params.body_params.is_a? String - body = params.body_params - json['headers']['Content-Length'] ||= body.length.to_s - else - body = Rack::Multipart.build_multipart(params.body_params) - - if body - json['headers']['Content-Length'] ||= body.length.to_s - json['headers']['Content-Type'] = "multipart/form-data; boundary=#{Rack::Multipart::MULTIPART_BOUNDARY}" - else - body = Rack::Utils.build_nested_query(params.body_params) - end - end - - json['body'] = Base64.encode64(body) - json['isBase64Encoded'] = true - end - - json['queryStringParameters'] = params.query_params if params.query_params.present? - - json - end - - def path_params - params.path_params.reverse_merge(extract_parameters) - end - memoize :path_params - - def extract_parameters - route.extract_parameters(normalized_path).symbolize_keys - end - - def normalized_path - path = self.path - path = path[0..-2] if path.end_with? '/' - path = path[1..-1] if path.start_with? '/' - path = strip_query_string(path) - path - end - - def strip_query_string(path) - path.sub(/\?.*/, '') - end - - def route - Jets::Router::Matcher.new(Jets.application.routes).find_by_env( - "REQUEST_METHOD" => method.to_s.upcase, - "PATH_INFO" => path, - ) - end - memoize :route - - def request - Jets::Controller::Request.new(event: event) - end - memoize :request - - def dispatch! - klass = Object.const_get(route.controller_name) - context = Jets::Controller::Middleware::Mimic::LambdaContext.new - rack_env = Jets::Controller::RackAdapter::Env.new(event, context).convert - controller = klass.new(event, context, route.action_name, rack_env) - response = controller.process! # response is API Gateway Hash - - unless response['statusCode'] && response['body'] - raise "Expected response to an API Gateway Hash Structure. Are you rendering correctly?" - end - - Response.new(response) # converts APIGW hash to prettier object - end - end -end diff --git a/lib/jets/spec_helpers/controllers/response.rb b/lib/jets/spec_helpers/controllers/response.rb deleted file mode 100644 index 0e896f6e5..000000000 --- a/lib/jets/spec_helpers/controllers/response.rb +++ /dev/null @@ -1,10 +0,0 @@ -module Jets::SpecHelpers::Controllers - class Response - attr_reader :status, :headers, :body - def initialize(response) - @status = response['statusCode'].to_i - @headers = response['headers'] - @body = response['body'] - end - end -end diff --git a/lib/jets/spec_helpers/fixtures.rb b/lib/jets/spec_helpers/fixtures.rb deleted file mode 100644 index a102648de..000000000 --- a/lib/jets/spec_helpers/fixtures.rb +++ /dev/null @@ -1,11 +0,0 @@ -module Jets::SpecHelpers - module Fixtures - def fixture_path(filename) - "#{Jets.root}/spec/fixtures/#{filename}" - end - - def fixture_file(filename) - File.new(fixture_path(filename)) - end - end -end diff --git a/lib/jets/stack.rb b/lib/jets/stack.rb index c063755e9..cd64c79d3 100644 --- a/lib/jets/stack.rb +++ b/lib/jets/stack.rb @@ -1,9 +1,6 @@ module Jets class Stack - include Main::Dsl - include Parameter::Dsl - include Output::Dsl - include Resource::Dsl + include Dsl class << self extend Memoist @@ -15,65 +12,21 @@ def subclasses def inherited(base) super - self.subclasses << base if base.name + subclasses << base if base.name end - # klass = Jets::Stack.new_class("Bucket3") - def new_class(class_name, &block) - # https://stackoverflow.com/questions/4113479/dynamic-class-definition-with-a-class-name - # Defining the constant this way gets around: SyntaxError: dynamic constant assignment error - klass = Class.new(Jets::Stack) # First klass is an anonymous class. IE: class.name is nil - klass = Object.const_set(class_name, klass) # now klass is a named class - Jets::Stack.subclasses << klass # mimic inherited hook because - - # Must run class_eval after adding to subclasses in order for the resource declarations in the - # so that the resources get registered to the right subclass. - klass.class_eval(&block) - klass # return klass - end - - # Build it to figure out if we need to build the stack for the SharedBuilder - def build? - empty = template == {"Parameters"=>{"IamRole"=>{"Type"=>"String"}, "S3Bucket"=>{"Type"=>"String"}}} - !empty - end - - def functions - stack = new - # All the & because resources might be nil - templates = stack.resources&.map(&:template)&.select do |t| - attributes = t.values.first - attributes[:Type] == 'AWS::Lambda::Function' - end - templates ||= [] - templates.map { |t| Function.new(t) } - end - - def template - # Pretty funny looking, creating an instance of stack to be passed to the Builder. - # Another way of looking at it: - # - # stack = new # MyStack.new - # builder = Jets::Stack::Builder.new(stack) - # - builder = Jets::Stack::Builder.new(new) - builder.template - end - memoize :template - - def lookup(logical_id) - looker.output(logical_id) - end - - def looker - Jets::Stack::Output::Lookup.new(self) + # Do not name this output, it'll collide with the output DSL method + def output_value(logical_id) + puts "lookup logical_id: #{logical_id}" + outputs.value(logical_id) end - memoize :looker + # Keep lookup for backwards compatibility + alias_method :lookup, :output_value - def output_keys - outputs = new.outputs || [] - outputs.map(&:template).map {|o| o.keys.first} + def outputs + Outputs.new(self) end + memoize :outputs end end end diff --git a/lib/jets/stack/builder.rb b/lib/jets/stack/builder.rb deleted file mode 100644 index 8f1503022..000000000 --- a/lib/jets/stack/builder.rb +++ /dev/null @@ -1,44 +0,0 @@ -class Jets::Stack - class Builder - extend Memoist - include Jets::Util::Camelize - - def initialize(stack) - @stack = stack - @template = {} # will build this structure up - end - - def template - build_section(:parameters) - build_section(:resources) - build_section(:outputs) - camelize(@template).deep_stringify_keys - end - memoize :template - - def build_section(section) - elements = build_elements(section) - return unless elements - - if section == :parameters - elements[:GemLayer] = {Type: "String"} if Jets.gem_layer? - end - @template[section] = elements - end - - def build_elements(section) - # s is a "section element". Examples: - # - # Jets::Stack::Parameter - # Jets::Stack::Resource - # Jets::Stack::Output - # - section_elements = @stack.send(section) - return unless section_elements - - section_elements.inject({}) do |template_section, s| - template_section.merge(s.template) - end - end - end -end diff --git a/lib/jets/stack/definition.rb b/lib/jets/stack/definition.rb deleted file mode 100644 index be0b51379..000000000 --- a/lib/jets/stack/definition.rb +++ /dev/null @@ -1,47 +0,0 @@ -# Class that include Definition should implement: -# -# template - method should use @definition to build a CloudFormation template section -# -class Jets::Stack - module Definition - extend ActiveSupport::Concern - include Jets::Util::Camelize - - # Example of usage that leads here: - # - # Parameter.new(self, *definition).register - # - # Which is defined in parameter/dsl.rb - # - # Example subclass: ExampleStack < Jets::Stack - def initialize(subclass, *definition) - @subclass = subclass.to_s # important to use to_s, dont want the object as keys in @definitions - @definition = definition.flatten - end - - def register - self.class.register(@subclass, *@definition) - end - - class_methods do - def register(subclass, *definition) - @definitions ||= {} - @definitions[subclass.to_s] ||= [] - # Create instance of the CloudFormation section class and register it. Examples: - # Stack::Parameter.new(definition) - # Stack::Resource.new(definition) - # Stack::Output.new(definition) - @definitions[subclass.to_s] << new(subclass, definition) - end - - def definitions(subclass) - @definitions ||= {} - @definitions[subclass.to_s] - end - - def all_definitions - @definitions - end - end - end -end diff --git a/lib/jets/stack/depends.rb b/lib/jets/stack/depends.rb deleted file mode 100644 index e06285965..000000000 --- a/lib/jets/stack/depends.rb +++ /dev/null @@ -1,34 +0,0 @@ -class Jets::Stack - class Depends - def initialize(items) - @items = items # Jets::Stack::Depends::Item - has stack and options properties - end - - def params - result = {} - @items.each do |item| - class_name = item.class_name - dependency_outputs(class_name).each do |output| - dependency_class = class_name.to_s.camelize - output_key = item.options[:class_prefix] ? - "#{dependency_class}#{output}" : # already camelized - output - - output_value = "!GetAtt #{dependency_class}.Outputs.#{output}" - result[output_key] = output_value - end - end - result - end - - # Returns CloudFormation template logical ids - def stack_list - @items.map(&:logical_id) - end - - private - def dependency_outputs(class_name) - class_name.to_s.camelize.constantize.output_keys - end - end -end diff --git a/lib/jets/stack/depends/item.rb b/lib/jets/stack/depends/item.rb deleted file mode 100644 index 34708b645..000000000 --- a/lib/jets/stack/depends/item.rb +++ /dev/null @@ -1,26 +0,0 @@ -# Usage examples: -# -# Jets::Stack::Depends::Item.new(:custom) -# Jets::Stack::Depends::Item.new(:custom, :alert) -# Jets::Stack::Depends::Item.new(:custom, class_prefix: true) -# Jets::Stack::Depends::Item.new(:custom, :alert, class_prefix: true) -# -# The Jets::Stack::Depends#params uses the options to determine if the class prefix should be added. -# -class Jets::Stack::Depends - class Item - attr_reader :stack, :options - def initialize(stack, options={}) - @stack = stack # should be underscore format. IE: admin/posts_controller - @options = options - end - - def logical_id - @stack.to_s.gsub('::','').gsub('/','_').camelize - end - - def class_name - @stack.to_s.camelize - end - end -end diff --git a/lib/jets/stack/dsl.rb b/lib/jets/stack/dsl.rb new file mode 100644 index 000000000..2af4a5e83 --- /dev/null +++ b/lib/jets/stack/dsl.rb @@ -0,0 +1,10 @@ +class Jets::Stack + module Dsl + extend ActiveSupport::Concern + + include Main + include Output + include Parameter + include Resource + end +end diff --git a/lib/jets/stack/dsl/main.rb b/lib/jets/stack/dsl/main.rb new file mode 100644 index 000000000..aef41a457 --- /dev/null +++ b/lib/jets/stack/dsl/main.rb @@ -0,0 +1,15 @@ +module Jets::Stack::Dsl + module Main + extend ActiveSupport::Concern + + class_methods do + include Base + include Cloudwatch + include Iam + include Lambda + include S3 + include Sns + include Sqs + end + end +end diff --git a/lib/jets/stack/dsl/main/base.rb b/lib/jets/stack/dsl/main/base.rb new file mode 100644 index 000000000..e92546b6d --- /dev/null +++ b/lib/jets/stack/dsl/main/base.rb @@ -0,0 +1,42 @@ +module Jets::Stack::Dsl::Main + module Base + def ref(value) + end + + # Examples: + # get_attr("logical_id.attribute") + # get_attr("logical_id", "attribute") + # get_attr(["logical_id", "attribute"]) + def get_att(*item) + end + + def logical_id(value) + end + + def depends_on(*stacks) + end + + # Due to `if Jets::Stack.has_resources?` check early on in the bootstraping process + # The code has not been built at that point. So we use a placeholder and will replace + # the placeholder as part of the cfn template build process after the code has been built + # and the code_s3_key with md5 is available. + def code_s3_key + end + + # resource(:hello, + # function_name: "hello", + # code: { + # s3_bucket: "!Ref S3Bucket", + # s3_key: code_s3_key + # }, + # description: "Hello world", + # handler: handler_function("hello.lambda_handler"), + # memory_size: 128, + # role: "!Ref IamRole", + # runtime: "python3.6", + # timeout: 20, + # ) + def handler(name) + end + end +end diff --git a/lib/jets/stack/dsl/main/cloudwatch.rb b/lib/jets/stack/dsl/main/cloudwatch.rb new file mode 100644 index 000000000..c2d4f1dd6 --- /dev/null +++ b/lib/jets/stack/dsl/main/cloudwatch.rb @@ -0,0 +1,6 @@ +module Jets::Stack::Dsl::Main + module Cloudwatch + def cloudwatch_alarm(id, hash = {}) + end + end +end diff --git a/lib/jets/stack/dsl/main/iam.rb b/lib/jets/stack/dsl/main/iam.rb new file mode 100644 index 000000000..22f61ee91 --- /dev/null +++ b/lib/jets/stack/dsl/main/iam.rb @@ -0,0 +1,6 @@ +module Jets::Stack::Dsl::Main + module Iam + def iam_role(id, props = {}) + end + end +end diff --git a/lib/jets/stack/dsl/main/kinesis.rb b/lib/jets/stack/dsl/main/kinesis.rb new file mode 100644 index 000000000..b49a0b5e7 --- /dev/null +++ b/lib/jets/stack/dsl/main/kinesis.rb @@ -0,0 +1,6 @@ +module Jets::Stack::Dsl::Main + module Kinesis + def kinesis_stream(id, props = {}) + end + end +end diff --git a/lib/jets/stack/dsl/main/lambda.rb b/lib/jets/stack/dsl/main/lambda.rb new file mode 100644 index 000000000..288ed1725 --- /dev/null +++ b/lib/jets/stack/dsl/main/lambda.rb @@ -0,0 +1,39 @@ +module Jets::Stack::Dsl::Main + module Lambda + # Example: + # + # function(:hello, + # handler: handler("hello.lambda_hander"), + # runtime: "python3.6" + # ) + # + # Defaults to ruby. So: + # + # function(:hello) + # + # is the same as: + # + # function(:hello, + # handler: handler("hello.hande"), + # runtime: :ruby + # ) + # + def function(id, props = {}) + end + alias_method :ruby_function, :function + alias_method :lambda_function, :function + + def python_function(id, props = {}) + end + + def node_function(id, props = {}) + end + + # Usage: + # + # permission(:my_permission, principal: "events.amazonaws.com") + # + def permission(id, props = {}) + end + end +end diff --git a/lib/jets/stack/dsl/main/s3.rb b/lib/jets/stack/dsl/main/s3.rb new file mode 100644 index 000000000..5f4218370 --- /dev/null +++ b/lib/jets/stack/dsl/main/s3.rb @@ -0,0 +1,9 @@ +module Jets::Stack::Dsl::Main + module S3 + def s3_bucket(id, props = {}) + end + + def s3_bucket_configuration(id, props = {}) + end + end +end diff --git a/lib/jets/stack/dsl/main/sns.rb b/lib/jets/stack/dsl/main/sns.rb new file mode 100644 index 000000000..9fe5a0383 --- /dev/null +++ b/lib/jets/stack/dsl/main/sns.rb @@ -0,0 +1,12 @@ +module Jets::Stack::Dsl::Main + module Sns + def sns_topic(id, props = {}) + end + + def sns_topic_policy(id, props = {}) + end + + def sns_subscription(id, props = {}) + end + end +end diff --git a/lib/jets/stack/dsl/main/sqs.rb b/lib/jets/stack/dsl/main/sqs.rb new file mode 100644 index 000000000..d08decbd5 --- /dev/null +++ b/lib/jets/stack/dsl/main/sqs.rb @@ -0,0 +1,6 @@ +module Jets::Stack::Dsl::Main + module Sqs + def sqs_queue(id, props = {}) + end + end +end diff --git a/lib/jets/stack/dsl/output.rb b/lib/jets/stack/dsl/output.rb new file mode 100644 index 000000000..ff795dfa9 --- /dev/null +++ b/lib/jets/stack/dsl/output.rb @@ -0,0 +1,11 @@ +module Jets::Stack::Dsl + module Output + extend ActiveSupport::Concern + + class_methods do + def output(*definition) + {} + end + end + end +end diff --git a/lib/jets/stack/dsl/parameter.rb b/lib/jets/stack/dsl/parameter.rb new file mode 100644 index 000000000..b02019290 --- /dev/null +++ b/lib/jets/stack/dsl/parameter.rb @@ -0,0 +1,11 @@ +module Jets::Stack::Dsl + module Parameter + extend ActiveSupport::Concern + + class_methods do + def parameter(*definition) + {} + end + end + end +end diff --git a/lib/jets/stack/dsl/resource.rb b/lib/jets/stack/dsl/resource.rb new file mode 100644 index 000000000..f555dadce --- /dev/null +++ b/lib/jets/stack/dsl/resource.rb @@ -0,0 +1,11 @@ +module Jets::Stack::Dsl + module Resource + extend ActiveSupport::Concern + + class_methods do + def resource(*definition) + {} + end + end + end +end diff --git a/lib/jets/stack/function.rb b/lib/jets/stack/function.rb deleted file mode 100644 index ce37842a4..000000000 --- a/lib/jets/stack/function.rb +++ /dev/null @@ -1,71 +0,0 @@ -class Jets::Stack - class Function - extend Memoist - - attr_reader :template - def initialize(template) - @template = template - end - - def meth - attributes = @template.values.first - handler = attributes[:Properties][:Handler] - handler.split('.').last - end - - def lang - return if internal? - - if source_file - # Detect language from file extension - ext = File.extname(source_file).sub(/^\./,'').to_sym - lang_map[ext] - else - puts "WARN: Unable to find a source file for function. Looked at: #{search_expression}".color(:yellow) - end - end - - def lang_map - { - rb: :ruby, - py: :python, - js: :node, - } - end - - def source_file - Dir.glob(search_expression).first - end - memoize :source_file - - def search_expression - base_search_expression.sub('handlers/shared/', "#{Jets.root}/app/shared/") - end - - def internal_search_expression - internal = File.expand_path("../internal", File.dirname(__FILE__)) - base_search_expression.sub('handlers/shared/', "#{internal}/app/shared/") - end - - def base_search_expression - attributes = @template.values.first - handler = attributes[:Properties][:Handler] - handler.split('.')[0..-2].join('.') + '.*' # search_expression - # Example: handlers/shared/functions/jets/s3_bucket_config.* - end - - # Internal flag is mainly used to disable WARN messages - def internal? - return true if internal_search_expression.include?("jets/base_path") - return true if internal_search_expression.include?("jets/s3_bucket_config") - !!Dir.glob(internal_search_expression).first - end - - # Relative path - # app/shared/functions/kevin.py => handlers/shared/functions/kevin.py - def handler_dest - return unless source_file - source_file.sub(%r{.*/app/}, "handlers/") - end - end -end \ No newline at end of file diff --git a/lib/jets/stack/main.rb b/lib/jets/stack/main.rb deleted file mode 100644 index b0cea1105..000000000 --- a/lib/jets/stack/main.rb +++ /dev/null @@ -1,4 +0,0 @@ -class Jets::Stack - class Main - end -end diff --git a/lib/jets/stack/main/dsl.rb b/lib/jets/stack/main/dsl.rb deleted file mode 100644 index 2bef56642..000000000 --- a/lib/jets/stack/main/dsl.rb +++ /dev/null @@ -1,17 +0,0 @@ -class Jets::Stack - class Main - module Dsl - extend ActiveSupport::Concern - - class_methods do - include Base - include Cloudwatch - include Iam - include Lambda - include S3 - include Sns - include Sqs - end - end - end -end diff --git a/lib/jets/stack/main/dsl/base.rb b/lib/jets/stack/main/dsl/base.rb deleted file mode 100644 index 30d04e9de..000000000 --- a/lib/jets/stack/main/dsl/base.rb +++ /dev/null @@ -1,63 +0,0 @@ -module Jets::Stack::Main::Dsl - module Base - def ref(value) - "!Ref #{value.to_s.camelize}" - end - - # Examples: - # get_attr("logical_id.attribute") - # get_attr("logical_id", "attribute") - # get_attr(["logical_id", "attribute"]) - def get_att(*item) - item = item.flatten - options = item.last.is_a?(Hash) ? item.pop : {} - - # list is an Array - list = if item.size == 1 - item.first.split('.') - else - item - end - list.map! { |s| s.to_s.camelize } unless options[:autoformat] == false - { "Fn::GetAtt" => list } - end - - def logical_id(value) - value.to_s.camelize - end - - def depends_on(*stacks) - if stacks == [] - @depends_on - else - @depends_on ||= [] - @depends_on += stacks - end - end - - # Due to `if Jets::Stack.has_resources?` check early on in the bootstraping process - # The code has not been built at that point. So we use a placeholder and will replace - # the placeholder as part of the cfn template build process after the code has been built - # and the code_s3_key with md5 is available. - def code_s3_key - "code_s3_key_placeholder" - end - - # resource(:hello, - # function_name: "hello", - # code: { - # s3_bucket: "!Ref S3Bucket", - # s3_key: code_s3_key - # }, - # description: "Hello world", - # handler: handler_function("hello.lambda_handler"), - # memory_size: 128, - # role: "!Ref IamRole", - # runtime: "python3.6", - # timeout: 20, - # ) - def handler(name) - "handlers/shared/functions/#{name}" # generated handler - end - end -end \ No newline at end of file diff --git a/lib/jets/stack/main/dsl/cloudwatch.rb b/lib/jets/stack/main/dsl/cloudwatch.rb deleted file mode 100644 index e4315bc83..000000000 --- a/lib/jets/stack/main/dsl/cloudwatch.rb +++ /dev/null @@ -1,19 +0,0 @@ -module Jets::Stack::Main::Dsl - module Cloudwatch - def cloudwatch_alarm(id, hash={}) - if hash.key?(:DependsOn) - attributes = hash # leave structure alone and add type only - attributes[:Type] = "AWS::CloudWatch::Alarm" - else - # the attributes are properties - properties = hash - attributes = { - Type: "AWS::CloudWatch::Alarm", - Properties: properties, - } - end - resource(id, attributes) - output(id) - end - end -end \ No newline at end of file diff --git a/lib/jets/stack/main/dsl/iam.rb b/lib/jets/stack/main/dsl/iam.rb deleted file mode 100644 index dec1f217f..000000000 --- a/lib/jets/stack/main/dsl/iam.rb +++ /dev/null @@ -1,8 +0,0 @@ -module Jets::Stack::Main::Dsl - module Iam - def iam_role(id, props={}) - resource(id, "AWS::IAM::Role", props) - output(id) # IAM Arn - end - end -end diff --git a/lib/jets/stack/main/dsl/kinesis.rb b/lib/jets/stack/main/dsl/kinesis.rb deleted file mode 100644 index 319934b9e..000000000 --- a/lib/jets/stack/main/dsl/kinesis.rb +++ /dev/null @@ -1,15 +0,0 @@ -module Jets::Stack::Main::Dsl - module Kinesis - def kinesis_stream(id, props={}) - defaults = { - Name: id, - ShardCount: 1 - } - - props = defaults.merge(props) - - resource(id, "AWS::Kinesis::Stream", props) - output(id) - end - end -end diff --git a/lib/jets/stack/main/dsl/lambda.rb b/lib/jets/stack/main/dsl/lambda.rb deleted file mode 100644 index 9bfb5f810..000000000 --- a/lib/jets/stack/main/dsl/lambda.rb +++ /dev/null @@ -1,90 +0,0 @@ -module Jets::Stack::Main::Dsl - module Lambda - include Jets::Util::Camelize - - # Example: - # - # function(:hello, - # handler: handler("hello.lambda_hander"), - # runtime: "python3.6" - # ) - # - # Defaults to ruby. So: - # - # function(:hello) - # - # is the same as: - # - # function(:hello, - # handler: handler("hello.hande"), - # runtime: :ruby - # ) - # - def function(id, props={}) - # Required: code, handler, role, runtime Docs: https://amzn.to/2pdot7S - meth = sanitize_method_name(id) - class_namespace = self.to_s.underscore.gsub('/','-') # IE: Jets::Domain => jets-domain - description = "#{self.to_s} #{meth}" # not bother adding extension - defaults = { - Code: { - S3Bucket: "!Ref S3Bucket", - S3Key: code_s3_key - }, - Role: "!Ref IamRole", - Handler: "#{id}.lambda_handler", # default ruby convention - Timeout: Jets.config.function.timeout, - MemorySize: Jets.config.function.memory_size, - EphemeralStorage: Jets.config.function.ephemeral_storage, - Description: description, - } - - function_name = "#{Jets.project_namespace}-#{class_namespace}-#{meth}" - function_name = function_name.size > Jets::Cfn::Resource::Lambda::Function::MAX_FUNCTION_NAME_SIZE ? nil : function_name - defaults[:FunctionName] = function_name if function_name - - props = defaults.merge(props) - # shared/functions do not include the GemLayer and no custom runtime support - props[:Runtime] ||= Jets.ruby_runtime - props[:Handler] = handler(props[:Handler]) - - logical_id = id.to_s.gsub('/','_') - resource(logical_id, "AWS::Lambda::Function", props) - end - alias_method :ruby_function, :function - alias_method :lambda_function, :function - - def python_function(id, props={}) - meth = sanitize_method_name(id) - props[:Handler] ||= "#{meth}.lambda_handler" # default python convention - props[:Runtime] ||= default_runtimes[:python] - function(id, props) - end - - def default_runtimes - Jets::Cfn::Resource::Lambda::Function.default_runtimes - end - - def node_function(id, props={}) - meth = sanitize_method_name(id) - props[:Handler] ||= "#{meth}.handler" # default python convention - props[:Runtime] ||= default_runtimes[:node] - function(id, props) - end - - # Usage: - # - # permission(:my_permission, principal: "events.amazonaws.com") - # - def permission(id, props={}) - defaults = { Action: "lambda:InvokeFunction" } - props = defaults.merge(props) - resource(id, "AWS::Lambda::Permission", props) - end - - private - # demo-dev-hard_job-dig_me - def sanitize_method_name(id) - id.to_s.gsub('/','-') - end - end -end \ No newline at end of file diff --git a/lib/jets/stack/main/dsl/s3.rb b/lib/jets/stack/main/dsl/s3.rb deleted file mode 100644 index d27bfd3e3..000000000 --- a/lib/jets/stack/main/dsl/s3.rb +++ /dev/null @@ -1,12 +0,0 @@ -module Jets::Stack::Main::Dsl - module S3 - def s3_bucket(id, props={}) - resource(id, "AWS::S3::Bucket", props) - output(id) # Bucket name - end - - def s3_bucket_configuration(id, props={}) - resource(id, "Custom::S3BucketConfiguration", props) - end - end -end diff --git a/lib/jets/stack/main/dsl/sns.rb b/lib/jets/stack/main/dsl/sns.rb deleted file mode 100644 index 48ccc18d7..000000000 --- a/lib/jets/stack/main/dsl/sns.rb +++ /dev/null @@ -1,16 +0,0 @@ -module Jets::Stack::Main::Dsl - module Sns - def sns_topic(id, props={}) - resource(id, "AWS::SNS::Topic", props) - output(id) # Topic Arn - end - - def sns_topic_policy(id, props={}) - resource(id, "AWS::SNS::TopicPolicy", props) - end - - def sns_subscription(id, props={}) - resource(id, "AWS::SNS::Subscription", props) - end - end -end \ No newline at end of file diff --git a/lib/jets/stack/main/dsl/sqs.rb b/lib/jets/stack/main/dsl/sqs.rb deleted file mode 100644 index 66fc62e2a..000000000 --- a/lib/jets/stack/main/dsl/sqs.rb +++ /dev/null @@ -1,10 +0,0 @@ -module Jets::Stack::Main::Dsl - module Sqs - def sqs_queue(id, props={}) - # props[:queue_name] ||= id.to_s # comment out to allow CloudFormation to generate name - resource(id, "AWS::SQS::Queue", props) - output(id, Value: get_att("#{id}.Arn")) # normal !Ref returns the sqs url the ARN is useful for nested stacks depends_on - output("#{id}_url", ref(id)) # useful for Stack.lookup method. IE: List.lookup(:waitlist_url) - end - end -end diff --git a/lib/jets/stack/output.rb b/lib/jets/stack/output.rb deleted file mode 100644 index 2f2f7844a..000000000 --- a/lib/jets/stack/output.rb +++ /dev/null @@ -1,35 +0,0 @@ -# Implements: -# -# template - uses @definition to build a CloudFormation template section -# -class Jets::Stack - class Output - include Definition - - def template - camelize(standarize(@definition)) - end - - # Value is the only required property: https://amzn.to/2xbhmk3 - def standarize(definition) - first, second, _ = definition - if definition.size == 1 && first.is_a?(Hash) # long form - first # pass through - elsif definition.size == 2 && second.is_a?(Hash) # medium form - logical_id, properties = first, second - { logical_id => properties } - elsif definition.size == 2 && second.is_a?(String) # short form - logical_id = first - properties = second.is_a?(String) ? { Value: second } : {} - { logical_id => properties } - elsif definition.size == 1 - logical_id = first.to_s - properties = {Value: "!Ref #{logical_id.camelize}"} - { logical_id => properties } - else # I dont know what form - raise "Invalid form provided. definition #{definition.inspect}" - end - end - end -end - diff --git a/lib/jets/stack/output/dsl.rb b/lib/jets/stack/output/dsl.rb deleted file mode 100644 index e757ae86f..000000000 --- a/lib/jets/stack/output/dsl.rb +++ /dev/null @@ -1,19 +0,0 @@ -class Jets::Stack - class Output - module Dsl - extend ActiveSupport::Concern - - def outputs - Output.definitions(self.class) - end - - class_methods do - def output(*definition) - # self is subclass is the stack that inherits from Jets::Stack - # IE: ExampleStack < Jets::Stack - Output.new(self, *definition).register - end - end - end - end -end diff --git a/lib/jets/stack/output/lookup.rb b/lib/jets/stack/output/lookup.rb deleted file mode 100644 index 446afba81..000000000 --- a/lib/jets/stack/output/lookup.rb +++ /dev/null @@ -1,40 +0,0 @@ -class Jets::Stack::Output - class Lookup - include Jets::AwsServices - - def initialize(stack_subclass) - @stack_subclass = stack_subclass - end - - @@cache = {} - def output(logical_id) - cache_key = "#{@stack_subclass}-#{logical_id}" - return @@cache[cache_key] if @@cache[cache_key] - - child_stack_id = @stack_subclass.to_s.camelize - - stack_arn = shared_stack_arn(child_stack_id) - resp = cfn.describe_stacks(stack_name: stack_arn) - child = resp.stacks.first - return unless child - - @@cache[cache_key] = output_value(child, logical_id) - end - - # Shared child stack arn - def shared_stack_arn(logical_id) - parent_stack = Jets.project_namespace - resp = cfn.describe_stacks(stack_name: parent_stack) - parent = resp.stacks.first - output_value(parent, logical_id) - end - - def output_value(stack, key) - key = key.to_s.camelize - output = stack.outputs.find do |o| - o.output_key == key - end - output&.output_value - end - end -end \ No newline at end of file diff --git a/lib/jets/stack/outputs.rb b/lib/jets/stack/outputs.rb new file mode 100644 index 000000000..0da5367ca --- /dev/null +++ b/lib/jets/stack/outputs.rb @@ -0,0 +1,41 @@ +class Jets::Stack + class Outputs + include Jets::AwsServices + + def initialize(stack_subclass) + @stack_subclass = stack_subclass + end + + @@cache = {} + def value(logical_id) + logical_id = logical_id.to_s.camelize + cache_key = "#{@stack_subclass}:#{logical_id}" + return @@cache[cache_key] if @@cache[cache_key] + + child_stack_id = @stack_subclass.to_s.camelize + + stack_arn = shared_stack_arn(child_stack_id) + resp = cfn.describe_stacks(stack_name: stack_arn) + child = resp.stacks.first + return unless child + + @@cache[cache_key] = output_value(child, logical_id) + end + + # Shared child stack arn + def shared_stack_arn(logical_id) + parent_stack = Jets.project.namespace + resp = cfn.describe_stacks(stack_name: parent_stack) + parent = resp.stacks.first + output_value(parent, logical_id) + end + + def output_value(stack, key) + key = key.to_s.camelize + output = stack.outputs.find do |o| + o.output_key == key + end + output&.output_value + end + end +end diff --git a/lib/jets/stack/parameter.rb b/lib/jets/stack/parameter.rb deleted file mode 100644 index 2c0c56fae..000000000 --- a/lib/jets/stack/parameter.rb +++ /dev/null @@ -1,37 +0,0 @@ -# Implements: -# -# template - uses @definition to build a CloudFormation template section -# -class Jets::Stack - class Parameter - include Definition - - def template - camelize(add_required(standarize(@definition))) - end - - # Type is the only required property: https://amzn.to/2x8W5aD - def standarize(definition) - first, second, _ = definition - if definition.size == 1 && first.is_a?(Hash) # long form - first # pass through - elsif definition.size == 2 && second.is_a?(Hash) # medium form - logical_id, properties = first, second - { logical_id => properties } - elsif (definition.size == 2 && second.is_a?(String)) || # short form - definition.size == 1 - logical_id = first - properties = second.is_a?(String) ? { Default: second } : {} - { logical_id => properties } - else # I dont know what form - raise "Invalid form provided. definition #{definition.inspect}" - end - end - - def add_required(attributes) - properties = attributes.values.first - properties[:Type] ||= 'String' - attributes - end - end -end diff --git a/lib/jets/stack/parameter/dsl.rb b/lib/jets/stack/parameter/dsl.rb deleted file mode 100644 index 5473b6e69..000000000 --- a/lib/jets/stack/parameter/dsl.rb +++ /dev/null @@ -1,42 +0,0 @@ -class Jets::Stack - class Parameter - module Dsl - extend ActiveSupport::Concern - extend Memoist - - def parameters - add_common_parameters - add_depends_on_parameters - Parameter.definitions(self.class) - end - - def add_common_parameters - self.class.parameter(:iam_role) - self.class.parameter(:s3_bucket) - end - - def add_depends_on_parameters - depends_on = self.class.depends_on - depends_on.each do |dependency| - dependency_outputs(dependency).each do |output| - self.class.parameter(output) - end - end if depends_on - end - memoize :add_depends_on_parameters # only run once - - # Returns output keys associated with the stack. They are the resource logical ids. - def dependency_outputs(dependency) - dependency.to_s.camelize.constantize.output_keys - end - - class_methods do - def parameter(*definition) - # self is subclass is the stack that inherits from Jets::Stack - # IE: ExampleStack < Jets::Stack - Parameter.new(self, *definition).register - end - end - end - end -end diff --git a/lib/jets/stack/resource.rb b/lib/jets/stack/resource.rb deleted file mode 100644 index 6b561c443..000000000 --- a/lib/jets/stack/resource.rb +++ /dev/null @@ -1,31 +0,0 @@ -# Implements: -# -# template - uses @definition to build a CloudFormation template section -# -class Jets::Stack - class Resource - include Definition - - def template - template = camelize(standarize(@definition)) - template = replace_placeholers(template) - template - end - - # CloudFormation Resources reference: https://amzn.to/2NKg6ip - def standarize(definition) - Jets::Cfn::Resource::Standardizer.new(definition).template - end - - def replace_placeholers(template) - attributes = template.values.first - s3_key = attributes.dig(:Properties,:Code,:S3Key) - if s3_key == "code_s3_key_placeholder" - checksum = Jets::Builders::Md5.checksums["stage/code"] - code_zip = "code-#{checksum}.zip" - attributes[:Properties][:Code][:S3Key] = "jets/code/#{code_zip}" - end - template - end - end -end diff --git a/lib/jets/stack/resource/dsl.rb b/lib/jets/stack/resource/dsl.rb deleted file mode 100644 index ab97e2b8d..000000000 --- a/lib/jets/stack/resource/dsl.rb +++ /dev/null @@ -1,19 +0,0 @@ -class Jets::Stack - class Resource - module Dsl - extend ActiveSupport::Concern - - def resources - Resource.definitions(self.class) - end - - class_methods do - def resource(*definition) - # self is subclass is the stack that inherits from Jets::Stack - # IE: ExampleStack < Jets::Stack - Resource.new(self, *definition).register - end - end - end - end -end diff --git a/lib/jets/stack/s3_event.rb b/lib/jets/stack/s3_event.rb deleted file mode 100644 index a497b243e..000000000 --- a/lib/jets/stack/s3_event.rb +++ /dev/null @@ -1,101 +0,0 @@ -class Jets::Stack - class S3Event - def initialize(bucket_name) - @bucket_name = bucket_name - end - - # Stack names can only contain alpha numeric chars. - # Bucket names are limit to 64 chars: https://amzn.to/2SIzvme - # Stack names are limit to 128 chars: https://amzn.to/2SFkrG0 - # This gsub should handle this. - def stack_name - @bucket_name.gsub(/[^0-9a-z\-_]/i, '').gsub('-','_').camelize - end - - def build_stack - # assign to local variable so its available in the block - bucket = @bucket_name - # closures - configure_bucket = unless Jets.config.s3_event.configure_bucket.nil? - puts "DEPRECATION WARNING: Jets.config.s3_event.configure_bucket is deprecated. Please use Jets.config.events.s3.configure_bucket instead." - Jets.config.s3_event.configure_bucket - else - Jets.config.events.s3.configure_bucket - end - - notification_configuration = unless Jets.config.s3_event.notification_configuration.nil? - puts "DEPRECATION WARNING: Jets.config.s3_event.notification_configuration is deprecated. Please use Jets.config.events.s3.notification_configuration instead." - Jets.config.s3_event.notification_configuration - else - Jets.config.events.s3.notification_configuration - end - - Jets::Stack.new_class(stack_name) do - s3_bucket_configuration(:S3BucketConfiguration, - ServiceToken: "!GetAtt JetsS3BucketConfig.Arn", # Cannot change this w/o changing the logical id - # These properties correspond to the ruby aws-sdk s3.put_bucket_notification_configuration - # in jets/s3_bucket_config.rb, not the CloudFormation Bucket properties. The CloudFormation - # bucket properties have a similiar structure but is slightly different so it can be confusing. - # - # Ruby aws-sdk S3 Docs: https://amzn.to/2N7m5Lr - Bucket: bucket, - NotificationConfiguration: notification_configuration, - ) if configure_bucket - - # Important note: If we change the name of this function we should also change the - # logical id of the s3_bucket_configuration custom resource or we'll get this error: - # Modifying service token is not allowed. - function("jets/s3_bucket_config", - Role: "!GetAtt BucketConfigIamRole.Arn", - Layers: ["!Ref GemLayer"], - ) - - sns_topic(:SnsTopic) - sns_topic_policy(:SnsTopicPolicy, - PolicyDocument: { - Version: "2012-10-17", - Statement: { - Effect: "Allow", - Principal: { Service: "s3.amazonaws.com"}, - Action: "sns:Publish", - Resource: "!Ref SnsTopic", - Condition: { - ArnLike: { - "Aws:SourceArn" => "!Sub arn:aws:s3:*:*:#{bucket}" - } - } - } - }, - Topics: ["!Ref SnsTopic"], - ) - - iam_role(:BucketConfigIamRole, - AssumeRolePolicyDocument: { - Version: '2012-10-17', - Statement: [ - Effect: "Allow", - Principal: {Service: ["lambda.amazonaws.com"]}, - Action: ['sts:AssumeRole'], - ] - }, - Path: "/", - ManagedPolicyArns: ["arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"], - Policies: [ - PolicyName: "S3Policy", - PolicyDocument: { - Version: '2012-10-17', - Statement: [ - Effect: "Allow", - Action: [ - 's3:GetBucketNotification', - 's3:PutBucketNotification', - ], - Resource: "*" - ] - } - ] - ) - end - end - end -end \ No newline at end of file diff --git a/lib/jets/tasks.rb b/lib/jets/tasks.rb deleted file mode 100644 index 0257a9341..000000000 --- a/lib/jets/tasks.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -require "rake" - -# Load Jets Rakefile extensions -%w( - framework - log - middleware - misc - tmp - yarn - zeitwerk -).tap { |arr| - arr << "statistics" if Rake.application.current_scope.empty? -}.each do |task| - load "jets/tasks/#{task}.rake" -end diff --git a/lib/jets/tasks/engine.rake b/lib/jets/tasks/engine.rake deleted file mode 100644 index fa5eac629..000000000 --- a/lib/jets/tasks/engine.rake +++ /dev/null @@ -1,85 +0,0 @@ -# frozen_string_literal: true - -task "load_app" do - namespace :app do - load APP_RAKEFILE - - desc "Update some initially generated files" - task update: [ "update:bin" ] - - namespace :update do - require "rails/engine/updater" - # desc "Adds new executables to the engine bin/ directory" - task :bin do - Rails::Engine::Updater.run(:create_bin_files) - end - end - end - task environment: "app:environment" - - if !defined?(ENGINE_ROOT) || !ENGINE_ROOT - ENGINE_ROOT = find_engine_path(APP_RAKEFILE) - end -end - -def app_task(name) - task name => [:load_app, "app:db:#{name}"] -end - -namespace :db do - app_task "reset" - - desc "Migrate the database (options: VERSION=x, VERBOSE=false)." - app_task "migrate" - app_task "migrate:up" - app_task "migrate:down" - app_task "migrate:redo" - app_task "migrate:reset" - - desc "Display status of migrations" - app_task "migrate:status" - - desc "Create the database from config/database.yml for the current Rails.env (use db:create:all to create all databases in the config)" - app_task "create" - app_task "create:all" - - desc "Drops the database for the current Rails.env (use db:drop:all to drop all databases)" - app_task "drop" - app_task "drop:all" - - desc "Load fixtures into the current environment's database." - app_task "fixtures:load" - - desc "Rolls the schema back to the previous version (specify steps w/ STEP=n)." - app_task "rollback" - - desc "Creates a database schema file (either db/schema.rb or db/structure.sql, depending on `config.active_record.schema_format`)" - app_task "schema:dump" - - desc "Load a schema.rb file into the database" - app_task "schema:load" - - desc "Load the seed data from db/seeds.rb" - app_task "seed" - - desc "Create the database, load the schema, and initialize with the seed data (use db:reset to also drop the database first)" - app_task "setup" - - desc "Retrieves the current schema version number" - app_task "version" - - # desc 'Load the test schema' - app_task "test:prepare" -end - -def find_engine_path(path) - return File.expand_path(Dir.pwd) if path == "/" - - if Rails::Engine.find(path) - path - else - find_engine_path(File.expand_path("..", path)) - end -end - -Rake.application.invoke_task(:load_app) diff --git a/lib/jets/tasks/framework.rake b/lib/jets/tasks/framework.rake deleted file mode 100644 index 9e918d7ca..000000000 --- a/lib/jets/tasks/framework.rake +++ /dev/null @@ -1,66 +0,0 @@ -# frozen_string_literal: true - -namespace :app do - desc "Update configs and some other initially generated files (or use just update:configs or update:bin)" - task update: [ "update:configs", "update:bin", "update:db", "update:active_storage", "update:upgrade_guide_info" ] - - desc "Applies the template supplied by LOCATION=(/path/to/template) or URL" - task template: :environment do - template = ENV["LOCATION"] - raise "No LOCATION value given. Please set LOCATION either as path to a file or a URL" if template.blank? - template = File.expand_path(template) unless %r{\A[A-Za-z][A-Za-z0-9+\-.]*://}.match?(template) - require "rails/generators" - require "rails/generators/rails/app/app_generator" - generator = Rails::Generators::AppGenerator.new [Jets.root], {}, { destination_root: Jets.root } - generator.apply template, verbose: false - end - - namespace :templates do - # desc "Copy all the templates from rails to the application directory for customization. Already existing local copies will be overwritten" - task :copy do - generators_lib = File.expand_path("../generators", __dir__) - project_templates = "#{Jets.root}/lib/templates" - - default_templates = { "erb" => %w{controller mailer scaffold}, - "rails" => %w{controller helper scaffold_controller} } - - default_templates.each do |type, names| - local_template_type_dir = File.join(project_templates, type) - mkdir_p local_template_type_dir, verbose: false - - names.each do |name| - dst_name = File.join(local_template_type_dir, name) - src_name = File.join(generators_lib, type, name, "templates") - cp_r src_name, dst_name, verbose: false - end - end - end - end - - namespace :update do - require "rails/app_updater" - - # desc "Update config files from your current rails install" - task :configs do - Rails::AppUpdater.invoke_from_app_generator :create_boot_file - Rails::AppUpdater.invoke_from_app_generator :update_config_files - end - - # desc "Adds new executables to the application bin/ directory" - task :bin do - Rails::AppUpdater.invoke_from_app_generator :update_bin_files - end - - task :db do - Rails::AppUpdater.invoke_from_app_generator :update_db_schema - end - - task :active_storage do - Rails::AppUpdater.invoke_from_app_generator :update_active_storage - end - - task :upgrade_guide_info do - Rails::AppUpdater.invoke_from_app_generator :display_upgrade_guide_info - end - end -end diff --git a/lib/jets/tasks/log.rake b/lib/jets/tasks/log.rake deleted file mode 100644 index ec5695720..000000000 --- a/lib/jets/tasks/log.rake +++ /dev/null @@ -1,41 +0,0 @@ -# frozen_string_literal: true - -namespace :log do - ## - # Truncates all/specified log files - # ENV['LOGS'] - # - defaults to all environments log files i.e. 'development,test,production' - # - ENV['LOGS']=all truncates all files i.e. log/*.log - # - ENV['LOGS']='test,development' truncates only specified files - desc "Truncates all/specified *.log files in log/ to zero bytes (specify which logs with LOGS=test,development)" - task :clear do - log_files.each do |file| - clear_log_file(file) - end - end - - def log_files - if ENV["LOGS"] == "all" - FileList["log/*.log"] - elsif ENV["LOGS"] - log_files_to_truncate(ENV["LOGS"]) - else - log_files_to_truncate(all_environments.join(",")) - end - end - - def log_files_to_truncate(envs) - envs.split(",") - .map { |file| "log/#{file.strip}.log" } - .select { |file| File.exist?(file) } - end - - def clear_log_file(file) - f = File.open(file, "w") - f.close - end - - def all_environments - Dir["config/environments/*.rb"].map { |fname| File.basename(fname, ".*") } - end -end diff --git a/lib/jets/tasks/middleware.rake b/lib/jets/tasks/middleware.rake deleted file mode 100644 index e5619a22a..000000000 --- a/lib/jets/tasks/middleware.rake +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -desc "Prints out your Rack middleware stack" -task middleware: :environment do - Jets.configuration.middleware.each do |middleware| - puts "use #{middleware.name}" - end - puts "run #{Jets.application.endpoint}" -end diff --git a/lib/jets/tasks/misc.rake b/lib/jets/tasks/misc.rake deleted file mode 100644 index 8ca7122c8..000000000 --- a/lib/jets/tasks/misc.rake +++ /dev/null @@ -1,81 +0,0 @@ -# frozen_string_literal: true - -# raise "tung" - -desc "Generate a cryptographically secure secret key (this is typically used to generate a secret for cookie sessions)." -task :secret do - require "securerandom" - puts SecureRandom.hex(64) -end - -desc "List versions of all Jets frameworks and the environment" -task about: :environment do - puts Jets::Info -end - -namespace :time do - desc "List all time zones, list by two-letter country code (`bin/rails time:zones[US]`), or list by UTC offset (`bin/rails time:zones[-8]`)" - task :zones, :country_or_offset do |t, args| - zones, offset = ActiveSupport::TimeZone.all, nil - - if country_or_offset = args[:country_or_offset] - begin - zones = ActiveSupport::TimeZone.country_zones(country_or_offset) - rescue TZInfo::InvalidCountryCode - offset = country_or_offset - end - end - - build_time_zone_list zones, offset - end - - namespace :zones do - # desc 'Displays all time zones, also available: time:zones:us, time:zones:local -- filter with OFFSET parameter, e.g., OFFSET=-6' - task :all do - build_time_zone_list ActiveSupport::TimeZone.all - end - - # desc 'Displays names of US time zones recognized by the Jets TimeZone class, grouped by offset. Results can be filtered with optional OFFSET parameter, e.g., OFFSET=-6' - task :us do - build_time_zone_list ActiveSupport::TimeZone.us_zones - end - - # desc 'Displays names of time zones recognized by the Jets TimeZone class with the same offset as the system local time' - task :local do - require "active_support" - require "active_support/time" - - jan_offset = Time.now.beginning_of_year.utc_offset - jul_offset = Time.now.beginning_of_year.change(month: 7).utc_offset - offset = jan_offset < jul_offset ? jan_offset : jul_offset - - build_time_zone_list(ActiveSupport::TimeZone.all, offset) - end - - # to find UTC -06:00 zones, OFFSET can be set to either -6, -6:00 or 21600 - def build_time_zone_list(zones, offset = ENV["OFFSET"]) - require "active_support" - require "active_support/time" - if offset - offset = if offset.to_s.match(/(\+|-)?(\d+):(\d+)/) - sign = $1 == "-" ? -1 : 1 - hours, minutes = $2.to_f, $3.to_f - ((hours * 3600) + (minutes.to_f * 60)) * sign - elsif offset.to_f.abs <= 13 - offset.to_f * 3600 - else - offset.to_f - end - end - previous_offset = nil - zones.each do |zone| - if offset.nil? || offset == zone.utc_offset - puts "\n* UTC #{zone.formatted_offset} *" unless zone.utc_offset == previous_offset - puts zone.name - previous_offset = zone.utc_offset - end - end - puts "\n" - end - end -end diff --git a/lib/jets/tasks/statistics.rake b/lib/jets/tasks/statistics.rake deleted file mode 100644 index c315e4f3c..000000000 --- a/lib/jets/tasks/statistics.rake +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true - -# While global constants are bad, many 3rd party tools depend on this one (e.g -# rspec-rails & cucumber-rails). So a deprecation warning is needed if we want -# to remove it. -STATS_DIRECTORIES ||= [ - %w(Controllers app/controllers), - %w(Helpers app/helpers), - %w(Jobs app/jobs), - %w(Models app/models), - %w(Mailers app/mailers), - %w(Mailboxes app/mailboxes), - %w(Channels app/channels), - %w(Views app/views), - %w(JavaScripts app/assets/javascripts), - %w(Stylesheets app/assets/stylesheets), - %w(JavaScript app/javascript), - %w(Libraries lib/), - %w(APIs app/apis), - %w(Controller\ tests test/controllers), - %w(Helper\ tests test/helpers), - %w(Job\ tests test/jobs), - %w(Model\ tests test/models), - %w(Mailer\ tests test/mailers), - %w(Mailbox\ tests test/mailboxes), - %w(Channel\ tests test/channels), - %w(Integration\ tests test/integration), - %w(System\ tests test/system), -].collect do |name, dir| - [ name, "#{File.dirname(Rake.application.rakefile_location)}/#{dir}" ] -end.select { |name, dir| File.directory?(dir) } - -desc "Report code statistics (KLOCs, etc) from the application or engine" -task :stats do - require "rails/code_statistics" - CodeStatistics.new(*STATS_DIRECTORIES).to_s -end diff --git a/lib/jets/tasks/tmp.rake b/lib/jets/tasks/tmp.rake deleted file mode 100644 index 0405e7be5..000000000 --- a/lib/jets/tasks/tmp.rake +++ /dev/null @@ -1,51 +0,0 @@ -# frozen_string_literal: true - -namespace :tmp do - desc "Clear cache, socket and screenshot files from tmp/ (narrow w/ tmp:cache:clear, tmp:sockets:clear, tmp:screenshots:clear)" - task clear: ["tmp:cache:clear", "tmp:sockets:clear", "tmp:screenshots:clear", "tmp:storage:clear"] - - tmp_dirs = [ "tmp/cache", - "tmp/sockets", - "tmp/pids", - "tmp/cache/assets" ] - - tmp_dirs.each { |d| directory d } - - desc "Creates tmp directories for cache, sockets, and pids" - task create: tmp_dirs - - namespace :cache do - # desc "Clears all files and directories in tmp/cache" - task :clear do - rm_rf Dir["tmp/cache/[^.]*"], verbose: false - end - end - - namespace :sockets do - # desc "Clears all files in tmp/sockets" - task :clear do - rm Dir["tmp/sockets/[^.]*"], verbose: false - end - end - - namespace :pids do - # desc "Clears all files in tmp/pids" - task :clear do - rm Dir["tmp/pids/[^.]*"], verbose: false - end - end - - namespace :screenshots do - # desc "Clears all files in tmp/screenshots" - task :clear do - rm Dir["tmp/screenshots/[^.]*"], verbose: false - end - end - - namespace :storage do - # desc "Clear all files and directories in tmp/storage" - task :clear do - rm_rf Dir["tmp/storage/[^.]*"], verbose: false - end - end -end diff --git a/lib/jets/tasks/yarn.rake b/lib/jets/tasks/yarn.rake deleted file mode 100644 index 6096bd920..000000000 --- a/lib/jets/tasks/yarn.rake +++ /dev/null @@ -1,29 +0,0 @@ -# frozen_string_literal: true - -namespace :yarn do - desc "Install all JavaScript dependencies as specified via Yarn" - task :install do - # Install only production deps when for not usual envs. - valid_node_envs = %w[test development production] - node_env = ENV.fetch("NODE_ENV") do - valid_node_envs.include?(Rails.env) ? Rails.env : "production" - end - - yarn_flags = - if `yarn --version`.start_with?("1") - "--no-progress --frozen-lockfile" - else - "--immutable" - end - - system( - { "NODE_ENV" => node_env }, - "yarn install #{yarn_flags}", - exception: true - ) - rescue Errno::ENOENT - $stderr.puts "yarn install failed to execute." - $stderr.puts "Ensure yarn is installed and `yarn --version` runs without errors." - exit 1 - end -end diff --git a/lib/jets/tasks/zeitwerk.rake b/lib/jets/tasks/zeitwerk.rake deleted file mode 100644 index 154b25d8d..000000000 --- a/lib/jets/tasks/zeitwerk.rake +++ /dev/null @@ -1,61 +0,0 @@ -# frozen_string_literal: true - -eager_load = ->() do - puts "Hold on, I am eager loading the application." - Zeitwerk::Loader.eager_load_all -end - -report_not_checked = ->(not_checked) do - puts - puts <<~EOS - WARNING: The following directories will only be checked if you configure - them to be eager loaded: - EOS - puts - - not_checked.each { |dir| puts " #{dir}" } - puts - - puts <<~EOS - You may verify them manually, or add them to config.eager_load_paths - in config/application.rb and run zeitwerk:check again. - EOS - puts -end - -report = ->(not_checked) do - if not_checked.any? - report_not_checked[not_checked] - puts "Otherwise, all is good!" - else - puts "All is good!" - end -end - -namespace :zeitwerk do - desc "Checks project structure for Zeitwerk compatibility" - task check: :environment do - begin - eager_load[] - rescue NameError => e - if e.message =~ /expected file .*? to define constant [\w:]+/ - abort $&.sub(/expected file #{Regexp.escape(Jets.root.to_s)}./, "expected file ") - else - raise - end - end - - require "active_support/core_ext/object/try" - eager_load_paths = Jets.configuration.eager_load_namespaces.filter_map do |eln| - # Quick regression fix for 6.0.3 to support namespaces that do not have - # eager load paths, like the recently added i18n. I'll rewrite this task. - eln.try(:config).try(:eager_load_paths) - end.flatten - - not_checked = ActiveSupport::Dependencies.autoload_paths - eager_load_paths - not_checked.select! { |dir| Dir.exist?(dir) } - not_checked.reject! { |dir| Dir.empty?(dir) } - - report[not_checked] - end -end diff --git a/lib/jets/thor/auth.rb b/lib/jets/thor/auth.rb new file mode 100644 index 000000000..a6b82aead --- /dev/null +++ b/lib/jets/thor/auth.rb @@ -0,0 +1,96 @@ +module Jets::Thor + class Auth + def initialize(args) + @args = args + end + + # Only check once. Thor subcommands result in 2 calls to Thor dispatch. + @@success = false + def check! + return @@success if @@success + # Commands that do not require authentication + return if no_auth_command? + + if api_key? + resp = ping + puts resp["message"] unless resp["message"] == "pong" + @@success = true + else + login_help_message + exit 1 + end + end + + # interface method + def api_key? + Jets::Api::Config.instance.api_key? + end + + # interface method + def ping + Jets::Api::Ping.create + end + + # Tricky: Thor load the command and then the subcommand. + # IE: jets generate:event + # @args = ["dotenv", "list"] # first pass + # @args = ["list"] # second pass + # We only check first pass to see if it is a no_auth_command. + # And cache it so the second pass never occurs. + @@no_auth_command = nil + def no_auth_command? + return @@no_auth_command unless @@no_auth_command.nil? + base = %w[ + generate + init + ] + more = %w[ + ci:info + ci:init + ci:logs + ci:start + ci:status + ci:stop + clean + concurrency + curl + dotenv + env + exec + functions + funs + logs + maintenance + schedule + waf:info + waf:init + ] + commands = base + more + commands += ProjectCheck.new(@args).no_project_commands + commands.uniq! + # @args contains the command and subcommand + # IE: jets ci:init => ["ci", "init"] + + @@no_auth_command = (commands & @args).any? || # IE: ["ci"] & ["ci", "init"] + (commands & [@args.join(":")]).any? || # IE: ["ci:init"] & ["ci:init"] + @args.empty? + end + + def login_help_message + puts <<~EOL + An account is required to use Jets. + You can sign up at https://www.rubyonjets.com + + Please login. You can login with + + jets login + + EOL + end + + def handle_unauthorized(e) + puts "Unauthorized: #{e.message}".color(:red) + exit 1 + end + end +end diff --git a/lib/jets/thor/base.rb b/lib/jets/thor/base.rb new file mode 100644 index 000000000..546fb5226 --- /dev/null +++ b/lib/jets/thor/base.rb @@ -0,0 +1,155 @@ +require "thor" +require "tty-screen" + +module Thor::StartOverride + class << self + def included(base) + super + base.extend ClassMethods + end + end + + module ClassMethods + def start(given_args = ARGV, config = {}) + # Trap ^C + Signal.trap("INT") { + puts "\nCtrl-C detected. Exiting..." + sleep 0.1 + exit + } + + # Namespace subcommands separate by colon instead of space + if given_args && given_args[0]&.include?(":") + commands = given_args.shift.split(":") + given_args.unshift(commands) + given_args.flatten! + end + + super + end + end +end +Thor.include Thor::StartOverride + +module Thor::CreateCommandOverride + def create_command(meth) # :nodoc: + @long_desc ||= long_desc_from_help_file(self, meth) + super + end + + # So we don't have to duplicate the long_desc Thor CLI classes + def long_desc_from_help_file(klass, meth) + folder = klass.name.demodulize.underscore unless klass == Jets::CLI + path = ["../cli/help", folder, "#{meth}.md"].compact.join("/") + path = File.expand_path(path, __dir__) + if File.exist?(path) + IO.read(path) + end + end +end + +class Thor + class << self + prepend Thor::CreateCommandOverride + end +end + +module Thor::FormattedUsageOverride + def formatted_usage(klass, namespace = true, subcommand = false) + usage = super + # Namespace subcommands separate by colon instead of space + items = formatted_usage_by_colons(usage) + end + + def formatted_usage_by_colons(input) + words = input.split(/\s+/) + + formatted = words.map.with_index do |word, index| + next_word = words[index + 1] + if next_word.nil? || next_word == next_word.upcase + "#{word} " + else + "#{word}:" + end + end + + formatted.join.gsub(": ", ":").strip + end +end +Thor::Command.prepend Thor::FormattedUsageOverride + +# Override thor's long_desc identation behavior +# https://github.com/erikhuda/thor/issues/398 +class Thor + module Shell + class Basic + def print_wrapped(message, options = {}) + message = "\n#{message}" unless message[0] == "\n" + stdout.puts message + end + end + end +end + +module Jets::Thor + class Base < Thor + include SharedOptions + include Help + + class << self + def dispatch(m, args, options, config) + # Allow calling for help via: + # jets command help + # jets command -h + # jets command --help + # + # as well thor's normal way: + # + # jets help command + if args.length > 1 && !(args & help_flags).empty? + args -= help_flags + args.insert(-2, "help") + end + + if args.length == 1 && !(args & version_flags).empty? + args = ["version"] + end + + VersionCheck.new.check! + ProjectCheck.new(args).check! + auth = Jets::Thor::Auth.new(args) + auth.check! + super + rescue Jets::Api::Error::Unauthorized => e + auth.handle_unauthorized(e) + rescue ProjectCheck::NotProjectError + puts "Not a Jets project. Please run this command from a Jets project folder.".color(:red) + end + + # Also used by Jets::Thor::Auth + def help_flags + Thor::HELP_MAPPINGS + ["help"] + end + + # Also used by Jets::Thor::Auth + # jets version + # jets --version + # jets -v + def version_flags + ["--version", "-v"] + end + + # meant to be overriden + def website + "" + end + + # https://github.com/erikhuda/thor/issues/244 + # Deprecation warning: Thor exit with status 0 on errors. To keep this behavior, you must define `exit_on_failure?` in `Lono::Commands` + # You can silence deprecations warning by setting the environment variable THOR_SILENCE_DEPRECATION. + def exit_on_failure? + true + end + end + end +end diff --git a/lib/jets/thor/help.rb b/lib/jets/thor/help.rb new file mode 100644 index 000000000..8c25f1f16 --- /dev/null +++ b/lib/jets/thor/help.rb @@ -0,0 +1,129 @@ +module Jets::Thor + module Help + extend ActiveSupport::Concern + + def help(command = nil, subcommand = false) + help_output = capture_stdout_for_help { super } + paginate_output(help_output) + end + + def capture_stdout_for_help + stdout_old = $stdout + io = StringIO.new + $stdout = io + yield + $stdout = stdout_old + io.string + end + + # Method to paginate the output using less if necessary + def paginate_output(output) + unless system("type less > /dev/null 2>&1") + puts output + return + end + + # Paginate the output if it's taller than the terminal + terminal_height = TTY::Screen.height + if output.lines.count > terminal_height + IO.popen("less -R", "w") { |less| less.puts(output) } + else + puts output + end + end + + module ClassMethods + # Override command_help to include the description at the top of the + # long_description. + def command_help(shell, command_name) + meth = normalize_command_name(command_name) + command = all_commands[meth] + alter_command_description(command) + super + end + + def alter_command_description(command) + return unless command + + # Add description to beginning of long_description + long_desc = if command.long_description + "#{command.description}\n\n#{command.long_description}" + else + command.description + end + + # add reference url to end of the long_description + unless website.empty? + full_command = [command.ancestor_name, command.name].compact.join("-") + url = "#{website}/reference/Jets::Pro-#{full_command}" + long_desc += "\n\nHelp also available at: #{url}" + end + + command.long_description = long_desc + end + private :alter_command_description + + # override main help menu + def help(shell, subcommand = false) + if subcommand + help_subcommand(shell, subcommand) + else + help_main(shell, subcommand) + end + end + + def help_subcommand(shell, subcommand) + list = command_list(subcommand) + shell.say "Commands:\n\n" + shell.print_table(list, indent: 2, truncate: true) + end + + def help_main(shell, subcommand = false) + list = command_list(subcommand) + + filter = proc do |command, desc| + main_commands.detect { |name| command =~ Regexp.new("^jets #{name}") } + end + main = list.select(&filter) + other = list.reject(&filter) + + shell.say "Usage: jets COMMAND [args]" + shell.say "\nMain Commands:\n\n" + shell.print_table(main, indent: 2, truncate: true) + shell.say "\nOther Commands:\n\n" + shell.print_table(other, indent: 2, truncate: true) + shell.say <<~EOL + + For more help on each command, you can use the -h option. Example: + + jets deploy -h + + CLI Reference also available at: https://docs.rubyonjets.com/reference/ + EOL + end + + def command_list(subcommand) + list = printable_commands(true, subcommand) + Thor::Util.thor_classes_in(self).each do |klass| + list += klass.printable_commands(false) + end + list.reject! do |arr| + c = arr[0] # IE: jets release:SUBCOMMAND + c.include?("help") || + c.include?("COMMAND") || + c.include?("c_l_i") + end + sort_commands!(list) + list + end + + def main_commands + %w[ + deploy + logs + url + ] + end + end + end +end diff --git a/lib/jets/thor/project_check.rb b/lib/jets/thor/project_check.rb new file mode 100644 index 000000000..b77702b09 --- /dev/null +++ b/lib/jets/thor/project_check.rb @@ -0,0 +1,50 @@ +module Jets::Thor + class ProjectCheck + class NotProjectError < StandardError; end + + def initialize(args) + @args = args + end + + def check! + return if no_project_command? || project? + raise NotProjectError, "Not a Jets project. Please run this command from a Jets project folder." + end + + def project? + File.exist?("config/jets") + end + + # Tricky: Thor load the command and then the subcommand. + # IE: jets generate:event + # @args = ["generate", "event"] # first pass + # @args = ["event"] # second pass + # We only check first pass to see if it is a no_project_command. + # And cache it so the second pass never occurs. + @@no_project_command = nil + def no_project_command? + return @@no_project_command unless @@no_project_command.nil? + @@no_project_command = (no_project_commands & @args).any? || @args.empty? + end + + def no_project_commands + # generate for generate:event + # Allow generate in case `jets init` has not been called yet and user + # can generate event classes before `jets init` is called. + # + # The delete command is a special case. It is allowed to run without a project + # in an empty folder. This is because the delete command is used to clean up + # Jets API deployment record. + commands = %w[ + delete + generate + init + login + logout + projects + version + ] + commands + Jets::Thor::Base.help_flags + Jets::Thor::Base.version_flags + end + end +end diff --git a/lib/jets/thor/shared_options.rb b/lib/jets/thor/shared_options.rb new file mode 100644 index 000000000..80f17c4b1 --- /dev/null +++ b/lib/jets/thor/shared_options.rb @@ -0,0 +1,31 @@ +module Jets::Thor + # Not naming Options to avoid conflict with Thor::Options + module SharedOptions + extend ActiveSupport::Concern + module ClassMethods + def paging_options(defaults = {}) + option :limit, default: defaults[:limit] || 25, aliases: :l, type: :numeric, desc: "Per page limit" + option :order, default: defaults[:order] || "asc", aliases: :o, desc: "Order: asc or desc" + option :page, aliases: :p, type: :numeric, desc: "Page number" + end + + def yes_option + option :yes, aliases: :y, type: :boolean, desc: "Skip are you sure prompt" + end + + def format_option(defaults = {}) + default = defaults[:default] || "table" + option :format, default: default, desc: "Output format: #{CliFormat.formats.join(", ")}" + end + + def verbose_option + option :verbose, aliases: :v, default: false, type: :boolean, desc: "Show more verbose logging output. Useful for debugging what's under the hood" + end + + def function_name_option(defaults = {}) + default = defaults[:default] || "controller" + option :function, aliases: :n, default: default, desc: "Lambda Function name" + end + end + end +end diff --git a/lib/jets/thor/version_check.rb b/lib/jets/thor/version_check.rb new file mode 100644 index 000000000..a82583bf8 --- /dev/null +++ b/lib/jets/thor/version_check.rb @@ -0,0 +1,51 @@ +require "fileutils" +require "gems" + +module Jets::Thor + class VersionCheck + def check! + return unless check_needed? + + remote_version = Gems.info("jets")["version"] + local_version = Gem.loaded_specs["jets"].version + + return if remote_version.nil? + + if Gem::Version.new(remote_version) > Gem::Version.new(local_version) + puts <<~EOL + jets has a newer version available. + + installed version: #{local_version} + latest version: #{remote_version} + + Please update jets + EOL + end + + save_last_checked_time + end + + def check_needed? + check_interval = 24 * 60 * 60 # 24 hours in seconds + Time.now - last_checked_time >= check_interval + end + + def last_checked_time + last_time = File.exist?(last_check_file) ? File.read(last_check_file) : "1970-01-01 00:00:00 UTC" + Time.parse(last_time) + end + + def save_last_checked_time + FileUtils.mkdir_p(File.dirname(last_check_file)) + File.write(last_check_file, Time.now) + end + + def last_check_file + # Do not define last_check_file as a LAST_CHECK_FILE constant + # On AWS lambda, Jets eager load errors since ENV["HOME"] is nil + # Note: Added an extra safeguard in case ENV["HOME"] is nil + home = ENV["HOME"] || "/root" + File.join(home, ".jets/tmp/last-checked.txt") + end + end +end diff --git a/lib/jets/tmp_loader.rb b/lib/jets/tmp_loader.rb deleted file mode 100644 index 2aa0b4868..000000000 --- a/lib/jets/tmp_loader.rb +++ /dev/null @@ -1,53 +0,0 @@ -module Jets - class TmpLoader - include AwsServices - - def self.load! - new.load - end - - def initialize(yaml_path=nil) - yaml_path ||= "#{Jets.root}/handlers/data.yml" - return unless File.exist?(yaml_path) - @data = Jets::Util::Yamler.load_file(yaml_path) - @s3_bucket = @data['s3_bucket'] - @rack_zip = @data['rack_zip'] - end - - def load - rack - end - - def rack - return unless @rack_zip - download_and_extract(@rack_zip, '/tmp/rack') - end - - def download_and_extract(zip_file, folder_dest) - s3_key = "jets/code/#{zip_file}" # jets/code/rack-checksum.zip - download_path = "/tmp/#{zip_file}" # /tmp/rack-checksum.zip - - download(s3_key, download_path) - unzip(download_path, folder_dest) - end - - def download(key, dest) - # https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Client.html#get_object-instance_method - # stream object directly to disk - s3.get_object(response_target: dest, - bucket: @s3_bucket, - key: key) - end - - def unzip(zipfile, folder_dest) - sh "unzip -qo #{zipfile} -d #{folder_dest}" - end - - def sh(command) - puts "=> #{command}" - success = system(command) - raise "Command #{command} failed" unless success - success - end - end -end diff --git a/lib/jets/turbine.rb b/lib/jets/turbine.rb deleted file mode 100644 index a748bcb19..000000000 --- a/lib/jets/turbine.rb +++ /dev/null @@ -1,185 +0,0 @@ -# frozen_string_literal: true - -require "active_support/descendants_tracker" -require "active_support/inflector" -require "active_support/core_ext/module/introspection" -require "active_support/core_ext/module/delegation" - -module Jets - class Turbine - extend ActiveSupport::DescendantsTracker - include Initializable - - ABSTRACT_TURBINES = %w(Jets::Turbine Jets::Engine Jets::Application) - - class << self - private :new - delegate :config, to: :instance - - def subclasses - super.reject(&:abstract_turbine?).sort - end - - def rake_tasks(&blk) - register_block_for(:rake_tasks, &blk) - end - - def console(&blk) - register_block_for(:load_console, &blk) - end - - def runner(&blk) - register_block_for(:runner, &blk) - end - - def generators(&blk) - register_block_for(:generators, &blk) - end - - def server(&blk) - register_block_for(:server, &blk) - end - - # Jets specific. The label is no longer used and kept for backwards compatibility. - # Note it uses the same register_block_for but works a bit differently. - # See on_exception_blocks method to see how it works. - def on_exception(label, &blk) - register_block_for(:on_exception, &blk) - end - - def abstract_turbine? - ABSTRACT_TURBINES.include?(name) - end - - def turbine_name(name = nil) - @turbine_name = name.to_s if name - @turbine_name ||= generate_turbine_name(self.name) - end - - # Since Jets::Turbine cannot be instantiated, any methods that call - # +instance+ are intended to be called only on subclasses of a Turbine. - def instance - @instance ||= new - end - - # Allows you to configure the turbine. This is the same method seen in - # Turbine::Configurable, but this module is no longer required for all - # subclasses of Turbine so we provide the class method here. - def configure(&block) - instance.configure(&block) - end - - def <=>(other) # :nodoc: - load_index <=> other.load_index - end - - def inherited(subclass) - subclass.increment_load_index - super - end - - protected - attr_reader :load_index - - def increment_load_index - @@load_counter ||= 0 - @load_index = (@@load_counter += 1) - end - - private - def generate_turbine_name(string) - ActiveSupport::Inflector.underscore(string).tr("/", "_") - end - - def respond_to_missing?(name, _) - return super if abstract_turbine? - - instance.respond_to?(name) || super - end - - # If the class method does not have a method, then send the method call - # to the Turbine instance. - def method_missing(name, *args, &block) - if !abstract_turbine? && instance.respond_to?(name) - instance.public_send(name, *args, &block) - else - super - end - end - ruby2_keywords(:method_missing) - - # receives an instance variable identifier, set the variable value if is - # blank and append given block to value, which will be used later in - # `#each_registered_block(type, &block)` - def register_block_for(type, &blk) - var_name = "@#{type}" - blocks = instance_variable_defined?(var_name) ? instance_variable_get(var_name) : instance_variable_set(var_name, []) - blocks << blk if blk - blocks - end - end - - delegate :turbine_name, to: :class - - def initialize # :nodoc: - if self.class.abstract_turbine? - raise "#{self.class.name} is abstract, you cannot instantiate it directly." - end - end - - def inspect # :nodoc: - "#<#{self.class.name}>" - end - - def configure(&block) # :nodoc: - instance_eval(&block) - end - - # This is used to create the config object on Turbines, an instance of - # Turbine::Configuration, that is used by Turbines and Application to store - # related configuration. - def config - @config ||= Turbine::Configuration.new - end - - def turbine_namespace # :nodoc: - @turbine_namespace ||= self.class.module_parents.detect { |n| n.respond_to?(:turbine_namespace) } - end - - def on_exception_blocks - self.class.instance_variable_get(:@on_exception) || [] - end - - protected - def run_console_blocks(app) # :nodoc: - each_registered_block(:console) { |block| block.call(app) } - end - - def run_generators_blocks(app) # :nodoc: - each_registered_block(:generators) { |block| block.call(app) } - end - - def run_runner_blocks(app) # :nodoc: - each_registered_block(:runner) { |block| block.call(app) } - end - - def run_tasks_blocks(app) # :nodoc: - extend Rake::DSL - each_registered_block(:rake_tasks) { |block| instance_exec(app, &block) } - end - - def run_server_blocks(app) # :nodoc: - each_registered_block(:server) { |block| block.call(app) } - end - - private - # run `&block` in every registered block in `#register_block_for` - def each_registered_block(type, &block) - klass = self.class - while klass.respond_to?(type) - klass.public_send(type).each(&block) - klass = klass.superclass - end - end - end -end diff --git a/lib/jets/turbine/configurable.rb b/lib/jets/turbine/configurable.rb deleted file mode 100644 index 36ad72548..000000000 --- a/lib/jets/turbine/configurable.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -require "active_support/concern" - -module Jets - class Turbine - module Configurable - extend ActiveSupport::Concern - - module ClassMethods - delegate :config, to: :instance - - def inherited(base) - raise "You cannot inherit from a #{superclass.name} child" - end - - def instance - @instance ||= new - end - - def respond_to?(*args) - super || instance.respond_to?(*args) - end - - def configure(&block) - class_eval(&block) - end - - private - def method_missing(*args, &block) - instance.send(*args, &block) - end - end - end - end -end diff --git a/lib/jets/turbine/configuration.rb b/lib/jets/turbine/configuration.rb deleted file mode 100644 index d0788a8bb..000000000 --- a/lib/jets/turbine/configuration.rb +++ /dev/null @@ -1,101 +0,0 @@ -# frozen_string_literal: true - -require "jets/configuration" - -module Jets - class Turbine - class Configuration - def initialize - @@options ||= {} - end - - # Expose the eager_load_namespaces at "module" level for convenience. - def self.eager_load_namespaces # :nodoc: - @@eager_load_namespaces ||= [] - end - - # All namespaces that are eager loaded - def eager_load_namespaces - @@eager_load_namespaces ||= [] - end - - # Add files that should be watched for change. - def watchable_files - @@watchable_files ||= [] - end - - # Add directories that should be watched for change. - # The key of the hashes should be directories and the values should - # be an array of extensions to match in each directory. - def watchable_dirs - @@watchable_dirs ||= {} - end - - # This allows you to modify the application's middlewares from Engines. - # - # All operations you run on the app_middleware will be replayed on the - # application once it is defined and the default_middlewares are - # created - def app_middleware - @@app_middleware ||= Jets::Configuration::MiddlewareStackProxy.new - end - - # This allows you to modify application's generators from Turbines. - # - # Values set on app_generators will become defaults for application, unless - # application overwrites them. - def app_generators - @@app_generators ||= Jets::Configuration::Generators.new - yield(@@app_generators) if block_given? - @@app_generators - end - - # First configurable block to run. Called before any initializers are run. - def before_configuration(&block) - ActiveSupport.on_load(:before_configuration, yield: true, &block) - end - - # Third configurable block to run. Does not run if +config.eager_load+ - # set to false. - def before_eager_load(&block) - ActiveSupport.on_load(:before_eager_load, yield: true, &block) - end - - # Second configurable block to run. Called before frameworks initialize. - def before_initialize(&block) - ActiveSupport.on_load(:before_initialize, yield: true, &block) - end - - # Last configurable block to run. Called after frameworks initialize. - def after_initialize(&block) - ActiveSupport.on_load(:after_initialize, yield: true, &block) - end - - # Array of callbacks defined by #to_prepare. - def to_prepare_blocks - @@to_prepare_blocks ||= [] - end - - # Defines generic callbacks to run before #after_initialize. Useful for - # Jets::Turbine subclasses. - def to_prepare(&blk) - to_prepare_blocks << blk if blk - end - - def respond_to?(name, include_private = false) - super || @@options.key?(name.to_sym) - end - - private - def method_missing(name, *args, &blk) - if name.end_with?("=") - @@options[:"#{name[0..-2]}"] = args.first - elsif @@options.key?(name) - @@options[name] - else - super - end - end - end - end -end diff --git a/lib/jets/util.rb b/lib/jets/util.rb deleted file mode 100644 index 9bfcae2c8..000000000 --- a/lib/jets/util.rb +++ /dev/null @@ -1,40 +0,0 @@ -require 'shellwords' - -class Jets::Util - class << self - # Make sure that the result is a text. - def normalize_result(result) - JSON.dump(result) - end - - def cp_r(src, dest) - # Fix for https://github.com/tongueroo/jets/issues/122 - # - # Using FileUtils.cp_r doesnt work if there are special files like socket files in the src dir. - # Instead of using this hack https://bugs.ruby-lang.org/issues/10104 - # Using rsync to perform the copy. - src.chop! if src.ends_with?('/') - dest.chop! if dest.ends_with?('/') - check_rsync_installed! - sh "rsync -a --links --no-specials --no-devices #{Shellwords.escape(src)}/ #{Shellwords.escape(dest)}/", quiet: true - end - - @@rsync_installed = false - def check_rsync_installed! - return if @@rsync_installed # only check once - if system "type rsync > /dev/null 2>&1" - @@rsync_installed = true - else - raise Jets::Error.new("Rsync is required. Rsync does not seem to be installed.") - end - end - - def sh(command, quiet: false) - puts "=> #{command}" unless quiet - system(command) - success = $?.success? - raise Jets::Error.new("Command failed: #{command}") unless success - success - end - end -end diff --git a/lib/jets/util/call_line.rb b/lib/jets/util/call_line.rb new file mode 100644 index 000000000..931f5a807 --- /dev/null +++ b/lib/jets/util/call_line.rb @@ -0,0 +1,9 @@ +module Jets::Util + module CallLine + include Pretty + + def jets_call_line + caller.find { |l| l.include?("#{Jets.root}/") } + end + end +end diff --git a/lib/jets/util/camelize.rb b/lib/jets/util/camelize.rb index 61f2d31c4..2caa76fe5 100644 --- a/lib/jets/util/camelize.rb +++ b/lib/jets/util/camelize.rb @@ -1,4 +1,4 @@ -class Jets::Util +module Jets::Util module Camelize # Not named camelize! because it conflicts with zeitwerk's camelize! def camelize(object) diff --git a/lib/jets/util/git.rb b/lib/jets/util/git.rb new file mode 100644 index 000000000..79483ad67 --- /dev/null +++ b/lib/jets/util/git.rb @@ -0,0 +1,11 @@ +module Jets::Util + module Git + def git? + File.exist?("#{Jets.root}/.git") && git_installed? + end + + def git_installed? + system("type git > /dev/null 2>&1") + end + end +end diff --git a/lib/jets/util/logging.rb b/lib/jets/util/logging.rb new file mode 100644 index 000000000..8d940dbd1 --- /dev/null +++ b/lib/jets/util/logging.rb @@ -0,0 +1,16 @@ +module Jets::Util + module Logging + # Both work within the Jets source code. + # + # logger.info + # log.info (encouraged) + # + # Jets.logger also points to this via jets/core.rb by default. + # Hoewever, it can be overridden by other frameworks. + # + delegate :logger, to: "Jets.bootstrap.config" + def log + logger + end + end +end diff --git a/lib/jets/util/pretty.rb b/lib/jets/util/pretty.rb new file mode 100644 index 000000000..e6c62564f --- /dev/null +++ b/lib/jets/util/pretty.rb @@ -0,0 +1,23 @@ +module Jets::Util + module Pretty + def pretty_path(path) + path.sub("#{Jets.root}/", "").sub(/^\.\//, "") + end + + # Replace HOME with ~ - different from the main pretty_path + def pretty_home(path) + path.sub(ENV["HOME"], "~") + end + + # http://stackoverflow.com/questions/4175733/convert-duration-to-hoursminutesseconds-or-similar-in-rails-3-or-ruby + def pretty_time(total_seconds) + minutes = (total_seconds / 60) % 60 + seconds = total_seconds % 60 + if total_seconds < 60 + "#{seconds.to_i}s" + else + "#{minutes.to_i}m #{seconds.to_i}s" + end + end + end +end diff --git a/lib/jets/util/sh.rb b/lib/jets/util/sh.rb new file mode 100644 index 000000000..dc99eca5d --- /dev/null +++ b/lib/jets/util/sh.rb @@ -0,0 +1,38 @@ +module Jets::Util + module Sh + def sh(command, options = {}) + quiet = options[:quiet] + on_fail = options[:on_fail] || :raise + + puts "=> #{command}" unless quiet + system(command) + success = $?.success? + + case on_fail + when :raise + raise Jets::Error.new("Command failed: #{command}\n#{caller(1..1).first}") unless success + when :exit + unless success + if quiet + abort("Command failed: #{command}\n") + else + abort("Command failed: #{command}\n#{caller.join("\n")}") + end + end + end + + success + end + + def quiet_sh(command, options = {}) + options = options.merge(quiet: true) unless ENV["JETS_DEBUG"] + sh(command, options) + end + + def capture(command) + out = `#{command}`.strip + raise Jets::Error.new("Command failed: #{command}\n#{caller(1..1).first}") unless $?.success? + out + end + end +end diff --git a/lib/jets/util/sure.rb b/lib/jets/util/sure.rb new file mode 100644 index 000000000..665c78bc5 --- /dev/null +++ b/lib/jets/util/sure.rb @@ -0,0 +1,25 @@ +module Jets::Util + module Sure + private + + def sure?(message = nil) + confirm = "Are you sure?" + if @options[:yes] + yes = "y" + else + out = if message + "#{message}\n#{confirm} (y/N) " + else + "#{confirm} (y/N) " + end + print out + yes = $stdin.gets + end + + unless /^y/.match?(yes) + puts "Whew! Exiting." + exit 0 + end + end + end +end diff --git a/lib/jets/util/truthy.rb b/lib/jets/util/truthy.rb new file mode 100644 index 000000000..71e223e3c --- /dev/null +++ b/lib/jets/util/truthy.rb @@ -0,0 +1,9 @@ +module Jets::Util + module Truthy + # Allows use non-truthy values like + # n no false off null nil 0 + def truthy?(value) + %w[y yes true on 1].include?(value.to_s.downcase) + end + end +end diff --git a/lib/jets/util/yamler.rb b/lib/jets/util/yamler.rb index 3bb8661ef..ae8bb2aaf 100644 --- a/lib/jets/util/yamler.rb +++ b/lib/jets/util/yamler.rb @@ -1,16 +1,14 @@ # Named Yamler to make it clear it's not the YAML class. -class Jets::Util +module Jets::Util class Yamler class << self def load(text) - options = { permitted_classes: [Date] } - options[:aliases] = true if RUBY_VERSION =~ /^3/ # Ruby 3.0.0 deprecates aliases: true + options = RUBY_VERSION.match?(/^3/) ? {aliases: true} : {} # Ruby 3.0.0 deprecates aliases: true YAML.load(text, **options) end def load_file(path) - options = { permitted_classes: [Date] } - options[:aliases] = true if RUBY_VERSION =~ /^3/ # Ruby 3.0.0 deprecates aliases: true + options = RUBY_VERSION.match?(/^3/) ? {aliases: true} : {} # Ruby 3.0.0 deprecates aliases: true YAML.load_file(path, **options) end end diff --git a/lib/jets/version.rb b/lib/jets/version.rb index 13111376b..b7fe71a8a 100644 --- a/lib/jets/version.rb +++ b/lib/jets/version.rb @@ -1,3 +1,3 @@ module Jets - VERSION = "5.0.14" + VERSION = "6.0.0.beta1" end diff --git a/spec/fixtures/authorizers/token.json b/spec/fixtures/authorizers/token.json deleted file mode 100644 index c50529ab3..000000000 --- a/spec/fixtures/authorizers/token.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "type": "TOKEN", - "methodArn": "arn:aws:execute-api:us-west-2:112233445566:f0ivxw7nkl/dev/GET/posts", - "authorizationToken": "test" -} \ No newline at end of file diff --git a/spec/fixtures/db_configs/database.multi.yml b/spec/fixtures/db_configs/database.multi.yml deleted file mode 100644 index 64421b355..000000000 --- a/spec/fixtures/db_configs/database.multi.yml +++ /dev/null @@ -1,26 +0,0 @@ -default: &default - adapter: mysql2 - pool: <%= ENV.fetch("JETS_MAX_THREADS") { 5 } %> - timeout: 5000 - encoding: utf8mb4 - -test: - primary: - <<: *default - database: my_primary_database - user: root - primary_replica: - <<: *default - database: my_primary_database - user: root # user: root_readonly - replica: true - animals: - <<: *default - database: my_animals_database - user: root # user: animals_root - migrations_paths: db/animals_migrate - animals_replica: - <<: *default - database: my_animals_database - user: root # user: animals_readonly - replica: true diff --git a/spec/fixtures/db_configs/database.single.yml b/spec/fixtures/db_configs/database.single.yml deleted file mode 100644 index 2b24a55d3..000000000 --- a/spec/fixtures/db_configs/database.single.yml +++ /dev/null @@ -1,22 +0,0 @@ -default: &default - adapter: mysql2 - encoding: utf8mb4 - pool: <%= ENV["DB_POOL"] || 5 %> - database: <%= ENV['DB_NAME'] || 'demo_development' %> - username: <%= ENV['DB_USER'] || 'root' %> - password: <%= ENV['DB_PASS'] %> - host: <%= ENV["DB_HOST"] %> - url: <%= ENV['DATABASE_URL'] %> # takes higher precedence than other settings - -development: - <<: *default - database: <%= ENV['DB_NAME'] || 'demo_development' %> - -test: - <<: *default - database: demo_test - -production: - <<: *default - database: demo_production - url: <%= ENV['DATABASE_URL'] %> diff --git a/spec/fixtures/demo/.babelrc b/spec/fixtures/demo/.babelrc deleted file mode 100644 index ded31c0d8..000000000 --- a/spec/fixtures/demo/.babelrc +++ /dev/null @@ -1,18 +0,0 @@ -{ - "presets": [ - ["env", { - "modules": false, - "targets": { - "browsers": "> 1%", - "uglify": true - }, - "useBuiltIns": true - }] - ], - - "plugins": [ - "syntax-dynamic-import", - "transform-object-rest-spread", - ["transform-class-properties", { "spec": true }] - ] -} diff --git a/spec/fixtures/demo/.env b/spec/fixtures/demo/.env deleted file mode 100644 index 9cc0ec7b6..000000000 --- a/spec/fixtures/demo/.env +++ /dev/null @@ -1 +0,0 @@ -ENV_KEY=example1 diff --git a/spec/fixtures/demo/.env.development b/spec/fixtures/demo/.env.development deleted file mode 100644 index 1d97a7703..000000000 --- a/spec/fixtures/demo/.env.development +++ /dev/null @@ -1 +0,0 @@ -ENV_DEVELOPMENT_KEY=example1 diff --git a/spec/fixtures/demo/.env.staging b/spec/fixtures/demo/.env.staging deleted file mode 100644 index 384af9cc0..000000000 --- a/spec/fixtures/demo/.env.staging +++ /dev/null @@ -1 +0,0 @@ -MYKEY=example1 diff --git a/spec/fixtures/demo/.env.test b/spec/fixtures/demo/.env.test deleted file mode 100644 index 1b7f9f5ea..000000000 --- a/spec/fixtures/demo/.env.test +++ /dev/null @@ -1,3 +0,0 @@ -env_key1=env_value1 -env_key2=env_value2 -SKIP_MIGRATION_CHECK=true diff --git a/spec/fixtures/demo/.env.test.local b/spec/fixtures/demo/.env.test.local deleted file mode 100644 index 13b6459d3..000000000 --- a/spec/fixtures/demo/.env.test.local +++ /dev/null @@ -1 +0,0 @@ -ONLY_LOCAL=1 diff --git a/spec/fixtures/demo/.env.test.remote b/spec/fixtures/demo/.env.test.remote deleted file mode 100644 index f6d722756..000000000 --- a/spec/fixtures/demo/.env.test.remote +++ /dev/null @@ -1 +0,0 @@ -ONLY_REMOTE=1 diff --git a/spec/fixtures/demo/.gitignore b/spec/fixtures/demo/.gitignore deleted file mode 100644 index 32ef5b2dc..000000000 --- a/spec/fixtures/demo/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -.DS_Store -*.gem -.bundle -coverage -pkg -tmp -bundled -.byebug_history -/public/packs -/public/packs-test -/node_modules diff --git a/spec/fixtures/demo/.jetskeep b/spec/fixtures/demo/.jetskeep deleted file mode 100644 index 33dbcf7f2..000000000 --- a/spec/fixtures/demo/.jetskeep +++ /dev/null @@ -1 +0,0 @@ -pack diff --git a/spec/fixtures/demo/.postcssrc.yml b/spec/fixtures/demo/.postcssrc.yml deleted file mode 100644 index a123d1fd2..000000000 --- a/spec/fixtures/demo/.postcssrc.yml +++ /dev/null @@ -1,3 +0,0 @@ -plugins: - postcss-smart-import: {} - postcss-cssnext: {} diff --git a/spec/fixtures/demo/Gemfile b/spec/fixtures/demo/Gemfile deleted file mode 100644 index b76a5f2d2..000000000 --- a/spec/fixtures/demo/Gemfile +++ /dev/null @@ -1,42 +0,0 @@ -source "https://rubygems.org" - -if File.exist?("dev.mode") - gem "jets", path: "#{ENV['HOME']}/data/rubyonjets/jets" - gem "sprockets-jets", path: "#{ENV['HOME']}/data/rubyonjets/sprockets-jets" # , require: "sprockets/jets/turbine" - gem "importmap", path: "#{ENV['HOME']}/data/rubyonjets/importmap" - gem "importmap-jets", path: "#{ENV['HOME']}/data/rubyonjets/importmap-jets" - gem "kingsman", path: "#{ENV['HOME']}/data/rubyonjets/kingsman/kingsman" - gem "jets-responders", path: "#{ENV['HOME']}/data/rubyonjets/kingsman/jets-responders" - gem "dynomite", path: "#{ENV['HOME']}/data/rubyonjets/dynomite" -else - gem "jets", github: "boltops-tools/jets", branch: "v5" - gem "sprockets-jets", github: "boltops-tools/sprockets-jets", branch: "main" - gem "importmap", github: "boltops-tools/importmap", branch: "main" - gem "importmap-jets", github: "boltops-tools/importmap-jets", branch: "main" - gem "kingsman", github: "boltops-tools/kingsman", branch: "main" - gem "jets-responders", github: "boltops-tools/jets-responders", branch: "main" - gem "dynomite", github: "boltops-tools/dynomite", branch: "edge2" -end - -gem "jbuilder" - -# Include mysql2 gem if you are using ActiveRecord, remove next line -# and config/database.yml file if you are not -gem "mysql2", "~> 0.5.3" - -gem "zeitwerk", ">= 2.5.0" - -# development and test groups are not bundled as part of the deployment -group :development, :test do - # Call 'byebug' anywhere in the code to stop execution and get a debugger console - gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] - gem 'shotgun' - gem 'rack' - gem 'puma' -end - -group :test do - gem 'rspec' # rspec test group only or we get the "irb: warn: can't alias context from irb_context warning" when starting jets console - gem 'launchy' - gem 'capybara' -end diff --git a/spec/fixtures/demo/Procfile b/spec/fixtures/demo/Procfile deleted file mode 100644 index 94fd719f5..000000000 --- a/spec/fixtures/demo/Procfile +++ /dev/null @@ -1,8 +0,0 @@ -local: dynamodb-local # port 8000 -admin: env AWS_ACCESS_KEY_ID=$DYNAMODB_ADMIN_AWS_ACCESS_KEY_ID PORT=8001 dynamodb-admin # port 8001 - -# web: jets server # port 8888 - -# Using Procfile to just start local dynamodb services for now. -# To start jets server for now use: -# bin/dev/server diff --git a/spec/fixtures/demo/README.md b/spec/fixtures/demo/README.md deleted file mode 100644 index 1a6da51fd..000000000 --- a/spec/fixtures/demo/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Franky Jets Project - -Franky is short for Frankenstein. This is a Frankenstein project that is not meant to be deployable but meant to help with specs. This project contains a lot of different usages of resources, properties, and settings. This provides a quick way to add sanity checks with the Jets DSL. - diff --git a/spec/fixtures/demo/Rakefile b/spec/fixtures/demo/Rakefile deleted file mode 100644 index 6e613d1a8..000000000 --- a/spec/fixtures/demo/Rakefile +++ /dev/null @@ -1,2 +0,0 @@ -require 'jets' -Jets.application.load_tasks diff --git a/spec/fixtures/demo/app/authorizers/application_authorizer.rb b/spec/fixtures/demo/app/authorizers/application_authorizer.rb deleted file mode 100644 index 16e5c2f51..000000000 --- a/spec/fixtures/demo/app/authorizers/application_authorizer.rb +++ /dev/null @@ -1,2 +0,0 @@ -class ApplicationAuthorizer < Jets::Authorizer::Base -end diff --git a/spec/fixtures/demo/app/authorizers/main_authorizer.rb b/spec/fixtures/demo/app/authorizers/main_authorizer.rb deleted file mode 100644 index 8caa5dec7..000000000 --- a/spec/fixtures/demo/app/authorizers/main_authorizer.rb +++ /dev/null @@ -1,28 +0,0 @@ -class MainAuthorizer < ApplicationAuthorizer - authorizer( - name: "MainProtect", # required - type: :token, - ) - def protect - resource = event[:methodArn] # "arn:aws:execute-api:us-west-2:123456789012:ymy8tbxw7b/*/GET/" - build_policy(resource, "current_user") - end - - authorizer( - name: "MainLock", # required - # type: :request, # default: request - ) - def lock - resource = event[:methodArn] # "arn:aws:execute-api:us-west-2:123456789012:ymy8tbxw7b/*/GET/" - build_policy(resource, "current_user") - end - - authorizer( - name: "MainCognito", # required - type: :cognito_user_pools, - provider_arns: [ - "arn:aws:cognito-idp:us-west-2:112233445566:userpool/us-west-2_DjXxf8cP7", - ], - ) - # no function -end diff --git a/spec/fixtures/demo/app/controllers/admin/pages_controller.rb b/spec/fixtures/demo/app/controllers/admin/pages_controller.rb deleted file mode 100644 index 9a73c3ef8..000000000 --- a/spec/fixtures/demo/app/controllers/admin/pages_controller.rb +++ /dev/null @@ -1,5 +0,0 @@ -class Admin::PagesController < ApplicationController - def index - render json: {"action": "index"} - end -end diff --git a/spec/fixtures/demo/app/controllers/admin/related_pages_controller.rb b/spec/fixtures/demo/app/controllers/admin/related_pages_controller.rb deleted file mode 100644 index 30076f0a1..000000000 --- a/spec/fixtures/demo/app/controllers/admin/related_pages_controller.rb +++ /dev/null @@ -1,9 +0,0 @@ -class Admin::RelatedPagesController < ApplicationController - def index - render json: {"action": "index"} - end - - def list_all - render json: {"action": "list_all"} - end -end diff --git a/spec/fixtures/demo/app/controllers/admin/stores_controller.rb b/spec/fixtures/demo/app/controllers/admin/stores_controller.rb deleted file mode 100644 index 5c9369e06..000000000 --- a/spec/fixtures/demo/app/controllers/admin/stores_controller.rb +++ /dev/null @@ -1,2 +0,0 @@ -class Admin::StoresController < StoresController -end diff --git a/spec/fixtures/demo/app/controllers/application_controller.rb b/spec/fixtures/demo/app/controllers/application_controller.rb deleted file mode 100644 index 6de3cf92b..000000000 --- a/spec/fixtures/demo/app/controllers/application_controller.rb +++ /dev/null @@ -1,2 +0,0 @@ -class ApplicationController < Jets::Controller::Base -end diff --git a/spec/fixtures/demo/app/controllers/articles_controller.rb b/spec/fixtures/demo/app/controllers/articles_controller.rb deleted file mode 100644 index 16addfe06..000000000 --- a/spec/fixtures/demo/app/controllers/articles_controller.rb +++ /dev/null @@ -1,62 +0,0 @@ -class ArticlesController < ApplicationController - before_action :set_article, only: [:show, :edit, :update, :delete] - - # GET /articles - description "All articles" - def index - @articles = Article.all - end - - # GET /articles/1 - def show - end - - # GET /articles/new - def new - @article = Article.new - end - - # GET /articles/1/edit - def edit - end - - # POST /articles - def create - @article = Article.new(article_params) - - if @article.save - redirect_to "/articles/#{@article.id}" - else - render :new - end - end - - # PUT /articles/1 - def update - if @article.update(article_params) - redirect_to "/articles/#{@article.id}" - else - render :edit - end - end - - # DELETE /articles/1 - def delete - @article.destroy - if request.xhr? - render json: {success: true} - else - redirect_to "/articles" - end - end - -private - # Use callbacks to share common setup or constraints between actions. - def set_article - @article = Article.find(params[:id]) - end - - def article_params - params[:article] - end -end diff --git a/spec/fixtures/demo/app/controllers/books_controller.rb b/spec/fixtures/demo/app/controllers/books_controller.rb deleted file mode 100644 index 39386e2a1..000000000 --- a/spec/fixtures/demo/app/controllers/books_controller.rb +++ /dev/null @@ -1,8 +0,0 @@ -class BooksController < ApplicationController - python :show - python :error_test - - node :list - node :node_error_test - node :node_async -end \ No newline at end of file diff --git a/spec/fixtures/demo/app/controllers/books_controller/node/hello.js b/spec/fixtures/demo/app/controllers/books_controller/node/hello.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/spec/fixtures/demo/app/controllers/books_controller/node/list.js b/spec/fixtures/demo/app/controllers/books_controller/node/list.js deleted file mode 100644 index 0936a55b6..000000000 --- a/spec/fixtures/demo/app/controllers/books_controller/node/list.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict'; - -exports.handler = function(event, context, callback) { - var body = {'message': 'hi from node'}; - var response = { - statusCode: "200", - headers: { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*' - }, - body: JSON.stringify(body) - }; - callback(null, response); -}; - -// if (require.main === module) { -// console.log('called directly'); -// } else { -// console.log('required as a module'); -// } \ No newline at end of file diff --git a/spec/fixtures/demo/app/controllers/books_controller/node/node_async.js b/spec/fixtures/demo/app/controllers/books_controller/node/node_async.js deleted file mode 100644 index d5e4ec144..000000000 --- a/spec/fixtures/demo/app/controllers/books_controller/node/node_async.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict'; - -exports.handler = async function(event, context) { - var body = {'message': 'hi from node'}; - var response = { - statusCode: "200", - headers: { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*' - }, - body: JSON.stringify(body) - }; - return(response); - // or - // throw new Error("some error type”); -} diff --git a/spec/fixtures/demo/app/controllers/books_controller/node/node_error_test.js b/spec/fixtures/demo/app/controllers/books_controller/node/node_error_test.js deleted file mode 100644 index 9eaa198ec..000000000 --- a/spec/fixtures/demo/app/controllers/books_controller/node/node_error_test.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict'; - -exports.handler = function(event, context, callback) { - INTENTIONAL_NODE_ERROR - var body = {'message': 'hi from node'}; - var response = { - statusCode: "200", - headers: { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*' - }, - body: JSON.stringify(body) - }; - callback(null, response); -}; diff --git a/spec/fixtures/demo/app/controllers/books_controller/python/error_test.py b/spec/fixtures/demo/app/controllers/books_controller/python/error_test.py deleted file mode 100644 index 3dd7174e2..000000000 --- a/spec/fixtures/demo/app/controllers/books_controller/python/error_test.py +++ /dev/null @@ -1,27 +0,0 @@ -import pprint -import json -import sys - -def response(message, status_code): - ERROR_RIGHT_HERE - return { - 'statusCode': str(status_code), - 'body': json.dumps(message), - 'headers': { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*' - }, - } - -def lambda_handler(event, context): - # TODO: figure out why this does not print out to stderr with python 3 - # print("BooksController#show", file=sys.stderr) - # print(pprint.pformat(event), file=sys.stderr)) - - try: - return response({'message': 'Big Thumbs up'}, 200) - except Exception as e: - return response({'message': e.message}, 400) - -if __name__ == '__main__': - print(lambda_handler({"test": "1"}, {})) \ No newline at end of file diff --git a/spec/fixtures/demo/app/controllers/books_controller/python/show.py b/spec/fixtures/demo/app/controllers/books_controller/python/show.py deleted file mode 100644 index 38c4a3e1a..000000000 --- a/spec/fixtures/demo/app/controllers/books_controller/python/show.py +++ /dev/null @@ -1,26 +0,0 @@ -import pprint -import json -import sys - -def response(message, status_code): - return { - 'statusCode': str(status_code), - 'body': json.dumps(message), - 'headers': { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*' - }, - } - -def lambda_handler(event, context): - # TODO: figure out why this does not print out to stderr with python 3 - # print("BooksController#show", file=sys.stderr) - # print(pprint.pformat(event), file=sys.stderr)) - - try: - return response({'message': 'Big Thumbs up'}, 200) - except Exception as e: - return response({'message': e.message}, 400) - -if __name__ == '__main__': - print(lambda_handler({"test": "1"}, {})) \ No newline at end of file diff --git a/spec/fixtures/demo/app/controllers/child_posts_controller.rb b/spec/fixtures/demo/app/controllers/child_posts_controller.rb deleted file mode 100644 index b0013be06..000000000 --- a/spec/fixtures/demo/app/controllers/child_posts_controller.rb +++ /dev/null @@ -1,9 +0,0 @@ -class ChildPostsController < PostsController - def index - render json: "test" - end - - def foobar - render plain: "foobar" - end -end \ No newline at end of file diff --git a/spec/fixtures/demo/app/controllers/comments_controller.rb b/spec/fixtures/demo/app/controllers/comments_controller.rb deleted file mode 100644 index 41876fe73..000000000 --- a/spec/fixtures/demo/app/controllers/comments_controller.rb +++ /dev/null @@ -1,6 +0,0 @@ -class CommentsController < ApplicationController - def hot - post = Post.find("tung") - render json: {action: "hot", ruby: RUBY_VERSION, post: post} - end -end diff --git a/spec/fixtures/demo/app/controllers/others_controller.rb b/spec/fixtures/demo/app/controllers/others_controller.rb deleted file mode 100644 index c56a73e40..000000000 --- a/spec/fixtures/demo/app/controllers/others_controller.rb +++ /dev/null @@ -1,5 +0,0 @@ -class OthersController < ApplicationController - def catchall - render json: {action: "all", event: event} - end -end diff --git a/spec/fixtures/demo/app/controllers/posts_controller.rb b/spec/fixtures/demo/app/controllers/posts_controller.rb deleted file mode 100644 index 4f393fcfe..000000000 --- a/spec/fixtures/demo/app/controllers/posts_controller.rb +++ /dev/null @@ -1,37 +0,0 @@ -class PostsController < ApplicationController - def index - posts = Post.scan # should not use scan for production - session[:foo] = "barbar" - render json: {action: "index", posts: posts} - end - - def new - render json: params.merge(action: "new") - end - - def show - post = Post.find(params[:id]) - render json: {action: "show", post: post} - end - - def create - render json: {action: "create", event: event} - end - - def edit - post = Post.find(params[:id]) - render json: {action: "edit", post: post} - end - - def update - post = Post.find(params[:id]) - post.attrs(title: params[:title], desc: params[:desc]) - post.replace - render json: {action: "update", post: post} - end - - def delete - Post.delete(params[:id]) - render json: {action: "delete"} - end -end diff --git a/spec/fixtures/demo/app/controllers/public_files_controller.rb b/spec/fixtures/demo/app/controllers/public_files_controller.rb deleted file mode 100644 index f9fc99349..000000000 --- a/spec/fixtures/demo/app/controllers/public_files_controller.rb +++ /dev/null @@ -1,11 +0,0 @@ -class PublicFilesController < ApplicationController - def show - path = Jets.root + "public" + params[:catchall] - if path.exist? - # TODO: only works for text files. Add support for binary data like images - render file: path - else - render status: 404 - end - end -end diff --git a/spec/fixtures/demo/app/controllers/related_posts_controller.rb b/spec/fixtures/demo/app/controllers/related_posts_controller.rb deleted file mode 100644 index 1a7709b98..000000000 --- a/spec/fixtures/demo/app/controllers/related_posts_controller.rb +++ /dev/null @@ -1,5 +0,0 @@ -class RelatedPostsController < ApplicationController - def show - render json: {"action": "show"} - end -end diff --git a/spec/fixtures/demo/app/controllers/stores_controller.rb b/spec/fixtures/demo/app/controllers/stores_controller.rb deleted file mode 100644 index c326cb5f6..000000000 --- a/spec/fixtures/demo/app/controllers/stores_controller.rb +++ /dev/null @@ -1,22 +0,0 @@ -class StoresController < ApplicationController - layout false # for specs to pass - - class_properties(memory_size: 768) - class_env(my_test: "data") - - # timeout 30 - properties(timeout: 20, memory_size: 1000, ephemeral_storage: { size: 1024 }) - def index - response.headers["Set-Cookie"] = "foo=bar" - end - - timeout 35 - def new - @item = {name: "soda"} - end - - environment(key1: "value1", key2: "value2") - memory_size(1024) - def show - end -end diff --git a/spec/fixtures/demo/app/controllers/toys_controller.rb b/spec/fixtures/demo/app/controllers/toys_controller.rb deleted file mode 100644 index 26e812499..000000000 --- a/spec/fixtures/demo/app/controllers/toys_controller.rb +++ /dev/null @@ -1,62 +0,0 @@ -class ToysController < ApplicationController - before_action :set_toy, only: [:show, :edit, :update, :delete] - authorization_scopes %w[create delete] - - # GET /toys - def index - @toys = Toy.all - end - - # GET /toys/1 - def show - end - - # GET /toys/new - def new - @toy = Toy.new - end - - # GET /toys/1/edit - def edit - end - - # POST /toys - def create - @toy = Toy.new(toy_params) - - if @toy.save - redirect_to "/toys/#{@toy.id}" - else - render :new - end - end - - # PUT /toys/1 - def update - if @toy.update(toy_params) - redirect_to "/toys/#{@toy.id}" - else - render :edit - end - end - - # DELETE /toys/1 - def delete - @toy.destroy - if request.xhr? - render json: {success: true} - else - redirect_to "/toys" - end - end - -private - # Use callbacks to share common setup or constraints between actions. - def set_toy - @toy = Toy.find(params[:id]) - end - - def toy_params - params[:toy] - end -end diff --git a/spec/fixtures/demo/app/extensions/iot_extension.rb b/spec/fixtures/demo/app/extensions/iot_extension.rb deleted file mode 100644 index 5ce9df316..000000000 --- a/spec/fixtures/demo/app/extensions/iot_extension.rb +++ /dev/null @@ -1,21 +0,0 @@ -module IotExtension - def thermostat_rule(logical_id, props={}) - logical_id = "#{logical_id}_topic_rule" - defaults = { - topic_rule_payload: { - sql: "select * from TemperatureTopic where temperature > 60" - }, - actions: [ - lambda: { function_arn: "!Ref {namespace}LambdaFunction" } - ] - } - props = defaults.deep_merge(props) - resource(logical_id, "AWS::Iot::TopicRule", props) - # resource( - # logical_id => { - # type: "AWS::Iot::TopicRule", - # properites: props - # } - # ) - end -end diff --git a/spec/fixtures/demo/app/functions/hello.rb b/spec/fixtures/demo/app/functions/hello.rb deleted file mode 100644 index 06121c11e..000000000 --- a/spec/fixtures/demo/app/functions/hello.rb +++ /dev/null @@ -1,5 +0,0 @@ -require "json" - -def world(event, context) - "hello world: #{event['key1'].inspect}" # Echo back the first key value -end diff --git a/spec/fixtures/demo/app/functions/simple_function.rb b/spec/fixtures/demo/app/functions/simple_function.rb deleted file mode 100644 index 6a8bbf0ef..000000000 --- a/spec/fixtures/demo/app/functions/simple_function.rb +++ /dev/null @@ -1,5 +0,0 @@ -require "json" - -def handler(event, context) - "simple handler: #{event['key1'].inspect}" # Echo back the first key value -end diff --git a/spec/fixtures/demo/app/helpers/application_helper.rb b/spec/fixtures/demo/app/helpers/application_helper.rb deleted file mode 100644 index de6be7945..000000000 --- a/spec/fixtures/demo/app/helpers/application_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module ApplicationHelper -end diff --git a/spec/fixtures/demo/app/helpers/articles_helper.rb b/spec/fixtures/demo/app/helpers/articles_helper.rb deleted file mode 100644 index 296827759..000000000 --- a/spec/fixtures/demo/app/helpers/articles_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module ArticlesHelper -end diff --git a/spec/fixtures/demo/app/helpers/toys_helper.rb b/spec/fixtures/demo/app/helpers/toys_helper.rb deleted file mode 100644 index 754b6d2bc..000000000 --- a/spec/fixtures/demo/app/helpers/toys_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module ToysHelper -end diff --git a/spec/fixtures/demo/app/javascript/packs/application.js b/spec/fixtures/demo/app/javascript/packs/application.js deleted file mode 100644 index 03016ba8e..000000000 --- a/spec/fixtures/demo/app/javascript/packs/application.js +++ /dev/null @@ -1,12 +0,0 @@ -/* eslint no-console:0 */ -// This file is automatically compiled by Webpack and Jets, along with any other files -// present in this directory. You're encouraged to place your actual application logic in -// a relevant structure within app/javascript and only use these pack files to reference -// that code so it'll be compiled. -// -// To reference this file, add <%= javascript_pack_tag 'application' %> to the appropriate -// layout file, like app/views/layouts/application.html.erb - -import '../src/application' -import './delete-item' -console.log('Hello World from Webpacker') diff --git a/spec/fixtures/demo/app/javascript/packs/delete-item.js b/spec/fixtures/demo/app/javascript/packs/delete-item.js deleted file mode 100644 index 2dc50b87c..000000000 --- a/spec/fixtures/demo/app/javascript/packs/delete-item.js +++ /dev/null @@ -1,47 +0,0 @@ -// This file is automatically generated by Jets. It is used by -// app/javascript/packs/application.js. -// -// It handles the delete action in an unobstrusive way. Code could be improved -// and is meant to provide only a starting point. - -$(function() { - function handler(e) { - var link = $(e.target); - if (link.hasClass("jets-delete")) { - event.preventDefault(); - var message = link.data("confirm"); - if (message) { - var sure = confirm(message); - if (sure) { - deleteItem(link); - } else { - console.log("Deletion cancelled"); - } - } - } - } - - function deleteItem(link) { - var node = link.closest('.jets-element-to-delete'); - node.hide(); // immediately hide element - - var resource = link.attr("href"); - var request = $.ajax({ - url: resource, - method: "DELETE", - dataType: "json" - }); - - request.done(function(msg) { - console.log("msg %o", msg) - node.remove(); - }); - - request.fail(function(jqXHR, textStatus) { - console.log("textStatus %o", textStatus) - node.show(); // in the event of a failure re-display the node - }); - } - - $('body').click(handler) -}); diff --git a/spec/fixtures/demo/app/javascript/packs/framework.js b/spec/fixtures/demo/app/javascript/packs/framework.js deleted file mode 100644 index a90c6ba0c..000000000 --- a/spec/fixtures/demo/app/javascript/packs/framework.js +++ /dev/null @@ -1,11 +0,0 @@ -/* eslint no-console:0 */ -// This file is automatically compiled by Webpack, along with any other files -// present in this directory. You're encouraged to place your actual framework logic in -// a relevant structure within app/javascript and only use these pack files to reference -// that code so it'll be compiled. -// -// To reference this file, add <%= javascript_pack_tag 'framework' %> to the appropriate -// layout file, like app/views/layouts/framework.html.erb - -import '../src/framework' -import './delete-item' diff --git a/spec/fixtures/demo/app/jobs/application_job.rb b/spec/fixtures/demo/app/jobs/application_job.rb deleted file mode 100644 index 05e7e5712..000000000 --- a/spec/fixtures/demo/app/jobs/application_job.rb +++ /dev/null @@ -1,2 +0,0 @@ -class ApplicationJob < Jets::Job::Base -end diff --git a/spec/fixtures/demo/app/jobs/easy_job.rb b/spec/fixtures/demo/app/jobs/easy_job.rb deleted file mode 100644 index 31625e11b..000000000 --- a/spec/fixtures/demo/app/jobs/easy_job.rb +++ /dev/null @@ -1,8 +0,0 @@ -class EasyJob < ApplicationJob - rate "1 day" - def sleep - seconds = Jets.env.test? ? 0 : 1 - sleep seconds - {done: "sleeping"} - end -end diff --git a/spec/fixtures/demo/app/jobs/error_job.rb b/spec/fixtures/demo/app/jobs/error_job.rb deleted file mode 100644 index eb23f9f40..000000000 --- a/spec/fixtures/demo/app/jobs/error_job.rb +++ /dev/null @@ -1,6 +0,0 @@ -class ErrorJob < ApplicationJob - rate "10 hours" # every 10 hours - def break - raise "break me" - end -end diff --git a/spec/fixtures/demo/app/jobs/hard_job.rb b/spec/fixtures/demo/app/jobs/hard_job.rb deleted file mode 100644 index b5a46194f..000000000 --- a/spec/fixtures/demo/app/jobs/hard_job.rb +++ /dev/null @@ -1,76 +0,0 @@ -class HardJob < ApplicationJob - rate "10 hours" # every 10 hours - def dig - puts "done digging" - {done: "digging"} - end - - rate "8 hours" # every 8 hours - def drive - puts("event data: #{event.inspect}") - {done: "driving"} - end - - cron "0 */12 * * ? *" # every 12 hours - def lift - puts "done lifting" - {done: "lifting"} - end -end - -# Configuring Job Rate -# -# AWS scheduled events supports a rate expression. It takes a unit plus a -# unit of time. Valid units of times are: minute, minutes, hour, hours, day, days -# Rate expression examples: -# -# every 1 minute: -# rate: "1 minute" -# -# everyday: -# cron: "1 day" -# -# AWS supports their own cron-like expression. AWS Cron expressions examples: -# -# every day at 12:00pm UTC: -# cron: "0 12 * * ? *" -# -# every day, at 5 and 35 minutes past 2:00pm UTC: -# cron: "5,35 14 * * ? *" -# -# 10:15am UTC on the last Friday of each month during the years 2002 to 2005: -# cron: "15 10 ? * 6L 2002-2005" -# -# Sample Data -# -# All job methods have the `event` and `context` available. Here's an example of what that data looks like: -# -# event: -# { -# "account": "123456789012", -# "region": "us-east-1", -# "detail": {}, -# "detail-type": "Scheduled Event", -# "source": "aws.events", -# "time": "1970-01-01T00:00:00Z", -# "id": "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c", -# "resources": [ -# "arn:aws:events:us-east-1:123456789012:rule/my-schedule" -# ] -# } -# -# context: -# { -# "callbackWaitsForEmptyEventLoop": true, -# "logGroupName": "/aws/lambda/demo-dev-2-posts-controller-new", -# "logStreamName": "2017/11/07/[$LATEST]3cefcb18a8bc49acbfb3f29907a36391", -# "functionName": "demo-dev-2-posts-controller-new", -# "memoryLimitInMB": "3008", -# "functionVersion": "$LATEST", -# "invokeid": "cd68b58a-c379-11e7-bab1-855c4fa0d379", -# "awsRequestId": "cd68b58a-c379-11e7-bab1-855c4fa0d379", -# "invokedFunctionArn": -# "arn:aws:lambda:us-east-1:123456789012:function:demo-dev-2-posts-controller-new" -# } -# -# For more info: http://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html diff --git a/spec/fixtures/demo/app/jobs/security_job.rb b/spec/fixtures/demo/app/jobs/security_job.rb deleted file mode 100644 index b137d1ba5..000000000 --- a/spec/fixtures/demo/app/jobs/security_job.rb +++ /dev/null @@ -1,20 +0,0 @@ -class SecurityJob < ApplicationJob - rule_event( - source: ["aws.ec2"], - detail_type: ["EC2 Instance State-change Notification"], - detail: { - state: ["stopping"], - } - ) - rule_event( - detail_type: ["AWS API Call via CloudTrail"], - detail: { - userIdentity: { - type: ["Root"] - } - } - ) - rate "10 hours" - def lock - end -end diff --git a/spec/fixtures/demo/app/jobs/temperature_job.rb b/spec/fixtures/demo/app/jobs/temperature_job.rb deleted file mode 100644 index 9d873845d..000000000 --- a/spec/fixtures/demo/app/jobs/temperature_job.rb +++ /dev/null @@ -1,6 +0,0 @@ -class TemperatureJob < ApplicationJob - thermostat_rule(:room) - def record - # custom business logic - end -end \ No newline at end of file diff --git a/spec/fixtures/demo/app/models/application_item.rb b/spec/fixtures/demo/app/models/application_item.rb deleted file mode 100644 index 80833a303..000000000 --- a/spec/fixtures/demo/app/models/application_item.rb +++ /dev/null @@ -1,2 +0,0 @@ -class ApplicationItem < Dynomite::Item -end diff --git a/spec/fixtures/demo/app/models/application_record.rb b/spec/fixtures/demo/app/models/application_record.rb deleted file mode 100644 index 10a4cba84..000000000 --- a/spec/fixtures/demo/app/models/application_record.rb +++ /dev/null @@ -1,3 +0,0 @@ -class ApplicationRecord < ActiveRecord::Base - self.abstract_class = true -end diff --git a/spec/fixtures/demo/app/models/article.rb b/spec/fixtures/demo/app/models/article.rb deleted file mode 100644 index b7a72b589..000000000 --- a/spec/fixtures/demo/app/models/article.rb +++ /dev/null @@ -1,2 +0,0 @@ -class Article < ApplicationRecord -end diff --git a/spec/fixtures/demo/app/models/comment.rb b/spec/fixtures/demo/app/models/comment.rb deleted file mode 100644 index fac3a9957..000000000 --- a/spec/fixtures/demo/app/models/comment.rb +++ /dev/null @@ -1,2 +0,0 @@ -class Comment < ApplicationItem -end diff --git a/spec/fixtures/demo/app/models/ns/a.rb b/spec/fixtures/demo/app/models/ns/a.rb deleted file mode 100644 index e76555e2a..000000000 --- a/spec/fixtures/demo/app/models/ns/a.rb +++ /dev/null @@ -1,3 +0,0 @@ -# Test for namespace autovivification: spec/lib/jets/namespace_spec.rb -class Ns::A -end \ No newline at end of file diff --git a/spec/fixtures/demo/app/models/post.rb b/spec/fixtures/demo/app/models/post.rb deleted file mode 100644 index 960ea573b..000000000 --- a/spec/fixtures/demo/app/models/post.rb +++ /dev/null @@ -1,2 +0,0 @@ -class Post < ApplicationItem -end diff --git a/spec/fixtures/demo/app/models/toy.rb b/spec/fixtures/demo/app/models/toy.rb deleted file mode 100644 index 146740a3c..000000000 --- a/spec/fixtures/demo/app/models/toy.rb +++ /dev/null @@ -1,2 +0,0 @@ -class Toy < ApplicationRecord -end diff --git a/spec/fixtures/demo/app/rules/game_rule.rb b/spec/fixtures/demo/app/rules/game_rule.rb deleted file mode 100644 index 9964be347..000000000 --- a/spec/fixtures/demo/app/rules/game_rule.rb +++ /dev/null @@ -1,26 +0,0 @@ -class GameRule < Jets::Rule::Base - # "ConfigRuleName" : String, - # "Description" : String, - # "InputParameters" : { ParameterName : Value }, - # "MaximumExecutionFrequency" : String, - # "Scope" : Scope, - # "Source" : Source - - # scope("ComplianceResourceTypes" => [ "AWS::EC2::SecurityGroup" ]) - - # config_rule( - # config_rule_name: "String", - # description: "String", - # input_parameters: { "k1" => "v1" }, - # maximum_execution_frequency: "String", # One_Hour | Three_Hours | Six_Hours | Twelve_Hours | TwentyFour_Hours # https://docs.aws.amazon.com/config/latest/APIReference/API_ConfigRule.html - # scope: {"ComplianceResourceTypes" => [ "AWS::EC2::SecurityGroup" ]}, - # # source: # the method below here automatically is the source - # score: { - # "Owner" => "String", # CUSTOM_LAMBDA | AWS - # } - # ) - scope "AWS::EC2::SecurityGroup" - def protect - puts "event #{event.inspect}" - end -end diff --git a/spec/fixtures/demo/app/shared/functions/admin/send_message.rb b/spec/fixtures/demo/app/shared/functions/admin/send_message.rb deleted file mode 100644 index e69de29bb..000000000 diff --git a/spec/fixtures/demo/app/shared/functions/bob.rb b/spec/fixtures/demo/app/shared/functions/bob.rb deleted file mode 100644 index e69de29bb..000000000 diff --git a/spec/fixtures/demo/app/shared/functions/gru.rb b/spec/fixtures/demo/app/shared/functions/gru.rb deleted file mode 100644 index e69de29bb..000000000 diff --git a/spec/fixtures/demo/app/shared/functions/hello.rb b/spec/fixtures/demo/app/shared/functions/hello.rb deleted file mode 100644 index e69de29bb..000000000 diff --git a/spec/fixtures/demo/app/shared/functions/howdy.py b/spec/fixtures/demo/app/shared/functions/howdy.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/spec/fixtures/demo/app/shared/functions/kevin.py b/spec/fixtures/demo/app/shared/functions/kevin.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/spec/fixtures/demo/app/shared/functions/stuart.js b/spec/fixtures/demo/app/shared/functions/stuart.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/spec/fixtures/demo/app/shared/functions/whatever.rb b/spec/fixtures/demo/app/shared/functions/whatever.rb deleted file mode 100644 index 87251cbc1..000000000 --- a/spec/fixtures/demo/app/shared/functions/whatever.rb +++ /dev/null @@ -1,5 +0,0 @@ -require "json" - -def handle(event, context) - "hello world: #{event['key1'].inspect}" # Echo back the first key value -end diff --git a/spec/fixtures/demo/app/shared/resources/alert.rb b/spec/fixtures/demo/app/shared/resources/alert.rb deleted file mode 100644 index daf7fddaf..000000000 --- a/spec/fixtures/demo/app/shared/resources/alert.rb +++ /dev/null @@ -1,3 +0,0 @@ -class Alert < Jets::Stack - sns_topic "delivered" -end diff --git a/spec/fixtures/demo/app/shared/resources/custom.rb b/spec/fixtures/demo/app/shared/resources/custom.rb deleted file mode 100644 index 4b6a52e1c..000000000 --- a/spec/fixtures/demo/app/shared/resources/custom.rb +++ /dev/null @@ -1,23 +0,0 @@ -class Custom < Jets::Stack - resource(:howdy, - FunctionName: "howdy", - Code: { - S3Bucket: "!Ref S3Bucket", - S3Key: code_s3_key - }, - Description: "Hello world", - Handler: handler("howdy.lambda_handler"), - MemorySize: 128, - Role: "!Ref IamRole", - Runtime: "python3.6", - Timeout: 20, - ) - - function(:gru, runtime: :ruby, handler: "gru.handle") - - ruby_function(:bob) - python_function(:kevin) - node_function(:stuart) - - output("test") -end diff --git a/spec/fixtures/demo/app/views/articles/_form.html.erb b/spec/fixtures/demo/app/views/articles/_form.html.erb deleted file mode 100644 index 919be8ca7..000000000 --- a/spec/fixtures/demo/app/views/articles/_form.html.erb +++ /dev/null @@ -1,32 +0,0 @@ -<%= form_tag("/articles") do %> - <% if article.errors.any? %> -
-

<%= pluralize(article.errors.count, "error") %> prohibited this article from being saved:

- -
    - <% article.errors.full_messages.each do |message| %> -
  • <%= message %>
  • - <% end %> -
-
- <% end %> - -
- <%= label_tag :title %> - <%= text_field_tag "article[title]", article.title, id: :article_title %> -
- -
- <%= label_tag :body %> - <%= text_area_tag "article[body]", article.body, id: :article_body %> -
- -
- <%= label_tag :published %> - <%= check_box_tag "article[published]", 'yes', article.published, id: :article_published %> -
- -
- <%= submit_tag("Submit") %> -
-<% end %> diff --git a/spec/fixtures/demo/app/views/articles/edit.html.erb b/spec/fixtures/demo/app/views/articles/edit.html.erb deleted file mode 100644 index 2eb38fb29..000000000 --- a/spec/fixtures/demo/app/views/articles/edit.html.erb +++ /dev/null @@ -1,6 +0,0 @@ -

Editing Article

- -<%= render 'form', article: @article %> - -<%= link_to 'Show', "/articles/#{@article.id}/edit" %> | -<%= link_to 'Back', "/articles" %> diff --git a/spec/fixtures/demo/app/views/articles/index.html.erb b/spec/fixtures/demo/app/views/articles/index.html.erb deleted file mode 100644 index 42f62d375..000000000 --- a/spec/fixtures/demo/app/views/articles/index.html.erb +++ /dev/null @@ -1,29 +0,0 @@ -

Articles

- - - - - - - - - - - - - <% @articles.each do |article| %> - - - - - - - - - <% end %> - -
TitleBodyPublished
<%= article.title %><%= article.body %><%= article.published %><%= link_to 'Show', "/articles/#{article.id}" %><%= link_to 'Edit', "/articles/#{article.id}/edit" %><%= link_to 'Destroy', "/articles/#{article.id}", method: :delete, data: { confirm: 'Are you sure?' } %>
- -
- -<%= link_to 'New Article', "/articles/new" %> diff --git a/spec/fixtures/demo/app/views/articles/new.html.erb b/spec/fixtures/demo/app/views/articles/new.html.erb deleted file mode 100644 index 8b5b0a082..000000000 --- a/spec/fixtures/demo/app/views/articles/new.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -

New Article

- -<%= render 'form', article: @article %> - -<%= link_to 'Back', "/articles" %> diff --git a/spec/fixtures/demo/app/views/articles/show.html.erb b/spec/fixtures/demo/app/views/articles/show.html.erb deleted file mode 100644 index 41abdc940..000000000 --- a/spec/fixtures/demo/app/views/articles/show.html.erb +++ /dev/null @@ -1,17 +0,0 @@ -

- Title: - <%= @article.title %> -

- -

- Body: - <%= @article.body %> -

- -

- Published: - <%= @article.published %> -

- -<%= link_to 'Edit', "/articles/#{@article.id}/edit" %> | -<%= link_to 'Back', "/articles" %> diff --git a/spec/fixtures/demo/app/views/layouts/application.html.erb b/spec/fixtures/demo/app/views/layouts/application.html.erb deleted file mode 100644 index 860739ea9..000000000 --- a/spec/fixtures/demo/app/views/layouts/application.html.erb +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - Demo Jets App - - <%= javascript_include_tag "https://code.jquery.com/jquery-3.2.1.min.js", integrity: "sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=", - crossorigin: "anonymous" %> - - <%= javascript_pack_tag "framework" %> - <%= stylesheet_pack_tag "framework" %> - <%= javascript_pack_tag "application" %> - - <%= javascript_include_tag "test" %> - <%= stylesheet_link_tag "test" %> - - - -

Application layout

- -<%= yield %> - - - diff --git a/spec/fixtures/demo/app/views/posts/_test.html.erb b/spec/fixtures/demo/app/views/posts/_test.html.erb deleted file mode 100644 index 6a1cf9125..000000000 --- a/spec/fixtures/demo/app/views/posts/_test.html.erb +++ /dev/null @@ -1 +0,0 @@ -partial test diff --git a/spec/fixtures/demo/app/views/posts/index.html.erb b/spec/fixtures/demo/app/views/posts/index.html.erb deleted file mode 100644 index d6e58665d..000000000 --- a/spec/fixtures/demo/app/views/posts/index.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -

app/views/index.html.erb

- -<%= render "test" %> - -

test

diff --git a/spec/fixtures/demo/app/views/stores/index.html.erb b/spec/fixtures/demo/app/views/stores/index.html.erb deleted file mode 100644 index 8588d4580..000000000 --- a/spec/fixtures/demo/app/views/stores/index.html.erb +++ /dev/null @@ -1 +0,0 @@ -stores index test page \ No newline at end of file diff --git a/spec/fixtures/demo/app/views/stores/new.html.erb b/spec/fixtures/demo/app/views/stores/new.html.erb deleted file mode 100644 index ad5a57e0c..000000000 --- a/spec/fixtures/demo/app/views/stores/new.html.erb +++ /dev/null @@ -1,3 +0,0 @@ -

stores/new.html.erb file

- -

Item name: <%= @item[:name] %>

diff --git a/spec/fixtures/demo/app/views/toys/_form.html.erb b/spec/fixtures/demo/app/views/toys/_form.html.erb deleted file mode 100644 index a20e1ba73..000000000 --- a/spec/fixtures/demo/app/views/toys/_form.html.erb +++ /dev/null @@ -1,32 +0,0 @@ -<%= form_tag("/toys") do %> - <% if toy.errors.any? %> -
-

<%= pluralize(toy.errors.count, "error") %> prohibited this toy from being saved:

- -
    - <% toy.errors.full_messages.each do |message| %> -
  • <%= message %>
  • - <% end %> -
-
- <% end %> - -
- <%= label_tag :title %> - <%= text_field_tag "toy[title]", toy.title, id: :toy_title %> -
- -
- <%= label_tag :body %> - <%= text_area_tag "toy[body]", toy.body, id: :toy_body %> -
- -
- <%= label_tag :published %> - <%= check_box_tag "toy[published]", 'yes', toy.published, id: :toy_published %> -
- -
- <%= submit_tag("Submit") %> -
-<% end %> diff --git a/spec/fixtures/demo/app/views/toys/edit.html.erb b/spec/fixtures/demo/app/views/toys/edit.html.erb deleted file mode 100644 index 8526c6a00..000000000 --- a/spec/fixtures/demo/app/views/toys/edit.html.erb +++ /dev/null @@ -1,6 +0,0 @@ -

Editing Toy

- -<%= render 'form', toy: @toy %> - -<%= link_to 'Show', "/toys/#{@toy.id}/edit" %> | -<%= link_to 'Back', "/toys" %> diff --git a/spec/fixtures/demo/app/views/toys/index.html.erb b/spec/fixtures/demo/app/views/toys/index.html.erb deleted file mode 100644 index cf71d04b8..000000000 --- a/spec/fixtures/demo/app/views/toys/index.html.erb +++ /dev/null @@ -1,29 +0,0 @@ -

Toys

- - - - - - - - - - - - - <% @toys.each do |toy| %> - - - - - - - - - <% end %> - -
TitleBodyPublished
<%= toy.title %><%= toy.body %><%= toy.published %><%= link_to 'Show', "/toys/#{toy.id}" %><%= link_to 'Edit', "/toys/#{toy.id}/edit" %><%= link_to 'Destroy', "/toys/#{toy.id}", method: :delete, data: { confirm: 'Are you sure?' } %>
- -
- -<%= link_to 'New Toy', "/toys/new" %> diff --git a/spec/fixtures/demo/app/views/toys/new.html.erb b/spec/fixtures/demo/app/views/toys/new.html.erb deleted file mode 100644 index 1eba45f3c..000000000 --- a/spec/fixtures/demo/app/views/toys/new.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -

New Toy

- -<%= render 'form', toy: @toy %> - -<%= link_to 'Back', "/toys" %> diff --git a/spec/fixtures/demo/app/views/toys/show.html.erb b/spec/fixtures/demo/app/views/toys/show.html.erb deleted file mode 100644 index 02c29fefb..000000000 --- a/spec/fixtures/demo/app/views/toys/show.html.erb +++ /dev/null @@ -1,17 +0,0 @@ -

- Title: - <%= @toy.title %> -

- -

- Body: - <%= @toy.body %> -

- -

- Published: - <%= @toy.published %> -

- -<%= link_to 'Edit', "/toys/#{@toy.id}/edit" %> | -<%= link_to 'Back', "/toys" %> diff --git a/spec/fixtures/demo/config.ru b/spec/fixtures/demo/config.ru deleted file mode 100644 index ddd69de64..000000000 --- a/spec/fixtures/demo/config.ru +++ /dev/null @@ -1,5 +0,0 @@ -# This file is used by Rack-based servers to start the application. - -require "jets" -Jets.boot -run Jets.application diff --git a/spec/fixtures/demo/config/application.rb b/spec/fixtures/demo/config/application.rb deleted file mode 100644 index 606b67e97..000000000 --- a/spec/fixtures/demo/config/application.rb +++ /dev/null @@ -1,26 +0,0 @@ -module Demo - class Application < Jets::Application - config.project_name = "demo" - # config.extra = 2 - # config.autoload_paths = [] - - config.function.timeout = 30 - # config.function.memory_size = 3008 - # config.function.cors = true - config.function.environment = { - global_app_key1: "global_app_value1", - global_app_key2: "global_app_value2", - } - # More examples: - # config.function.dead_letter_queue = { target_arn: "arn" } - # config.function.vpc_config = { - # security_group_ids: [ "sg-1", "sg-2" ], - # subnet_ids: [ "subnet-1", "subnet-2" ] - # } - # The config.function settings to the CloudFormation Lambda Function properties. - # http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-function.html - # Underscored format can be used for keys to make it look more ruby-ish. - - config.controllers.default_protect_from_forgery = false - end -end diff --git a/spec/fixtures/demo/config/database.yml b/spec/fixtures/demo/config/database.yml deleted file mode 100644 index 6838ddcdc..000000000 --- a/spec/fixtures/demo/config/database.yml +++ /dev/null @@ -1,24 +0,0 @@ -default: &default - adapter: mysql2 - encoding: utf8mb4 - pool: <%= ENV["DB_POOL"] || 5 %> - database: <%= ENV['DB_NAME'] || 'demo_dev' %> - username: <%= ENV['DB_USER'] || 'tung' %> - password: <%= ENV['DB_PASS'] %> - host: <%= ENV["DB_HOST"] %> - -development: - <<: *default - database: demo_dev - -test: - <<: *default - database: demo_test - -staging: - <<: *default - url: <%= ENV['DB_URL'] %> - -production: - <<: *default - url: <%= ENV['DB_URL'] %> diff --git a/spec/fixtures/demo/config/environments/development.rb b/spec/fixtures/demo/config/environments/development.rb deleted file mode 100644 index 9024d4c1d..000000000 --- a/spec/fixtures/demo/config/environments/development.rb +++ /dev/null @@ -1,27 +0,0 @@ -Jets.application.configure do - config.enable_reloading = true - config.eager_load = false - config.consider_all_requests_local = true - config.server_timing = true - config.jets_controller.perform_caching = false - config.jets_controller.enable_fragment_cache_logging = false - - config.action_dispatch.show_exceptions = true - - # Enable/disable caching. By default caching is disabled. - if Jets.root.join("tmp/caching-dev.txt").exist? - config.jets_controller.perform_caching = true - config.cache_store = :memory_store - else - config.jets_controller.perform_caching = false - config.cache_store = :null_store - end - - config.action_mailer.raise_delivery_errors = false - config.action_mailer.perform_caching = false - # config.action_mailer.default_url_options = { host: 'localhost', port: 8888 } - - config.logging.event = false # useful for CloudWatch logs - # Example: - # config.function.memory_size = 1536 -end \ No newline at end of file diff --git a/spec/fixtures/demo/config/environments/production.rb b/spec/fixtures/demo/config/environments/production.rb deleted file mode 100644 index 36d564eee..000000000 --- a/spec/fixtures/demo/config/environments/production.rb +++ /dev/null @@ -1,13 +0,0 @@ -Jets.application.configure do - config.cache_classes = true - config.eager_load = true - config.log_level = :info - config.logging.event = true # useful for CloudWatch logs - config.consider_all_requests_local = false - config.assets.compile = false - config.action_mailer.raise_delivery_errors = true - config.action_mailer.perform_caching = false - # config.action_mailer.default_url_options = { host: 'localhost', port: 8888 } - # Example: - # config.function.memory_size = 2048 -end \ No newline at end of file diff --git a/spec/fixtures/demo/config/environments/test.rb b/spec/fixtures/demo/config/environments/test.rb deleted file mode 100644 index 3f1991a40..000000000 --- a/spec/fixtures/demo/config/environments/test.rb +++ /dev/null @@ -1,3 +0,0 @@ -Jets.application.configure do - config.eager_load = false -end \ No newline at end of file diff --git a/spec/fixtures/demo/config/initializers/01_initializer.rb b/spec/fixtures/demo/config/initializers/01_initializer.rb deleted file mode 100644 index 9f8cce322..000000000 --- a/spec/fixtures/demo/config/initializers/01_initializer.rb +++ /dev/null @@ -1 +0,0 @@ -JETS_TEST_INITIALIZER_ONE_TIME = DateTime.current diff --git a/spec/fixtures/demo/config/initializers/02_initializer.rb b/spec/fixtures/demo/config/initializers/02_initializer.rb deleted file mode 100644 index 21f926f01..000000000 --- a/spec/fixtures/demo/config/initializers/02_initializer.rb +++ /dev/null @@ -1 +0,0 @@ -JETS_TEST_INITIALIZER_TWO_TIME = DateTime.current diff --git a/spec/fixtures/demo/config/routes.rb b/spec/fixtures/demo/config/routes.rb deleted file mode 100644 index 9b64525ae..000000000 --- a/spec/fixtures/demo/config/routes.rb +++ /dev/null @@ -1,44 +0,0 @@ -Jets.application.routes.draw do - root "posts#index" - - resources :toys - resources :posts - # resources :posts expands to: - # get "posts", to: "posts#index" - # get "posts/new", to: "posts#new" - # get "posts/:id", to: "posts#show" - # post "posts", to: "posts#create" - # get "posts/:id/edit", to: "posts#edit" - # put "posts/:id", to: "posts#update" - # delete "posts/:id", to: "posts#delete" - - any "comments/hot", to: "comments#hot" - get "landing/posts", to: "posts#index" - - get "admin/pages", to: "admin/pages#index" - get "related_posts/:id", to: "related_posts#show" - - resources :stores - - # to demo ActiveRecord support - resources :articles - - any "others/*proxy", to: "others#catchall" - # # jets routes these special paths to the JetsPublicFilesController - # any "public/*proxy", to: "jets/public_files#catchall" - # any "javascripts/*proxy", to: "jets/public_files#catchall" - # any "stylesheets/*proxy", to: "jets/public_files#catchall" - - # Catchall routes at the root level work differently in local development - # than on AWS API Gateway. Locally, the rack middleware routes the static - # files directly never hits Jets. On AWS though, there is no rack middleware - # it's all API Gateway. So for a root level catchall route like - # - # any "*catchall", to: "jets/public_files#show" - # - # It cannot be tested locally - at least, I'm don't how to do that yet. - any "static/*catchall", to: "jets/public_files#show" - any "*catchall", to: "jets/public#show" - - # public2/stylesheets/test.css -end diff --git a/spec/fixtures/demo/config/webpack/development.js b/spec/fixtures/demo/config/webpack/development.js deleted file mode 100644 index 81269f651..000000000 --- a/spec/fixtures/demo/config/webpack/development.js +++ /dev/null @@ -1,3 +0,0 @@ -const environment = require('./environment') - -module.exports = environment.toWebpackConfig() diff --git a/spec/fixtures/demo/config/webpack/environment.js b/spec/fixtures/demo/config/webpack/environment.js deleted file mode 100644 index d16d9af74..000000000 --- a/spec/fixtures/demo/config/webpack/environment.js +++ /dev/null @@ -1,3 +0,0 @@ -const { environment } = require('@rails/webpacker') - -module.exports = environment diff --git a/spec/fixtures/demo/config/webpack/production.js b/spec/fixtures/demo/config/webpack/production.js deleted file mode 100644 index 81269f651..000000000 --- a/spec/fixtures/demo/config/webpack/production.js +++ /dev/null @@ -1,3 +0,0 @@ -const environment = require('./environment') - -module.exports = environment.toWebpackConfig() diff --git a/spec/fixtures/demo/config/webpack/staging.js b/spec/fixtures/demo/config/webpack/staging.js deleted file mode 100644 index 81269f651..000000000 --- a/spec/fixtures/demo/config/webpack/staging.js +++ /dev/null @@ -1,3 +0,0 @@ -const environment = require('./environment') - -module.exports = environment.toWebpackConfig() diff --git a/spec/fixtures/demo/config/webpack/test.js b/spec/fixtures/demo/config/webpack/test.js deleted file mode 100644 index 81269f651..000000000 --- a/spec/fixtures/demo/config/webpack/test.js +++ /dev/null @@ -1,3 +0,0 @@ -const environment = require('./environment') - -module.exports = environment.toWebpackConfig() diff --git a/spec/fixtures/demo/config/webpacker.yml b/spec/fixtures/demo/config/webpacker.yml deleted file mode 100644 index bcdf35731..000000000 --- a/spec/fixtures/demo/config/webpacker.yml +++ /dev/null @@ -1,73 +0,0 @@ -# Note: You must restart bin/webpack-dev-server for changes to take effect - -default: &default - source_path: app/javascript - source_entry_path: packs - public_output_path: packs - cache_path: tmp/cache/webpacker - - # Additional paths webpack should lookup modules - # ['app/assets', 'engine/foo/app/assets'] - # resolved_paths: [] - additional_paths: [] - - # Reload manifest.json on all requests so we reload latest compiled packs - cache_manifest: false - - extensions: - - .coffee - - .erb - - .js - - .jsx - - .ts - - .vue - - .sass - - .scss - - .css - - .png - - .svg - - .gif - - .jpeg - - .jpg - -development: - <<: *default - compile: true - - # Reference: https://webpack.js.org/configuration/dev-server/ - dev_server: - https: false - host: localhost - port: 3035 - public: localhost:3035 - hmr: false - # Inline should be set to true if using HMR - inline: true - overlay: true - disable_host_check: true - use_local_ip: false - -test: - <<: *default - compile: true - - # Compile test packs to a separate directory - public_output_path: packs-test - -staging: - <<: *default - - # Production depends on precompilation of packs prior to booting for performance. - compile: false - - # Cache manifest.json for performance - cache_manifest: true - -production: - <<: *default - - # Production depends on precompilation of packs prior to booting for performance. - compile: false - - # Cache manifest.json for performance - cache_manifest: true diff --git a/spec/fixtures/demo/db/migrate/20171114235317_create_articles.rb b/spec/fixtures/demo/db/migrate/20171114235317_create_articles.rb deleted file mode 100644 index ec9080906..000000000 --- a/spec/fixtures/demo/db/migrate/20171114235317_create_articles.rb +++ /dev/null @@ -1,9 +0,0 @@ -class CreateArticles < ActiveRecord::Migration[5.1] - def change - create_table :articles do |t| - t.string :title - t.text :body - t.boolean :published - end - end -end diff --git a/spec/fixtures/demo/db/migrate/20171127132819_create_toys.rb b/spec/fixtures/demo/db/migrate/20171127132819_create_toys.rb deleted file mode 100644 index 44cfd69b4..000000000 --- a/spec/fixtures/demo/db/migrate/20171127132819_create_toys.rb +++ /dev/null @@ -1,11 +0,0 @@ -class CreateToys < ActiveRecord::Migration[5.1] - def change - create_table :toys do |t| - t.string :title - t.text :body - t.boolean :published - - t.timestamps - end - end -end diff --git a/spec/fixtures/demo/db/schema.rb b/spec/fixtures/demo/db/schema.rb deleted file mode 100644 index c92312025..000000000 --- a/spec/fixtures/demo/db/schema.rb +++ /dev/null @@ -1,28 +0,0 @@ -# This file is auto-generated from the current state of the database. Instead -# of editing this file, please use the migrations feature of Active Record to -# incrementally modify your database, and then regenerate this schema definition. -# -# Note that this schema.rb definition is the authoritative source for your -# database schema. If you need to create the application database on another -# system, you should be using db:schema:load, not running all the migrations -# from scratch. The latter is a flawed and unsustainable approach (the more migrations -# you'll amass, the slower it'll run and the greater likelihood for issues). -# -# It's strongly recommended that you check this file into your version control system. - -ActiveRecord::Schema.define(version: 20171114235317) do - - # These are extensions that must be enabled in order to support this database - enable_extension "plpgsql" - - create_table "articles", force: :cascade do |t| - t.string "title" - t.text "body" - t.boolean "published" - end - - create_table "films", id: false, force: :cascade do |t| - t.string "title", limit: 40, null: false - end - -end diff --git a/spec/fixtures/demo/dev.mode b/spec/fixtures/demo/dev.mode deleted file mode 100644 index e69de29bb..000000000 diff --git a/spec/fixtures/demo/package.json b/spec/fixtures/demo/package.json deleted file mode 100644 index 6f224b3b3..000000000 --- a/spec/fixtures/demo/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "dependencies": { - "@rails/webpacker": "^3.0.2", - "coffeescript": "1.12.7" - }, - "devDependencies": { - "webpack-dev-server": "^2.9.5" - } -} diff --git a/spec/fixtures/demo/payloads/create.json b/spec/fixtures/demo/payloads/create.json deleted file mode 100644 index 884c014ef..000000000 --- a/spec/fixtures/demo/payloads/create.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "id": "myid", - "title": "test title", - "desc": "test desc" -} diff --git a/spec/fixtures/demo/payloads/hello.json b/spec/fixtures/demo/payloads/hello.json deleted file mode 100644 index fd2722e85..000000000 --- a/spec/fixtures/demo/payloads/hello.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "key1": "value1", - "key2": "value2", - "key3": "value3" -} diff --git a/spec/fixtures/demo/public/assets/a.txt b/spec/fixtures/demo/public/assets/a.txt deleted file mode 100644 index b21bb5fdc..000000000 --- a/spec/fixtures/demo/public/assets/a.txt +++ /dev/null @@ -1 +0,0 @@ -a.txt file diff --git a/spec/fixtures/demo/public/assets/photo.jpg b/spec/fixtures/demo/public/assets/photo.jpg deleted file mode 100644 index 347d1b0e4..000000000 Binary files a/spec/fixtures/demo/public/assets/photo.jpg and /dev/null differ diff --git a/spec/fixtures/demo/public/assets/sample.pdf b/spec/fixtures/demo/public/assets/sample.pdf deleted file mode 100644 index 78232e652..000000000 Binary files a/spec/fixtures/demo/public/assets/sample.pdf and /dev/null differ diff --git a/spec/fixtures/demo/public/favicon.ico b/spec/fixtures/demo/public/favicon.ico deleted file mode 100644 index b79b3ea03..000000000 Binary files a/spec/fixtures/demo/public/favicon.ico and /dev/null differ diff --git a/spec/fixtures/demo/public/javascripts/test.js b/spec/fixtures/demo/public/javascripts/test.js deleted file mode 100644 index e03308d4f..000000000 --- a/spec/fixtures/demo/public/javascripts/test.js +++ /dev/null @@ -1 +0,0 @@ -console.log("test from public/assets/test.js"); diff --git a/spec/fixtures/demo/public/stylesheets/test.css b/spec/fixtures/demo/public/stylesheets/test.css deleted file mode 100644 index f6bf76fdb..000000000 --- a/spec/fixtures/demo/public/stylesheets/test.css +++ /dev/null @@ -1,3 +0,0 @@ -/* public/assets/test.css */ -body { -} diff --git a/spec/fixtures/demo/spec/fixtures/payloads/posts-index.json b/spec/fixtures/demo/spec/fixtures/payloads/posts-index.json deleted file mode 100644 index 346d39f83..000000000 --- a/spec/fixtures/demo/spec/fixtures/payloads/posts-index.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "resource": "/posts", - "path": "/posts", - "httpMethod": "GET", - "headers": { - "Accept": "*/*", - "CloudFront-Forwarded-Proto": "https", - "CloudFront-Is-Desktop-Viewer": "true", - "CloudFront-Is-Mobile-Viewer": "false", - "CloudFront-Is-SmartTV-Viewer": "false", - "CloudFront-Is-Tablet-Viewer": "false", - "CloudFront-Viewer-Country": "US", - "Host": "qg45s7uvg2.execute-api.us-east-1.amazonaws.com", - "User-Agent": "curl/7.54.0", - "Via": "1.1 3d3d633d266d05d90a4eea7a6a59b514.cloudfront.net (CloudFront)", - "X-Amz-Cf-Id": "4mAgowukJJbA7lgTWITzgOPmdiDsXPCwy6vonS8VKPXCdEsmldVgdg==", - "X-Amzn-Trace-Id": "Root=1-59fb8ea5-38c5ad176dac130f3eb9ce97", - "X-Forwarded-For": "69.42.1.180, 54.239.203.118", - "X-Forwarded-Port": "443", - "X-Forwarded-Proto": "https" - }, - "queryStringParameters": null, - "pathParameters": null, - "stageVariables": null, - "requestContext": { - "path": "/prod/posts", - "accountId": "123456789012", - "resourceId": "ery965", - "stage": "prod", - "requestId": "292fbcc8-c015-11e7-94fa-cd109b693f3c", - "identity": { - "cognitoIdentityPoolId": null, - "accountId": null, - "cognitoIdentityId": null, - "caller": null, - "apiKey": "", - "sourceIp": "69.42.1.180", - "accessKey": null, - "cognitoAuthenticationType": null, - "cognitoAuthenticationProvider": null, - "userArn": null, - "userAgent": "curl/7.54.0", - "user": null - }, - "resourcePath": "/posts", - "httpMethod": "GET", - "apiId": "qg45s7uvg2" - }, - "body": null, - "isBase64Encoded": false -} diff --git a/spec/fixtures/demo/spec/fixtures/payloads/posts-show.json b/spec/fixtures/demo/spec/fixtures/payloads/posts-show.json deleted file mode 100644 index 1ea12a479..000000000 --- a/spec/fixtures/demo/spec/fixtures/payloads/posts-show.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "resource": "/posts/{id}", - "path": "/posts/tung", - "httpMethod": "GET", - "headers": { - "Accept": "*/*", - "CloudFront-Forwarded-Proto": "https", - "CloudFront-Is-Desktop-Viewer": "true", - "CloudFront-Is-Mobile-Viewer": "false", - "CloudFront-Is-SmartTV-Viewer": "false", - "CloudFront-Is-Tablet-Viewer": "false", - "CloudFront-Viewer-Country": "US", - "Host": "qg45s7uvg2.execute-api.us-east-1.amazonaws.com", - "User-Agent": "curl/7.54.0", - "Via": "1.1 dc553909528b8b63475c922dc07d8ba6.cloudfront.net (CloudFront)", - "X-Amz-Cf-Id": "s9NB2f_Z1scd6ksA4fcXA2r4QhpSKQ6QcTLQHGfxDPfI-X7sXM3YLg==", - "X-Amzn-Trace-Id": "Root=1-59fb8ecb-586ab81d73e36910793d767c", - "X-Forwarded-For": "69.42.1.180, 54.239.203.97", - "X-Forwarded-Port": "443", - "X-Forwarded-Proto": "https" - }, - "queryStringParameters": null, - "pathParameters": { - "id": "tung" - }, - "stageVariables": null, - "requestContext": { - "path": "/prod/posts/tung", - "accountId": "123456789012", - "resourceId": "wf2gvu", - "stage": "prod", - "requestId": "3feafb4e-c015-11e7-85c9-194f38f4c414", - "identity": { - "cognitoIdentityPoolId": null, - "accountId": null, - "cognitoIdentityId": null, - "caller": null, - "apiKey": "", - "sourceIp": "69.42.1.180", - "accessKey": null, - "cognitoAuthenticationType": null, - "cognitoAuthenticationProvider": null, - "userArn": null, - "userAgent": "curl/7.54.0", - "user": null - }, - "resourcePath": "/posts/{id}", - "httpMethod": "GET", - "apiId": "qg45s7uvg2" - }, - "body": null, - "isBase64Encoded": false -} diff --git a/spec/fixtures/demo/spec/jets_helper.rb b/spec/fixtures/demo/spec/jets_helper.rb deleted file mode 100644 index ea9510e42..000000000 --- a/spec/fixtures/demo/spec/jets_helper.rb +++ /dev/null @@ -1,11 +0,0 @@ -# This file is copied to spec/ when you run 'rails generate rspec:install' -require 'spec_helper' -ENV['JETS_ENV'] ||= 'test' - -abort("The Jets environment is running in production mode!") if Jets::Config.env == "production" - -# Check for pending migrations -# TODO: Jets::Migration.maintain_test_schema! - -RSpec.configure do |config| -end diff --git a/spec/fixtures/demo/spec/spec_helper.rb b/spec/fixtures/demo/spec/spec_helper.rb deleted file mode 100644 index 774666d71..000000000 --- a/spec/fixtures/demo/spec/spec_helper.rb +++ /dev/null @@ -1,28 +0,0 @@ -ENV['JETS_TEST'] = "1" -# Ensures aws api never called. Fixture home folder does not contain ~/.aws/credentails -ENV['HOME'] = "spec/fixtures/home" -ENV['JETS_ENV'] = "test" - -# require "simplecov" -# SimpleCov.start - -require "pp" -require "byebug" -require "fileutils" - -require "jets" -Jets.boot - -module Helpers - def payload(name) - JSON.load(IO.read("spec/fixtures/payloads/#{name}.json")) - end -end - -RSpec.configure do |c| - c.before(:suite) do - Aws.config.update(stub_responses: true) - end - c.include Helpers -end - diff --git a/spec/fixtures/demo/yarn.lock b/spec/fixtures/demo/yarn.lock deleted file mode 100644 index 321640c72..000000000 --- a/spec/fixtures/demo/yarn.lock +++ /dev/null @@ -1,5514 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@rails/webpacker@^3.0.2": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@rails/webpacker/-/webpacker-3.0.2.tgz#574b021c1f3d700b40a934576c9bdac5c9f9c744" - dependencies: - babel-core "^6.26.0" - babel-loader "^7.1.2" - babel-plugin-syntax-dynamic-import "^6.18.0" - babel-plugin-transform-class-properties "^6.24.1" - babel-plugin-transform-object-rest-spread "^6.26.0" - babel-polyfill "^6.26.0" - babel-preset-env "^1.6.0" - coffee-loader "^0.8.0" - compression-webpack-plugin "^1.0.0" - css-loader "^0.28.5" - extract-text-webpack-plugin "^3.0.0" - file-loader "^0.11.2" - glob "^7.1.2" - js-yaml "^3.9.1" - node-sass "^4.5.3" - path-complete-extname "^0.1.0" - postcss-cssnext "^3.0.2" - postcss-loader "^2.0.6" - postcss-smart-import "^0.7.5" - rails-erb-loader "^5.2.1" - resolve-url-loader "^2.1.0" - sass-loader "^6.0.6" - style-loader "^0.18.2" - webpack "^3.5.5" - webpack-manifest-plugin "^1.3.1" - -abbrev@1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" - -accepts@~1.3.4: - version "1.3.4" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.4.tgz#86246758c7dd6d21a6474ff084a4740ec05eb21f" - dependencies: - mime-types "~2.1.16" - negotiator "0.6.1" - -acorn-dynamic-import@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz#c752bd210bef679501b6c6cb7fc84f8f47158cc4" - dependencies: - acorn "^4.0.3" - -acorn@^4.0.3: - version "4.0.13" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787" - -acorn@^5.0.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.2.1.tgz#317ac7821826c22c702d66189ab8359675f135d7" - -adjust-sourcemap-loader@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/adjust-sourcemap-loader/-/adjust-sourcemap-loader-1.1.0.tgz#412d92404eb61e4113635012cba53a33d008e0e2" - dependencies: - assert "^1.3.0" - camelcase "^1.2.1" - loader-utils "^1.0.2" - lodash.assign "^4.0.1" - lodash.defaults "^3.1.2" - object-path "^0.9.2" - regex-parser "^2.2.1" - -ajv-keywords@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762" - -ajv@^4.9.1: - version "4.11.8" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" - dependencies: - co "^4.6.0" - json-stable-stringify "^1.0.1" - -ajv@^5.0.0, ajv@^5.1.0, ajv@^5.1.5: - version "5.3.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.3.0.tgz#4414ff74a50879c208ee5fdc826e32c303549eda" - dependencies: - co "^4.6.0" - fast-deep-equal "^1.0.0" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.3.0" - -align-text@^0.1.1, align-text@^0.1.3: - version "0.1.4" - resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" - dependencies: - kind-of "^3.0.2" - longest "^1.0.1" - repeat-string "^1.5.2" - -alphanum-sort@^1.0.1, alphanum-sort@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" - -amdefine@>=0.0.4: - version "1.0.1" - resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" - -ansi-html@0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e" - -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - -ansi-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" - -ansi-styles@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" - -ansi-styles@^3.1.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88" - dependencies: - color-convert "^1.9.0" - -any-promise@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-0.1.0.tgz#830b680aa7e56f33451d4b049f3bd8044498ee27" - -anymatch@^1.3.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a" - dependencies: - micromatch "^2.1.5" - normalize-path "^2.0.0" - -aproba@^1.0.3: - version "1.2.0" - resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" - -are-we-there-yet@~1.1.2: - version "1.1.4" - resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz#bb5dca382bb94f05e15194373d16fd3ba1ca110d" - dependencies: - delegates "^1.0.0" - readable-stream "^2.0.6" - -argparse@^1.0.7: - version "1.0.9" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86" - dependencies: - sprintf-js "~1.0.2" - -arr-diff@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf" - dependencies: - arr-flatten "^1.0.1" - -arr-flatten@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" - -array-find-index@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" - -array-flatten@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" - -array-flatten@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.1.tgz#426bb9da84090c1838d812c8150af20a8331e296" - -array-includes@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.0.3.tgz#184b48f62d92d7452bb31b323165c7f8bd02266d" - dependencies: - define-properties "^1.1.2" - es-abstract "^1.7.0" - -array-union@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" - dependencies: - array-uniq "^1.0.1" - -array-uniq@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" - -array-unique@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" - -asn1.js@^4.0.0: - version "4.9.2" - resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.9.2.tgz#8117ef4f7ed87cd8f89044b5bff97ac243a16c9a" - dependencies: - bn.js "^4.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - -asn1@~0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" - -assert-plus@1.0.0, assert-plus@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" - -assert-plus@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" - -assert@^1.1.1, assert@^1.3.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91" - dependencies: - util "0.10.3" - -async-each@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" - -async-foreach@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542" - -async@2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/async/-/async-2.4.1.tgz#62a56b279c98a11d0987096a01cc3eeb8eb7bbd7" - dependencies: - lodash "^4.14.0" - -async@^1.5.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" - -async@^2.1.2, async@^2.1.5, async@^2.4.1: - version "2.6.0" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4" - dependencies: - lodash "^4.14.0" - -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - -atob@~1.1.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/atob/-/atob-1.1.3.tgz#95f13629b12c3a51a5d215abdce2aa9f32f80773" - -autoprefixer@^6.3.1: - version "6.7.7" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-6.7.7.tgz#1dbd1c835658e35ce3f9984099db00585c782014" - dependencies: - browserslist "^1.7.6" - caniuse-db "^1.0.30000634" - normalize-range "^0.1.2" - num2fraction "^1.2.2" - postcss "^5.2.16" - postcss-value-parser "^3.2.3" - -autoprefixer@^7.1.1: - version "7.1.6" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-7.1.6.tgz#fb933039f74af74a83e71225ce78d9fd58ba84d7" - dependencies: - browserslist "^2.5.1" - caniuse-lite "^1.0.30000748" - normalize-range "^0.1.2" - num2fraction "^1.2.2" - postcss "^6.0.13" - postcss-value-parser "^3.2.3" - -aws-sign2@~0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" - -aws-sign2@~0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" - -aws4@^1.2.1, aws4@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" - -babel-code-frame@^6.11.0, babel-code-frame@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" - dependencies: - chalk "^1.1.3" - esutils "^2.0.2" - js-tokens "^3.0.2" - -babel-core@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.0.tgz#af32f78b31a6fcef119c87b0fd8d9753f03a0bb8" - dependencies: - babel-code-frame "^6.26.0" - babel-generator "^6.26.0" - babel-helpers "^6.24.1" - babel-messages "^6.23.0" - babel-register "^6.26.0" - babel-runtime "^6.26.0" - babel-template "^6.26.0" - babel-traverse "^6.26.0" - babel-types "^6.26.0" - babylon "^6.18.0" - convert-source-map "^1.5.0" - debug "^2.6.8" - json5 "^0.5.1" - lodash "^4.17.4" - minimatch "^3.0.4" - path-is-absolute "^1.0.1" - private "^0.1.7" - slash "^1.0.0" - source-map "^0.5.6" - -babel-generator@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.0.tgz#ac1ae20070b79f6e3ca1d3269613053774f20dc5" - dependencies: - babel-messages "^6.23.0" - babel-runtime "^6.26.0" - babel-types "^6.26.0" - detect-indent "^4.0.0" - jsesc "^1.3.0" - lodash "^4.17.4" - source-map "^0.5.6" - trim-right "^1.0.1" - -babel-helper-builder-binary-assignment-operator-visitor@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz#cce4517ada356f4220bcae8a02c2b346f9a56664" - dependencies: - babel-helper-explode-assignable-expression "^6.24.1" - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-helper-call-delegate@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz#ece6aacddc76e41c3461f88bfc575bd0daa2df8d" - dependencies: - babel-helper-hoist-variables "^6.24.1" - babel-runtime "^6.22.0" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-helper-define-map@^6.24.1: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz#a5f56dab41a25f97ecb498c7ebaca9819f95be5f" - dependencies: - babel-helper-function-name "^6.24.1" - babel-runtime "^6.26.0" - babel-types "^6.26.0" - lodash "^4.17.4" - -babel-helper-explode-assignable-expression@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz#f25b82cf7dc10433c55f70592d5746400ac22caa" - dependencies: - babel-runtime "^6.22.0" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-helper-function-name@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz#d3475b8c03ed98242a25b48351ab18399d3580a9" - dependencies: - babel-helper-get-function-arity "^6.24.1" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-helper-get-function-arity@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz#8f7782aa93407c41d3aa50908f89b031b1b6853d" - dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-helper-hoist-variables@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz#1ecb27689c9d25513eadbc9914a73f5408be7a76" - dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-helper-optimise-call-expression@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz#f7a13427ba9f73f8f4fa993c54a97882d1244257" - dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-helper-regex@^6.24.1: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz#325c59f902f82f24b74faceed0363954f6495e72" - dependencies: - babel-runtime "^6.26.0" - babel-types "^6.26.0" - lodash "^4.17.4" - -babel-helper-remap-async-to-generator@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz#5ec581827ad723fecdd381f1c928390676e4551b" - dependencies: - babel-helper-function-name "^6.24.1" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-helper-replace-supers@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz#bf6dbfe43938d17369a213ca8a8bf74b6a90ab1a" - dependencies: - babel-helper-optimise-call-expression "^6.24.1" - babel-messages "^6.23.0" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-helpers@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2" - dependencies: - babel-runtime "^6.22.0" - babel-template "^6.24.1" - -babel-loader@^7.1.2: - version "7.1.2" - resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.2.tgz#f6cbe122710f1aa2af4d881c6d5b54358ca24126" - dependencies: - find-cache-dir "^1.0.0" - loader-utils "^1.0.2" - mkdirp "^0.5.1" - -babel-messages@^6.23.0: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e" - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-check-es2015-constants@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz#35157b101426fd2ffd3da3f75c7d1e91835bbf8a" - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-syntax-async-functions@^6.8.0: - version "6.13.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95" - -babel-plugin-syntax-class-properties@^6.8.0: - version "6.13.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz#d7eb23b79a317f8543962c505b827c7d6cac27de" - -babel-plugin-syntax-dynamic-import@^6.18.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz#8d6a26229c83745a9982a441051572caa179b1da" - -babel-plugin-syntax-exponentiation-operator@^6.8.0: - version "6.13.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de" - -babel-plugin-syntax-object-rest-spread@^6.8.0: - version "6.13.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5" - -babel-plugin-syntax-trailing-function-commas@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz#ba0360937f8d06e40180a43fe0d5616fff532cf3" - -babel-plugin-transform-async-to-generator@^6.22.0: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz#6536e378aff6cb1d5517ac0e40eb3e9fc8d08761" - dependencies: - babel-helper-remap-async-to-generator "^6.24.1" - babel-plugin-syntax-async-functions "^6.8.0" - babel-runtime "^6.22.0" - -babel-plugin-transform-class-properties@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz#6a79763ea61d33d36f37b611aa9def81a81b46ac" - dependencies: - babel-helper-function-name "^6.24.1" - babel-plugin-syntax-class-properties "^6.8.0" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - -babel-plugin-transform-es2015-arrow-functions@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz#452692cb711d5f79dc7f85e440ce41b9f244d221" - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-block-scoped-functions@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz#bbc51b49f964d70cb8d8e0b94e820246ce3a6141" - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-block-scoping@^6.23.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz#d70f5299c1308d05c12f463813b0a09e73b1895f" - dependencies: - babel-runtime "^6.26.0" - babel-template "^6.26.0" - babel-traverse "^6.26.0" - babel-types "^6.26.0" - lodash "^4.17.4" - -babel-plugin-transform-es2015-classes@^6.23.0: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz#5a4c58a50c9c9461e564b4b2a3bfabc97a2584db" - dependencies: - babel-helper-define-map "^6.24.1" - babel-helper-function-name "^6.24.1" - babel-helper-optimise-call-expression "^6.24.1" - babel-helper-replace-supers "^6.24.1" - babel-messages "^6.23.0" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-plugin-transform-es2015-computed-properties@^6.22.0: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz#6fe2a8d16895d5634f4cd999b6d3480a308159b3" - dependencies: - babel-runtime "^6.22.0" - babel-template "^6.24.1" - -babel-plugin-transform-es2015-destructuring@^6.23.0: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz#997bb1f1ab967f682d2b0876fe358d60e765c56d" - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-duplicate-keys@^6.22.0: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz#73eb3d310ca969e3ef9ec91c53741a6f1576423e" - dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-plugin-transform-es2015-for-of@^6.23.0: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz#f47c95b2b613df1d3ecc2fdb7573623c75248691" - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-function-name@^6.22.0: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz#834c89853bc36b1af0f3a4c5dbaa94fd8eacaa8b" - dependencies: - babel-helper-function-name "^6.24.1" - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-plugin-transform-es2015-literals@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz#4f54a02d6cd66cf915280019a31d31925377ca2e" - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-modules-amd@^6.22.0, babel-plugin-transform-es2015-modules-amd@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz#3b3e54017239842d6d19c3011c4bd2f00a00d154" - dependencies: - babel-plugin-transform-es2015-modules-commonjs "^6.24.1" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - -babel-plugin-transform-es2015-modules-commonjs@^6.23.0, babel-plugin-transform-es2015-modules-commonjs@^6.24.1: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.0.tgz#0d8394029b7dc6abe1a97ef181e00758dd2e5d8a" - dependencies: - babel-plugin-transform-strict-mode "^6.24.1" - babel-runtime "^6.26.0" - babel-template "^6.26.0" - babel-types "^6.26.0" - -babel-plugin-transform-es2015-modules-systemjs@^6.23.0: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz#ff89a142b9119a906195f5f106ecf305d9407d23" - dependencies: - babel-helper-hoist-variables "^6.24.1" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - -babel-plugin-transform-es2015-modules-umd@^6.23.0: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz#ac997e6285cd18ed6176adb607d602344ad38468" - dependencies: - babel-plugin-transform-es2015-modules-amd "^6.24.1" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - -babel-plugin-transform-es2015-object-super@^6.22.0: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz#24cef69ae21cb83a7f8603dad021f572eb278f8d" - dependencies: - babel-helper-replace-supers "^6.24.1" - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-parameters@^6.23.0: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz#57ac351ab49caf14a97cd13b09f66fdf0a625f2b" - dependencies: - babel-helper-call-delegate "^6.24.1" - babel-helper-get-function-arity "^6.24.1" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-plugin-transform-es2015-shorthand-properties@^6.22.0: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz#24f875d6721c87661bbd99a4622e51f14de38aa0" - dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-plugin-transform-es2015-spread@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz#d6d68a99f89aedc4536c81a542e8dd9f1746f8d1" - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-sticky-regex@^6.22.0: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz#00c1cdb1aca71112cdf0cf6126c2ed6b457ccdbc" - dependencies: - babel-helper-regex "^6.24.1" - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-plugin-transform-es2015-template-literals@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz#a84b3450f7e9f8f1f6839d6d687da84bb1236d8d" - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-typeof-symbol@^6.23.0: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz#dec09f1cddff94b52ac73d505c84df59dcceb372" - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-unicode-regex@^6.22.0: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz#d38b12f42ea7323f729387f18a7c5ae1faeb35e9" - dependencies: - babel-helper-regex "^6.24.1" - babel-runtime "^6.22.0" - regexpu-core "^2.0.0" - -babel-plugin-transform-exponentiation-operator@^6.22.0: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz#2ab0c9c7f3098fa48907772bb813fe41e8de3a0e" - dependencies: - babel-helper-builder-binary-assignment-operator-visitor "^6.24.1" - babel-plugin-syntax-exponentiation-operator "^6.8.0" - babel-runtime "^6.22.0" - -babel-plugin-transform-object-rest-spread@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz#0f36692d50fef6b7e2d4b3ac1478137a963b7b06" - dependencies: - babel-plugin-syntax-object-rest-spread "^6.8.0" - babel-runtime "^6.26.0" - -babel-plugin-transform-regenerator@^6.22.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz#e0703696fbde27f0a3efcacf8b4dca2f7b3a8f2f" - dependencies: - regenerator-transform "^0.10.0" - -babel-plugin-transform-strict-mode@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz#d5faf7aa578a65bbe591cf5edae04a0c67020758" - dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-polyfill@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-polyfill/-/babel-polyfill-6.26.0.tgz#379937abc67d7895970adc621f284cd966cf2153" - dependencies: - babel-runtime "^6.26.0" - core-js "^2.5.0" - regenerator-runtime "^0.10.5" - -babel-preset-env@^1.6.0: - version "1.6.1" - resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.6.1.tgz#a18b564cc9b9afdf4aae57ae3c1b0d99188e6f48" - dependencies: - babel-plugin-check-es2015-constants "^6.22.0" - babel-plugin-syntax-trailing-function-commas "^6.22.0" - babel-plugin-transform-async-to-generator "^6.22.0" - babel-plugin-transform-es2015-arrow-functions "^6.22.0" - babel-plugin-transform-es2015-block-scoped-functions "^6.22.0" - babel-plugin-transform-es2015-block-scoping "^6.23.0" - babel-plugin-transform-es2015-classes "^6.23.0" - babel-plugin-transform-es2015-computed-properties "^6.22.0" - babel-plugin-transform-es2015-destructuring "^6.23.0" - babel-plugin-transform-es2015-duplicate-keys "^6.22.0" - babel-plugin-transform-es2015-for-of "^6.23.0" - babel-plugin-transform-es2015-function-name "^6.22.0" - babel-plugin-transform-es2015-literals "^6.22.0" - babel-plugin-transform-es2015-modules-amd "^6.22.0" - babel-plugin-transform-es2015-modules-commonjs "^6.23.0" - babel-plugin-transform-es2015-modules-systemjs "^6.23.0" - babel-plugin-transform-es2015-modules-umd "^6.23.0" - babel-plugin-transform-es2015-object-super "^6.22.0" - babel-plugin-transform-es2015-parameters "^6.23.0" - babel-plugin-transform-es2015-shorthand-properties "^6.22.0" - babel-plugin-transform-es2015-spread "^6.22.0" - babel-plugin-transform-es2015-sticky-regex "^6.22.0" - babel-plugin-transform-es2015-template-literals "^6.22.0" - babel-plugin-transform-es2015-typeof-symbol "^6.23.0" - babel-plugin-transform-es2015-unicode-regex "^6.22.0" - babel-plugin-transform-exponentiation-operator "^6.22.0" - babel-plugin-transform-regenerator "^6.22.0" - browserslist "^2.1.2" - invariant "^2.2.2" - semver "^5.3.0" - -babel-register@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071" - dependencies: - babel-core "^6.26.0" - babel-runtime "^6.26.0" - core-js "^2.5.0" - home-or-tmp "^2.0.0" - lodash "^4.17.4" - mkdirp "^0.5.1" - source-map-support "^0.4.15" - -babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.23.0, babel-runtime@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" - dependencies: - core-js "^2.4.0" - regenerator-runtime "^0.11.0" - -babel-template@^6.24.1, babel-template@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02" - dependencies: - babel-runtime "^6.26.0" - babel-traverse "^6.26.0" - babel-types "^6.26.0" - babylon "^6.18.0" - lodash "^4.17.4" - -babel-traverse@^6.24.1, babel-traverse@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee" - dependencies: - babel-code-frame "^6.26.0" - babel-messages "^6.23.0" - babel-runtime "^6.26.0" - babel-types "^6.26.0" - babylon "^6.18.0" - debug "^2.6.8" - globals "^9.18.0" - invariant "^2.2.2" - lodash "^4.17.4" - -babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" - dependencies: - babel-runtime "^6.26.0" - esutils "^2.0.2" - lodash "^4.17.4" - to-fast-properties "^1.0.3" - -babylon@^6.18.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" - -balanced-match@0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.1.0.tgz#b504bd05869b39259dd0c5efc35d843176dccc4a" - -balanced-match@^0.4.2: - version "0.4.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" - -balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - -base64-js@^1.0.2: - version "1.2.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.1.tgz#a91947da1f4a516ea38e5b4ec0ec3773675e0886" - -batch@0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" - -bcrypt-pbkdf@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" - dependencies: - tweetnacl "^0.14.3" - -big.js@^3.1.3: - version "3.2.0" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" - -binary-extensions@^1.0.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.10.0.tgz#9aeb9a6c5e88638aad171e167f5900abe24835d0" - -block-stream@*: - version "0.0.9" - resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" - dependencies: - inherits "~2.0.0" - -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: - version "4.11.8" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" - -body-parser@1.18.2: - version "1.18.2" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.2.tgz#87678a19d84b47d859b83199bd59bce222b10454" - dependencies: - bytes "3.0.0" - content-type "~1.0.4" - debug "2.6.9" - depd "~1.1.1" - http-errors "~1.6.2" - iconv-lite "0.4.19" - on-finished "~2.3.0" - qs "6.5.1" - raw-body "2.3.2" - type-is "~1.6.15" - -bonjour@^3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5" - dependencies: - array-flatten "^2.1.0" - deep-equal "^1.0.1" - dns-equal "^1.0.0" - dns-txt "^2.0.2" - multicast-dns "^6.0.1" - multicast-dns-service-types "^1.1.0" - -boom@2.x.x: - version "2.10.1" - resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" - dependencies: - hoek "2.x.x" - -boom@4.x.x: - version "4.3.1" - resolved "https://registry.yarnpkg.com/boom/-/boom-4.3.1.tgz#4f8a3005cb4a7e3889f749030fd25b96e01d2e31" - dependencies: - hoek "4.x.x" - -boom@5.x.x: - version "5.2.0" - resolved "https://registry.yarnpkg.com/boom/-/boom-5.2.0.tgz#5dd9da6ee3a5f302077436290cb717d3f4a54e02" - dependencies: - hoek "4.x.x" - -brace-expansion@^1.1.7: - version "1.1.8" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -braces@^1.8.2: - version "1.8.5" - resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7" - dependencies: - expand-range "^1.8.1" - preserve "^0.2.0" - repeat-element "^1.1.2" - -brorand@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" - -browserify-aes@^1.0.0, browserify-aes@^1.0.4: - version "1.1.1" - resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.1.1.tgz#38b7ab55edb806ff2dcda1a7f1620773a477c49f" - dependencies: - buffer-xor "^1.0.3" - cipher-base "^1.0.0" - create-hash "^1.1.0" - evp_bytestokey "^1.0.3" - inherits "^2.0.1" - safe-buffer "^5.0.1" - -browserify-cipher@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.0.tgz#9988244874bf5ed4e28da95666dcd66ac8fc363a" - dependencies: - browserify-aes "^1.0.4" - browserify-des "^1.0.0" - evp_bytestokey "^1.0.0" - -browserify-des@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.0.tgz#daa277717470922ed2fe18594118a175439721dd" - dependencies: - cipher-base "^1.0.1" - des.js "^1.0.0" - inherits "^2.0.1" - -browserify-rsa@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524" - dependencies: - bn.js "^4.1.0" - randombytes "^2.0.1" - -browserify-sign@^4.0.0: - version "4.0.4" - resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.0.4.tgz#aa4eb68e5d7b658baa6bf6a57e630cbd7a93d298" - dependencies: - bn.js "^4.1.1" - browserify-rsa "^4.0.0" - create-hash "^1.1.0" - create-hmac "^1.1.2" - elliptic "^6.0.0" - inherits "^2.0.1" - parse-asn1 "^5.0.0" - -browserify-zlib@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" - dependencies: - pako "~1.0.5" - -browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6: - version "1.7.7" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-1.7.7.tgz#0bd76704258be829b2398bb50e4b62d1a166b0b9" - dependencies: - caniuse-db "^1.0.30000639" - electron-to-chromium "^1.2.7" - -browserslist@^2.0.0, browserslist@^2.1.2, browserslist@^2.5.1: - version "2.9.0" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-2.9.0.tgz#706aca15c53be15610f466e348cbfa0c00a6a379" - dependencies: - caniuse-lite "^1.0.30000760" - electron-to-chromium "^1.3.27" - -buffer-indexof@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c" - -buffer-xor@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" - -buffer@^4.3.0: - version "4.9.1" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298" - dependencies: - base64-js "^1.0.2" - ieee754 "^1.1.4" - isarray "^1.0.0" - -builtin-modules@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" - -builtin-status-codes@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" - -bytes@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" - -camelcase-keys@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" - dependencies: - camelcase "^2.0.0" - map-obj "^1.0.0" - -camelcase@^1.0.2, camelcase@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39" - -camelcase@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" - -camelcase@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" - -camelcase@^4.0.0, camelcase@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" - -caniuse-api@^1.5.2: - version "1.6.1" - resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-1.6.1.tgz#b534e7c734c4f81ec5fbe8aca2ad24354b962c6c" - dependencies: - browserslist "^1.3.6" - caniuse-db "^1.0.30000529" - lodash.memoize "^4.1.2" - lodash.uniq "^4.5.0" - -caniuse-api@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-2.0.0.tgz#b1ddb5a5966b16f48dc4998444d4bbc6c7d9d834" - dependencies: - browserslist "^2.0.0" - caniuse-lite "^1.0.0" - lodash.memoize "^4.1.2" - lodash.uniq "^4.5.0" - -caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639: - version "1.0.30000766" - resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000766.tgz#4c911aa3747f01388452fa4b927b78fcf1430680" - -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000748, caniuse-lite@^1.0.30000760: - version "1.0.30000766" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000766.tgz#8a095cc5eb9923c27008ce4d0db23e65a3e28843" - -caseless@~0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.11.0.tgz#715b96ea9841593cc33067923f5ec60ebda4f7d7" - -caseless@~0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" - -center-align@^0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad" - dependencies: - align-text "^0.1.3" - lazy-cache "^1.0.3" - -chalk@^1.1.1, chalk@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" - dependencies: - ansi-styles "^2.2.1" - escape-string-regexp "^1.0.2" - has-ansi "^2.0.0" - strip-ansi "^3.0.0" - supports-color "^2.0.0" - -chalk@^2.0.1, chalk@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.0.tgz#b5ea48efc9c1793dccc9b4767c93914d3f2d52ba" - dependencies: - ansi-styles "^3.1.0" - escape-string-regexp "^1.0.5" - supports-color "^4.0.0" - -chokidar@^1.6.0, chokidar@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" - dependencies: - anymatch "^1.3.0" - async-each "^1.0.0" - glob-parent "^2.0.0" - inherits "^2.0.1" - is-binary-path "^1.0.0" - is-glob "^2.0.0" - path-is-absolute "^1.0.0" - readdirp "^2.0.0" - optionalDependencies: - fsevents "^1.0.0" - -cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -clap@^1.0.9: - version "1.2.3" - resolved "https://registry.yarnpkg.com/clap/-/clap-1.2.3.tgz#4f36745b32008492557f46412d66d50cb99bce51" - dependencies: - chalk "^1.1.3" - -cliui@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" - dependencies: - center-align "^0.1.1" - right-align "^0.1.1" - wordwrap "0.0.2" - -cliui@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" - dependencies: - string-width "^1.0.1" - strip-ansi "^3.0.1" - wrap-ansi "^2.0.0" - -clone-deep@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-0.3.0.tgz#348c61ae9cdbe0edfe053d91ff4cc521d790ede8" - dependencies: - for-own "^1.0.0" - is-plain-object "^2.0.1" - kind-of "^3.2.2" - shallow-clone "^0.1.2" - -clone@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.3.tgz#298d7e2231660f40c003c2ed3140decf3f53085f" - -co@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" - -coa@~1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/coa/-/coa-1.0.4.tgz#a9ef153660d6a86a8bdec0289a5c684d217432fd" - dependencies: - q "^1.1.2" - -code-point-at@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" - -coffee-loader@^0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/coffee-loader/-/coffee-loader-0.8.0.tgz#ec48e7327da8e3a99047a99d9bdcfcac12df3694" - dependencies: - loader-utils "^1.0.2" - -coffeescript@1.12.7: - version "1.12.7" - resolved "https://registry.yarnpkg.com/coffeescript/-/coffeescript-1.12.7.tgz#e57ee4c4867cf7f606bfc4a0f2d550c0981ddd27" - -color-convert@^1.3.0, color-convert@^1.8.2, color-convert@^1.9.0: - version "1.9.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.1.tgz#c1261107aeb2f294ebffec9ed9ecad529a6097ed" - dependencies: - color-name "^1.1.1" - -color-name@^1.0.0, color-name@^1.1.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - -color-string@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/color-string/-/color-string-0.3.0.tgz#27d46fb67025c5c2fa25993bfbf579e47841b991" - dependencies: - color-name "^1.0.0" - -color-string@^1.4.0: - version "1.5.2" - resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.2.tgz#26e45814bc3c9a7cbd6751648a41434514a773a9" - dependencies: - color-name "^1.0.0" - simple-swizzle "^0.2.2" - -color@^0.11.0: - version "0.11.4" - resolved "https://registry.yarnpkg.com/color/-/color-0.11.4.tgz#6d7b5c74fb65e841cd48792ad1ed5e07b904d764" - dependencies: - clone "^1.0.2" - color-convert "^1.3.0" - color-string "^0.3.0" - -color@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/color/-/color-1.0.3.tgz#e48e832d85f14ef694fb468811c2d5cfe729b55d" - dependencies: - color-convert "^1.8.2" - color-string "^1.4.0" - -colormin@^1.0.5: - version "1.1.2" - resolved "https://registry.yarnpkg.com/colormin/-/colormin-1.1.2.tgz#ea2f7420a72b96881a38aae59ec124a6f7298133" - dependencies: - color "^0.11.0" - css-color-names "0.0.4" - has "^1.0.1" - -colors@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" - -combined-stream@^1.0.5, combined-stream@~1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" - dependencies: - delayed-stream "~1.0.0" - -commander@^2.9.0: - version "2.11.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" - -commondir@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" - -complex.js@2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/complex.js/-/complex.js-2.0.4.tgz#d8e7cfb9652d1e853e723386421c1a0ca7a48373" - -compressible@~2.0.11: - version "2.0.12" - resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.12.tgz#c59a5c99db76767e9876500e271ef63b3493bd66" - dependencies: - mime-db ">= 1.30.0 < 2" - -compression-webpack-plugin@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-1.0.1.tgz#7f0a2af9f642b4f87b5989516a3b9e9b41bb4b3f" - dependencies: - async "2.4.1" - webpack-sources "^1.0.1" - -compression@^1.5.2: - version "1.7.1" - resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.1.tgz#eff2603efc2e22cf86f35d2eb93589f9875373db" - dependencies: - accepts "~1.3.4" - bytes "3.0.0" - compressible "~2.0.11" - debug "2.6.9" - on-headers "~1.0.1" - safe-buffer "5.1.1" - vary "~1.1.2" - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - -connect-history-api-fallback@^1.3.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz#b06873934bc5e344fef611a196a6faae0aee015a" - -console-browserify@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" - dependencies: - date-now "^0.1.4" - -console-control-strings@^1.0.0, console-control-strings@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" - -constants-browserify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" - -content-disposition@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" - -content-type@~1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" - -convert-source-map@^0.3.3: - version "0.3.5" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-0.3.5.tgz#f1d802950af7dd2631a1febe0596550c86ab3190" - -convert-source-map@^1.1.1, convert-source-map@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.0.tgz#9acd70851c6d5dfdd93d9282e5edf94a03ff46b5" - -cookie-signature@1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" - -cookie@0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" - -core-js@^2.4.0, core-js@^2.5.0: - version "2.5.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.1.tgz#ae6874dc66937789b80754ff5428df66819ca50b" - -core-util-is@1.0.2, core-util-is@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - -cosmiconfig@^2.1.0, cosmiconfig@^2.1.1: - version "2.2.2" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-2.2.2.tgz#6173cebd56fac042c1f4390edf7af6c07c7cb892" - dependencies: - is-directory "^0.3.1" - js-yaml "^3.4.3" - minimist "^1.2.0" - object-assign "^4.1.0" - os-homedir "^1.0.1" - parse-json "^2.2.0" - require-from-string "^1.1.0" - -create-ecdh@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.0.tgz#888c723596cdf7612f6498233eebd7a35301737d" - dependencies: - bn.js "^4.1.0" - elliptic "^6.0.0" - -create-hash@^1.1.0, create-hash@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.1.3.tgz#606042ac8b9262750f483caddab0f5819172d8fd" - dependencies: - cipher-base "^1.0.1" - inherits "^2.0.1" - ripemd160 "^2.0.0" - sha.js "^2.4.0" - -create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: - version "1.1.6" - resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.6.tgz#acb9e221a4e17bdb076e90657c42b93e3726cf06" - dependencies: - cipher-base "^1.0.3" - create-hash "^1.1.0" - inherits "^2.0.1" - ripemd160 "^2.0.0" - safe-buffer "^5.0.1" - sha.js "^2.4.8" - -cross-spawn@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982" - dependencies: - lru-cache "^4.0.1" - which "^1.2.9" - -cross-spawn@^5.0.1: - version "5.1.0" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" - dependencies: - lru-cache "^4.0.1" - shebang-command "^1.2.0" - which "^1.2.9" - -cryptiles@2.x.x: - version "2.0.5" - resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" - dependencies: - boom "2.x.x" - -cryptiles@3.x.x: - version "3.1.2" - resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-3.1.2.tgz#a89fbb220f5ce25ec56e8c4aa8a4fd7b5b0d29fe" - dependencies: - boom "5.x.x" - -crypto-browserify@^3.11.0: - version "3.12.0" - resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" - dependencies: - browserify-cipher "^1.0.0" - browserify-sign "^4.0.0" - create-ecdh "^4.0.0" - create-hash "^1.1.0" - create-hmac "^1.1.0" - diffie-hellman "^5.0.0" - inherits "^2.0.1" - pbkdf2 "^3.0.3" - public-encrypt "^4.0.0" - randombytes "^2.0.0" - randomfill "^1.0.3" - -css-color-function@~1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/css-color-function/-/css-color-function-1.3.3.tgz#8ed24c2c0205073339fafa004bc8c141fccb282e" - dependencies: - balanced-match "0.1.0" - color "^0.11.0" - debug "^3.1.0" - rgb "~0.1.0" - -css-color-names@0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" - -css-loader@^0.28.5: - version "0.28.7" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-0.28.7.tgz#5f2ee989dd32edd907717f953317656160999c1b" - dependencies: - babel-code-frame "^6.11.0" - css-selector-tokenizer "^0.7.0" - cssnano ">=2.6.1 <4" - icss-utils "^2.1.0" - loader-utils "^1.0.2" - lodash.camelcase "^4.3.0" - object-assign "^4.0.1" - postcss "^5.0.6" - postcss-modules-extract-imports "^1.0.0" - postcss-modules-local-by-default "^1.0.1" - postcss-modules-scope "^1.0.0" - postcss-modules-values "^1.1.0" - postcss-value-parser "^3.3.0" - source-list-map "^2.0.0" - -css-selector-tokenizer@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/css-selector-tokenizer/-/css-selector-tokenizer-0.7.0.tgz#e6988474ae8c953477bf5e7efecfceccd9cf4c86" - dependencies: - cssesc "^0.1.0" - fastparse "^1.1.1" - regexpu-core "^1.0.0" - -css-unit-converter@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/css-unit-converter/-/css-unit-converter-1.1.1.tgz#d9b9281adcfd8ced935bdbaba83786897f64e996" - -css@^2.0.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/css/-/css-2.2.1.tgz#73a4c81de85db664d4ee674f7d47085e3b2d55dc" - dependencies: - inherits "^2.0.1" - source-map "^0.1.38" - source-map-resolve "^0.3.0" - urix "^0.1.0" - -cssesc@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-0.1.0.tgz#c814903e45623371a0477b40109aaafbeeaddbb4" - -"cssnano@>=2.6.1 <4": - version "3.10.0" - resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-3.10.0.tgz#4f38f6cea2b9b17fa01490f23f1dc68ea65c1c38" - dependencies: - autoprefixer "^6.3.1" - decamelize "^1.1.2" - defined "^1.0.0" - has "^1.0.1" - object-assign "^4.0.1" - postcss "^5.0.14" - postcss-calc "^5.2.0" - postcss-colormin "^2.1.8" - postcss-convert-values "^2.3.4" - postcss-discard-comments "^2.0.4" - postcss-discard-duplicates "^2.0.1" - postcss-discard-empty "^2.0.1" - postcss-discard-overridden "^0.1.1" - postcss-discard-unused "^2.2.1" - postcss-filter-plugins "^2.0.0" - postcss-merge-idents "^2.1.5" - postcss-merge-longhand "^2.0.1" - postcss-merge-rules "^2.0.3" - postcss-minify-font-values "^1.0.2" - postcss-minify-gradients "^1.0.1" - postcss-minify-params "^1.0.4" - postcss-minify-selectors "^2.0.4" - postcss-normalize-charset "^1.1.0" - postcss-normalize-url "^3.0.7" - postcss-ordered-values "^2.1.0" - postcss-reduce-idents "^2.2.2" - postcss-reduce-initial "^1.0.0" - postcss-reduce-transforms "^1.0.3" - postcss-svgo "^2.1.1" - postcss-unique-selectors "^2.0.2" - postcss-value-parser "^3.2.3" - postcss-zindex "^2.0.1" - -csso@~2.3.1: - version "2.3.2" - resolved "https://registry.yarnpkg.com/csso/-/csso-2.3.2.tgz#ddd52c587033f49e94b71fc55569f252e8ff5f85" - dependencies: - clap "^1.0.9" - source-map "^0.5.3" - -currently-unhandled@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" - dependencies: - array-find-index "^1.0.1" - -d@1: - version "1.0.0" - resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f" - dependencies: - es5-ext "^0.10.9" - -dashdash@^1.12.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" - dependencies: - assert-plus "^1.0.0" - -date-now@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" - -debug@2.6.9, debug@^2.2.0, debug@^2.6.6, debug@^2.6.8: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - dependencies: - ms "2.0.0" - -debug@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" - dependencies: - ms "2.0.0" - -decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2: - version "1.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - -decimal.js@7.2.3: - version "7.2.3" - resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-7.2.3.tgz#6434c3b8a8c375780062fc633d0d2bbdb264cc78" - -deep-equal@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" - -deep-extend@~0.4.0: - version "0.4.2" - resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" - -define-properties@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.2.tgz#83a73f2fea569898fb737193c8f873caf6d45c94" - dependencies: - foreach "^2.0.5" - object-keys "^1.0.8" - -defined@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" - -del@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/del/-/del-3.0.0.tgz#53ecf699ffcbcb39637691ab13baf160819766e5" - dependencies: - globby "^6.1.0" - is-path-cwd "^1.0.0" - is-path-in-cwd "^1.0.0" - p-map "^1.1.1" - pify "^3.0.0" - rimraf "^2.2.8" - -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - -delegates@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" - -depd@1.1.1, depd@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359" - -des.js@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc" - dependencies: - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - -destroy@~1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" - -detect-indent@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" - dependencies: - repeating "^2.0.0" - -detect-libc@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.2.tgz#71ad5d204bf17a6a6ca8f450c61454066ef461e1" - -detect-node@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.3.tgz#a2033c09cc8e158d37748fbde7507832bd6ce127" - -diffie-hellman@^5.0.0: - version "5.0.2" - resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e" - dependencies: - bn.js "^4.1.0" - miller-rabin "^4.0.0" - randombytes "^2.0.0" - -dns-equal@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" - -dns-packet@^1.0.1: - version "1.2.2" - resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.2.2.tgz#a8a26bec7646438963fc86e06f8f8b16d6c8bf7a" - dependencies: - ip "^1.1.0" - safe-buffer "^5.0.1" - -dns-txt@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/dns-txt/-/dns-txt-2.0.2.tgz#b91d806f5d27188e4ab3e7d107d881a1cc4642b6" - dependencies: - buffer-indexof "^1.0.0" - -domain-browser@^1.1.1: - version "1.1.7" - resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc" - -ecc-jsbn@~0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" - dependencies: - jsbn "~0.1.0" - -ee-first@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" - -electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.27: - version "1.3.27" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.27.tgz#78ecb8a399066187bb374eede35d9c70565a803d" - -elliptic@^6.0.0: - version "6.4.0" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df" - dependencies: - bn.js "^4.4.0" - brorand "^1.0.1" - hash.js "^1.0.0" - hmac-drbg "^1.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.0" - -emojis-list@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" - -encodeurl@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20" - -enhanced-resolve@^3.4.0: - version "3.4.1" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz#0421e339fd71419b3da13d129b3979040230476e" - dependencies: - graceful-fs "^4.1.2" - memory-fs "^0.4.0" - object-assign "^4.0.1" - tapable "^0.2.7" - -errno@^0.1.3: - version "0.1.4" - resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.4.tgz#b896e23a9e5e8ba33871fc996abd3635fc9a1c7d" - dependencies: - prr "~0.0.0" - -error-ex@^1.2.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc" - dependencies: - is-arrayish "^0.2.1" - -es-abstract@^1.7.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.9.0.tgz#690829a07cae36b222e7fd9b75c0d0573eb25227" - dependencies: - es-to-primitive "^1.1.1" - function-bind "^1.1.1" - has "^1.0.1" - is-callable "^1.1.3" - is-regex "^1.0.4" - -es-to-primitive@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.1.1.tgz#45355248a88979034b6792e19bb81f2b7975dd0d" - dependencies: - is-callable "^1.1.1" - is-date-object "^1.0.1" - is-symbol "^1.0.1" - -es5-ext@^0.10.14, es5-ext@^0.10.35, es5-ext@^0.10.9, es5-ext@~0.10.14: - version "0.10.35" - resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.35.tgz#18ee858ce6a3c45c7d79e91c15fcca9ec568494f" - dependencies: - es6-iterator "~2.0.1" - es6-symbol "~3.1.1" - -es6-iterator@^2.0.1, es6-iterator@~2.0.1: - version "2.0.3" - resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" - dependencies: - d "1" - es5-ext "^0.10.35" - es6-symbol "^3.1.1" - -es6-map@^0.1.3: - version "0.1.5" - resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.5.tgz#9136e0503dcc06a301690f0bb14ff4e364e949f0" - dependencies: - d "1" - es5-ext "~0.10.14" - es6-iterator "~2.0.1" - es6-set "~0.1.5" - es6-symbol "~3.1.1" - event-emitter "~0.3.5" - -es6-set@~0.1.5: - version "0.1.5" - resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.5.tgz#d2b3ec5d4d800ced818db538d28974db0a73ccb1" - dependencies: - d "1" - es5-ext "~0.10.14" - es6-iterator "~2.0.1" - es6-symbol "3.1.1" - event-emitter "~0.3.5" - -es6-symbol@3.1.1, es6-symbol@^3.1.1, es6-symbol@~3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77" - dependencies: - d "1" - es5-ext "~0.10.14" - -es6-weak-map@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.2.tgz#5e3ab32251ffd1538a1f8e5ffa1357772f92d96f" - dependencies: - d "1" - es5-ext "^0.10.14" - es6-iterator "^2.0.1" - es6-symbol "^3.1.1" - -escape-html@~1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" - -escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - -escope@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/escope/-/escope-3.6.0.tgz#e01975e812781a163a6dadfdd80398dc64c889c3" - dependencies: - es6-map "^0.1.3" - es6-weak-map "^2.0.1" - esrecurse "^4.1.0" - estraverse "^4.1.1" - -esprima@^2.6.0: - version "2.7.3" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" - -esprima@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804" - -esrecurse@^4.1.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.0.tgz#fa9568d98d3823f9a41d91e902dcab9ea6e5b163" - dependencies: - estraverse "^4.1.0" - object-assign "^4.0.1" - -estraverse@^4.1.0, estraverse@^4.1.1: - version "4.2.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" - -esutils@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" - -etag@~1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" - -event-emitter@~0.3.5: - version "0.3.5" - resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" - dependencies: - d "1" - es5-ext "~0.10.14" - -eventemitter3@1.x.x: - version "1.2.0" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508" - -events@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" - -eventsource@0.1.6: - version "0.1.6" - resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-0.1.6.tgz#0acede849ed7dd1ccc32c811bb11b944d4f29232" - dependencies: - original ">=0.0.5" - -evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" - dependencies: - md5.js "^1.3.4" - safe-buffer "^5.1.1" - -execa@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" - dependencies: - cross-spawn "^5.0.1" - get-stream "^3.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - -expand-brackets@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" - dependencies: - is-posix-bracket "^0.1.0" - -expand-range@^1.8.1: - version "1.8.2" - resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337" - dependencies: - fill-range "^2.1.0" - -express@^4.16.2: - version "4.16.2" - resolved "https://registry.yarnpkg.com/express/-/express-4.16.2.tgz#e35c6dfe2d64b7dca0a5cd4f21781be3299e076c" - dependencies: - accepts "~1.3.4" - array-flatten "1.1.1" - body-parser "1.18.2" - content-disposition "0.5.2" - content-type "~1.0.4" - cookie "0.3.1" - cookie-signature "1.0.6" - debug "2.6.9" - depd "~1.1.1" - encodeurl "~1.0.1" - escape-html "~1.0.3" - etag "~1.8.1" - finalhandler "1.1.0" - fresh "0.5.2" - merge-descriptors "1.0.1" - methods "~1.1.2" - on-finished "~2.3.0" - parseurl "~1.3.2" - path-to-regexp "0.1.7" - proxy-addr "~2.0.2" - qs "6.5.1" - range-parser "~1.2.0" - safe-buffer "5.1.1" - send "0.16.1" - serve-static "1.13.1" - setprototypeof "1.1.0" - statuses "~1.3.1" - type-is "~1.6.15" - utils-merge "1.0.1" - vary "~1.1.2" - -extend@~3.0.0, extend@~3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" - -extglob@^0.3.1: - version "0.3.2" - resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" - dependencies: - is-extglob "^1.0.0" - -extract-text-webpack-plugin@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extract-text-webpack-plugin/-/extract-text-webpack-plugin-3.0.2.tgz#5f043eaa02f9750a9258b78c0a6e0dc1408fb2f7" - dependencies: - async "^2.4.1" - loader-utils "^1.1.0" - schema-utils "^0.3.0" - webpack-sources "^1.0.1" - -extsprintf@1.3.0, extsprintf@^1.2.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" - -fast-deep-equal@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" - -fast-json-stable-stringify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" - -fastparse@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8" - -faye-websocket@^0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4" - dependencies: - websocket-driver ">=0.5.1" - -faye-websocket@~0.11.0: - version "0.11.1" - resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.1.tgz#f0efe18c4f56e4f40afc7e06c719fd5ee6188f38" - dependencies: - websocket-driver ">=0.5.1" - -file-loader@^0.11.2: - version "0.11.2" - resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-0.11.2.tgz#4ff1df28af38719a6098093b88c82c71d1794a34" - dependencies: - loader-utils "^1.0.2" - -filename-regex@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26" - -fill-range@^2.1.0: - version "2.2.3" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.3.tgz#50b77dfd7e469bc7492470963699fe7a8485a723" - dependencies: - is-number "^2.1.0" - isobject "^2.0.0" - randomatic "^1.1.3" - repeat-element "^1.1.2" - repeat-string "^1.5.2" - -finalhandler@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.0.tgz#ce0b6855b45853e791b2fcc680046d88253dd7f5" - dependencies: - debug "2.6.9" - encodeurl "~1.0.1" - escape-html "~1.0.3" - on-finished "~2.3.0" - parseurl "~1.3.2" - statuses "~1.3.1" - unpipe "~1.0.0" - -find-cache-dir@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-1.0.0.tgz#9288e3e9e3cc3748717d39eade17cf71fc30ee6f" - dependencies: - commondir "^1.0.1" - make-dir "^1.0.0" - pkg-dir "^2.0.0" - -find-up@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" - dependencies: - path-exists "^2.0.0" - pinkie-promise "^2.0.0" - -find-up@^2.0.0, find-up@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" - dependencies: - locate-path "^2.0.0" - -flatten@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782" - -for-in@^0.1.3: - version "0.1.8" - resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1" - -for-in@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" - -for-own@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce" - dependencies: - for-in "^1.0.1" - -for-own@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/for-own/-/for-own-1.0.0.tgz#c63332f415cedc4b04dbfe70cf836494c53cb44b" - dependencies: - for-in "^1.0.1" - -foreach@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" - -forever-agent@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" - -form-data@~2.1.1: - version "2.1.4" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1" - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.5" - mime-types "^2.1.12" - -form-data@~2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.1.tgz#6fb94fbd71885306d73d15cc497fe4cc4ecd44bf" - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.5" - mime-types "^2.1.12" - -forwarded@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" - -fraction.js@4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.0.2.tgz#0eae896626f334b1bde763371347a83b5575d7f0" - -fresh@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" - -fs-extra@^0.30.0: - version "0.30.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.30.0.tgz#f233ffcc08d4da7d432daa449776989db1df93f0" - dependencies: - graceful-fs "^4.1.2" - jsonfile "^2.1.0" - klaw "^1.0.0" - path-is-absolute "^1.0.0" - rimraf "^2.2.8" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - -fsevents@^1.0.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.3.tgz#11f82318f5fe7bb2cd22965a108e9306208216d8" - dependencies: - nan "^2.3.0" - node-pre-gyp "^0.6.39" - -fstream-ignore@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105" - dependencies: - fstream "^1.0.0" - inherits "2" - minimatch "^3.0.0" - -fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2: - version "1.0.11" - resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171" - dependencies: - graceful-fs "^4.1.2" - inherits "~2.0.0" - mkdirp ">=0.5 0" - rimraf "2" - -function-bind@^1.0.2, function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - -gauge@~2.7.3: - version "2.7.4" - resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" - dependencies: - aproba "^1.0.3" - console-control-strings "^1.0.0" - has-unicode "^2.0.0" - object-assign "^4.1.0" - signal-exit "^3.0.0" - string-width "^1.0.1" - strip-ansi "^3.0.1" - wide-align "^1.1.0" - -gaze@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.2.tgz#847224677adb8870d679257ed3388fdb61e40105" - dependencies: - globule "^1.0.0" - -generate-function@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.0.0.tgz#6858fe7c0969b7d4e9093337647ac79f60dfbe74" - -generate-object-property@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/generate-object-property/-/generate-object-property-1.2.0.tgz#9c0e1c40308ce804f4783618b937fa88f99d50d0" - dependencies: - is-property "^1.0.0" - -get-caller-file@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5" - -get-stdin@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" - -get-stream@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" - -getpass@^0.1.1: - version "0.1.7" - resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" - dependencies: - assert-plus "^1.0.0" - -glob-base@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" - dependencies: - glob-parent "^2.0.0" - is-glob "^2.0.0" - -glob-parent@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28" - dependencies: - is-glob "^2.0.0" - -glob@^6.0.4: - version "6.0.4" - resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" - dependencies: - inflight "^1.0.4" - inherits "2" - minimatch "2 || 3" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.2, glob@~7.1.1: - version "7.1.2" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -globals@^9.18.0: - version "9.18.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" - -globby@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" - dependencies: - array-union "^1.0.1" - glob "^7.0.3" - object-assign "^4.0.1" - pify "^2.0.0" - pinkie-promise "^2.0.0" - -globule@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/globule/-/globule-1.2.0.tgz#1dc49c6822dd9e8a2fa00ba2a295006e8664bd09" - dependencies: - glob "~7.1.1" - lodash "~4.17.4" - minimatch "~3.0.2" - -gonzales-pe@^4.0.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/gonzales-pe/-/gonzales-pe-4.2.3.tgz#41091703625433285e0aee3aa47829fc1fbeb6f2" - dependencies: - minimist "1.1.x" - -graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9: - version "4.1.11" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" - -handle-thing@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-1.2.5.tgz#fd7aad726bf1a5fd16dfc29b2f7a6601d27139c4" - -har-schema@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e" - -har-schema@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" - -har-validator@~2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-2.0.6.tgz#cdcbc08188265ad119b6a5a7c8ab70eecfb5d27d" - dependencies: - chalk "^1.1.1" - commander "^2.9.0" - is-my-json-valid "^2.12.4" - pinkie-promise "^2.0.0" - -har-validator@~4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a" - dependencies: - ajv "^4.9.1" - har-schema "^1.0.5" - -har-validator@~5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd" - dependencies: - ajv "^5.1.0" - har-schema "^2.0.0" - -has-ansi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" - dependencies: - ansi-regex "^2.0.0" - -has-flag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" - -has-flag@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" - -has-unicode@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" - -has@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28" - dependencies: - function-bind "^1.0.2" - -hash-base@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-2.0.2.tgz#66ea1d856db4e8a5470cadf6fce23ae5244ef2e1" - dependencies: - inherits "^2.0.1" - -hash-base@^3.0.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918" - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -hash.js@^1.0.0, hash.js@^1.0.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.3.tgz#340dedbe6290187151c1ea1d777a3448935df846" - dependencies: - inherits "^2.0.3" - minimalistic-assert "^1.0.0" - -hawk@3.1.3, hawk@~3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" - dependencies: - boom "2.x.x" - cryptiles "2.x.x" - hoek "2.x.x" - sntp "1.x.x" - -hawk@~6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/hawk/-/hawk-6.0.2.tgz#af4d914eb065f9b5ce4d9d11c1cb2126eecc3038" - dependencies: - boom "4.x.x" - cryptiles "3.x.x" - hoek "4.x.x" - sntp "2.x.x" - -hmac-drbg@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" - dependencies: - hash.js "^1.0.3" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.1" - -hoek@2.x.x: - version "2.16.3" - resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" - -hoek@4.x.x: - version "4.2.0" - resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d" - -home-or-tmp@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" - dependencies: - os-homedir "^1.0.0" - os-tmpdir "^1.0.1" - -hosted-git-info@^2.1.4: - version "2.5.0" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c" - -hpack.js@^2.1.6: - version "2.1.6" - resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" - dependencies: - inherits "^2.0.1" - obuf "^1.0.0" - readable-stream "^2.0.1" - wbuf "^1.1.0" - -html-comment-regex@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.1.tgz#668b93776eaae55ebde8f3ad464b307a4963625e" - -html-entities@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f" - -http-deceiver@^1.2.7: - version "1.2.7" - resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" - -http-errors@1.6.2, http-errors@~1.6.2: - version "1.6.2" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736" - dependencies: - depd "1.1.1" - inherits "2.0.3" - setprototypeof "1.0.3" - statuses ">= 1.3.1 < 2" - -http-parser-js@>=0.4.0: - version "0.4.9" - resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.4.9.tgz#ea1a04fb64adff0242e9974f297dd4c3cad271e1" - -http-proxy-middleware@~0.17.4: - version "0.17.4" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.17.4.tgz#642e8848851d66f09d4f124912846dbaeb41b833" - dependencies: - http-proxy "^1.16.2" - is-glob "^3.1.0" - lodash "^4.17.2" - micromatch "^2.3.11" - -http-proxy@^1.16.2: - version "1.16.2" - resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.16.2.tgz#06dff292952bf64dbe8471fa9df73066d4f37742" - dependencies: - eventemitter3 "1.x.x" - requires-port "1.x.x" - -http-signature@~1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" - dependencies: - assert-plus "^0.2.0" - jsprim "^1.2.2" - sshpk "^1.7.0" - -http-signature@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" - dependencies: - assert-plus "^1.0.0" - jsprim "^1.2.2" - sshpk "^1.7.0" - -https-browserify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" - -iconv-lite@0.4.19: - version "0.4.19" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" - -icss-replace-symbols@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" - -icss-utils@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-2.1.0.tgz#83f0a0ec378bf3246178b6c2ad9136f135b1c962" - dependencies: - postcss "^6.0.1" - -ieee754@^1.1.4: - version "1.1.8" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" - -import-local@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/import-local/-/import-local-0.1.1.tgz#b1179572aacdc11c6a91009fb430dbcab5f668a8" - dependencies: - pkg-dir "^2.0.0" - resolve-cwd "^2.0.0" - -in-publish@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/in-publish/-/in-publish-2.0.0.tgz#e20ff5e3a2afc2690320b6dc552682a9c7fadf51" - -indent-string@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" - dependencies: - repeating "^2.0.0" - -indexes-of@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" - -indexof@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - -inherits@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" - -ini@~1.3.0: - version "1.3.4" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e" - -internal-ip@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-1.2.0.tgz#ae9fbf93b984878785d50a8de1b356956058cf5c" - dependencies: - meow "^3.3.0" - -interpret@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.4.tgz#820cdd588b868ffb191a809506d6c9c8f212b1b0" - -invariant@^2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360" - dependencies: - loose-envify "^1.0.0" - -invert-kv@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" - -ip@^1.1.0, ip@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" - -ipaddr.js@1.5.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.5.2.tgz#d4b505bde9946987ccf0fc58d9010ff9607e3fa0" - -is-absolute-url@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6" - -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - -is-arrayish@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.1.tgz#c2dfc386abaa0c3e33c48db3fe87059e69065efd" - -is-binary-path@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" - dependencies: - binary-extensions "^1.0.0" - -is-buffer@^1.0.2, is-buffer@^1.1.5: - version "1.1.6" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" - -is-builtin-module@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" - dependencies: - builtin-modules "^1.0.0" - -is-callable@^1.1.1, is-callable@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.3.tgz#86eb75392805ddc33af71c92a0eedf74ee7604b2" - -is-date-object@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" - -is-directory@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" - -is-dotfile@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1" - -is-equal-shallow@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534" - dependencies: - is-primitive "^2.0.0" - -is-extendable@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" - -is-extglob@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" - -is-extglob@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - -is-finite@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" - dependencies: - number-is-nan "^1.0.0" - -is-fullwidth-code-point@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" - dependencies: - number-is-nan "^1.0.0" - -is-fullwidth-code-point@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" - -is-glob@^2.0.0, is-glob@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" - dependencies: - is-extglob "^1.0.0" - -is-glob@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" - dependencies: - is-extglob "^2.1.0" - -is-my-json-valid@^2.12.4: - version "2.16.1" - resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.16.1.tgz#5a846777e2c2620d1e69104e5d3a03b1f6088f11" - dependencies: - generate-function "^2.0.0" - generate-object-property "^1.1.0" - jsonpointer "^4.0.0" - xtend "^4.0.0" - -is-number@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" - dependencies: - kind-of "^3.0.2" - -is-number@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" - dependencies: - kind-of "^3.0.2" - -is-path-cwd@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" - -is-path-in-cwd@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz#6477582b8214d602346094567003be8a9eac04dc" - dependencies: - is-path-inside "^1.0.0" - -is-path-inside@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.0.tgz#fc06e5a1683fbda13de667aff717bbc10a48f37f" - dependencies: - path-is-inside "^1.0.1" - -is-plain-obj@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" - -is-plain-object@^2.0.1: - version "2.0.4" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" - dependencies: - isobject "^3.0.1" - -is-posix-bracket@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4" - -is-primitive@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575" - -is-property@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" - -is-regex@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" - dependencies: - has "^1.0.1" - -is-stream@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" - -is-svg@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-2.1.0.tgz#cf61090da0d9efbcab8722deba6f032208dbb0e9" - dependencies: - html-comment-regex "^1.1.0" - -is-symbol@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.1.tgz#3cc59f00025194b6ab2e38dbae6689256b660572" - -is-typedarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - -is-utf8@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" - -is-wsl@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" - -isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - -isnumeric@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/isnumeric/-/isnumeric-0.2.0.tgz#a2347ba360de19e33d0ffd590fddf7755cbf2e64" - -isobject@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" - dependencies: - isarray "1.0.0" - -isobject@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" - -isstream@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" - -javascript-natural-sort@0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz#f9e2303d4507f6d74355a73664d1440fb5a0ef59" - -js-base64@^2.1.8, js-base64@^2.1.9: - version "2.3.2" - resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.3.2.tgz#a79a923666372b580f8e27f51845c6f7e8fbfbaf" - -js-tokens@^3.0.0, js-tokens@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" - -js-yaml@^3.4.3, js-yaml@^3.9.1: - version "3.10.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc" - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - -js-yaml@~3.7.0: - version "3.7.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.7.0.tgz#5c967ddd837a9bfdca5f2de84253abe8a1c03b80" - dependencies: - argparse "^1.0.7" - esprima "^2.6.0" - -jsbn@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" - -jsesc@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" - -jsesc@~0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" - -json-loader@^0.5.4: - version "0.5.7" - resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.7.tgz#dca14a70235ff82f0ac9a3abeb60d337a365185d" - -json-schema-traverse@^0.3.0: - version "0.3.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" - -json-schema@0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" - -json-stable-stringify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" - dependencies: - jsonify "~0.0.0" - -json-stringify-safe@~5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" - -json3@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1" - -json5@^0.5.0, json5@^0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" - -jsonfile@^2.1.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" - optionalDependencies: - graceful-fs "^4.1.6" - -jsonify@~0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" - -jsonpointer@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" - -jsprim@^1.2.2: - version "1.4.1" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" - dependencies: - assert-plus "1.0.0" - extsprintf "1.3.0" - json-schema "0.2.3" - verror "1.10.0" - -killable@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.0.tgz#da8b84bd47de5395878f95d64d02f2449fe05e6b" - -kind-of@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-2.0.1.tgz#018ec7a4ce7e3a86cb9141be519d24c8faa981b5" - dependencies: - is-buffer "^1.0.2" - -kind-of@^3.0.2, kind-of@^3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" - dependencies: - is-buffer "^1.1.5" - -kind-of@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" - dependencies: - is-buffer "^1.1.5" - -klaw@^1.0.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439" - optionalDependencies: - graceful-fs "^4.1.9" - -lazy-cache@^0.2.3: - version "0.2.7" - resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-0.2.7.tgz#7feddf2dcb6edb77d11ef1d117ab5ffdf0ab1b65" - -lazy-cache@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" - -lcid@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" - dependencies: - invert-kv "^1.0.0" - -load-json-file@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" - dependencies: - graceful-fs "^4.1.2" - parse-json "^2.2.0" - pify "^2.0.0" - pinkie-promise "^2.0.0" - strip-bom "^2.0.0" - -load-json-file@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" - dependencies: - graceful-fs "^4.1.2" - parse-json "^2.2.0" - pify "^2.0.0" - strip-bom "^3.0.0" - -loader-runner@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2" - -loader-utils@^1.0.0, loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd" - dependencies: - big.js "^3.1.3" - emojis-list "^2.0.0" - json5 "^0.5.0" - -locate-path@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" - dependencies: - p-locate "^2.0.0" - path-exists "^3.0.0" - -lodash._baseassign@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz#8c38a099500f215ad09e59f1722fd0c52bfe0a4e" - dependencies: - lodash._basecopy "^3.0.0" - lodash.keys "^3.0.0" - -lodash._basecopy@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36" - -lodash._bindcallback@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" - -lodash._createassigner@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/lodash._createassigner/-/lodash._createassigner-3.1.1.tgz#838a5bae2fdaca63ac22dee8e19fa4e6d6970b11" - dependencies: - lodash._bindcallback "^3.0.0" - lodash._isiterateecall "^3.0.0" - lodash.restparam "^3.0.0" - -lodash._getnative@^3.0.0: - version "3.9.1" - resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" - -lodash._isiterateecall@^3.0.0: - version "3.0.9" - resolved "https://registry.yarnpkg.com/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz#5203ad7ba425fae842460e696db9cf3e6aac057c" - -lodash._reinterpolate@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" - -lodash.assign@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-3.2.0.tgz#3ce9f0234b4b2223e296b8fa0ac1fee8ebca64fa" - dependencies: - lodash._baseassign "^3.0.0" - lodash._createassigner "^3.0.0" - lodash.keys "^3.0.0" - -lodash.assign@^4.0.1, lodash.assign@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" - -lodash.camelcase@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" - -lodash.clonedeep@^4.3.2: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" - -lodash.defaults@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-3.1.2.tgz#c7308b18dbf8bc9372d701a73493c61192bd2e2c" - dependencies: - lodash.assign "^3.0.0" - lodash.restparam "^3.0.0" - -lodash.defaults@^4.0.0, lodash.defaults@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" - -lodash.isarguments@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" - -lodash.isarray@^3.0.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" - -lodash.keys@^3.0.0: - version "3.1.2" - resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" - dependencies: - lodash._getnative "^3.0.0" - lodash.isarguments "^3.0.0" - lodash.isarray "^3.0.0" - -lodash.memoize@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" - -lodash.mergewith@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.0.tgz#150cf0a16791f5903b8891eab154609274bdea55" - -lodash.restparam@^3.0.0: - version "3.6.1" - resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" - -lodash.tail@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664" - -lodash.template@^4.2.4: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.4.0.tgz#e73a0385c8355591746e020b99679c690e68fba0" - dependencies: - lodash._reinterpolate "~3.0.0" - lodash.templatesettings "^4.0.0" - -lodash.templatesettings@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.1.0.tgz#2b4d4e95ba440d915ff08bc899e4553666713316" - dependencies: - lodash._reinterpolate "~3.0.0" - -lodash.uniq@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" - -"lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.4, lodash@~4.17.4: - version "4.17.4" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" - -loglevel@^1.4.1: - version "1.6.0" - resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.0.tgz#ae0caa561111498c5ba13723d6fb631d24003934" - -longest@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" - -loose-envify@^1.0.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" - dependencies: - js-tokens "^3.0.0" - -loud-rejection@^1.0.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" - dependencies: - currently-unhandled "^0.4.1" - signal-exit "^3.0.0" - -lru-cache@^4.0.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55" - dependencies: - pseudomap "^1.0.2" - yallist "^2.1.2" - -macaddress@^0.2.8: - version "0.2.8" - resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12" - -make-dir@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.1.0.tgz#19b4369fe48c116f53c2af95ad102c0e39e85d51" - dependencies: - pify "^3.0.0" - -map-obj@^1.0.0, map-obj@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" - -math-expression-evaluator@^1.2.14: - version "1.2.17" - resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz#de819fdbcd84dccd8fae59c6aeb79615b9d266ac" - -mathjs@^3.11.5: - version "3.16.5" - resolved "https://registry.yarnpkg.com/mathjs/-/mathjs-3.16.5.tgz#d75a5265435d2824b067b37a478771deebf6aacc" - dependencies: - complex.js "2.0.4" - decimal.js "7.2.3" - fraction.js "4.0.2" - javascript-natural-sort "0.7.1" - seed-random "2.2.0" - tiny-emitter "2.0.0" - typed-function "0.10.5" - -md5.js@^1.3.4: - version "1.3.4" - resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.4.tgz#e9bdbde94a20a5ac18b04340fc5764d5b09d901d" - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - -media-typer@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" - -mem@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76" - dependencies: - mimic-fn "^1.0.0" - -memory-fs@^0.4.0, memory-fs@~0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" - dependencies: - errno "^0.1.3" - readable-stream "^2.0.1" - -meow@^3.3.0, meow@^3.7.0: - version "3.7.0" - resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" - dependencies: - camelcase-keys "^2.0.0" - decamelize "^1.1.2" - loud-rejection "^1.0.0" - map-obj "^1.0.1" - minimist "^1.1.3" - normalize-package-data "^2.3.4" - object-assign "^4.0.1" - read-pkg-up "^1.0.1" - redent "^1.0.0" - trim-newlines "^1.0.0" - -merge-descriptors@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" - -methods@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" - -micromatch@^2.1.5, micromatch@^2.3.11: - version "2.3.11" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" - dependencies: - arr-diff "^2.0.0" - array-unique "^0.2.1" - braces "^1.8.2" - expand-brackets "^0.1.4" - extglob "^0.3.1" - filename-regex "^2.0.0" - is-extglob "^1.0.0" - is-glob "^2.0.1" - kind-of "^3.0.2" - normalize-path "^2.0.1" - object.omit "^2.0.0" - parse-glob "^3.0.4" - regex-cache "^0.4.2" - -miller-rabin@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" - dependencies: - bn.js "^4.0.0" - brorand "^1.0.1" - -"mime-db@>= 1.30.0 < 2": - version "1.31.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.31.0.tgz#a49cd8f3ebf3ed1a482b60561d9105ad40ca74cb" - -mime-db@~1.30.0: - version "1.30.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" - -mime-types@^2.1.12, mime-types@~2.1.15, mime-types@~2.1.16, mime-types@~2.1.17, mime-types@~2.1.7: - version "2.1.17" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a" - dependencies: - mime-db "~1.30.0" - -mime@1.4.1, mime@^1.3.4: - version "1.4.1" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" - -mimic-fn@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18" - -minimalistic-assert@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3" - -minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" - -"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@~3.0.2: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - dependencies: - brace-expansion "^1.1.7" - -minimist@0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" - -minimist@1.1.x: - version "1.1.3" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.1.3.tgz#3bedfd91a92d39016fcfaa1c681e8faa1a1efda8" - -minimist@^1.1.3, minimist@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" - -mixin-object@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/mixin-object/-/mixin-object-2.0.1.tgz#4fb949441dab182540f1fe035ba60e1947a5e57e" - dependencies: - for-in "^0.1.3" - is-extendable "^0.1.1" - -mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" - dependencies: - minimist "0.0.8" - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - -multicast-dns-service-types@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" - -multicast-dns@^6.0.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-6.1.1.tgz#6e7de86a570872ab17058adea7160bbeca814dde" - dependencies: - dns-packet "^1.0.1" - thunky "^0.1.0" - -nan@^2.3.0, nan@^2.3.2: - version "2.8.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.8.0.tgz#ed715f3fe9de02b57a5e6252d90a96675e1f085a" - -negotiator@0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" - -node-forge@0.6.33: - version "0.6.33" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.6.33.tgz#463811879f573d45155ad6a9f43dc296e8e85ebc" - -node-gyp@^3.3.1: - version "3.6.2" - resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.6.2.tgz#9bfbe54562286284838e750eac05295853fa1c60" - dependencies: - fstream "^1.0.0" - glob "^7.0.3" - graceful-fs "^4.1.2" - minimatch "^3.0.2" - mkdirp "^0.5.0" - nopt "2 || 3" - npmlog "0 || 1 || 2 || 3 || 4" - osenv "0" - request "2" - rimraf "2" - semver "~5.3.0" - tar "^2.0.0" - which "1" - -node-libs-browser@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.1.0.tgz#5f94263d404f6e44767d726901fff05478d600df" - dependencies: - assert "^1.1.1" - browserify-zlib "^0.2.0" - buffer "^4.3.0" - console-browserify "^1.1.0" - constants-browserify "^1.0.0" - crypto-browserify "^3.11.0" - domain-browser "^1.1.1" - events "^1.0.0" - https-browserify "^1.0.0" - os-browserify "^0.3.0" - path-browserify "0.0.0" - process "^0.11.10" - punycode "^1.2.4" - querystring-es3 "^0.2.0" - readable-stream "^2.3.3" - stream-browserify "^2.0.1" - stream-http "^2.7.2" - string_decoder "^1.0.0" - timers-browserify "^2.0.4" - tty-browserify "0.0.0" - url "^0.11.0" - util "^0.10.3" - vm-browserify "0.0.4" - -node-pre-gyp@^0.6.39: - version "0.6.39" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz#c00e96860b23c0e1420ac7befc5044e1d78d8649" - dependencies: - detect-libc "^1.0.2" - hawk "3.1.3" - mkdirp "^0.5.1" - nopt "^4.0.1" - npmlog "^4.0.2" - rc "^1.1.7" - request "2.81.0" - rimraf "^2.6.1" - semver "^5.3.0" - tar "^2.2.1" - tar-pack "^3.4.0" - -node-sass@^4.5.3: - version "4.7.1" - resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.7.1.tgz#bec978ab33b5cf56825bf72922323a56ebaf4f66" - dependencies: - async-foreach "^0.1.3" - chalk "^1.1.1" - cross-spawn "^3.0.0" - gaze "^1.0.0" - get-stdin "^4.0.1" - glob "^7.0.3" - in-publish "^2.0.0" - lodash.assign "^4.2.0" - lodash.clonedeep "^4.3.2" - lodash.mergewith "^4.6.0" - meow "^3.7.0" - mkdirp "^0.5.1" - nan "^2.3.2" - node-gyp "^3.3.1" - npmlog "^4.0.0" - request "~2.79.0" - sass-graph "^2.2.4" - stdout-stream "^1.4.0" - "true-case-path" "^1.0.2" - -"nopt@2 || 3": - version "3.0.6" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" - dependencies: - abbrev "1" - -nopt@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" - dependencies: - abbrev "1" - osenv "^0.1.4" - -normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: - version "2.4.0" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" - dependencies: - hosted-git-info "^2.1.4" - is-builtin-module "^1.0.0" - semver "2 || 3 || 4 || 5" - validate-npm-package-license "^3.0.1" - -normalize-path@^2.0.0, normalize-path@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" - dependencies: - remove-trailing-separator "^1.0.1" - -normalize-range@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" - -normalize-url@^1.4.0: - version "1.9.1" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-1.9.1.tgz#2cc0d66b31ea23036458436e3620d85954c66c3c" - dependencies: - object-assign "^4.0.1" - prepend-http "^1.0.0" - query-string "^4.1.0" - sort-keys "^1.0.0" - -npm-run-path@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" - dependencies: - path-key "^2.0.0" - -"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0, npmlog@^4.0.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" - dependencies: - are-we-there-yet "~1.1.2" - console-control-strings "~1.1.0" - gauge "~2.7.3" - set-blocking "~2.0.0" - -num2fraction@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" - -number-is-nan@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" - -oauth-sign@~0.8.1, oauth-sign@~0.8.2: - version "0.8.2" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" - -object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - -object-keys@^1.0.8: - version "1.0.11" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d" - -object-path@^0.9.2: - version "0.9.2" - resolved "https://registry.yarnpkg.com/object-path/-/object-path-0.9.2.tgz#0fd9a74fc5fad1ae3968b586bda5c632bd6c05a5" - -object.omit@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" - dependencies: - for-own "^0.1.4" - is-extendable "^0.1.1" - -obuf@^1.0.0, obuf@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.1.tgz#104124b6c602c6796881a042541d36db43a5264e" - -on-finished@~2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" - dependencies: - ee-first "1.1.1" - -on-headers@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7" - -once@^1.3.0, once@^1.3.3: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - dependencies: - wrappy "1" - -onecolor@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/onecolor/-/onecolor-3.0.4.tgz#75a46f80da6c7aaa5b4daae17a47198bd9652494" - -opn@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/opn/-/opn-5.1.0.tgz#72ce2306a17dbea58ff1041853352b4a8fc77519" - dependencies: - is-wsl "^1.1.0" - -original@>=0.0.5: - version "1.0.0" - resolved "https://registry.yarnpkg.com/original/-/original-1.0.0.tgz#9147f93fa1696d04be61e01bd50baeaca656bd3b" - dependencies: - url-parse "1.0.x" - -os-browserify@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" - -os-homedir@^1.0.0, os-homedir@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" - -os-locale@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9" - dependencies: - lcid "^1.0.0" - -os-locale@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2" - dependencies: - execa "^0.7.0" - lcid "^1.0.0" - mem "^1.1.0" - -os-tmpdir@^1.0.0, os-tmpdir@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" - -osenv@0, osenv@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644" - dependencies: - os-homedir "^1.0.0" - os-tmpdir "^1.0.0" - -p-finally@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" - -p-limit@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.1.0.tgz#b07ff2d9a5d88bec806035895a2bab66a27988bc" - -p-locate@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" - dependencies: - p-limit "^1.1.0" - -p-map@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b" - -pako@~1.0.5: - version "1.0.6" - resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258" - -parse-asn1@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.0.tgz#37c4f9b7ed3ab65c74817b5f2480937fbf97c712" - dependencies: - asn1.js "^4.0.0" - browserify-aes "^1.0.0" - create-hash "^1.1.0" - evp_bytestokey "^1.0.0" - pbkdf2 "^3.0.3" - -parse-glob@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" - dependencies: - glob-base "^0.3.0" - is-dotfile "^1.0.0" - is-extglob "^1.0.0" - is-glob "^2.0.0" - -parse-json@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" - dependencies: - error-ex "^1.2.0" - -parseurl@~1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" - -path-browserify@0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a" - -path-complete-extname@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/path-complete-extname/-/path-complete-extname-0.1.0.tgz#c454702669f31452f8193aa6168915fa31692f4a" - -path-exists@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" - dependencies: - pinkie-promise "^2.0.0" - -path-exists@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" - -path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - -path-is-inside@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" - -path-key@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" - -path-parse@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" - -path-to-regexp@0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" - -path-type@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" - dependencies: - graceful-fs "^4.1.2" - pify "^2.0.0" - pinkie-promise "^2.0.0" - -path-type@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" - dependencies: - pify "^2.0.0" - -pbkdf2@^3.0.3: - version "3.0.14" - resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.14.tgz#a35e13c64799b06ce15320f459c230e68e73bade" - dependencies: - create-hash "^1.1.2" - create-hmac "^1.1.4" - ripemd160 "^2.0.1" - safe-buffer "^5.0.1" - sha.js "^2.4.8" - -performance-now@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" - -performance-now@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" - -pify@^2.0.0, pify@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" - -pify@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" - -pinkie-promise@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" - dependencies: - pinkie "^2.0.0" - -pinkie@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" - -pixrem@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/pixrem/-/pixrem-4.0.1.tgz#2da4a1de6ec4423c5fc3794e930b81d4490ec686" - dependencies: - browserslist "^2.0.0" - postcss "^6.0.0" - reduce-css-calc "^1.2.7" - -pkg-dir@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" - dependencies: - find-up "^2.1.0" - -pleeease-filters@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/pleeease-filters/-/pleeease-filters-4.0.0.tgz#6632b2fb05648d2758d865384fbced79e1ccaec7" - dependencies: - onecolor "^3.0.4" - postcss "^6.0.1" - -portfinder@^1.0.9: - version "1.0.13" - resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.13.tgz#bb32ecd87c27104ae6ee44b5a3ccbf0ebb1aede9" - dependencies: - async "^1.5.2" - debug "^2.2.0" - mkdirp "0.5.x" - -postcss-apply@^0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/postcss-apply/-/postcss-apply-0.8.0.tgz#14e544bbb5cb6f1c1e048857965d79ae066b1343" - dependencies: - babel-runtime "^6.23.0" - balanced-match "^0.4.2" - postcss "^6.0.0" - -postcss-attribute-case-insensitive@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-2.0.0.tgz#94dc422c8f90997f16bd33a3654bbbec084963b4" - dependencies: - postcss "^6.0.0" - postcss-selector-parser "^2.2.3" - -postcss-calc@^5.2.0: - version "5.3.1" - resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-5.3.1.tgz#77bae7ca928ad85716e2fda42f261bf7c1d65b5e" - dependencies: - postcss "^5.0.2" - postcss-message-helpers "^2.0.0" - reduce-css-calc "^1.2.6" - -postcss-calc@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-6.0.1.tgz#3d24171bbf6e7629d422a436ebfe6dd9511f4330" - dependencies: - css-unit-converter "^1.1.1" - postcss "^6.0.0" - postcss-selector-parser "^2.2.2" - reduce-css-calc "^2.0.0" - -postcss-color-function@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-color-function/-/postcss-color-function-4.0.1.tgz#402b3f2cebc3f6947e618fb6be3654fbecef6444" - dependencies: - css-color-function "~1.3.3" - postcss "^6.0.1" - postcss-message-helpers "^2.0.0" - postcss-value-parser "^3.3.0" - -postcss-color-gray@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-color-gray/-/postcss-color-gray-4.0.0.tgz#681bf305097dd66bfef0e1e6282d5d99b5acc95d" - dependencies: - color "^1.0.3" - postcss "^6.0.1" - postcss-message-helpers "^2.0.0" - reduce-function-call "^1.0.2" - -postcss-color-hex-alpha@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-color-hex-alpha/-/postcss-color-hex-alpha-3.0.0.tgz#1e53e6c8acb237955e8fd08b7ecdb1b8b8309f95" - dependencies: - color "^1.0.3" - postcss "^6.0.1" - postcss-message-helpers "^2.0.0" - -postcss-color-hsl@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-color-hsl/-/postcss-color-hsl-2.0.0.tgz#12703666fa310430e3f30a454dac1386317d5844" - dependencies: - postcss "^6.0.1" - postcss-value-parser "^3.3.0" - units-css "^0.4.0" - -postcss-color-hwb@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-color-hwb/-/postcss-color-hwb-3.0.0.tgz#3402b19ef4d8497540c1fb5072be9863ca95571e" - dependencies: - color "^1.0.3" - postcss "^6.0.1" - postcss-message-helpers "^2.0.0" - reduce-function-call "^1.0.2" - -postcss-color-rebeccapurple@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-3.0.0.tgz#eebaf03d363b4300b96792bd3081c19ed66513d3" - dependencies: - postcss "^6.0.1" - postcss-value-parser "^3.3.0" - -postcss-color-rgb@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-color-rgb/-/postcss-color-rgb-2.0.0.tgz#14539c8a7131494b482e0dd1cc265ff6514b5263" - dependencies: - postcss "^6.0.1" - postcss-value-parser "^3.3.0" - -postcss-color-rgba-fallback@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-color-rgba-fallback/-/postcss-color-rgba-fallback-3.0.0.tgz#37d5c9353a07a09270912a82606bb42a0d702c04" - dependencies: - postcss "^6.0.6" - postcss-value-parser "^3.3.0" - rgb-hex "^2.1.0" - -postcss-colormin@^2.1.8: - version "2.2.2" - resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-2.2.2.tgz#6631417d5f0e909a3d7ec26b24c8a8d1e4f96e4b" - dependencies: - colormin "^1.0.5" - postcss "^5.0.13" - postcss-value-parser "^3.2.3" - -postcss-convert-values@^2.3.4: - version "2.6.1" - resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-2.6.1.tgz#bbd8593c5c1fd2e3d1c322bb925dcae8dae4d62d" - dependencies: - postcss "^5.0.11" - postcss-value-parser "^3.1.2" - -postcss-cssnext@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/postcss-cssnext/-/postcss-cssnext-3.0.2.tgz#63b77adb0b8a4c1d5ec32cd345539535a3417d48" - dependencies: - autoprefixer "^7.1.1" - caniuse-api "^2.0.0" - chalk "^2.0.1" - pixrem "^4.0.0" - pleeease-filters "^4.0.0" - postcss "^6.0.5" - postcss-apply "^0.8.0" - postcss-attribute-case-insensitive "^2.0.0" - postcss-calc "^6.0.0" - postcss-color-function "^4.0.0" - postcss-color-gray "^4.0.0" - postcss-color-hex-alpha "^3.0.0" - postcss-color-hsl "^2.0.0" - postcss-color-hwb "^3.0.0" - postcss-color-rebeccapurple "^3.0.0" - postcss-color-rgb "^2.0.0" - postcss-color-rgba-fallback "^3.0.0" - postcss-custom-media "^6.0.0" - postcss-custom-properties "^6.1.0" - postcss-custom-selectors "^4.0.1" - postcss-font-family-system-ui "^2.0.1" - postcss-font-variant "^3.0.0" - postcss-image-set-polyfill "^0.3.5" - postcss-initial "^2.0.0" - postcss-media-minmax "^3.0.0" - postcss-nesting "^4.0.1" - postcss-pseudo-class-any-link "^4.0.0" - postcss-pseudoelements "^5.0.0" - postcss-replace-overflow-wrap "^2.0.0" - postcss-selector-matches "^3.0.1" - postcss-selector-not "^3.0.1" - -postcss-custom-media@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-6.0.0.tgz#be532784110ecb295044fb5395a18006eb21a737" - dependencies: - postcss "^6.0.1" - -postcss-custom-properties@^6.1.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-6.2.0.tgz#5d929a7f06e9b84e0f11334194c0ba9a30acfbe9" - dependencies: - balanced-match "^1.0.0" - postcss "^6.0.13" - -postcss-custom-selectors@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-custom-selectors/-/postcss-custom-selectors-4.0.1.tgz#781382f94c52e727ef5ca4776ea2adf49a611382" - dependencies: - postcss "^6.0.1" - postcss-selector-matches "^3.0.0" - -postcss-discard-comments@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-2.0.4.tgz#befe89fafd5b3dace5ccce51b76b81514be00e3d" - dependencies: - postcss "^5.0.14" - -postcss-discard-duplicates@^2.0.1: - version "2.1.0" - resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-2.1.0.tgz#b9abf27b88ac188158a5eb12abcae20263b91932" - dependencies: - postcss "^5.0.4" - -postcss-discard-empty@^2.0.1: - version "2.1.0" - resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-2.1.0.tgz#d2b4bd9d5ced5ebd8dcade7640c7d7cd7f4f92b5" - dependencies: - postcss "^5.0.14" - -postcss-discard-overridden@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-0.1.1.tgz#8b1eaf554f686fb288cd874c55667b0aa3668d58" - dependencies: - postcss "^5.0.16" - -postcss-discard-unused@^2.2.1: - version "2.2.3" - resolved "https://registry.yarnpkg.com/postcss-discard-unused/-/postcss-discard-unused-2.2.3.tgz#bce30b2cc591ffc634322b5fb3464b6d934f4433" - dependencies: - postcss "^5.0.14" - uniqs "^2.0.0" - -postcss-filter-plugins@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/postcss-filter-plugins/-/postcss-filter-plugins-2.0.2.tgz#6d85862534d735ac420e4a85806e1f5d4286d84c" - dependencies: - postcss "^5.0.4" - uniqid "^4.0.0" - -postcss-font-family-system-ui@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/postcss-font-family-system-ui/-/postcss-font-family-system-ui-2.0.1.tgz#318a075fdcb84b864aa823a51935ef0a5872e911" - dependencies: - lodash "^4.17.4" - postcss "^6.0.1" - postcss-value-parser "^3.3.0" - -postcss-font-variant@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-font-variant/-/postcss-font-variant-3.0.0.tgz#08ccc88f6050ba82ed8ef2cc76c0c6a6b41f183e" - dependencies: - postcss "^6.0.1" - -postcss-image-set-polyfill@^0.3.5: - version "0.3.5" - resolved "https://registry.yarnpkg.com/postcss-image-set-polyfill/-/postcss-image-set-polyfill-0.3.5.tgz#0f193413700cf1f82bd39066ef016d65a4a18181" - dependencies: - postcss "^6.0.1" - postcss-media-query-parser "^0.2.3" - -postcss-initial@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-initial/-/postcss-initial-2.0.0.tgz#72715f7336e0bb79351d99ee65c4a253a8441ba4" - dependencies: - lodash.template "^4.2.4" - postcss "^6.0.1" - -postcss-load-config@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-1.2.0.tgz#539e9afc9ddc8620121ebf9d8c3673e0ce50d28a" - dependencies: - cosmiconfig "^2.1.0" - object-assign "^4.1.0" - postcss-load-options "^1.2.0" - postcss-load-plugins "^2.3.0" - -postcss-load-options@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/postcss-load-options/-/postcss-load-options-1.2.0.tgz#b098b1559ddac2df04bc0bb375f99a5cfe2b6d8c" - dependencies: - cosmiconfig "^2.1.0" - object-assign "^4.1.0" - -postcss-load-plugins@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/postcss-load-plugins/-/postcss-load-plugins-2.3.0.tgz#745768116599aca2f009fad426b00175049d8d92" - dependencies: - cosmiconfig "^2.1.1" - object-assign "^4.1.0" - -postcss-loader@^2.0.6: - version "2.0.8" - resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-2.0.8.tgz#8c67ddb029407dfafe684a406cfc16bad2ce0814" - dependencies: - loader-utils "^1.1.0" - postcss "^6.0.0" - postcss-load-config "^1.2.0" - schema-utils "^0.3.0" - -postcss-media-minmax@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-media-minmax/-/postcss-media-minmax-3.0.0.tgz#675256037a43ef40bc4f0760bfd06d4dc69d48d2" - dependencies: - postcss "^6.0.1" - -postcss-media-query-parser@^0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz#27b39c6f4d94f81b1a73b8f76351c609e5cef244" - -postcss-merge-idents@^2.1.5: - version "2.1.7" - resolved "https://registry.yarnpkg.com/postcss-merge-idents/-/postcss-merge-idents-2.1.7.tgz#4c5530313c08e1d5b3bbf3d2bbc747e278eea270" - dependencies: - has "^1.0.1" - postcss "^5.0.10" - postcss-value-parser "^3.1.1" - -postcss-merge-longhand@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-2.0.2.tgz#23d90cd127b0a77994915332739034a1a4f3d658" - dependencies: - postcss "^5.0.4" - -postcss-merge-rules@^2.0.3: - version "2.1.2" - resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-2.1.2.tgz#d1df5dfaa7b1acc3be553f0e9e10e87c61b5f721" - dependencies: - browserslist "^1.5.2" - caniuse-api "^1.5.2" - postcss "^5.0.4" - postcss-selector-parser "^2.2.2" - vendors "^1.0.0" - -postcss-message-helpers@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-message-helpers/-/postcss-message-helpers-2.0.0.tgz#a4f2f4fab6e4fe002f0aed000478cdf52f9ba60e" - -postcss-minify-font-values@^1.0.2: - version "1.0.5" - resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-1.0.5.tgz#4b58edb56641eba7c8474ab3526cafd7bbdecb69" - dependencies: - object-assign "^4.0.1" - postcss "^5.0.4" - postcss-value-parser "^3.0.2" - -postcss-minify-gradients@^1.0.1: - version "1.0.5" - resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-1.0.5.tgz#5dbda11373703f83cfb4a3ea3881d8d75ff5e6e1" - dependencies: - postcss "^5.0.12" - postcss-value-parser "^3.3.0" - -postcss-minify-params@^1.0.4: - version "1.2.2" - resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-1.2.2.tgz#ad2ce071373b943b3d930a3fa59a358c28d6f1f3" - dependencies: - alphanum-sort "^1.0.1" - postcss "^5.0.2" - postcss-value-parser "^3.0.2" - uniqs "^2.0.0" - -postcss-minify-selectors@^2.0.4: - version "2.1.1" - resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-2.1.1.tgz#b2c6a98c0072cf91b932d1a496508114311735bf" - dependencies: - alphanum-sort "^1.0.2" - has "^1.0.1" - postcss "^5.0.14" - postcss-selector-parser "^2.0.0" - -postcss-modules-extract-imports@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.1.0.tgz#b614c9720be6816eaee35fb3a5faa1dba6a05ddb" - dependencies: - postcss "^6.0.1" - -postcss-modules-local-by-default@^1.0.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz#f7d80c398c5a393fa7964466bd19500a7d61c069" - dependencies: - css-selector-tokenizer "^0.7.0" - postcss "^6.0.1" - -postcss-modules-scope@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz#d6ea64994c79f97b62a72b426fbe6056a194bb90" - dependencies: - css-selector-tokenizer "^0.7.0" - postcss "^6.0.1" - -postcss-modules-values@^1.1.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz#ecffa9d7e192518389f42ad0e83f72aec456ea20" - dependencies: - icss-replace-symbols "^1.1.0" - postcss "^6.0.1" - -postcss-nesting@^4.0.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-4.2.1.tgz#0483bce338b3f0828ced90ff530b29b98b00300d" - dependencies: - postcss "^6.0.11" - -postcss-normalize-charset@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-1.1.1.tgz#ef9ee71212d7fe759c78ed162f61ed62b5cb93f1" - dependencies: - postcss "^5.0.5" - -postcss-normalize-url@^3.0.7: - version "3.0.8" - resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-3.0.8.tgz#108f74b3f2fcdaf891a2ffa3ea4592279fc78222" - dependencies: - is-absolute-url "^2.0.0" - normalize-url "^1.4.0" - postcss "^5.0.14" - postcss-value-parser "^3.2.3" - -postcss-ordered-values@^2.1.0: - version "2.2.3" - resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-2.2.3.tgz#eec6c2a67b6c412a8db2042e77fe8da43f95c11d" - dependencies: - postcss "^5.0.4" - postcss-value-parser "^3.0.1" - -postcss-pseudo-class-any-link@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-4.0.0.tgz#9152a0613d3450720513e8892854bae42d0ee68e" - dependencies: - postcss "^6.0.1" - postcss-selector-parser "^2.2.3" - -postcss-pseudoelements@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/postcss-pseudoelements/-/postcss-pseudoelements-5.0.0.tgz#eef194e8d524645ca520a949e95e518e812402cb" - dependencies: - postcss "^6.0.0" - -postcss-reduce-idents@^2.2.2: - version "2.4.0" - resolved "https://registry.yarnpkg.com/postcss-reduce-idents/-/postcss-reduce-idents-2.4.0.tgz#c2c6d20cc958284f6abfbe63f7609bf409059ad3" - dependencies: - postcss "^5.0.4" - postcss-value-parser "^3.0.2" - -postcss-reduce-initial@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-1.0.1.tgz#68f80695f045d08263a879ad240df8dd64f644ea" - dependencies: - postcss "^5.0.4" - -postcss-reduce-transforms@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-1.0.4.tgz#ff76f4d8212437b31c298a42d2e1444025771ae1" - dependencies: - has "^1.0.1" - postcss "^5.0.8" - postcss-value-parser "^3.0.1" - -postcss-replace-overflow-wrap@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-2.0.0.tgz#794db6faa54f8db100854392a93af45768b4e25b" - dependencies: - postcss "^6.0.1" - -postcss-sass@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/postcss-sass/-/postcss-sass-0.1.0.tgz#0d2a655b5d241ec8f419bb3da38de5ca11746ddb" - dependencies: - gonzales-pe "^4.0.3" - mathjs "^3.11.5" - postcss "^5.2.6" - -postcss-scss@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-1.0.2.tgz#ff45cf3354b879ee89a4eb68680f46ac9bb14f94" - dependencies: - postcss "^6.0.3" - -postcss-selector-matches@^3.0.0, postcss-selector-matches@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/postcss-selector-matches/-/postcss-selector-matches-3.0.1.tgz#e5634011e13950881861bbdd58c2d0111ffc96ab" - dependencies: - balanced-match "^0.4.2" - postcss "^6.0.1" - -postcss-selector-not@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/postcss-selector-not/-/postcss-selector-not-3.0.1.tgz#2e4db2f0965336c01e7cec7db6c60dff767335d9" - dependencies: - balanced-match "^0.4.2" - postcss "^6.0.1" - -postcss-selector-parser@^2.0.0, postcss-selector-parser@^2.2.2, postcss-selector-parser@^2.2.3: - version "2.2.3" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz#f9437788606c3c9acee16ffe8d8b16297f27bb90" - dependencies: - flatten "^1.0.2" - indexes-of "^1.0.1" - uniq "^1.0.1" - -postcss-smart-import@^0.7.5: - version "0.7.5" - resolved "https://registry.yarnpkg.com/postcss-smart-import/-/postcss-smart-import-0.7.5.tgz#df9a9c6dd60d916e5e0670d1c57d03af5d3dcc31" - dependencies: - babel-runtime "^6.23.0" - lodash "^4.17.4" - object-assign "^4.1.1" - postcss "^6.0.6" - postcss-sass "^0.1.0" - postcss-scss "^1.0.2" - postcss-value-parser "^3.3.0" - promise-each "^2.2.0" - read-cache "^1.0.0" - resolve "^1.3.3" - sugarss "^1.0.0" - -postcss-svgo@^2.1.1: - version "2.1.6" - resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-2.1.6.tgz#b6df18aa613b666e133f08adb5219c2684ac108d" - dependencies: - is-svg "^2.0.0" - postcss "^5.0.14" - postcss-value-parser "^3.2.3" - svgo "^0.7.0" - -postcss-unique-selectors@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-2.0.2.tgz#981d57d29ddcb33e7b1dfe1fd43b8649f933ca1d" - dependencies: - alphanum-sort "^1.0.1" - postcss "^5.0.4" - uniqs "^2.0.0" - -postcss-value-parser@^3.0.1, postcss-value-parser@^3.0.2, postcss-value-parser@^3.1.1, postcss-value-parser@^3.1.2, postcss-value-parser@^3.2.3, postcss-value-parser@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz#87f38f9f18f774a4ab4c8a232f5c5ce8872a9d15" - -postcss-zindex@^2.0.1: - version "2.2.0" - resolved "https://registry.yarnpkg.com/postcss-zindex/-/postcss-zindex-2.2.0.tgz#d2109ddc055b91af67fc4cb3b025946639d2af22" - dependencies: - has "^1.0.1" - postcss "^5.0.4" - uniqs "^2.0.0" - -postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0.14, postcss@^5.0.16, postcss@^5.0.2, postcss@^5.0.4, postcss@^5.0.5, postcss@^5.0.6, postcss@^5.0.8, postcss@^5.2.16, postcss@^5.2.6: - version "5.2.18" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.18.tgz#badfa1497d46244f6390f58b319830d9107853c5" - dependencies: - chalk "^1.1.3" - js-base64 "^2.1.9" - source-map "^0.5.6" - supports-color "^3.2.3" - -postcss@^6.0.0, postcss@^6.0.1, postcss@^6.0.11, postcss@^6.0.13, postcss@^6.0.14, postcss@^6.0.3, postcss@^6.0.5, postcss@^6.0.6: - version "6.0.14" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.14.tgz#5534c72114739e75d0afcf017db853099f562885" - dependencies: - chalk "^2.3.0" - source-map "^0.6.1" - supports-color "^4.4.0" - -prepend-http@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" - -preserve@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" - -private@^0.1.6, private@^0.1.7: - version "0.1.8" - resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" - -process-nextick-args@~1.0.6: - version "1.0.7" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" - -process@^0.11.10: - version "0.11.10" - resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" - -promise-each@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/promise-each/-/promise-each-2.2.0.tgz#3353174eff2694481037e04e01f77aa0fb6d1b60" - dependencies: - any-promise "^0.1.0" - -proxy-addr@~2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.2.tgz#6571504f47bb988ec8180253f85dd7e14952bdec" - dependencies: - forwarded "~0.1.2" - ipaddr.js "1.5.2" - -prr@~0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a" - -pseudomap@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" - -public-encrypt@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.0.tgz#39f699f3a46560dd5ebacbca693caf7c65c18cc6" - dependencies: - bn.js "^4.1.0" - browserify-rsa "^4.0.0" - create-hash "^1.1.0" - parse-asn1 "^5.0.0" - randombytes "^2.0.1" - -punycode@1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" - -punycode@^1.2.4, punycode@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - -q@^1.1.2: - version "1.5.1" - resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" - -qs@6.5.1, qs@~6.5.1: - version "6.5.1" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" - -qs@~6.3.0: - version "6.3.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.2.tgz#e75bd5f6e268122a2a0e0bda630b2550c166502c" - -qs@~6.4.0: - version "6.4.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" - -query-string@^4.1.0: - version "4.3.4" - resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb" - dependencies: - object-assign "^4.1.0" - strict-uri-encode "^1.0.0" - -querystring-es3@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" - -querystring@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" - -querystringify@0.0.x: - version "0.0.4" - resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-0.0.4.tgz#0cf7f84f9463ff0ae51c4c4b142d95be37724d9c" - -querystringify@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-1.0.0.tgz#6286242112c5b712fa654e526652bf6a13ff05cb" - -rails-erb-loader@^5.2.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/rails-erb-loader/-/rails-erb-loader-5.2.1.tgz#399b7781b88c129bc621a8256329ed2f855398e9" - dependencies: - loader-utils "^1.1.0" - lodash.defaults "^4.2.0" - -randomatic@^1.1.3: - version "1.1.7" - resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c" - dependencies: - is-number "^3.0.0" - kind-of "^4.0.0" - -randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.5.tgz#dc009a246b8d09a177b4b7a0ae77bc570f4b1b79" - dependencies: - safe-buffer "^5.1.0" - -randomfill@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.3.tgz#b96b7df587f01dd91726c418f30553b1418e3d62" - dependencies: - randombytes "^2.0.5" - safe-buffer "^5.1.0" - -range-parser@^1.0.3, range-parser@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" - -raw-body@2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89" - dependencies: - bytes "3.0.0" - http-errors "1.6.2" - iconv-lite "0.4.19" - unpipe "1.0.0" - -rc@^1.1.7: - version "1.2.2" - resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.2.tgz#d8ce9cb57e8d64d9c7badd9876c7c34cbe3c7077" - dependencies: - deep-extend "~0.4.0" - ini "~1.3.0" - minimist "^1.2.0" - strip-json-comments "~2.0.1" - -read-cache@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" - dependencies: - pify "^2.3.0" - -read-pkg-up@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" - dependencies: - find-up "^1.0.0" - read-pkg "^1.0.0" - -read-pkg-up@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" - dependencies: - find-up "^2.0.0" - read-pkg "^2.0.0" - -read-pkg@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" - dependencies: - load-json-file "^1.0.0" - normalize-package-data "^2.3.2" - path-type "^1.0.0" - -read-pkg@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8" - dependencies: - load-json-file "^2.0.0" - normalize-package-data "^2.3.2" - path-type "^2.0.0" - -readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.2.6, readable-stream@^2.2.9, readable-stream@^2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c" - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~1.0.6" - safe-buffer "~5.1.1" - string_decoder "~1.0.3" - util-deprecate "~1.0.1" - -readdirp@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78" - dependencies: - graceful-fs "^4.1.2" - minimatch "^3.0.2" - readable-stream "^2.0.2" - set-immediate-shim "^1.0.1" - -redent@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" - dependencies: - indent-string "^2.1.0" - strip-indent "^1.0.1" - -reduce-css-calc@^1.2.6, reduce-css-calc@^1.2.7: - version "1.3.0" - resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz#747c914e049614a4c9cfbba629871ad1d2927716" - dependencies: - balanced-match "^0.4.2" - math-expression-evaluator "^1.2.14" - reduce-function-call "^1.0.1" - -reduce-css-calc@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-2.1.1.tgz#f4ecd7a00ec3e5683773f208067ad7da117b9db0" - dependencies: - css-unit-converter "^1.1.1" - postcss-value-parser "^3.3.0" - -reduce-function-call@^1.0.1, reduce-function-call@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/reduce-function-call/-/reduce-function-call-1.0.2.tgz#5a200bf92e0e37751752fe45b0ab330fd4b6be99" - dependencies: - balanced-match "^0.4.2" - -regenerate@^1.2.1: - version "1.3.3" - resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.3.tgz#0c336d3980553d755c39b586ae3b20aa49c82b7f" - -regenerator-runtime@^0.10.5: - version "0.10.5" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz#336c3efc1220adcedda2c9fab67b5a7955a33658" - -regenerator-runtime@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.0.tgz#7e54fe5b5ccd5d6624ea6255c3473be090b802e1" - -regenerator-transform@^0.10.0: - version "0.10.1" - resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.10.1.tgz#1e4996837231da8b7f3cf4114d71b5691a0680dd" - dependencies: - babel-runtime "^6.18.0" - babel-types "^6.19.0" - private "^0.1.6" - -regex-cache@^0.4.2: - version "0.4.4" - resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.4.tgz#75bdc58a2a1496cec48a12835bc54c8d562336dd" - dependencies: - is-equal-shallow "^0.1.3" - -regex-parser@^2.2.1: - version "2.2.8" - resolved "https://registry.yarnpkg.com/regex-parser/-/regex-parser-2.2.8.tgz#da4c0cda5a828559094168930f455f532b6ffbac" - -regexpu-core@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-1.0.0.tgz#86a763f58ee4d7c2f6b102e4764050de7ed90c6b" - dependencies: - regenerate "^1.2.1" - regjsgen "^0.2.0" - regjsparser "^0.1.4" - -regexpu-core@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-2.0.0.tgz#49d038837b8dcf8bfa5b9a42139938e6ea2ae240" - dependencies: - regenerate "^1.2.1" - regjsgen "^0.2.0" - regjsparser "^0.1.4" - -regjsgen@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7" - -regjsparser@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c" - dependencies: - jsesc "~0.5.0" - -remove-trailing-separator@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" - -repeat-element@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a" - -repeat-string@^1.5.2: - version "1.6.1" - resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" - -repeating@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" - dependencies: - is-finite "^1.0.0" - -request@2: - version "2.83.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356" - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.6.0" - caseless "~0.12.0" - combined-stream "~1.0.5" - extend "~3.0.1" - forever-agent "~0.6.1" - form-data "~2.3.1" - har-validator "~5.0.3" - hawk "~6.0.2" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.17" - oauth-sign "~0.8.2" - performance-now "^2.1.0" - qs "~6.5.1" - safe-buffer "^5.1.1" - stringstream "~0.0.5" - tough-cookie "~2.3.3" - tunnel-agent "^0.6.0" - uuid "^3.1.0" - -request@2.81.0: - version "2.81.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" - dependencies: - aws-sign2 "~0.6.0" - aws4 "^1.2.1" - caseless "~0.12.0" - combined-stream "~1.0.5" - extend "~3.0.0" - forever-agent "~0.6.1" - form-data "~2.1.1" - har-validator "~4.2.1" - hawk "~3.1.3" - http-signature "~1.1.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.7" - oauth-sign "~0.8.1" - performance-now "^0.2.0" - qs "~6.4.0" - safe-buffer "^5.0.1" - stringstream "~0.0.4" - tough-cookie "~2.3.0" - tunnel-agent "^0.6.0" - uuid "^3.0.0" - -request@~2.79.0: - version "2.79.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de" - dependencies: - aws-sign2 "~0.6.0" - aws4 "^1.2.1" - caseless "~0.11.0" - combined-stream "~1.0.5" - extend "~3.0.0" - forever-agent "~0.6.1" - form-data "~2.1.1" - har-validator "~2.0.6" - hawk "~3.1.3" - http-signature "~1.1.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.7" - oauth-sign "~0.8.1" - qs "~6.3.0" - stringstream "~0.0.4" - tough-cookie "~2.3.0" - tunnel-agent "~0.4.1" - uuid "^3.0.0" - -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - -require-from-string@^1.1.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-1.2.1.tgz#529c9ccef27380adfec9a2f965b649bbee636418" - -require-main-filename@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" - -requires-port@1.0.x, requires-port@1.x.x, requires-port@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" - -resolve-cwd@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" - dependencies: - resolve-from "^3.0.0" - -resolve-from@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" - -resolve-url-loader@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/resolve-url-loader/-/resolve-url-loader-2.2.0.tgz#9662feaa11debf7cf8e3feb91dae9544aa7dee88" - dependencies: - adjust-sourcemap-loader "^1.1.0" - camelcase "^4.0.0" - convert-source-map "^1.1.1" - loader-utils "^1.0.0" - lodash.defaults "^4.0.0" - rework "^1.0.1" - rework-visit "^1.0.0" - source-map "^0.5.6" - urix "^0.1.0" - -resolve-url@~0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" - -resolve@^1.3.3: - version "1.5.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.5.0.tgz#1f09acce796c9a762579f31b2c1cc4c3cddf9f36" - dependencies: - path-parse "^1.0.5" - -rework-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/rework-visit/-/rework-visit-1.0.0.tgz#9945b2803f219e2f7aca00adb8bc9f640f842c9a" - -rework@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/rework/-/rework-1.0.1.tgz#30806a841342b54510aa4110850cd48534144aa7" - dependencies: - convert-source-map "^0.3.3" - css "^2.0.0" - -rgb-hex@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/rgb-hex/-/rgb-hex-2.1.0.tgz#c773c5fe2268a25578d92539a82a7a5ce53beda6" - -rgb@~0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/rgb/-/rgb-0.1.0.tgz#be27b291e8feffeac1bd99729721bfa40fc037b5" - -right-align@^0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" - dependencies: - align-text "^0.1.1" - -rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.6.1: - version "2.6.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" - dependencies: - glob "^7.0.5" - -ripemd160@^2.0.0, ripemd160@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.1.tgz#0f4584295c53a3628af7e6d79aca21ce57d1c6e7" - dependencies: - hash-base "^2.0.0" - inherits "^2.0.1" - -safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" - -sass-graph@^2.2.4: - version "2.2.4" - resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.4.tgz#13fbd63cd1caf0908b9fd93476ad43a51d1e0b49" - dependencies: - glob "^7.0.0" - lodash "^4.0.0" - scss-tokenizer "^0.2.3" - yargs "^7.0.0" - -sass-loader@^6.0.6: - version "6.0.6" - resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-6.0.6.tgz#e9d5e6c1f155faa32a4b26d7a9b7107c225e40f9" - dependencies: - async "^2.1.5" - clone-deep "^0.3.0" - loader-utils "^1.0.1" - lodash.tail "^4.1.1" - pify "^3.0.0" - -sax@~1.2.1: - version "1.2.4" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" - -schema-utils@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.3.0.tgz#f5877222ce3e931edae039f17eb3716e7137f8cf" - dependencies: - ajv "^5.0.0" - -scss-tokenizer@^0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1" - dependencies: - js-base64 "^2.1.8" - source-map "^0.4.2" - -seed-random@2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/seed-random/-/seed-random-2.2.0.tgz#2a9b19e250a817099231a5b99a4daf80b7fbed54" - -select-hose@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" - -selfsigned@^1.9.1: - version "1.10.1" - resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.1.tgz#bf8cb7b83256c4551e31347c6311778db99eec52" - dependencies: - node-forge "0.6.33" - -"semver@2 || 3 || 4 || 5", semver@^5.3.0: - version "5.4.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" - -semver@~5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" - -send@0.16.1: - version "0.16.1" - resolved "https://registry.yarnpkg.com/send/-/send-0.16.1.tgz#a70e1ca21d1382c11d0d9f6231deb281080d7ab3" - dependencies: - debug "2.6.9" - depd "~1.1.1" - destroy "~1.0.4" - encodeurl "~1.0.1" - escape-html "~1.0.3" - etag "~1.8.1" - fresh "0.5.2" - http-errors "~1.6.2" - mime "1.4.1" - ms "2.0.0" - on-finished "~2.3.0" - range-parser "~1.2.0" - statuses "~1.3.1" - -serve-index@^1.7.2: - version "1.9.1" - resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" - dependencies: - accepts "~1.3.4" - batch "0.6.1" - debug "2.6.9" - escape-html "~1.0.3" - http-errors "~1.6.2" - mime-types "~2.1.17" - parseurl "~1.3.2" - -serve-static@1.13.1: - version "1.13.1" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.1.tgz#4c57d53404a761d8f2e7c1e8a18a47dbf278a719" - dependencies: - encodeurl "~1.0.1" - escape-html "~1.0.3" - parseurl "~1.3.2" - send "0.16.1" - -set-blocking@^2.0.0, set-blocking@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" - -set-immediate-shim@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" - -setimmediate@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" - -setprototypeof@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" - -setprototypeof@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" - -sha.js@^2.4.0, sha.js@^2.4.8: - version "2.4.9" - resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.9.tgz#98f64880474b74f4a38b8da9d3c0f2d104633e7d" - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -shallow-clone@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-0.1.2.tgz#5909e874ba77106d73ac414cfec1ffca87d97060" - dependencies: - is-extendable "^0.1.1" - kind-of "^2.0.1" - lazy-cache "^0.2.3" - mixin-object "^2.0.1" - -shebang-command@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" - dependencies: - shebang-regex "^1.0.0" - -shebang-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" - -signal-exit@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" - -simple-swizzle@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" - dependencies: - is-arrayish "^0.3.1" - -slash@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" - -sntp@1.x.x: - version "1.0.9" - resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" - dependencies: - hoek "2.x.x" - -sntp@2.x.x: - version "2.1.0" - resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.1.0.tgz#2c6cec14fedc2222739caf9b5c3d85d1cc5a2cc8" - dependencies: - hoek "4.x.x" - -sockjs-client@1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.1.4.tgz#5babe386b775e4cf14e7520911452654016c8b12" - dependencies: - debug "^2.6.6" - eventsource "0.1.6" - faye-websocket "~0.11.0" - inherits "^2.0.1" - json3 "^3.3.2" - url-parse "^1.1.8" - -sockjs@0.3.18: - version "0.3.18" - resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.18.tgz#d9b289316ca7df77595ef299e075f0f937eb4207" - dependencies: - faye-websocket "^0.10.0" - uuid "^2.0.2" - -sort-keys@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad" - dependencies: - is-plain-obj "^1.0.0" - -source-list-map@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085" - -source-map-resolve@^0.3.0: - version "0.3.1" - resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.3.1.tgz#610f6122a445b8dd51535a2a71b783dfc1248761" - dependencies: - atob "~1.1.0" - resolve-url "~0.2.1" - source-map-url "~0.3.0" - urix "~0.1.0" - -source-map-support@^0.4.15: - version "0.4.18" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f" - dependencies: - source-map "^0.5.6" - -source-map-url@~0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.3.0.tgz#7ecaf13b57bcd09da8a40c5d269db33799d4aaf9" - -source-map@^0.1.38: - version "0.1.43" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.43.tgz#c24bc146ca517c1471f5dacbe2571b2b7f9e3346" - dependencies: - amdefine ">=0.0.4" - -source-map@^0.4.2: - version "0.4.4" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" - dependencies: - amdefine ">=0.0.4" - -source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1: - version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - -source-map@^0.6.1, source-map@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - -spdx-correct@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40" - dependencies: - spdx-license-ids "^1.0.2" - -spdx-expression-parse@~1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz#9bdf2f20e1f40ed447fbe273266191fced51626c" - -spdx-license-ids@^1.0.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57" - -spdy-transport@^2.0.18: - version "2.0.20" - resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-2.0.20.tgz#735e72054c486b2354fe89e702256004a39ace4d" - dependencies: - debug "^2.6.8" - detect-node "^2.0.3" - hpack.js "^2.1.6" - obuf "^1.1.1" - readable-stream "^2.2.9" - safe-buffer "^5.0.1" - wbuf "^1.7.2" - -spdy@^3.4.1: - version "3.4.7" - resolved "https://registry.yarnpkg.com/spdy/-/spdy-3.4.7.tgz#42ff41ece5cc0f99a3a6c28aabb73f5c3b03acbc" - dependencies: - debug "^2.6.8" - handle-thing "^1.2.5" - http-deceiver "^1.2.7" - safe-buffer "^5.0.1" - select-hose "^2.0.0" - spdy-transport "^2.0.18" - -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - -sshpk@^1.7.0: - version "1.13.1" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3" - dependencies: - asn1 "~0.2.3" - assert-plus "^1.0.0" - dashdash "^1.12.0" - getpass "^0.1.1" - optionalDependencies: - bcrypt-pbkdf "^1.0.0" - ecc-jsbn "~0.1.1" - jsbn "~0.1.0" - tweetnacl "~0.14.0" - -"statuses@>= 1.3.1 < 2": - version "1.4.0" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" - -statuses@~1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" - -stdout-stream@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/stdout-stream/-/stdout-stream-1.4.0.tgz#a2c7c8587e54d9427ea9edb3ac3f2cd522df378b" - dependencies: - readable-stream "^2.0.1" - -stream-browserify@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db" - dependencies: - inherits "~2.0.1" - readable-stream "^2.0.2" - -stream-http@^2.7.2: - version "2.7.2" - resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.7.2.tgz#40a050ec8dc3b53b33d9909415c02c0bf1abfbad" - dependencies: - builtin-status-codes "^3.0.0" - inherits "^2.0.1" - readable-stream "^2.2.6" - to-arraybuffer "^1.0.0" - xtend "^4.0.0" - -strict-uri-encode@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" - -string-width@^1.0.1, string-width@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" - dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - strip-ansi "^3.0.0" - -string-width@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" - dependencies: - is-fullwidth-code-point "^2.0.0" - strip-ansi "^4.0.0" - -string_decoder@^1.0.0, string_decoder@~1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" - dependencies: - safe-buffer "~5.1.0" - -stringstream@~0.0.4, stringstream@~0.0.5: - version "0.0.5" - resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" - -strip-ansi@^3.0.0, strip-ansi@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - dependencies: - ansi-regex "^2.0.0" - -strip-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" - dependencies: - ansi-regex "^3.0.0" - -strip-bom@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" - dependencies: - is-utf8 "^0.2.0" - -strip-bom@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" - -strip-eof@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" - -strip-indent@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" - dependencies: - get-stdin "^4.0.1" - -strip-json-comments@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" - -style-loader@^0.18.2: - version "0.18.2" - resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.18.2.tgz#cc31459afbcd6d80b7220ee54b291a9fd66ff5eb" - dependencies: - loader-utils "^1.0.2" - schema-utils "^0.3.0" - -sugarss@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/sugarss/-/sugarss-1.0.1.tgz#be826d9003e0f247735f92365dc3fd7f1bae9e44" - dependencies: - postcss "^6.0.14" - -supports-color@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" - -supports-color@^3.2.3: - version "3.2.3" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" - dependencies: - has-flag "^1.0.0" - -supports-color@^4.0.0, supports-color@^4.2.1, supports-color@^4.4.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b" - dependencies: - has-flag "^2.0.0" - -svgo@^0.7.0: - version "0.7.2" - resolved "https://registry.yarnpkg.com/svgo/-/svgo-0.7.2.tgz#9f5772413952135c6fefbf40afe6a4faa88b4bb5" - dependencies: - coa "~1.0.1" - colors "~1.1.2" - csso "~2.3.1" - js-yaml "~3.7.0" - mkdirp "~0.5.1" - sax "~1.2.1" - whet.extend "~0.9.9" - -tapable@^0.2.7: - version "0.2.8" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.8.tgz#99372a5c999bf2df160afc0d74bed4f47948cd22" - -tar-pack@^3.4.0: - version "3.4.1" - resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.1.tgz#e1dbc03a9b9d3ba07e896ad027317eb679a10a1f" - dependencies: - debug "^2.2.0" - fstream "^1.0.10" - fstream-ignore "^1.0.5" - once "^1.3.3" - readable-stream "^2.1.4" - rimraf "^2.5.1" - tar "^2.2.1" - uid-number "^0.0.6" - -tar@^2.0.0, tar@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1" - dependencies: - block-stream "*" - fstream "^1.0.2" - inherits "2" - -thunky@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/thunky/-/thunky-0.1.0.tgz#bf30146824e2b6e67b0f2d7a4ac8beb26908684e" - -time-stamp@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-2.0.0.tgz#95c6a44530e15ba8d6f4a3ecb8c3a3fac46da357" - -timers-browserify@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.4.tgz#96ca53f4b794a5e7c0e1bd7cc88a372298fa01e6" - dependencies: - setimmediate "^1.0.4" - -tiny-emitter@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.0.0.tgz#bad327adb1804b42a231afa741532bd884cd09ad" - -to-arraybuffer@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" - -to-fast-properties@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" - -tough-cookie@~2.3.0, tough-cookie@~2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561" - dependencies: - punycode "^1.4.1" - -trim-newlines@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" - -trim-right@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" - -"true-case-path@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/true-case-path/-/true-case-path-1.0.2.tgz#7ec91130924766c7f573be3020c34f8fdfd00d62" - dependencies: - glob "^6.0.4" - -tty-browserify@0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" - -tunnel-agent@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" - dependencies: - safe-buffer "^5.0.1" - -tunnel-agent@~0.4.1: - version "0.4.3" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb" - -tweetnacl@^0.14.3, tweetnacl@~0.14.0: - version "0.14.5" - resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" - -type-is@~1.6.15: - version "1.6.15" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" - dependencies: - media-typer "0.3.0" - mime-types "~2.1.15" - -typed-function@0.10.5: - version "0.10.5" - resolved "https://registry.yarnpkg.com/typed-function/-/typed-function-0.10.5.tgz#2e0f18abd065219fab694a446a65c6d1981832c0" - -uglify-js@^2.8.29: - version "2.8.29" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" - dependencies: - source-map "~0.5.1" - yargs "~3.10.0" - optionalDependencies: - uglify-to-browserify "~1.0.0" - -uglify-to-browserify@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" - -uglifyjs-webpack-plugin@^0.4.6: - version "0.4.6" - resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-0.4.6.tgz#b951f4abb6bd617e66f63eb891498e391763e309" - dependencies: - source-map "^0.5.6" - uglify-js "^2.8.29" - webpack-sources "^1.0.1" - -uid-number@^0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" - -uniq@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" - -uniqid@^4.0.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/uniqid/-/uniqid-4.1.1.tgz#89220ddf6b751ae52b5f72484863528596bb84c1" - dependencies: - macaddress "^0.2.8" - -uniqs@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02" - -units-css@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/units-css/-/units-css-0.4.0.tgz#d6228653a51983d7c16ff28f8b9dc3b1ffed3a07" - dependencies: - isnumeric "^0.2.0" - viewport-dimensions "^0.2.0" - -unpipe@1.0.0, unpipe@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" - -urix@^0.1.0, urix@~0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" - -url-parse@1.0.x: - version "1.0.5" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.0.5.tgz#0854860422afdcfefeb6c965c662d4800169927b" - dependencies: - querystringify "0.0.x" - requires-port "1.0.x" - -url-parse@^1.1.8: - version "1.2.0" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.2.0.tgz#3a19e8aaa6d023ddd27dcc44cb4fc8f7fec23986" - dependencies: - querystringify "~1.0.0" - requires-port "~1.0.0" - -url@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" - dependencies: - punycode "1.3.2" - querystring "0.2.0" - -util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - -util@0.10.3, util@^0.10.3: - version "0.10.3" - resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" - dependencies: - inherits "2.0.1" - -utils-merge@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" - -uuid@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" - -uuid@^3.0.0, uuid@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" - -validate-npm-package-license@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc" - dependencies: - spdx-correct "~1.0.0" - spdx-expression-parse "~1.0.0" - -vary@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" - -vendors@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.1.tgz#37ad73c8ee417fb3d580e785312307d274847f22" - -verror@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" - dependencies: - assert-plus "^1.0.0" - core-util-is "1.0.2" - extsprintf "^1.2.0" - -viewport-dimensions@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/viewport-dimensions/-/viewport-dimensions-0.2.0.tgz#de740747db5387fd1725f5175e91bac76afdf36c" - -vm-browserify@0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73" - dependencies: - indexof "0.0.1" - -watchpack@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.4.0.tgz#4a1472bcbb952bd0a9bb4036801f954dfb39faac" - dependencies: - async "^2.1.2" - chokidar "^1.7.0" - graceful-fs "^4.1.2" - -wbuf@^1.1.0, wbuf@^1.7.2: - version "1.7.2" - resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.2.tgz#d697b99f1f59512df2751be42769c1580b5801fe" - dependencies: - minimalistic-assert "^1.0.0" - -webpack-dev-middleware@^1.11.0: - version "1.12.0" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.12.0.tgz#d34efefb2edda7e1d3b5dbe07289513219651709" - dependencies: - memory-fs "~0.4.1" - mime "^1.3.4" - path-is-absolute "^1.0.0" - range-parser "^1.0.3" - time-stamp "^2.0.0" - -webpack-dev-server@^2.9.5: - version "2.9.5" - resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.9.5.tgz#79336fba0087a66ae491f4869f6545775b18daa8" - dependencies: - ansi-html "0.0.7" - array-includes "^3.0.3" - bonjour "^3.5.0" - chokidar "^1.6.0" - compression "^1.5.2" - connect-history-api-fallback "^1.3.0" - debug "^3.1.0" - del "^3.0.0" - express "^4.16.2" - html-entities "^1.2.0" - http-proxy-middleware "~0.17.4" - import-local "^0.1.1" - internal-ip "1.2.0" - ip "^1.1.5" - killable "^1.0.0" - loglevel "^1.4.1" - opn "^5.1.0" - portfinder "^1.0.9" - selfsigned "^1.9.1" - serve-index "^1.7.2" - sockjs "0.3.18" - sockjs-client "1.1.4" - spdy "^3.4.1" - strip-ansi "^3.0.1" - supports-color "^4.2.1" - webpack-dev-middleware "^1.11.0" - yargs "^6.6.0" - -webpack-manifest-plugin@^1.3.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/webpack-manifest-plugin/-/webpack-manifest-plugin-1.3.2.tgz#5ea8ee5756359ddc1d98814324fe43496349a7d4" - dependencies: - fs-extra "^0.30.0" - lodash ">=3.5 <5" - -webpack-sources@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.0.2.tgz#d0148ec083b3b5ccef1035a6b3ec16442983b27a" - dependencies: - source-list-map "^2.0.0" - source-map "~0.6.1" - -webpack@^3.5.5: - version "3.8.1" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.8.1.tgz#b16968a81100abe61608b0153c9159ef8bb2bd83" - dependencies: - acorn "^5.0.0" - acorn-dynamic-import "^2.0.0" - ajv "^5.1.5" - ajv-keywords "^2.0.0" - async "^2.1.2" - enhanced-resolve "^3.4.0" - escope "^3.6.0" - interpret "^1.0.0" - json-loader "^0.5.4" - json5 "^0.5.1" - loader-runner "^2.3.0" - loader-utils "^1.1.0" - memory-fs "~0.4.1" - mkdirp "~0.5.0" - node-libs-browser "^2.0.0" - source-map "^0.5.3" - supports-color "^4.2.1" - tapable "^0.2.7" - uglifyjs-webpack-plugin "^0.4.6" - watchpack "^1.4.0" - webpack-sources "^1.0.1" - yargs "^8.0.2" - -websocket-driver@>=0.5.1: - version "0.7.0" - resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.0.tgz#0caf9d2d755d93aee049d4bdd0d3fe2cca2a24eb" - dependencies: - http-parser-js ">=0.4.0" - websocket-extensions ">=0.1.1" - -websocket-extensions@>=0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29" - -whet.extend@~0.9.9: - version "0.9.9" - resolved "https://registry.yarnpkg.com/whet.extend/-/whet.extend-0.9.9.tgz#f877d5bf648c97e5aa542fadc16d6a259b9c11a1" - -which-module@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" - -which-module@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" - -which@1, which@^1.2.9: - version "1.3.0" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" - dependencies: - isexe "^2.0.0" - -wide-align@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710" - dependencies: - string-width "^1.0.2" - -window-size@0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" - -wordwrap@0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" - -wrap-ansi@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" - dependencies: - string-width "^1.0.1" - strip-ansi "^3.0.1" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - -xtend@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" - -y18n@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" - -yallist@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" - -yargs-parser@^4.2.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-4.2.1.tgz#29cceac0dc4f03c6c87b4a9f217dd18c9f74871c" - dependencies: - camelcase "^3.0.0" - -yargs-parser@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.0.tgz#275ecf0d7ffe05c77e64e7c86e4cd94bf0e1228a" - dependencies: - camelcase "^3.0.0" - -yargs-parser@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-7.0.0.tgz#8d0ac42f16ea55debd332caf4c4038b3e3f5dfd9" - dependencies: - camelcase "^4.1.0" - -yargs@^6.6.0: - version "6.6.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-6.6.0.tgz#782ec21ef403345f830a808ca3d513af56065208" - dependencies: - camelcase "^3.0.0" - cliui "^3.2.0" - decamelize "^1.1.1" - get-caller-file "^1.0.1" - os-locale "^1.4.0" - read-pkg-up "^1.0.1" - require-directory "^2.1.1" - require-main-filename "^1.0.1" - set-blocking "^2.0.0" - string-width "^1.0.2" - which-module "^1.0.0" - y18n "^3.2.1" - yargs-parser "^4.2.0" - -yargs@^7.0.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.0.tgz#6ba318eb16961727f5d284f8ea003e8d6154d0c8" - dependencies: - camelcase "^3.0.0" - cliui "^3.2.0" - decamelize "^1.1.1" - get-caller-file "^1.0.1" - os-locale "^1.4.0" - read-pkg-up "^1.0.1" - require-directory "^2.1.1" - require-main-filename "^1.0.1" - set-blocking "^2.0.0" - string-width "^1.0.2" - which-module "^1.0.0" - y18n "^3.2.1" - yargs-parser "^5.0.0" - -yargs@^8.0.2: - version "8.0.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-8.0.2.tgz#6299a9055b1cefc969ff7e79c1d918dceb22c360" - dependencies: - camelcase "^4.1.0" - cliui "^3.2.0" - decamelize "^1.1.1" - get-caller-file "^1.0.1" - os-locale "^2.0.0" - read-pkg-up "^2.0.0" - require-directory "^2.1.1" - require-main-filename "^1.0.1" - set-blocking "^2.0.0" - string-width "^2.0.0" - which-module "^2.0.0" - y18n "^3.2.1" - yargs-parser "^7.0.0" - -yargs@~3.10.0: - version "3.10.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1" - dependencies: - camelcase "^1.0.2" - cliui "^2.1.0" - decamelize "^1.0.0" - window-size "0.1.0" diff --git a/spec/fixtures/dumps/api_gateway/books/list.json b/spec/fixtures/dumps/api_gateway/books/list.json deleted file mode 100644 index 168a64295..000000000 --- a/spec/fixtures/dumps/api_gateway/books/list.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "resource": "/books/list", - "path": "/books/list", - "httpMethod": "GET", - "headers": { - "Accept": "*/*", - "Accept-Encoding": "gzip, deflate", - "cache-control": "no-cache", - "CloudFront-Forwarded-Proto": "https", - "CloudFront-Is-Desktop-Viewer": "true", - "CloudFront-Is-Mobile-Viewer": "false", - "CloudFront-Is-SmartTV-Viewer": "false", - "CloudFront-Is-Tablet-Viewer": "false", - "CloudFront-Viewer-Country": "US", - "Content-Type": "text/plain", - "Host": "uhghn8z6t1.execute-api.us-east-1.amazonaws.com", - "Postman-Token": "7166b11b-59de-4e7b-ad35-24e556b7a083", - "User-Agent": "PostmanRuntime/6.4.1", - "Via": "1.1 55676da1e5c0a9c4e60a94a95b01dc04.cloudfront.net (CloudFront)", - "X-Amz-Cf-Id": "iERhUw6ghRnv1uRYfxJaUsDGWVbERFSZ4K00CIgZtJ0T6yeFdItMeQ==", - "X-Amzn-Trace-Id": "Root=1-59f50229-587ec5271678236e50ad91b1", - "X-Forwarded-For": "69.42.1.180, 54.239.203.100", - "X-Forwarded-Port": "443", - "X-Forwarded-Proto": "https" - }, - "queryStringParameters": { - "a": "1", - "b": "2", - "c[]": "4" - }, - "multiValueQueryStringParameters": { - "a": ["1"], - "b": ["2"], - "c[]": ["3", "4"] - }, - "pathParameters": null, - "stageVariables": null, - "requestContext": { - "path": "/stag/books", - "accountId": "123456789012", - "resourceId": "c0yhg8", - "stage": "stag", - "requestId": "e5c39604-bc2d-11e7-abbe-1baaa0f8e02e", - "identity": { - "cognitoIdentityPoolId": null, - "accountId": null, - "cognitoIdentityId": null, - "caller": null, - "apiKey": "", - "sourceIp": "69.42.1.180", - "accessKey": null, - "cognitoAuthenticationType": null, - "cognitoAuthenticationProvider": null, - "userArn": null, - "userAgent": "PostmanRuntime/6.4.1", - "user": null - }, - "resourcePath": "/books/list", - "httpMethod": "GET", - "apiId": "uhghn8z6t1" - }, - "body": "{\n \"key3\": \"value3\",\n \"key2\": \"value2\",\n \"key1\": \"value1\"\n}", - "isBase64Encoded": false -} diff --git a/spec/fixtures/dumps/api_gateway/books/mounted_list.json b/spec/fixtures/dumps/api_gateway/books/mounted_list.json deleted file mode 100644 index 7dc1b1393..000000000 --- a/spec/fixtures/dumps/api_gateway/books/mounted_list.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "resource": "/mount/{path+}", - "path": "/mount/books/list", - "httpMethod": "GET", - "headers": { - "Accept": "*/*", - "Accept-Encoding": "gzip, deflate", - "cache-control": "no-cache", - "CloudFront-Forwarded-Proto": "https", - "CloudFront-Is-Desktop-Viewer": "true", - "CloudFront-Is-Mobile-Viewer": "false", - "CloudFront-Is-SmartTV-Viewer": "false", - "CloudFront-Is-Tablet-Viewer": "false", - "CloudFront-Viewer-Country": "US", - "Content-Type": "text/plain", - "Host": "uhghn8z6t1.execute-api.us-east-1.amazonaws.com", - "Postman-Token": "7166b11b-59de-4e7b-ad35-24e556b7a083", - "User-Agent": "PostmanRuntime/6.4.1", - "Via": "1.1 55676da1e5c0a9c4e60a94a95b01dc04.cloudfront.net (CloudFront)", - "X-Amz-Cf-Id": "iERhUw6ghRnv1uRYfxJaUsDGWVbERFSZ4K00CIgZtJ0T6yeFdItMeQ==", - "X-Amzn-Trace-Id": "Root=1-59f50229-587ec5271678236e50ad91b1", - "X-Forwarded-For": "69.42.1.180, 54.239.203.100", - "X-Forwarded-Port": "443", - "X-Forwarded-Proto": "https" - }, - "queryStringParameters": { - "a": "1", - "b": "2" - }, - "multiValueQueryStringParameters": { - "a": ["1"], - "b": ["2"] - }, - "pathParameters": {"path": "books/list"}, - "stageVariables": null, - "requestContext": { - "path": "/stag/books", - "accountId": "123456789012", - "resourceId": "c0yhg8", - "stage": "stag", - "requestId": "e5c39604-bc2d-11e7-abbe-1baaa0f8e02e", - "identity": { - "cognitoIdentityPoolId": null, - "accountId": null, - "cognitoIdentityId": null, - "caller": null, - "apiKey": "", - "sourceIp": "69.42.1.180", - "accessKey": null, - "cognitoAuthenticationType": null, - "cognitoAuthenticationProvider": null, - "userArn": null, - "userAgent": "PostmanRuntime/6.4.1", - "user": null - }, - "resourcePath": "/books/list", - "httpMethod": "GET", - "apiId": "uhghn8z6t1" - }, - "body": "{\n \"key3\": \"value3\",\n \"key2\": \"value2\",\n \"key1\": \"value1\"\n}", - "isBase64Encoded": false -} diff --git a/spec/fixtures/dumps/api_gateway/books/show.json b/spec/fixtures/dumps/api_gateway/books/show.json deleted file mode 100644 index befb7a226..000000000 --- a/spec/fixtures/dumps/api_gateway/books/show.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "resource": "/books/1", - "path": "/books/1", - "httpMethod": "GET", - "headers": { - "Accept": "*/*", - "Accept-Encoding": "gzip, deflate", - "cache-control": "no-cache", - "CloudFront-Forwarded-Proto": "https", - "CloudFront-Is-Desktop-Viewer": "true", - "CloudFront-Is-Mobile-Viewer": "false", - "CloudFront-Is-SmartTV-Viewer": "false", - "CloudFront-Is-Tablet-Viewer": "false", - "CloudFront-Viewer-Country": "US", - "Content-Type": "text/plain", - "Host": "uhghn8z6t1.execute-api.us-east-1.amazonaws.com", - "Postman-Token": "7166b11b-59de-4e7b-ad35-24e556b7a083", - "User-Agent": "PostmanRuntime/6.4.1", - "Via": "1.1 55676da1e5c0a9c4e60a94a95b01dc04.cloudfront.net (CloudFront)", - "X-Amz-Cf-Id": "iERhUw6ghRnv1uRYfxJaUsDGWVbERFSZ4K00CIgZtJ0T6yeFdItMeQ==", - "X-Amzn-Trace-Id": "Root=1-59f50229-587ec5271678236e50ad91b1", - "X-Forwarded-For": "69.42.1.180, 54.239.203.100", - "X-Forwarded-Port": "443", - "X-Forwarded-Proto": "https" - }, - "queryStringParameters": { - "a": "1", - "b": "2" - }, - "pathParameters": null, - "stageVariables": null, - "requestContext": { - "path": "/stag/books", - "accountId": "123456789012", - "resourceId": "c0yhg8", - "stage": "stag", - "requestId": "e5c39604-bc2d-11e7-abbe-1baaa0f8e02e", - "identity": { - "cognitoIdentityPoolId": null, - "accountId": null, - "cognitoIdentityId": null, - "caller": null, - "apiKey": "", - "sourceIp": "69.42.1.180", - "accessKey": null, - "cognitoAuthenticationType": null, - "cognitoAuthenticationProvider": null, - "userArn": null, - "userAgent": "PostmanRuntime/6.4.1", - "user": null - }, - "resourcePath": "/books/1", - "httpMethod": "GET", - "apiId": "uhghn8z6t1" - }, - "body": "{\n \"key3\": \"value3\",\n \"key2\": \"value2\",\n \"key1\": \"value1\"\n}", - "isBase64Encoded": false -} \ No newline at end of file diff --git a/spec/fixtures/dumps/api_gateway/event-headers-form-post.json b/spec/fixtures/dumps/api_gateway/event-headers-form-post.json deleted file mode 100644 index 7abeb9a8a..000000000 --- a/spec/fixtures/dumps/api_gateway/event-headers-form-post.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "headers": { - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", - "Accept-Encoding": "gzip, deflate, br", - "Accept-Language": "en-US,en;q=0.9,pt;q=0.8", - "cache-control": "max-age=0", - "CloudFront-Forwarded-Proto": "https", - "CloudFront-Is-Desktop-Viewer": "true", - "CloudFront-Is-Mobile-Viewer": "false", - "CloudFront-Is-SmartTV-Viewer": "false", - "CloudFront-Is-Tablet-Viewer": "false", - "CloudFront-Viewer-Country": "US", - "content-type": "application/x-www-form-urlencoded", - "Host": "nol1n8ho0j.execute-api.us-east-1.amazonaws.com", - "origin": "https://nol1n8ho0j.execute-api.us-east-1.amazonaws.com", - "Referer": "https://nol1n8ho0j.execute-api.us-east-1.amazonaws.com/dev/articles/new", - "upgrade-insecure-requests": "1", - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36", - "Via": "2.0 6e65abb04cb818a6ec78111935b507f7.cloudfront.net (CloudFront)", - "X-Amz-Cf-Id": "W8DF6J-lx1bkV00eCiBwIq5dldTSGGiG4BinJlxvN_4o8fCZtbsVjw==", - "X-Amzn-Trace-Id": "Root=1-5a0dc1ac-58a7db712a57d6aa4186c2ac", - "X-Forwarded-For": "69.42.1.180, 54.239.203.117", - "X-Forwarded-Port": "443", - "X-Forwarded-Proto": "https" - } -} diff --git a/spec/fixtures/dumps/api_gateway/others-proxy.json b/spec/fixtures/dumps/api_gateway/others-proxy.json deleted file mode 100644 index 53c7e43df..000000000 --- a/spec/fixtures/dumps/api_gateway/others-proxy.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "resource": "/others/{proxy+}", - "path": "/others/my/long/path", - "httpMethod": "GET", - "headers": { - "Accept": "*/*", - "Accept-Encoding": "identity", - "CloudFront-Forwarded-Proto": "https", - "CloudFront-Is-Desktop-Viewer": "true", - "CloudFront-Is-Mobile-Viewer": "false", - "CloudFront-Is-SmartTV-Viewer": "false", - "CloudFront-Is-Tablet-Viewer": "false", - "CloudFront-Viewer-Country": "US", - "Host": "vtm0f1z8d4.execute-api.us-east-1.amazonaws.com", - "User-Agent": "Wget/1.18 (darwin16.0.0)", - "Via": "1.1 f32e4aea3683be99c4324204c29f5852.cloudfront.net (CloudFront)", - "X-Amz-Cf-Id": "of9oHAW0XERmMPAL-nBzECLF9HRAY-cEkmHPY4tDs_M6l3-2tG16gg==", - "X-Amzn-Trace-Id": "Root=1-5a105ba5-77863b564444874b03b5025e", - "X-Forwarded-For": "69.42.1.180, 205.251.214.57", - "X-Forwarded-Port": "443", - "X-Forwarded-Proto": "https" - }, - "queryStringParameters": null, - "pathParameters": { - "proxy": "my/long/path" - }, - "stageVariables": null, - "requestContext": { - "path": "/stag/others/my/long/path", - "accountId": "123456789012", - "resourceId": "akzef2", - "stage": "stag", - "requestId": "1beb529a-cc7b-11e7-8ed0-35ca8e934ce5", - "identity": { - "cognitoIdentityPoolId": null, - "accountId": null, - "cognitoIdentityId": null, - "caller": null, - "apiKey": "", - "sourceIp": "69.42.1.180", - "accessKey": null, - "cognitoAuthenticationType": null, - "cognitoAuthenticationProvider": null, - "userArn": null, - "userAgent": "Wget/1.18 (darwin16.0.0)", - "user": null - }, - "resourcePath": "/others/{proxy+}", - "httpMethod": "GET", - "apiId": "vtm0f1z8d4" - }, - "body": null, - "isBase64Encoded": false -} diff --git a/spec/fixtures/dumps/api_gateway/posts/create.json b/spec/fixtures/dumps/api_gateway/posts/create.json deleted file mode 100644 index 33cc1027b..000000000 --- a/spec/fixtures/dumps/api_gateway/posts/create.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "resource": "/posts", - "path": "/posts", - "httpMethod": "POST", - "headers": { - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", - "Accept-Encoding": "gzip, deflate, br", - "Accept-Language": "en-US,en;q=0.5", - "CloudFront-Forwarded-Proto": "https", - "CloudFront-Is-Desktop-Viewer": "true", - "CloudFront-Is-Mobile-Viewer": "false", - "CloudFront-Is-SmartTV-Viewer": "false", - "CloudFront-Is-Tablet-Viewer": "false", - "CloudFront-Viewer-Country": "US", - "Content-Type": "application/x-www-form-urlencoded", - "Host": "8s1wzivnz4.execute-api.us-east-1.amazonaws.com", - "Referer": "https://8s1wzivnz4.execute-api.us-east-1.amazonaws.com/dev/posts/new", - "Upgrade-Insecure-Requests": "1", - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:57.0) Gecko/20100101 Firefox/57.0", - "Via": "1.1 3d3d633d266d05d90a4eea7a6a59b514.cloudfront.net (CloudFront)", - "X-Amz-Cf-Id": "8zpo4dBrdpjtaBISqas1etJJj8bnyOg86j8LS-xwjc7aqrKMWNyseg==", - "X-Amzn-Trace-Id": "Root=1-5a63e0dc-6116fa320d7ffbe74c298994", - "X-Forwarded-For": "104.220.39.135, 54.239.203.73", - "X-Forwarded-Port": "443", - "X-Forwarded-Proto": "https" - }, - "queryStringParameters": null, - "pathParameters": null, - "stageVariables": null, - "requestContext": { - "requestTime": "21/Jan/2018:00:37:48 +0000", - "path": "/dev/posts", - "accountId": "123456789012", - "protocol": "HTTP/1.1", - "resourceId": "7yokco", - "stage": "dev", - "requestTimeEpoch": 1516495068603, - "requestId": "4e2e9927-fe43-11e7-bd4e-792326c1a39a", - "identity": { - "cognitoIdentityPoolId": null, - "accountId": null, - "cognitoIdentityId": null, - "caller": null, - "sourceIp": "104.220.39.135", - "accessKey": null, - "cognitoAuthenticationType": null, - "cognitoAuthenticationProvider": null, - "userArn": null, - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:57.0) Gecko/20100101 Firefox/57.0", - "user": null - }, - "resourcePath": "/posts", - "httpMethod": "POST", - "apiId": "8s1wzivnz4" - }, - "body": "utf8=%E2%9C%93&authenticity_token=60S%2BuLUk9c8%2FnE7QIILaZOqG2Qx8fouNeUByfamzgAir7vrC%2BsR97dlmcIECc%2FfVLlWNS6nzj9GftDrxJLOW%2FQ%3D%3D&post%5Btitle%5D=Dear+Father+Dear+Son&commit=Submit", - "isBase64Encoded": false -} diff --git a/spec/fixtures/dumps/api_gateway/posts/index.json b/spec/fixtures/dumps/api_gateway/posts/index.json deleted file mode 100644 index ca8963caf..000000000 --- a/spec/fixtures/dumps/api_gateway/posts/index.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "resource": "/posts", - "path": "/posts", - "httpMethod": "GET", - "headers": { - "Accept": "*/*", - "Accept-Encoding": "gzip, deflate", - "cache-control": "no-cache", - "CloudFront-Forwarded-Proto": "https", - "CloudFront-Is-Desktop-Viewer": "true", - "CloudFront-Is-Mobile-Viewer": "false", - "CloudFront-Is-SmartTV-Viewer": "false", - "CloudFront-Is-Tablet-Viewer": "false", - "CloudFront-Viewer-Country": "US", - "Content-Type": "text/plain", - "Host": "uhghn8z6t1.execute-api.us-east-1.amazonaws.com", - "Postman-Token": "7166b11b-59de-4e7b-ad35-24e556b7a083", - "User-Agent": "PostmanRuntime/6.4.1", - "Via": "1.1 55676da1e5c0a9c4e60a94a95b01dc04.cloudfront.net (CloudFront)", - "X-Amz-Cf-Id": "iERhUw6ghRnv1uRYfxJaUsDGWVbERFSZ4K00CIgZtJ0T6yeFdItMeQ==", - "X-Amzn-Trace-Id": "Root=1-59f50229-587ec5271678236e50ad91b1", - "X-Forwarded-For": "69.42.1.180, 54.239.203.100", - "X-Forwarded-Port": "443", - "X-Forwarded-Proto": "https" - }, - "queryStringParameters": {}, - "pathParameters": null, - "stageVariables": null, - "requestContext": { - "path": "/stag/posts", - "accountId": "123456789012", - "resourceId": "c0yhg8", - "stage": "stag", - "requestId": "e5c39604-bc2d-11e7-abbe-1baaa0f8e02e", - "identity": { - "cognitoIdentityPoolId": null, - "accountId": null, - "cognitoIdentityId": null, - "caller": null, - "apiKey": "", - "sourceIp": "69.42.1.180", - "accessKey": null, - "cognitoAuthenticationType": null, - "cognitoAuthenticationProvider": null, - "userArn": null, - "userAgent": "PostmanRuntime/6.4.1", - "user": null - }, - "resourcePath": "/posts", - "httpMethod": "GET", - "apiId": "uhghn8z6t1" - }, - "body": "{}", - "isBase64Encoded": false -} \ No newline at end of file diff --git a/spec/fixtures/dumps/api_gateway/posts/show.json b/spec/fixtures/dumps/api_gateway/posts/show.json deleted file mode 100644 index 2411736b4..000000000 --- a/spec/fixtures/dumps/api_gateway/posts/show.json +++ /dev/null @@ -1,120 +0,0 @@ -{ - "resource": "/posts/{id}", - "path": "/posts/89", - "httpMethod": "GET", - "headers": { - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", - "Accept-Encoding": "gzip, deflate, br", - "Accept-Language": "en-US,en;q=0.9,pt;q=0.8", - "CloudFront-Forwarded-Proto": "https", - "CloudFront-Is-Desktop-Viewer": "true", - "CloudFront-Is-Mobile-Viewer": "false", - "CloudFront-Is-SmartTV-Viewer": "false", - "CloudFront-Is-Tablet-Viewer": "false", - "CloudFront-Viewer-Country": "US", - "Host": "demo.example.com", - "Referer": "https://demo.example.com/posts", - "upgrade-insecure-requests": "1", - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", - "Via": "2.0 e30ae5b3d9f6779a9b8bc992faad0b09.cloudfront.net (CloudFront)", - "X-Amz-Cf-Id": "iYDB6gVvwRUoEqqjYuJcs4ssEgW8B_q2rWy3nF08cznO2-qwkJk7Sw==", - "X-Amzn-Trace-Id": "Root=1-5bb00dbd-01ab4e74486e767430f3d714", - "X-Forwarded-For": "104.220.39.135, 54.239.134.14", - "X-Forwarded-Port": "443", - "X-Forwarded-Proto": "https" - }, - "multiValueHeaders": { - "Accept": [ - "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8" - ], - "Accept-Encoding": [ - "gzip, deflate, br" - ], - "Accept-Language": [ - "en-US,en;q=0.9,pt;q=0.8" - ], - "CloudFront-Forwarded-Proto": [ - "https" - ], - "CloudFront-Is-Desktop-Viewer": [ - "true" - ], - "CloudFront-Is-Mobile-Viewer": [ - "false" - ], - "CloudFront-Is-SmartTV-Viewer": [ - "false" - ], - "CloudFront-Is-Tablet-Viewer": [ - "false" - ], - "CloudFront-Viewer-Country": [ - "US" - ], - "Host": [ - "demo.example.com" - ], - "Referer": [ - "https://demo.example.com/posts" - ], - "upgrade-insecure-requests": [ - "1" - ], - "User-Agent": [ - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36" - ], - "Via": [ - "2.0 e30ae5b3d9f6779a9b8bc992faad0b09.cloudfront.net (CloudFront)" - ], - "X-Amz-Cf-Id": [ - "iYDB6gVvwRUoEqqjYuJcs4ssEgW8B_q2rWy3nF08cznO2-qwkJk7Sw==" - ], - "X-Amzn-Trace-Id": [ - "Root=1-5bb00dbd-01ab4e74486e767430f3d714" - ], - "X-Forwarded-For": [ - "104.220.39.135, 54.239.134.14" - ], - "X-Forwarded-Port": [ - "443" - ], - "X-Forwarded-Proto": [ - "https" - ] - }, - "queryStringParameters": {"foo": "bar", "hi": "there"}, - "multiValueQueryStringParameters": null, - "pathParameters": { - "id": "89" - }, - "stageVariables": null, - "requestContext": { - "resourceId": "ew754h", - "resourcePath": "/posts/{id}", - "httpMethod": "GET", - "extendedRequestId": "OAcVqHtmvHcFaeQ=", - "requestTime": "29/Sep/2018:23:41:49 +0000", - "path": "/posts/89", - "accountId": "123456789012", - "protocol": "HTTP/1.1", - "stage": "dev", - "requestTimeEpoch": 1538264509847, - "requestId": "3c4e1b45-c441-11e8-b52f-871dc189a92d", - "identity": { - "cognitoIdentityPoolId": null, - "accountId": null, - "cognitoIdentityId": null, - "caller": null, - "sourceIp": "104.220.39.135", - "accessKey": null, - "cognitoAuthenticationType": null, - "cognitoAuthenticationProvider": null, - "userArn": null, - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", - "user": null - }, - "apiId": "gxbmi4iqpc" - }, - "body": null, - "isBase64Encoded": false -} diff --git a/spec/fixtures/dumps/api_gateway/request-with-cookies.json b/spec/fixtures/dumps/api_gateway/request-with-cookies.json deleted file mode 100644 index 2f4b1c0a0..000000000 --- a/spec/fixtures/dumps/api_gateway/request-with-cookies.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "resource": "/posts", - "path": "/posts", - "httpMethod": "GET", - "headers": { - "Accept": "*/*", - "Accept-Encoding": "gzip, deflate", - "cache-control": "no-cache", - "CloudFront-Forwarded-Proto": "https", - "CloudFront-Is-Desktop-Viewer": "true", - "CloudFront-Is-Mobile-Viewer": "false", - "CloudFront-Is-SmartTV-Viewer": "false", - "CloudFront-Is-Tablet-Viewer": "false", - "CloudFront-Viewer-Country": "US", - "Content-Type": "text/plain", - "Cookie": "testcookie=testvalue", - "Host": "uhghn8z6t1.execute-api.us-east-1.amazonaws.com", - "Postman-Token": "7166b11b-59de-4e7b-ad35-24e556b7a083", - "User-Agent": "PostmanRuntime/6.4.1", - "Via": "1.1 55676da1e5c0a9c4e60a94a95b01dc04.cloudfront.net (CloudFront)", - "X-Amz-Cf-Id": "iERhUw6ghRnv1uRYfxJaUsDGWVbERFSZ4K00CIgZtJ0T6yeFdItMeQ==", - "X-Amzn-Trace-Id": "Root=1-59f50229-587ec5271678236e50ad91b1", - "X-Forwarded-For": "69.42.1.180, 54.239.203.100", - "X-Forwarded-Port": "443", - "X-Forwarded-Proto": "https" - }, - "queryStringParameters": {}, - "pathParameters": null, - "stageVariables": null, - "requestContext": { - "path": "/stag/posts", - "accountId": "123456789012", - "resourceId": "c0yhg8", - "stage": "stag", - "requestId": "e5c39604-bc2d-11e7-abbe-1baaa0f8e02e", - "identity": { - "cognitoIdentityPoolId": null, - "accountId": null, - "cognitoIdentityId": null, - "caller": null, - "apiKey": "", - "sourceIp": "69.42.1.180", - "accessKey": null, - "cognitoAuthenticationType": null, - "cognitoAuthenticationProvider": null, - "userArn": null, - "userAgent": "PostmanRuntime/6.4.1", - "user": null - }, - "resourcePath": "/posts", - "httpMethod": "GET", - "apiId": "uhghn8z6t1" - }, - "body": "{}", - "isBase64Encoded": false -} \ No newline at end of file diff --git a/spec/fixtures/dumps/api_gateway/request.json b/spec/fixtures/dumps/api_gateway/request.json deleted file mode 100644 index 69546021b..000000000 --- a/spec/fixtures/dumps/api_gateway/request.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "resource": "/posts", - "path": "/posts", - "httpMethod": "POST", - "headers": { - "Accept": "*/*", - "Accept-Encoding": "gzip, deflate", - "cache-control": "no-cache", - "CloudFront-Forwarded-Proto": "https", - "CloudFront-Is-Desktop-Viewer": "true", - "CloudFront-Is-Mobile-Viewer": "false", - "CloudFront-Is-SmartTV-Viewer": "false", - "CloudFront-Is-Tablet-Viewer": "false", - "CloudFront-Viewer-Country": "US", - "Content-Type": "text/plain", - "Host": "uhghn8z6t1.execute-api.us-east-1.amazonaws.com", - "Postman-Token": "7166b11b-59de-4e7b-ad35-24e556b7a083", - "User-Agent": "PostmanRuntime/6.4.1", - "Via": "1.1 55676da1e5c0a9c4e60a94a95b01dc04.cloudfront.net (CloudFront)", - "X-Amz-Cf-Id": "iERhUw6ghRnv1uRYfxJaUsDGWVbERFSZ4K00CIgZtJ0T6yeFdItMeQ==", - "X-Amzn-Trace-Id": "Root=1-59f50229-587ec5271678236e50ad91b1", - "X-Forwarded-For": "69.42.1.180, 54.239.203.100", - "X-Forwarded-Port": "443", - "X-Forwarded-Proto": "https" - }, - "queryStringParameters": { - "a": "1", - "b": "2" - }, - "pathParameters": null, - "stageVariables": null, - "requestContext": { - "path": "/stag/posts", - "accountId": "123456789012", - "resourceId": "c0yhg8", - "stage": "stag", - "requestId": "e5c39604-bc2d-11e7-abbe-1baaa0f8e02e", - "identity": { - "cognitoIdentityPoolId": null, - "accountId": null, - "cognitoIdentityId": null, - "caller": null, - "apiKey": "", - "sourceIp": "69.42.1.180", - "accessKey": null, - "cognitoAuthenticationType": null, - "cognitoAuthenticationProvider": null, - "userArn": null, - "userAgent": "PostmanRuntime/6.4.1", - "user": null - }, - "resourcePath": "/posts", - "httpMethod": "POST", - "apiId": "uhghn8z6t1" - }, - "body": "{\n \"key3\": \"value3\",\n \"key2\": \"value2\",\n \"key1\": \"value1\"\n}", - "isBase64Encoded": false -} \ No newline at end of file diff --git a/spec/fixtures/dumps/api_gateway/response.json b/spec/fixtures/dumps/api_gateway/response.json deleted file mode 100644 index 13302a352..000000000 --- a/spec/fixtures/dumps/api_gateway/response.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "statusCode": 200, - "body": "must be a string, even if it is json, it should be a string" -} \ No newline at end of file diff --git a/spec/fixtures/dumps/api_gateway/stores/index.json b/spec/fixtures/dumps/api_gateway/stores/index.json deleted file mode 100644 index c35196bdf..000000000 --- a/spec/fixtures/dumps/api_gateway/stores/index.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "resource": "/stores", - "path": "/stores", - "httpMethod": "GET", - "headers": { - "Accept": "*/*", - "Accept-Encoding": "gzip, deflate", - "cache-control": "no-cache", - "CloudFront-Forwarded-Proto": "https", - "CloudFront-Is-Desktop-Viewer": "true", - "CloudFront-Is-Mobile-Viewer": "false", - "CloudFront-Is-SmartTV-Viewer": "false", - "CloudFront-Is-Tablet-Viewer": "false", - "CloudFront-Viewer-Country": "US", - "Content-Type": "text/plain", - "Host": "uhghn8z6t1.execute-api.us-east-1.amazonaws.com", - "Postman-Token": "7166b11b-59de-4e7b-ad35-24e556b7a083", - "User-Agent": "PostmanRuntime/6.4.1", - "Via": "1.1 55676da1e5c0a9c4e60a94a95b01dc04.cloudfront.net (CloudFront)", - "X-Amz-Cf-Id": "iERhUw6ghRnv1uRYfxJaUsDGWVbERFSZ4K00CIgZtJ0T6yeFdItMeQ==", - "X-Amzn-Trace-Id": "Root=1-59f50229-587ec5271678236e50ad91b1", - "X-Forwarded-For": "69.42.1.180, 54.239.203.100", - "X-Forwarded-Port": "443", - "X-Forwarded-Proto": "https" - }, - "queryStringParameters": { - "a": "1", - "b": "2" - }, - "pathParameters": null, - "stageVariables": null, - "requestContext": { - "path": "/stag/stores", - "accountId": "123456789012", - "resourceId": "c0yhg8", - "stage": "stag", - "requestId": "e5c39604-bc2d-11e7-abbe-1baaa0f8e02e", - "identity": { - "cognitoIdentityPoolId": null, - "accountId": null, - "cognitoIdentityId": null, - "caller": null, - "apiKey": "", - "sourceIp": "69.42.1.180", - "accessKey": null, - "cognitoAuthenticationType": null, - "cognitoAuthenticationProvider": null, - "userArn": null, - "userAgent": "PostmanRuntime/6.4.1", - "user": null - }, - "resourcePath": "/stores", - "httpMethod": "GET", - "apiId": "uhghn8z6t1" - }, - "body": "{\n \"key3\": \"value3\",\n \"key2\": \"value2\",\n \"key1\": \"value1\"\n}", - "isBase64Encoded": false -} \ No newline at end of file diff --git a/spec/fixtures/dumps/api_gateway/xhr-delete.json b/spec/fixtures/dumps/api_gateway/xhr-delete.json deleted file mode 100644 index 4d9f57968..000000000 --- a/spec/fixtures/dumps/api_gateway/xhr-delete.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "resource": "/articles/{id}", - "path": "/articles/5", - "httpMethod": "DELETE", - "headers": { - "Host": "localhost:8888", - "Connection": "keep-alive", - "Pragma": "no-cache", - "Accept": "application/json, text/javascript, */*; q=0.01", - "X-Requested-With": "XMLHttpRequest", - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36", - "Referer": "http://localhost:8888/articles", - "Accept-Encoding": "gzip, deflate, br", - "Accept-Language": "en-US,en;q=0.9,pt;q=0.8", - "Version": "HTTP/1.1", - "cache-control": "no-cache", - "origin": "http://localhost:8888" - }, - "queryStringParameters": { - }, - "pathParameters": { - "id": "5" - }, - "stageVariables": null, - "requestContext": { - }, - "body": null, - "isBase64Encoded": false -} diff --git a/spec/fixtures/dumps/kinesis/records.json b/spec/fixtures/dumps/kinesis/records.json deleted file mode 100644 index 52c595809..000000000 --- a/spec/fixtures/dumps/kinesis/records.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "Records": [ - { - "kinesis": { - "kinesisSchemaVersion": "1.0", - "partitionKey": "1", - "sequenceNumber": "49593016666855735301798073856083438124424404568371101698", - "data": "aGVsbG8gd29ybGQ=", - "approximateArrivalTimestamp": 1550289189.474 - }, - "eventSource": "aws:kinesis", - "eventVersion": "1.0", - "eventID": "shardId-000000000000:49593016666855735301798073856083438124424404568371101698", - "eventName": "aws:kinesis:record", - "invokeIdentityArn": "arn:aws:iam::112233445566:role/demo-dev-DataJob-OUD26QSQSWKN-DataJobFileIamRole-CM2L7G0KZVY", - "awsRegion": "us-west-2", - "eventSourceARN": "arn:aws:kinesis:us-west-2:112233445566:stream/my-stream" - }, - { - "kinesis": { - "kinesisSchemaVersion": "1.0", - "partitionKey": "1", - "sequenceNumber": "49593016666855735301798073856084647050244019197545807874", - "data": "aGVsbG8gd29ybGQ=", - "approximateArrivalTimestamp": 1550289189.507 - }, - "eventSource": "aws:kinesis", - "eventVersion": "1.0", - "eventID": "shardId-000000000000:49593016666855735301798073856084647050244019197545807874", - "eventName": "aws:kinesis:record", - "invokeIdentityArn": "arn:aws:iam::112233445566:role/demo-dev-DataJob-OUD26QSQSWKN-DataJobFileIamRole-CM2L7G0KZVY", - "awsRegion": "us-west-2", - "eventSourceARN": "arn:aws:kinesis:us-west-2:112233445566:stream/my-stream" - }, - { - "kinesis": { - "kinesisSchemaVersion": "1.0", - "partitionKey": "1", - "sequenceNumber": "49593016666855735301798073856085855976063633895439990786", - "data": "aGVsbG8gd29ybGQ=", - "approximateArrivalTimestamp": 1550289189.621 - }, - "eventSource": "aws:kinesis", - "eventVersion": "1.0", - "eventID": "shardId-000000000000:49593016666855735301798073856085855976063633895439990786", - "eventName": "aws:kinesis:record", - "invokeIdentityArn": "arn:aws:iam::112233445566:role/demo-dev-DataJob-OUD26QSQSWKN-DataJobFileIamRole-CM2L7G0KZVY", - "awsRegion": "us-west-2", - "eventSourceARN": "arn:aws:kinesis:us-west-2:112233445566:stream/my-stream" - } - ] -} diff --git a/spec/fixtures/dumps/lambda/cloudwatch_event/event_and_context.rb b/spec/fixtures/dumps/lambda/cloudwatch_event/event_and_context.rb deleted file mode 100644 index 5cf0c3ffa..000000000 --- a/spec/fixtures/dumps/lambda/cloudwatch_event/event_and_context.rb +++ /dev/null @@ -1,22 +0,0 @@ -event: -{"version"=>"0", - "id"=>"baa6a51d-ef92-18fc-cd0f-2d72bcffebbf", - "detail-type"=>"Scheduled Event", - "source"=>"aws.events", - "account"=>"123456789012", - "time"=>"2017-11-07T05:08:58Z", - "region"=>"us-east-1", - "resources"=>["arn:aws:events:us-east-1:123456789012:rule/test-cloudwatch-event-rule"], - "detail"=>{}} - -context: -{"callbackWaitsForEmptyEventLoop"=>true, - "logGroupName"=>"/aws/lambda/demo-test-posts-controller-new", - "logStreamName"=>"2017/11/07/[$LATEST]3cefcb18a8bc49acbfb3f29907a36391", - "functionName"=>"demo-test-posts-controller-new", - "memoryLimitInMB"=>"3008", - "functionVersion"=>"$LATEST", - "invokeid"=>"cd68b58a-c379-11e7-bab1-855c4fa0d379", - "awsRequestId"=>"cd68b58a-c379-11e7-bab1-855c4fa0d379", - "invokedFunctionArn"=> - "arn:aws:lambda:us-east-1:123456789012:function:demo-test-posts-controller-new"} diff --git a/spec/fixtures/dumps/lambda/from_logs/request-after.json b/spec/fixtures/dumps/lambda/from_logs/request-after.json deleted file mode 100644 index 79fa8db83..000000000 --- a/spec/fixtures/dumps/lambda/from_logs/request-after.json +++ /dev/null @@ -1,38 +0,0 @@ -// http://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-set-up-simple-proxy.html#api-gateway-simple-proxy-for-lambda-input-format -{ - "resource": "Resource path", - "path": "Path parameter", - "httpMethod": "Incoming request's method name" - "headers": {Incoming request headers} - "queryStringParameters": {query string parameters } - "pathParameters": {path parameters} - "stageVariables": {Applicable stage variables} - "requestContext": {Request context, including authorizer-returned key-value pairs} - "body": "A JSON string of the request payload." - "isBase64Encoded": "A boolean flag to indicate if the applicable request payload is Base64-encode" -} - -{ - "resource": "/posts", - "path": "/posts", - "httpMethod": "GET", - "headers": { - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", - "Accept-Encoding": "gzip,deflate,br", - "Accept-Language": "en-US,en;q=0.9,pt;q=0.8", - "cache-control": "max-age=0", - "CloudFront-Forwarded-Proto": "https", - "CloudFront-Is-Desktop-Viewer": "true", - "CloudFront-Is-Mobile-Viewer": "false", - "CloudFront-Is-SmartTV-Viewer": "false", - "CloudFront-Is-Tablet-Viewer": "false", - "CloudFront-Viewer-Country": "US", - "Host": "g6epak8d60.execute-api.us-east-1.amazonaws.com", - "upgrade-insecure-requests": "1", - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML,like Gecko) Chrome/62.0.3202.75 Safari/537.36", - "Via": "2.0 515297ac55a7ae01bf8c7d03df4fecb1.cloudfront.net (CloudFront)", - "X-Amz-Cf-Id": "X0a8NPAklTJlWhcButfh-U-LwZ2oUC_qD3HDdpGzrwmHm5vUyXmVrA==", - "X-Amzn-Trace-Id": "Root=1-59f4ba22-68e5fa121f70dfaf3e5a4b7e", - "X-Forwarded-For": "69.42.1.180,54.239.203.17" - } -} \ No newline at end of file diff --git a/spec/fixtures/dumps/lambda/from_logs/request-before.json b/spec/fixtures/dumps/lambda/from_logs/request-before.json deleted file mode 100644 index b983af230..000000000 --- a/spec/fixtures/dumps/lambda/from_logs/request-before.json +++ /dev/null @@ -1 +0,0 @@ -[EMPTY] \ No newline at end of file diff --git a/spec/fixtures/dumps/lambda/from_logs/request-headers-before.json b/spec/fixtures/dumps/lambda/from_logs/request-headers-before.json deleted file mode 100644 index 51f8ac316..000000000 --- a/spec/fixtures/dumps/lambda/from_logs/request-headers-before.json +++ /dev/null @@ -1 +0,0 @@ -(f74ca045-bc02-11e7-a9d7-2f5a6ae1dc81) Method request headers: {Accept=text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8, CloudFront-Viewer-Country=US, CloudFront-Forwarded-Proto=https, CloudFront-Is-Tablet-Viewer=false, CloudFront-Is-Mobile-Viewer=false, User-Agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.75 Safari/537.36, X-Forwarded-Proto=https, CloudFront-Is-SmartTV-Viewer=false, Host=g6epak8d60.execute-api.us-east-1.amazonaws.com, Accept-Encoding=gzip, deflate, br, X-Forwarded-Port=443, X-Amzn-Trace-Id=Root=1-59f4ba22-68e5fa121f70dfaf3e5a4b7e, Via=2.0 515297ac55a7ae01bf8c7d03df4fecb1.cloudfront.net (CloudFront), upgrade-insecure-requests=1, X-Amz-Cf-Id=X0a8NPAklTJlWhcButfh-U-LwZ2oUC_qD3HDdpGzrwmHm5vUyXmVrA==, X-Forwarded-For=69.42.1.180, 54.239.203.17, Accept-Language=en-US,en;q=0.9,pt;q=0.8, cache-control=max-age=0, CloudFront-Is-Desktop-Viewer=true} diff --git a/spec/fixtures/dumps/lambda/from_logs/response-after.json b/spec/fixtures/dumps/lambda/from_logs/response-after.json deleted file mode 100644 index b5488e719..000000000 --- a/spec/fixtures/dumps/lambda/from_logs/response-after.json +++ /dev/null @@ -1 +0,0 @@ -(f74ca045-bc02-11e7-a9d7-2f5a6ae1dc81) Method response body after transformations: {"resource":"/posts","path":"/posts","httpMethod":"GET","headers":{"Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8","Accept-Encoding":"gzip, deflate, br","Accept-Language":"en-US,en;q=0.9,pt;q=0.8","cache-control":"max-age=0","CloudFront-Forwarded-Proto":"https","CloudFront-Is-Desktop-Viewer":"true","CloudFront-Is-Mobile-Viewer":"false","CloudFront-Is-SmartTV-Viewer":"false","CloudFront-Is-Tablet-Viewer":"false","CloudFront-Viewer-Country":"US","Host":"g6epak8d60.execute-api.us-east-1.amazonaws.com","upgrade-insecure-requests":"1","User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.75 Safari/537.36","Via":"2.0 515297ac55a7ae01bf8c7d03df4fecb1.cloudfront.net (CloudFront)","X-Amz-Cf-Id":"X0a8NPAklTJlWhcButfh-U-LwZ2oUC_qD3HDdpGzrwmHm5vUyXmVrA==","X-Amzn-Trace-Id":"Root=1-59f4ba22-68e5fa121f70dfaf3e5a4b7e","X-Forwarded-For":"69.42.1.180, 54.239.203.17","X-Fo [TRUNCATED] diff --git a/spec/fixtures/dumps/lambda/from_logs/response-before.json b/spec/fixtures/dumps/lambda/from_logs/response-before.json deleted file mode 100644 index 60860997a..000000000 --- a/spec/fixtures/dumps/lambda/from_logs/response-before.json +++ /dev/null @@ -1 +0,0 @@ -(f74ca045-bc02-11e7-a9d7-2f5a6ae1dc81) Endpoint response body before transformations: {"statusCode":200,"body":"{\"resource\":\"/posts\",\"path\":\"/posts\",\"httpMethod\":\"GET\",\"headers\":{\"Accept\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"Accept-Encoding\":\"gzip, deflate, br\",\"Accept-Language\":\"en-US,en;q=0.9,pt;q=0.8\",\"cache-control\":\"max-age=0\",\"CloudFront-Forwarded-Proto\":\"https\",\"CloudFront-Is-Desktop-Viewer\":\"true\",\"CloudFront-Is-Mobile-Viewer\":\"false\",\"CloudFront-Is-SmartTV-Viewer\":\"false\",\"CloudFront-Is-Tablet-Viewer\":\"false\",\"CloudFront-Viewer-Country\":\"US\",\"Host\":\"g6epak8d60.execute-api.us-east-1.amazonaws.com\",\"upgrade-insecure-requests\":\"1\",\"User-Agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.75 Safari/537.36\",\"Via\":\"2.0 515297ac55a7ae01bf8c7d03df4fecb1.cloudfront.net (CloudFront)\",\"X-Amz-Cf-Id\":\"X0a8NPAklTJlWhcButfh-U-LwZ2oUC_qD3HDdpGzrwmHm5vUyXmVrA==\",\"X-Amzn-T [TRUNCATED] diff --git a/spec/fixtures/dumps/logs/log_event.json b/spec/fixtures/dumps/logs/log_event.json deleted file mode 100644 index 8f21f6e45..000000000 --- a/spec/fixtures/dumps/logs/log_event.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "awslogs": { - "data": "H4sIAGPuZFwAA61SXY/SQBT9K83ER2pn7nzzVkJdVwEN7WYftsRM6YCNLcW2gEj4705xN2yiJms083Jzzsy55565J1TZtjVrmxy3Fg3ROEzCT9MojsObCA1QfdjYxsGEAFDKGOdCOLis1zdNvds6JjCHNihNleUm+GzLsv5Jx11jTeV4wEQHGALCgodXkzCJ4mRBLcYYhMkwFWy5IgYruTKgNNF5JlbaSbS7rF02xbYr6s2bouxs06LhA8ptVfu53fuTev2uznx+f/8ez2AKah76c7utmy7+5aWfjKaT8I7g2W3yFi0u/qK93XS95AkVubNJGRdKcglSUCw0BayIdEdJLbFkkmJGGFZUc0LdRAyUFFzoPoyucBF2pnJpEM4xIaJXInLwFK2TvyTjHeqmzNMNOg/+rat8YVfbD+mdUvTFHkmKhinam3JnXTm4YHDF4AmjV8yV59+71UpLygFzJRQWRIEW3LGcSQWOUFoDEPedgoCQ7E9uhVTP3UazsTe3X3fu4m0+9NiSaMUw9YnRzGcgM1/nyvqMG8BLt1YA5j+44y90N48+fpgnf22wG+8a06/i0COSvpbUq9q0GxVlaXPvygHGjvDSbuoWvDl6cfHduhegvOnIgeab90jctda15vyC9+Mvzj8ACHlavMMDAAA=" - } -} \ No newline at end of file diff --git a/spec/fixtures/dumps/rack/form-post.json b/spec/fixtures/dumps/rack/form-post.json deleted file mode 100644 index 1ee53f6c4..000000000 --- a/spec/fixtures/dumps/rack/form-post.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "CONTENT_LENGTH": "185", - "CONTENT_TYPE": "application/x-www-form-urlencoded", - "GATEWAY_INTERFACE": "CGI/1.1", - "PATH_INFO": "/articles", - "QUERY_STRING": "", - "REMOTE_ADDR": "127.0.0.1", - "REMOTE_HOST": "127.0.0.1", - "REQUEST_METHOD": "POST", - "REQUEST_URI": "http://localhost:8888/articles", - "SCRIPT_NAME": "", - "SERVER_NAME": "localhost", - "SERVER_PORT": "8888", - "SERVER_PROTOCOL": "HTTP/1.1", - "SERVER_SOFTWARE": "WEBrick/1.3.1 (Ruby/2.5.0/2017-09-14)", - "HTTP_HOST": "localhost:8888", - "HTTP_CONNECTION": "keep-alive", - "HTTP_CACHE_CONTROL": "max-age=0", - "HTTP_ORIGIN": "http://localhost:8888", - "HTTP_UPGRADE_INSECURE_REQUESTS": "1", - "HTTP_USER_AGENT": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36", - "HTTP_ACCEPT": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", - "HTTP_REFERER": "http://localhost:8888/articles/new", - "HTTP_ACCEPT_ENCODING": "gzip, deflate, br", - "HTTP_ACCEPT_LANGUAGE": "en-US,en;q=0.9,pt;q=0.8", - "rack.version": [ - 1, - 3 - ], - "rack.input": "", - "rack.errors": "", - "rack.multithread": true, - "rack.multiprocess": false, - "rack.run_once": false, - "rack.url_scheme": "http", - "rack.hijack?": true, - "rack.hijack": "", - "rack.hijack_io": null, - "HTTP_VERSION": "HTTP/1.1", - "REQUEST_PATH": "/articles" -} diff --git a/spec/fixtures/dumps/rack/form-post.txt b/spec/fixtures/dumps/rack/form-post.txt deleted file mode 100644 index 08a47797a..000000000 --- a/spec/fixtures/dumps/rack/form-post.txt +++ /dev/null @@ -1,34 +0,0 @@ -CONTENT_LENGTH: 96 -CONTENT_TYPE: application/x-www-form-urlencoded -GATEWAY_INTERFACE: CGI/1.1 -PATH_INFO: /articles -QUERY_STRING: -REMOTE_ADDR: 127.0.0.1 -REMOTE_HOST: 127.0.0.1 -REQUEST_METHOD: POST -REQUEST_URI: http://localhost:8888/articles -SCRIPT_NAME: -SERVER_NAME: localhost -SERVER_PORT: 8888 -SERVER_PROTOCOL: HTTP/1.1 -SERVER_SOFTWARE: WEBrick/1.3.1 (Ruby/2.5.0/2017-09-14) -HTTP_CACHE_CONTROL: no-cache -HTTP_POSTMAN_TOKEN: 82715a67-efab-4452-8ee9-bde46dbbec61 -HTTP_USER_AGENT: PostmanRuntime/6.4.1 -HTTP_ACCEPT: */* -HTTP_HOST: localhost:8888 -HTTP_ACCEPT_ENCODING: gzip, deflate -HTTP_CONNECTION: keep-alive -rack.version: [1, 3] -rack.input: # -rack.errors: # -rack.multithread: true -rack.multiprocess: false -rack.run_once: false -rack.url_scheme: http -rack.hijack?: true -rack.hijack: # -rack.hijack_io: -HTTP_VERSION: HTTP/1.1 -REQUEST_PATH: /articles -article_params nil diff --git a/spec/fixtures/dumps/rack/json-post.txt b/spec/fixtures/dumps/rack/json-post.txt deleted file mode 100644 index 5c9f7373e..000000000 --- a/spec/fixtures/dumps/rack/json-post.txt +++ /dev/null @@ -1,32 +0,0 @@ -CONTENT_LENGTH: "40" -CONTENT_TYPE: "application/json" -GATEWAY_INTERFACE: "CGI/1.1" -PATH_INFO: "/posts/tung/edit" -QUERY_STRING: "foo=bar" -REMOTE_ADDR: "::1" -REMOTE_HOST: "localhost" -REQUEST_METHOD: "POST" -REQUEST_URI: "http://localhost:8888/posts/tung/edit?foo=bar" -SCRIPT_NAME: "" -SERVER_NAME: "localhost" -SERVER_PORT: "8888" -SERVER_PROTOCOL: "HTTP/1.1" -SERVER_SOFTWARE: "WEBrick/1.3.1 (Ruby/2.2.2/2015-04-13)" -HTTP_CACHE_CONTROL: "no-cache" -HTTP_USER_AGENT: "PostmanRuntime/6.4.1" -HTTP_ACCEPT: "*/*" -HTTP_HOST: "localhost:8888" -HTTP_ACCEPT_ENCODING: "gzip, deflate" -HTTP_CONNECTION: "keep-alive" -rack.version: [1, 3] -rack.input: # -rack.errors: #> -rack.multithread: true -rack.multiprocess: false -rack.run_once: false -rack.url_scheme: "http" -rack.hijack?: true -rack.hijack: # -rack.hijack_io: nil -HTTP_VERSION: "HTTP/1.1" -REQUEST_PATH: "/posts/tung/edit" diff --git a/spec/fixtures/dumps/rack/posts/create.json b/spec/fixtures/dumps/rack/posts/create.json deleted file mode 100644 index 50b0baf7e..000000000 --- a/spec/fixtures/dumps/rack/posts/create.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "resource": "/posts", - "path": "/posts", - "httpMethod": "POST", - "headers": { - "Host": "localhost:8888", - "Connection": "keep-alive", - "Pragma": "no-cache", - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36", - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", - "Referer": "http://localhost:8888/posts/new", - "Accept-Encoding": "gzip, deflate, br", - "Accept-Language": "en-US,en;q=0.9,pt;q=0.8", - "Version": "HTTP/1.1", - "cache-control": "no-cache", - "content-type": "application/x-www-form-urlencoded", - "origin": "http://localhost:8888", - "upgrade-insecure-requests": "1" - }, - "queryStringParameters": { - }, - "pathParameters": null, - "stageVariables": null, - "requestContext": { - }, - "body": "utf8=%E2%9C%93&authenticity_token=ZlI2xj0H9Pa9eiJf00B9URX7Xg83rNUeZPgqbkIQim8%2B7W%2Fhkk6rd8ALfPf0jhLY4WP1deTdZdoTlLZPKqWa1Q%3D%3D&post%5Btitle%5D=Test+Post+5&commit=Submit", - "isBase64Encoded": false -} diff --git a/spec/fixtures/dumps/sns/s3_upload.json b/spec/fixtures/dumps/sns/s3_upload.json deleted file mode 100644 index f85630b04..000000000 --- a/spec/fixtures/dumps/sns/s3_upload.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "Records": [ - { - "EventSource": "aws:sns", - "EventVersion": "1.0", - "EventSubscriptionArn": "arn:aws:sns:us-west-2:112233445566:my-bucket:bdc903bb-a51e-4799-b472-bd7293ce245b", - "Sns": { - "Type": "Notification", - "MessageId": "434252ae-b80f-58ad-9c11-067761c040dd", - "TopicArn": "arn:aws:sns:us-west-2:112233445566:my-bucket", - "Subject": "Amazon S3 Notification", - "Message": "{\"Records\":[{\"eventVersion\":\"2.1\",\"eventSource\":\"aws:s3\",\"awsRegion\":\"us-west-2\",\"eventTime\":\"2019-02-10T07:49:35.265Z\",\"eventName\":\"ObjectCreated:Put\",\"userIdentity\":{\"principalId\":\"AWS:AROAJBZT6HZEMHALPTL4E:botocore-session-1549784243\"},\"requestParameters\":{\"sourceIPAddress\":\"11.22.33.44\"},\"responseElements\":{\"x-amz-request-id\":\"4F2A57016225F633\",\"x-amz-id-2\":\"ek4gBwsxZtNeiXV8xUfxocSy3cZLR8ws/HcI8qIyDx75ID7hekUMuMMsa4DYltRsX3v0zJ9kl1c=\"},\"s3\":{\"s3SchemaVersion\":\"1.0\",\"configurationId\":\"NzFkOWNiMzQtZDI1Ni00N2U2LTlmYzgtODdhNWJkOWVkNjNm\",\"bucket\":{\"name\":\"my-bucket\",\"ownerIdentity\":{\"principalId\":\"AYC6O1A20R123\"},\"arn\":\"arn:aws:s3:::my-bucket\"},\"object\":{\"key\":\"myfolder/subfolder/test.txt\",\"size\":0,\"eTag\":\"d41d8cd98f00b204e9800998ecf8427e\",\"sequencer\":\"005C5FD78F373E174B\"}}}]}", - "Timestamp": "2019-02-10T07:49:35.417Z", - "SignatureVersion": "1", - "Signature": "nCgvhLBccqWkbCnmDk5t0RfIUkVo0toGDu13zkGxz7wkqCyv+10n2HO+Xy+qegfX2yB/yC3Vxzu9Sg/RbwLuVzqRWxtJ8YUSFx3UnUZEjrWLGTWrjyfkgrlDm8ovhT9hs1tSsoHm07lsUsxF+uhnBitFS8fKbe/PNBiTQKwLr2nUZxGy1zMrA75cdh/Ft7yDHy0S9bfcK/gosyfdSb1ggBQTQRoL2gZ46TAtciZ2eiGTUYW+PjSfAE9uKQcInSi2qvCIUTcWmRLXPDgRj3i5dJdjgqor8uCS+93kTF1OVURKJ5oMMmKVAabBGgJulZYQfaONd2si+H5Y8aUz5AzZSg==", - "SigningCertUrl": "https://sns.us-west-2.amazonaws.com/SimpleNotificationService-ac565b8b1a6c5d002d285f9598aa1d9b.pem", - "UnsubscribeUrl": "https://sns.us-west-2.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-west-2:112233445566:my-bucket:bdc903bb-a51e-4799-b472-bd7293ce245b", - "MessageAttributes": {} - } - } - ] -} \ No newline at end of file diff --git a/spec/fixtures/dumps/sns/sns_event.json b/spec/fixtures/dumps/sns/sns_event.json deleted file mode 100644 index bb5f41d60..000000000 --- a/spec/fixtures/dumps/sns/sns_event.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "Records": [ - { - "EventSource": "aws:sns", - "EventVersion": "1.0", - "EventSubscriptionArn": "arn:aws:sns:us-west-2:112233445566:my-bucket:bdc903bb-a51e-4799-b472-bd7293ce245b", - "Sns": { - "Type": "Notification", - "MessageId": "434252ae-b80f-58ad-9c11-067761c040dd", - "TopicArn": "arn:aws:sns:us-west-2:112233445566:my-bucket", - "Subject": "Amazon S3 Notification", - "Message": "{\"body\": \"This is a sns hard job\"}", - "Timestamp": "2019-02-10T07:49:35.417Z", - "SignatureVersion": "1", - "Signature": "nCgvhLBccqWkbCnmDk5t0RfIUkVo0toGDu13zkGxz7wkqCyv+10n2HO+Xy+qegfX2yB/yC3Vxzu9Sg/RbwLuVzqRWxtJ8YUSFx3UnUZEjrWLGTWrjyfkgrlDm8ovhT9hs1tSsoHm07lsUsxF+uhnBitFS8fKbe/PNBiTQKwLr2nUZxGy1zMrA75cdh/Ft7yDHy0S9bfcK/gosyfdSb1ggBQTQRoL2gZ46TAtciZ2eiGTUYW+PjSfAE9uKQcInSi2qvCIUTcWmRLXPDgRj3i5dJdjgqor8uCS+93kTF1OVURKJ5oMMmKVAabBGgJulZYQfaONd2si+H5Y8aUz5AzZSg==", - "SigningCertUrl": "https://sns.us-west-2.amazonaws.com/SimpleNotificationService-ac565b8b1a6c5d002d285f9598aa1d9b.pem", - "UnsubscribeUrl": "https://sns.us-west-2.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-west-2:112233445566:my-bucket:bdc903bb-a51e-4799-b472-bd7293ce245b", - "MessageAttributes": {} - } - } - ] -} \ No newline at end of file diff --git a/spec/fixtures/dumps/sqs/sqs_event.json b/spec/fixtures/dumps/sqs/sqs_event.json deleted file mode 100644 index 596cec9dd..000000000 --- a/spec/fixtures/dumps/sqs/sqs_event.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "Records": [ - { - "messageId": "1e0bfe01-f9df-46c0-8d86-2fd898e4dee9", - "receiptHandle": "AQEBgxVw0hjHeNKB1brir4hr0Fxvz4ERJIqd7bP/iHw82/+UUx/r4W0KG3FSiEA4A+Vk0oS8dT6W8be/Bn7eJjKspZfW2KzC0xzsCmS+BihySk1SX9FM5SW1rFd3bFWYtT6s7pOX2inaU/THtn7Envp5Rs+zehmNIspnLPZkf9h3RFSQk12xaVaOmCQnHtz9o8uKIXwMEwn5IhlJgC0DIuM1v8NZK8Hc65b4xpf09vf01LEA/XdXm24SjfJ0fl7ev2rBXtkMitAfNmKd8x0fcbG3O7H7wB+CIKR4+QvGcI6u9QuAdPU5MpIJ46niJmrtnIx70S5Go1paUYMa77ABBjFWoJkJHvHouuiohEQHdMrH1QSyabNBS2Nw2dikhBcXVtLQW4iH+xNXwLIVUxarAk9EHokh1iGWZsG91whmPaAl0t2Vdfo6Dcm0/6IgXhKcLFIw", - "body": "{\"message\":\"This is a hard job\"}", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1550605918693", - "SenderId": "AIDAJTCD6O457Q7BMTLYM", - "ApproximateFirstReceiveTimestamp": "1550605918704" - }, - "messageAttributes": {}, - "md5OfBody": "3d635e69eb93fd184b47a31d460ca2b6", - "eventSource": "aws:sqs", - "eventSourceARN": "arn:aws:sqs:us-west-2:112233445566:demo-dev-List-3VJ13ADFT5VZ-Waitlist-X35N8JKWZTL3", - "awsRegion": "us-west-2" - } - ] -} \ No newline at end of file diff --git a/spec/fixtures/multipart/binary b/spec/fixtures/multipart/binary deleted file mode 100644 index a3bd67c49..000000000 Binary files a/spec/fixtures/multipart/binary and /dev/null differ diff --git a/spec/fixtures/multipart/nested b/spec/fixtures/multipart/nested deleted file mode 100644 index 519788244..000000000 --- a/spec/fixtures/multipart/nested +++ /dev/null @@ -1,10 +0,0 @@ ---AaB03x -Content-Disposition: form-data; name="foo[submit-name]" - -Larry ---AaB03x -Content-Disposition: form-data; name="foo[files]"; filename="file1.txt" -Content-Type: text/plain - -contents ---AaB03x-- diff --git a/spec/fixtures/multipart/simple_form b/spec/fixtures/multipart/simple_form deleted file mode 100644 index 3c5938da7..000000000 --- a/spec/fixtures/multipart/simple_form +++ /dev/null @@ -1,14 +0,0 @@ -------WebKitFormBoundaryVCeZcQHJQlRuehAZ -Content-Disposition: form-data; name="name" - -Tung -------WebKitFormBoundaryVCeZcQHJQlRuehAZ -Content-Disposition: form-data; name="title" - -Mr -------WebKitFormBoundaryVCeZcQHJQlRuehAZ -Content-Disposition: form-data; name="avatar"; filename="" -Content-Type: application/octet-stream - - -------WebKitFormBoundaryVCeZcQHJQlRuehAZ-- diff --git a/spec/fixtures/multipart/text b/spec/fixtures/multipart/text deleted file mode 100644 index 96da2b823..000000000 --- a/spec/fixtures/multipart/text +++ /dev/null @@ -1,14 +0,0 @@ -------WebKitFormBoundaryVCeZcQHJQlRuehAZ -Content-Disposition: form-data; name="name" - -Tung -------WebKitFormBoundaryVCeZcQHJQlRuehAZ -Content-Disposition: form-data; name="title" - -Mr -------WebKitFormBoundaryVCeZcQHJQlRuehAZ -Content-Disposition: form-data; name="files"; filename="file1.txt" -Content-Type: application/octet-stream - - -------WebKitFormBoundaryVCeZcQHJQlRuehAZ-- diff --git a/spec/fixtures/resource_pages/demo-test-api-resources-1.yml b/spec/fixtures/resource_pages/demo-test-api-resources-1.yml deleted file mode 100644 index 43f07b2df..000000000 --- a/spec/fixtures/resource_pages/demo-test-api-resources-1.yml +++ /dev/null @@ -1,25 +0,0 @@ ---- -Resources: - HiApiResource: - Type: AWS::ApiGateway::Resource - Properties: - ParentId: !Ref RootResourceId - PathPart: hi - RestApiId: !Ref RestApi - Hi1ApiResource: - Type: AWS::ApiGateway::Resource - Properties: - ParentId: !Ref HiApiResource - PathPart: '1' - RestApiId: !Ref RestApi -Parameters: - RestApi: - Type: String - Description: RestApi - RootResourceId: - Type: String -Outputs: - HiApiResource: - Value: !Ref HiApiResource - Hi1ApiResource: - Value: !Ref Hi1ApiResource diff --git a/spec/fixtures/resource_pages/demo-test-api-resources-2.yml b/spec/fixtures/resource_pages/demo-test-api-resources-2.yml deleted file mode 100644 index 31740490b..000000000 --- a/spec/fixtures/resource_pages/demo-test-api-resources-2.yml +++ /dev/null @@ -1,25 +0,0 @@ ---- -Resources: - Hi2ApiResource: - Type: AWS::ApiGateway::Resource - Properties: - ParentId: !Ref HiApiResource - PathPart: '2' - RestApiId: !Ref RestApi - Hi3ApiResource: - Type: AWS::ApiGateway::Resource - Properties: - ParentId: !Ref HiApiResource - PathPart: '3' - RestApiId: !Ref RestApi -Parameters: - RestApi: - Type: String - Description: RestApi - HiApiResource: - Type: String -Outputs: - Hi2ApiResource: - Value: !Ref Hi2ApiResource - Hi3ApiResource: - Value: !Ref Hi3ApiResource diff --git a/spec/fixtures/resource_pages/demo-test-api-resources-3.yml b/spec/fixtures/resource_pages/demo-test-api-resources-3.yml deleted file mode 100644 index 2b8825b6f..000000000 --- a/spec/fixtures/resource_pages/demo-test-api-resources-3.yml +++ /dev/null @@ -1,17 +0,0 @@ ---- -Resources: - Hi4ApiResource: - Type: AWS::ApiGateway::Resource - Properties: - ParentId: !Ref HiApiResource - PathPart: '4' - RestApiId: !Ref RestApi -Parameters: - RestApi: - Type: String - Description: RestApi - HiApiResource: - Type: String -Outputs: - Hi4ApiResource: - Value: !Ref Hi4ApiResource diff --git a/spec/fixtures/routes/resources.rb b/spec/fixtures/routes/resources.rb deleted file mode 100644 index 4e827db9e..000000000 --- a/spec/fixtures/routes/resources.rb +++ /dev/null @@ -1,6 +0,0 @@ -# Example routes.rb of resources -resources :posts - -get "landing", to: "posts#index" - -any "*catchall", to: "public_files#show" diff --git a/spec/fixtures/shim/events/alb.json b/spec/fixtures/shim/events/alb.json new file mode 100644 index 000000000..bea4c59b0 --- /dev/null +++ b/spec/fixtures/shim/events/alb.json @@ -0,0 +1,28 @@ +{ + "requestContext": { + "elb": { + "targetGroupArn": "arn:aws:elasticloadbalancing:us-east-2:123456789012:targetgroup/lambda-279XGJDqGZ5rsrHC2Fjr/49e9d65c45c6791a" + } + }, + "httpMethod": "POST", + "path": "/path/to/resource", + "queryStringParameters": { + "query": "1234ABCD" + }, + "headers": { + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", + "accept-encoding": "gzip", + "accept-language": "en-US,en;q=0.9", + "connection": "keep-alive", + "host": "lambda-alb-123578498.us-east-2.elb.amazonaws.com", + "upgrade-insecure-requests": "1", + "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", + "x-amzn-trace-id": "Root=1-5c536348-3d683b8b04734faae651f476", + "x-forwarded-for": "72.12.164.125", + "x-forwarded-port": "80", + "x-forwarded-proto": "http", + "x-imforwards": "20" + }, + "body": "eyJ0ZXN0IjoiYm9keSJ9", + "isBase64Encoded": true +} diff --git a/spec/fixtures/shim/events/apigw.json b/spec/fixtures/shim/events/apigw.json new file mode 100644 index 000000000..e379b8cdc --- /dev/null +++ b/spec/fixtures/shim/events/apigw.json @@ -0,0 +1,123 @@ +{ + "body": "eyJ0ZXN0IjoiYm9keSJ9", + "resource": "/{proxy+}", + "path": "/path/to/resource", + "httpMethod": "POST", + "isBase64Encoded": true, + "queryStringParameters": { + "foo": "bar" + }, + "multiValueQueryStringParameters": { + "foo": [ + "bar" + ] + }, + "pathParameters": { + "proxy": "/path/to/resource" + }, + "stageVariables": { + "baz": "qux" + }, + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, sdch", + "Accept-Language": "en-US,en;q=0.8", + "Cache-Control": "max-age=0", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "multiValueHeaders": { + "Accept": [ + "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8" + ], + "Accept-Encoding": [ + "gzip, deflate, sdch" + ], + "Accept-Language": [ + "en-US,en;q=0.8" + ], + "Cache-Control": [ + "max-age=0" + ], + "CloudFront-Forwarded-Proto": [ + "https" + ], + "CloudFront-Is-Desktop-Viewer": [ + "true" + ], + "CloudFront-Is-Mobile-Viewer": [ + "false" + ], + "CloudFront-Is-SmartTV-Viewer": [ + "false" + ], + "CloudFront-Is-Tablet-Viewer": [ + "false" + ], + "CloudFront-Viewer-Country": [ + "US" + ], + "Host": [ + "0123456789.execute-api.us-east-1.amazonaws.com" + ], + "Upgrade-Insecure-Requests": [ + "1" + ], + "User-Agent": [ + "Custom User Agent String" + ], + "Via": [ + "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)" + ], + "X-Amz-Cf-Id": [ + "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==" + ], + "X-Forwarded-For": [ + "127.0.0.1, 127.0.0.2" + ], + "X-Forwarded-Port": [ + "443" + ], + "X-Forwarded-Proto": [ + "https" + ] + }, + "requestContext": { + "accountId": "123456789012", + "resourceId": "123456", + "stage": "prod", + "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", + "requestTime": "09/Apr/2015:12:34:56 +0000", + "requestTimeEpoch": 1428582896000, + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "accessKey": null, + "sourceIp": "127.0.0.1", + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": "Custom User Agent String", + "user": null + }, + "path": "/prod/path/to/resource", + "resourcePath": "/{proxy+}", + "httpMethod": "POST", + "apiId": "1234567890", + "protocol": "HTTP/1.1" + } +} diff --git a/spec/fixtures/shim/events/lambda.json b/spec/fixtures/shim/events/lambda.json new file mode 100644 index 000000000..801370e14 --- /dev/null +++ b/spec/fixtures/shim/events/lambda.json @@ -0,0 +1,52 @@ +{ + "version": "2.0", + "routeKey": "$default", + "rawPath": "/path/to/resource", + "rawQueryString": "foo=bar", + "cookies": [ + "yummy1=value1", + "yummy2=value2" + ], + "headers": { + "sec-fetch-mode": "navigate", + "x-amzn-tls-version": "TLSv1.2", + "sec-fetch-site": "none", + "accept-language": "en-US,en;q=0.5", + "cookie": "yummy1=value1; yummy2=value2", + "x-forwarded-proto": "https", + "x-forwarded-port": "443", + "x-forwarded-for": "66.234.208.210", + "sec-fetch-user": "?1", + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8", + "sec-gpc": "1", + "x-amzn-tls-cipher-suite": "ECDHE-RSA-AES128-GCM-SHA256", + "sec-ch-ua": "\"Not_A Brand\";v=\"8\", \"Chromium\";v=\"120\", \"Brave\";v=\"120\"", + "sec-ch-ua-mobile": "?0", + "x-amzn-trace-id": "Root=1-657bdbd2-32c6a1c85618ccec4dd7feb7", + "sec-ch-ua-platform": "\"macOS\"", + "host": "w2g5utchkk6rgaky66qu4yzwoy0cslvm.lambda-url.us-west-2.on.aws", + "upgrade-insecure-requests": "1", + "accept-encoding": "gzip, deflate, br", + "sec-fetch-dest": "document", + "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + }, + "requestContext": { + "accountId": "anonymous", + "apiId": "w2g5utchkk6rgaky66qu4yzwoy0cslvm", + "domainName": "w2g5utchkk6rgaky66qu4yzwoy0cslvm.lambda-url.us-west-2.on.aws", + "domainPrefix": "w2g5utchkk6rgaky66qu4yzwoy0cslvm", + "http": { + "method": "GET", + "path": "/path/to/resource", + "protocol": "HTTP/1.1", + "sourceIp": "66.234.208.210", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + }, + "requestId": "3fca3afe-2fb7-4e93-ac3b-949519408c39", + "routeKey": "$default", + "stage": "$default", + "time": "15/Dec/2023:04:53:38 +0000", + "timeEpoch": 1702616018863 + }, + "isBase64Encoded": false +} diff --git a/spec/fixtures/shim/frameworks/rails/config.ru b/spec/fixtures/shim/frameworks/rails/config.ru new file mode 100644 index 000000000..4a3c09a68 --- /dev/null +++ b/spec/fixtures/shim/frameworks/rails/config.ru @@ -0,0 +1,6 @@ +# This file is used by Rack-based servers to start the application. + +require_relative "config/environment" + +run Rails.application +Rails.application.load_server diff --git a/spec/fixtures/shim/frameworks/sinatra/config.ru b/spec/fixtures/shim/frameworks/sinatra/config.ru new file mode 100644 index 000000000..608d5cf91 --- /dev/null +++ b/spec/fixtures/shim/frameworks/sinatra/config.ru @@ -0,0 +1,3 @@ +require File.expand_path("my_app", File.dirname(__FILE__)) + +run MyApp diff --git a/spec/fixtures/shim/frameworks/sinatra_modular/app.rb b/spec/fixtures/shim/frameworks/sinatra_modular/app.rb new file mode 100644 index 000000000..2d8c89509 --- /dev/null +++ b/spec/fixtures/shim/frameworks/sinatra_modular/app.rb @@ -0,0 +1,5 @@ +class App < Sinatra::Base + get "/" do + "hello world" + end +end diff --git a/spec/fixtures/shim/frameworks/sinatra_modular/config.ru b/spec/fixtures/shim/frameworks/sinatra_modular/config.ru new file mode 100644 index 000000000..ca8f8b20f --- /dev/null +++ b/spec/fixtures/shim/frameworks/sinatra_modular/config.ru @@ -0,0 +1,7 @@ +require "sinatra/base" + +class MyApp < Sinatra::Base + get "/" do + "hello world" + end +end diff --git a/spec/fixtures/shim/handlers/controller.rb b/spec/fixtures/shim/handlers/controller.rb new file mode 100644 index 000000000..caf48ddeb --- /dev/null +++ b/spec/fixtures/shim/handlers/controller.rb @@ -0,0 +1,27 @@ +require "bundler/setup" +require "jets/shim" + +Jets::Shim.boot + +def lambda_handler(event:, context:) + Jets::Shim.handler(event, context) +end + +if __FILE__ == $0 + event_path = ENV["EVENT"] + event = if event_path && File.exist?(event_path) + JSON.load(IO.read(event_path)) + else + # APIGW + { + path: "/posts", + httpMethod: "GET", + headers: { + Host: "foobar.execute-api.us-west-2.amazonaws.com" + } + } + end + resp = lambda_handler(event: event, context: {}) + puts "resp: " + pp resp +end diff --git a/spec/fixtures/shim/rack/rack-apigw.txt b/spec/fixtures/shim/rack/rack-apigw.txt new file mode 100644 index 000000000..14a92df56 --- /dev/null +++ b/spec/fixtures/shim/rack/rack-apigw.txt @@ -0,0 +1,41 @@ +HTTPS off +HTTP_ACCEPT text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8 +HTTP_ACCEPT_ENCODING gzip, deflate, br +HTTP_ACCEPT_LANGUAGE en-US,en;q=0.9 +HTTP_CLOUDFRONT_FORWARDED_PROTO https +HTTP_CLOUDFRONT_IS_DESKTOP_VIEWER true +HTTP_CLOUDFRONT_IS_MOBILE_VIEWER false +HTTP_CLOUDFRONT_IS_SMARTTV_VIEWER false +HTTP_CLOUDFRONT_IS_TABLET_VIEWER false +HTTP_CLOUDFRONT_VIEWER_ASN 11404 +HTTP_CLOUDFRONT_VIEWER_COUNTRY US +HTTP_COOKIE _demo_session=39C56LSJ9VTY5ytrKwvBDjMjODfKIj%2FSpnIGi%2FWupto1yJ7%2FSv1Uk2wc0q5Wf5rAQs9%2BmwLG9ogftXIBQHp24loyJc5J1rNByRyfJnpmx6aIrhtVfN3HbdYozEmkyePddmet3dB0ROpB8S0%2BVvRDxRjpc%2BhR0HxHyKxfD0F7rm1eaNLY%2B5bCx9e4iZAipkLFn%2BGCZq9e5CzNoijXUVsZTb9BFNJzI%2F14BNhFm82q2AVBteUzOmguvF12UyHYuCM2HgdgjrPYgmGlIWT3ZPX76Ux0rMbw--as2yHPMs4NFioQtK--Ff1RA2Ivn5Sp%2BZ%2B7kzQG6A%3D%3D +HTTP_HOST xra2cmx6kc.execute-api.us-west-2.amazonaws.com +HTTP_SEC_CH_UA "Not_A Brand";v="8", "Chromium";v="120", "Brave";v="120" +HTTP_SEC_CH_UA_MOBILE ?0 +HTTP_SEC_CH_UA_PLATFORM "macOS" +HTTP_SEC_FETCH_DEST document +HTTP_SEC_FETCH_MODE navigate +HTTP_SEC_FETCH_SITE none +HTTP_SEC_FETCH_USER ?1 +HTTP_SEC_GPC 1 +HTTP_UPGRADE_INSECURE_REQUESTS 1 +HTTP_USER_AGENT Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 +HTTP_VIA 2.0 5e4ed8b3ad7ea224ed1bfcac62553bf6.cloudfront.net (CloudFront) +HTTP_X_AMZN_TRACE_ID Root=1-657ba479-6138d70d4b24a2e30714a177 +HTTP_X_AMZ_CF_ID nrQi83L0WRzBsqkM3aMciecOFbfh_jWJDdQpsefG5pT4A4MnTxYcrQ== +HTTP_X_FORWARDED_FOR 66.234.208.210, 15.158.0.87 +HTTP_X_FORWARDED_PORT 443 +HTTP_X_FORWARDED_PROTO https +PATH_INFO /posts +QUERY_STRING +REMOTE_ADDR 66.234.208.210, 15.158.0.87 +REMOTE_HOST xra2cmx6kc.execute-api.us-west-2.amazonaws.com +REQUEST_METHOD GET +REQUEST_PATH /posts +REQUEST_URI https://xra2cmx6kc.execute-api.us-west-2.amazonaws.com/posts +SCRIPT_NAME +SERVER_NAME xra2cmx6kc.execute-api.us-west-2.amazonaws.com +SERVER_PORT 443 +SERVER_PROTOCOL HTTP/1.1 +SERVER_SOFTWARE Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 \ No newline at end of file diff --git a/spec/fixtures/shim/rack/rails-apigw.txt b/spec/fixtures/shim/rack/rails-apigw.txt new file mode 100644 index 000000000..41e159785 --- /dev/null +++ b/spec/fixtures/shim/rack/rails-apigw.txt @@ -0,0 +1,50 @@ +HTTPS on +HTTP_ACCEPT text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8 +HTTP_ACCEPT_ENCODING gzip, deflate, br +HTTP_ACCEPT_LANGUAGE en-US,en;q=0.9 +HTTP_CLOUDFRONT_FORWARDED_PROTO https +HTTP_CLOUDFRONT_IS_DESKTOP_VIEWER true +HTTP_CLOUDFRONT_IS_MOBILE_VIEWER false +HTTP_CLOUDFRONT_IS_SMARTTV_VIEWER false +HTTP_CLOUDFRONT_IS_TABLET_VIEWER false +HTTP_CLOUDFRONT_VIEWER_ASN 11404 +HTTP_CLOUDFRONT_VIEWER_COUNTRY US +HTTP_COOKIE _demo_session=qKCCflFJN2itGPma2LbkNnjseYUnT8vXhKnE%2B6luZORpwWZrElgpNQkDZefjoOFp7haoiaHpKU58QF6EguUS%2BMNT11mf%2BJmuBkelAAMpyM%2FPf%2BZ%2BXrrbdhhd%2BWXpjmYzaYMThuNoyEObsMt8tjJoCpUpW5C0%2FgEQ8BnxzeKrBiz3a%2FgNriBR5T2%2Fi6N%2BFZgYxOhWCGT%2Blo1Hhz%2FbVR0ornzwmROkXhR97GLSP0GIBBt4tcuRcofGroExuDYiPxkCozG1Xz4pZl39tBykan5i%2FJa0EKev--C%2F5%2B%2BeuaXCEcfAtU--zgK2m8At7jF8cclNVfeUGQ%3D%3D +HTTP_HOST xra2cmx6kc.execute-api.us-west-2.amazonaws.com +HTTP_IF_NONE_MATCH W/"3a3aa83ef1c91de47cc00ca3f6345401" +HTTP_PORT 443 +HTTP_SEC_CH_UA "Not_A Brand";v="8", "Chromium";v="120", "Brave";v="120" +HTTP_SEC_CH_UA_MOBILE ?0 +HTTP_SEC_CH_UA_PLATFORM "macOS" +HTTP_SEC_FETCH_DEST document +HTTP_SEC_FETCH_MODE navigate +HTTP_SEC_FETCH_SITE none +HTTP_SEC_FETCH_USER ?1 +HTTP_SEC_GPC 1 +HTTP_UPGRADE_INSECURE_REQUESTS 1 +HTTP_USER_AGENT Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 +HTTP_VIA 2.0 38eecd3ca21bf068d69a2f9cfe668d14.cloudfront.net (CloudFront) +HTTP_X_AMZN_TRACE_ID Root=1-657bd820-29c6575c257446bc69880090 +HTTP_X_AMZ_CF_ID wjp63PuZtU3Ha2ag67DaIntwryd6DBs5jeQKo6BIulX9NwFCP9ny1A== +HTTP_X_FORWARDED_FOR 66.234.208.210, 15.158.0.115 +HTTP_X_FORWARDED_PORT 443 +HTTP_X_FORWARDED_PROTO https +ORIGINAL_FULLPATH /posts +ORIGINAL_SCRIPT_NAME +PATH_INFO /posts +QUERY_STRING +REQUEST_METHOD GET +REQUEST_PATH /posts +ROUTES_12180_SCRIPT_NAME +SCRIPT_NAME +SERVER_NAME xra2cmx6kc.execute-api.us-west-2.amazonaws.com +SERVER_PORT 443 +SERVER_PROTOCOL HTTP/1.1 +action_controller.instance # +rack.request.form_hash {} +rack.request.form_input +rack.request.query_hash {} +rack.request.query_string +rack.session # +rack.session.options # +rack.tempfiles [] \ No newline at end of file diff --git a/spec/fixtures/shim/rack/rails-local.txt b/spec/fixtures/shim/rack/rails-local.txt new file mode 100644 index 000000000..9915228f6 --- /dev/null +++ b/spec/fixtures/shim/rack/rails-local.txt @@ -0,0 +1,55 @@ +GATEWAY_INTERFACE CGI/1.2 +HTTP_ACCEPT text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8 +HTTP_ACCEPT_ENCODING gzip, deflate, br +HTTP_ACCEPT_LANGUAGE en-US,en +HTTP_CACHE_CONTROL max-age=0 +HTTP_CONNECTION keep-alive +HTTP_COOKIE _demo_session=neOyAXfU3CuRufr%2BJlbDctDTVaWKqXuss7C4vHCes7weRojCn7lfIg0dtQRN2C2HgPIKgr6yCbXLbGaApsz3lRvRLvVANWJj9kAn2s%2BVuq4vLNiLAHR7NcnPjmm0xRnA2fL%2FenYkloy%2FbybuXxDpZ19BYSWmo3fDGcKMo92vIl0qop2xHp%2F4Btlr7vF4tfoJlBhL%2FICS%2BfjDp%2Ft%2BKAua2Qa1o7IpBEO78rO3Y0mSv6HVsgPZ02LDBU1rRoinFOgo4HIEDaXtpRKM5FgBFuJhZ1doP334--FABmCY1bcX14UCWN--00x2l2sFbPId96j0kNhxug%3D%3D +HTTP_HOST localhost:3000 +HTTP_IF_NONE_MATCH W/"46fc5c0d5a9fc802fee89d5b9d16c3b8" +HTTP_SEC_CH_UA "Not_A Brand";v="8", "Chromium";v="120", "Brave";v="120" +HTTP_SEC_CH_UA_MOBILE ?0 +HTTP_SEC_CH_UA_PLATFORM "macOS" +HTTP_SEC_FETCH_DEST document +HTTP_SEC_FETCH_MODE navigate +HTTP_SEC_FETCH_SITE same-origin +HTTP_SEC_FETCH_USER ?1 +HTTP_SEC_GPC 1 +HTTP_UPGRADE_INSECURE_REQUESTS 1 +HTTP_USER_AGENT Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 +HTTP_VERSION HTTP/1.1 +ORIGINAL_FULLPATH /posts?foo=bar +ORIGINAL_SCRIPT_NAME +PATH_INFO /posts +QUERY_STRING foo=bar +REMOTE_ADDR 127.0.0.1 +REQUEST_METHOD GET +REQUEST_PATH /posts +REQUEST_URI /posts?foo=bar +ROUTES_11800_SCRIPT_NAME +SCRIPT_NAME +SERVER_NAME localhost +SERVER_PORT 3000 +SERVER_PROTOCOL HTTP/1.1 +SERVER_SOFTWARE puma 6.4.0 The Eagle of Durango +action_controller.instance # +puma.config # +puma.request_body_wait 0.004851005971431732 +puma.socket # +rack.after_reply [] +rack.errors # +rack.hijack # +rack.hijack? true +rack.input # +rack.multiprocess false +rack.multithread true +rack.request.form_hash {} +rack.request.form_input # +rack.request.query_hash {"foo"=>"bar"} +rack.request.query_string foo=bar +rack.run_once false +rack.session # +rack.session.options # +rack.tempfiles [] +rack.url_scheme http +rack.version [1, 6] \ No newline at end of file diff --git a/spec/integration/fixtures/postman/collection.json b/spec/integration/fixtures/postman/collection.json deleted file mode 100644 index ce4993821..000000000 --- a/spec/integration/fixtures/postman/collection.json +++ /dev/null @@ -1,485 +0,0 @@ -{ - "info": { - "_postman_id": "c8ca4bd7-c5f4-4e75-95a3-e50290a06661", - "name": "Jets Integration Tests", - "description": "Tests basic CRUD", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" - }, - "item": [ - { - "name": "post index", - "event": [ - { - "listen": "test", - "script": { - "id": "f91acb11-eee6-4c83-956e-ef21f4adf246", - "exec": [ - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test(\"Content-Type is present\", function () {", - " pm.response.to.have.header(\"Content-Type\");", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "raw", - "raw": "" - }, - "url": { - "raw": "{{BASE_URL}}posts", - "host": [ - "{{BASE_URL}}posts" - ] - } - }, - "response": [] - }, - { - "name": "post create", - "event": [ - { - "listen": "test", - "script": { - "id": "4f349f21-09e6-436d-bd70-89324ad334e8", - "exec": [ - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test(\"Content-Type is present\", function () {", - " pm.response.to.have.header(\"Content-Type\");", - "});", - "", - "pm.test(\"Body matches string\", function () {", - " pm.expect(pm.response.text()).to.include(\"test1\");", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "value": "application/x-www-form-urlencoded", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "utf8=%E2%9C%93&post%5Btitle%5D=test1&post%5Bbody%5D=test1&commit=Submit" - }, - "url": { - "raw": "{{BASE_URL}}posts", - "host": [ - "{{BASE_URL}}posts" - ] - } - }, - "response": [] - }, - { - "name": "post new", - "event": [ - { - "listen": "test", - "script": { - "id": "7ece5182-aacd-45b9-ba1e-52552f5843cb", - "type": "text/javascript", - "exec": [ - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test(\"Content-Type is present\", function () {", - " pm.response.to.have.header(\"Content-Type\");", - "});", - "" - ] - } - } - ], - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "raw", - "raw": "" - }, - "url": { - "raw": "{{BASE_URL}}posts/new", - "host": [ - "{{BASE_URL}}posts" - ], - "path": [ - "new" - ] - } - }, - "response": [] - }, - { - "name": "post edit", - "event": [ - { - "listen": "test", - "script": { - "id": "f3ac4857-58f8-40b0-8161-d2e2d6d921f3", - "type": "text/javascript", - "exec": [ - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test(\"Content-Type is present\", function () {", - " pm.response.to.have.header(\"Content-Type\");", - "});", - "" - ] - } - } - ], - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "raw", - "raw": "" - }, - "url": { - "raw": "{{BASE_URL}}posts/1/edit", - "host": [ - "{{BASE_URL}}posts" - ], - "path": [ - "1", - "edit" - ] - } - }, - "response": [] - }, - { - "name": "post update", - "event": [ - { - "listen": "test", - "script": { - "id": "f597eb8f-34ea-461b-8793-3c257480f315", - "exec": [ - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test(\"Content-Type is present\", function () {", - " pm.response.to.have.header(\"Content-Type\");", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "PUT", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded; charset=UTF-8", - "disabled": false - } - ], - "body": { - "mode": "raw", - "raw": "utf8=%E2%9C%93&_method=put&post%5Btitle%5D=test1editanother&post%5Bbody%5D=test1" - }, - "url": { - "raw": "{{BASE_URL}}posts/1", - "host": [ - "{{BASE_URL}}posts" - ], - "path": [ - "1" - ] - } - }, - "response": [] - }, - { - "name": "post create2", - "event": [ - { - "listen": "test", - "script": { - "id": "100b5476-2194-4d25-8170-3b3416b2c9e2", - "type": "text/javascript", - "exec": [ - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test(\"Content-Type is present\", function () {", - " pm.response.to.have.header(\"Content-Type\");", - "});", - "", - "pm.test(\"Body matches string\", function () {", - " pm.expect(pm.response.text()).to.include(\"test2\");", - "});" - ] - } - } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded" - } - ], - "body": { - "mode": "raw", - "raw": "utf8=%E2%9C%93&post%5Btitle%5D=test2&post%5Bbody%5D=test2&commit=Submit" - }, - "url": { - "raw": "{{BASE_URL}}posts", - "host": [ - "{{BASE_URL}}posts" - ] - } - }, - "response": [] - }, - { - "name": "post delete", - "event": [ - { - "listen": "test", - "script": { - "id": "530e0c60-e3c3-4812-82d9-f3072ddb3995", - "type": "text/javascript", - "exec": [ - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test(\"Content-Type is present\", function () {", - " pm.response.to.have.header(\"Content-Type\");", - "});", - "" - ] - } - } - ], - "request": { - "method": "DELETE", - "header": [], - "body": {}, - "url": { - "raw": "{{BASE_URL}}posts/2", - "host": [ - "{{BASE_URL}}posts" - ], - "path": [ - "2" - ] - } - }, - "response": [] - }, - { - "name": "book index", - "event": [ - { - "listen": "test", - "script": { - "id": "f91acb11-eee6-4c83-956e-ef21f4adf246", - "exec": [ - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test(\"Content-Type is present\", function () {", - " pm.response.to.have.header(\"Content-Type\");", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "raw", - "raw": "" - }, - "url": { - "raw": "{{BASE_URL}}books", - "host": [ - "{{BASE_URL}}books" - ] - } - }, - "response": [] - }, - { - "name": "book new", - "event": [ - { - "listen": "test", - "script": { - "id": "8c80cb0d-9c9c-4e26-ae53-4eb43531cb58", - "exec": [ - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test(\"Content-Type is present\", function () {", - " pm.response.to.have.header(\"Content-Type\");", - "});", - "", - "var html = cheerio(pm.response.text());", - "var token = html.find('[name=\"authenticity_token\"]').val();", - "", - "console.log(token);", - "pm.globals.set(\"authenticity_token\", token);" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "raw", - "raw": "" - }, - "url": { - "raw": "{{BASE_URL}}books/new", - "host": [ - "{{BASE_URL}}books" - ], - "path": [ - "new" - ] - } - }, - "response": [] - }, - { - "name": "book edit", - "event": [ - { - "listen": "test", - "script": { - "id": "f3ac4857-58f8-40b0-8161-d2e2d6d921f3", - "exec": [ - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test(\"Content-Type is present\", function () {", - " pm.response.to.have.header(\"Content-Type\");", - "});", - "", - "var html = cheerio(pm.response.text());", - "var token = html.find('[name=\"authenticity_token\"]').val();", - "", - "pm.globals.set(\"authenticity_token\", token);" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "raw", - "raw": "" - }, - "url": { - "raw": "{{BASE_URL}}books/1/edit", - "host": [ - "{{BASE_URL}}books" - ], - "path": [ - "1", - "edit" - ] - } - }, - "response": [] - }, - { - "name": "info", - "event": [ - { - "listen": "test", - "script": { - "id": "f91acb11-eee6-4c83-956e-ef21f4adf246", - "exec": [ - "pm.test(\"response is ok\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test(\"Content-Type is present\", function () {", - " pm.response.to.have.header(\"Content-Type\");", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [], - "body": { - "mode": "raw", - "raw": "" - }, - "url": { - "raw": "{{BASE_URL}}info", - "host": [ - "{{BASE_URL}}info" - ] - } - }, - "response": [] - } - ], - "event": [ - { - "listen": "prerequest", - "script": { - "id": "a85e6949-f311-4c79-923c-46b55b7fb3e6", - "type": "text/javascript", - "exec": [ - "" - ] - } - }, - { - "listen": "test", - "script": { - "id": "7b5709b5-7a77-43b6-acfb-abee4a9362ff", - "type": "text/javascript", - "exec": [ - "" - ] - } - } - ] -} \ No newline at end of file diff --git a/spec/integration/fixtures/postman/environment.json b/spec/integration/fixtures/postman/environment.json deleted file mode 100644 index 100dd8c20..000000000 --- a/spec/integration/fixtures/postman/environment.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "id": "f50e05e5-c6dd-4707-9270-82c706b2bcef", - "name": "Jets Test1", - "values": [ - { - "key": "BASE_URL", - "value": "http://localhost:8888/", - "description": "", - "type": "text", - "enabled": true - } - ], - "_postman_variable_scope": "environment", - "_postman_exported_at": "2018-08-06T01:39:56.523Z", - "_postman_exported_using": "Postman/6.2.3" -} diff --git a/spec/integration/local.sh b/spec/integration/local.sh deleted file mode 100755 index cbdac2e2d..000000000 --- a/spec/integration/local.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/bash - -# NOTE: Script assumes that demo and jets folder are in your CDPATH - -unset DATABASE_URL - -# check if `jets server` is running -# https://serverfault.com/questions/562524/bash-script-to-check-if-a-public-https-site-is-up -if ! curl -s --head --request GET http://localhost:8888 | grep "200 OK" > /dev/null; then - echo "The jets server does not seem to be up. Please run: jets server" - echo "And then try running this script again." - exit 1 -fi - -# Assume demo project has been created -cd demo -# Create a data record that the postman tests assumes to exist. The postman collection deletes this record. -jets runner 'Post.create(id: 1) unless Post.find_by(id: 1)' -jets runner 'Post.create(id: 2) unless Post.find_by(id: 2)' - -# Assume rack project has been imported via mega mode -cd rack -# Create a data record that the postman tests assumes to exist. The postman collection deletes this record. -rails runner 'Book.create(id: 1) unless Book.find_by(id: 1)' -rails runner 'Book.create(id: 2) unless Book.find_by(id: 2)' - -# Integration postman script lives in jets -cd jets -newman run spec/integration/fixtures/postman/collection.json -e spec/integration/fixtures/postman/environment.json - -# TODO: export global variables and run multiple scripts to handle create, edit -# https://github.com/postmanlabs/newman/issues/831 -# Useful options: -# --export-globals globals.json -# -g globals.json -# Unsure how to grab authentication token for delete diff --git a/spec/integration/remote.sh b/spec/integration/remote.sh deleted file mode 100755 index 7cd5e6bd3..000000000 --- a/spec/integration/remote.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/bin/bash -eu - -# NOTE: Script assumes that demo and jets folder are in your CDPATH - -set +u -if [ -z "$BASE_URL" ]; then - echo "Please set the BASE_URL env var. Example:" - echo " export BASE_URL=https://t0a00hokcj.execute-api.us-west-2.amazonaws.com/dev" - exit 1 -fi -set -u - -# check if `jets server` is running -# https://serverfault.com/questions/562524/bash-script-to-check-if-a-public-https-site-is-up -if ! curl -s --head --request GET "$BASE_URL" | grep "HTTP/2 200" > /dev/null; then - echo "The application does not seem to be up. Please check that you've deployed to it." - echo "And then try running this script again." - exit 1 -fi - -# Assume demo project has been created -cd demo -# Create a data record that the postman tests assumes to exist. The postman collection deletes this record. -JETS_ENV_REMOTE=1 jets runner 'Post.create(id: 1) unless Post.find_by(id: 1)' -JETS_ENV_REMOTE=1 jets runner 'Post.create(id: 2) unless Post.find_by(id: 2)' - -cd rack -# Create a data record that the postman tests assumes to exist. The postman collection deletes this record. -rails runner 'Book.create(id: 1) unless Book.find_by(id: 1)' -rails runner 'Book.create(id: 2) unless Book.find_by(id: 2)' - -# Integration postman script lives in jets -cd jets - -cat > /tmp/postman-environment.json <1, "Variables"=>{"dont_touch"=>2}) - end - - # CloudWatch Event patterns have slightly different casing - it "dasherize anything under EventPattern at the top level" do - h = {foo_bar: 1, event_pattern: {dash_me: 2}} - result = Jets::Camelizer.transform(h) - # pp result - expect(result).to eq("FooBar"=>1, "EventPattern"=>{"dash-me"=>2}) - end - - it "camelize anything under EventPattern at any level beyond the top level" do - h = {foo_bar: 1, event_pattern: {dash_me: {camelize_me: 3}}} - result = Jets::Camelizer.transform(h) - # pp result - expect(result).to eq("FooBar"=>1, "EventPattern"=>{"dash-me"=>{"camelizeMe"=>3}}) - end - - it "dont touch anything under ResponseParameters" do - h = {foo_bar: 1, response_parameters: {dont_touch: 3}} - result = Jets::Camelizer.transform(h) - # pp result - expect(result).to eq("FooBar"=>1, "ResponseParameters"=>{"dont_touch"=>3}) - end - - it "dont touch anything with - or /" do - h = {foo_bar: 1, "has-dash" => 2, "has/slash" => 3, "application/json" => 4} - result = Jets::Camelizer.transform(h) - # pp result - expect(result).to eq("FooBar"=>1, "has-dash"=>2,"has/slash"=>3,"application/json"=>4) - end - - it "special map keys cloudformation stack" do - h = {type: "AWS::CloudFormation::Stack", properties: {template_url: 1}} - result = Jets::Camelizer.transform(h) - # pp result - expect(result).to eq({"Type" => "AWS::CloudFormation::Stack", "Properties" => {"TemplateURL"=>1}}) - end - - it "special map keys cloudformation stack" do - h = {type: "AWS::Route53::RecordSet", properties: {ttl: 60}} - result = Jets::Camelizer.transform(h) - # pp result - expect(result).to eq({"Type" => "AWS::Route53::RecordSet", "Properties" => {"TTL"=>60}}) - end -end diff --git a/spec/lib/jets/cfn/builder/api/deployment_spec.rb b/spec/lib/jets/cfn/builder/api/deployment_spec.rb deleted file mode 100644 index 13e2dd764..000000000 --- a/spec/lib/jets/cfn/builder/api/deployment_spec.rb +++ /dev/null @@ -1,18 +0,0 @@ -describe Jets::Cfn::Builder::Api::Deployment do - let(:builder) do - Jets::Cfn::Builder::Api::Deployment.new - end - - describe "Api::Deployment" do - it "builds a child stack the deployment" do - builder.compose - # puts builder.text # uncomment to see template text - - resources = builder.template["Resources"] - resource_types = resources.values.map { |i| i["Type"] } - expect(resource_types).to include("AWS::ApiGateway::Deployment") - - expect(builder.template_path).to eq "#{Jets.build_root}/templates/api-deployment.yml" - end - end -end diff --git a/spec/lib/jets/cfn/builder/api/gateway_spec.rb b/spec/lib/jets/cfn/builder/api/gateway_spec.rb deleted file mode 100644 index 8af6baf26..000000000 --- a/spec/lib/jets/cfn/builder/api/gateway_spec.rb +++ /dev/null @@ -1,76 +0,0 @@ -describe Jets::Cfn::Builder::Api::Gateway do - - let(:builder) do - Jets::Cfn::Builder::Api::Gateway.new({}) - end - - describe "Api::Gateway" do - it "builds a child stack with api gateway rest api" do - builder.compose - # puts builder.text # uncomment to see template text - - resources = builder.template["Resources"] - - expect(resources).to include("RestApi") - - expect(builder.template_path).to eq "#{Jets.build_root}/templates/api-gateway.yml" - end - - it "must create the DomainName and DnsRecord" do - allow(Jets.config.domain).to receive(:name).and_return("demo-test.example.com") - allow(Jets.config.domain).to receive(:hosted_zone_name).and_return("example.com") - - builder.compose - - resources = builder.template["Resources"] - - expect(resources).to include("DomainName") - expect(resources).to include("DnsRecord") - expect(resources).to include("RestApi") - - expect(builder.template_path).to eq "#{Jets.build_root}/templates/api-gateway.yml" - end - - it "must exclude DomainName and DnsRecord" do - allow(Jets.config.domain).to receive(:name).and_return("124demo-test.example.com") - allow(Jets.config.domain).to receive(:hosted_zone_name).and_return("example.com") - - allow(builder.apigateway).to receive(:get_domain_name).and_return({}) - allow(builder.cfn).to receive(:describe_stack_resource).with(hash_including(:logical_resource_id => "ApiGateway")).and_return(nil) - allow(builder.cfn).to receive(:describe_stack_resource).with(hash_including(:logical_resource_id => "DomainName")).and_raise(Aws::CloudFormation::Errors::ValidationError.new(nil, nil)) - allow(builder.cfn).to receive(:describe_stack_resource).with(hash_including(:logical_resource_id => "DnsRecord")).and_raise(Aws::CloudFormation::Errors::ValidationError.new(nil, nil)) - - builder.compose - - resources = builder.template["Resources"] - - expect(resources).to_not include("DomainName") - expect(resources).to_not include("DnsRecord") - expect(resources).to include("RestApi") - - expect(builder.template_path).to eq "#{Jets.build_root}/templates/api-gateway.yml" - end - - it "should not exclude DomainName and DnsRecord" do - allow(Jets.config.domain).to receive(:name).and_return("124demo-test.example.com") - allow(Jets.config.domain).to receive(:hosted_zone_name).and_return("example.com") - - allow(builder.apigateway).to receive(:get_domain_name).and_return({}) - allow(builder.cfn).to receive(:describe_stack_resource).with(hash_including(:logical_resource_id => "ApiGateway")).and_return(nil) - allow(builder.cfn).to receive(:describe_stack_resource).with(hash_including(:logical_resource_id => "DomainName")).and_return({}) - allow(builder.cfn).to receive(:describe_stack_resource).with(hash_including(:logical_resource_id => "DnsRecord")).and_return({}) - - - builder.compose - - resources = builder.template["Resources"] - - expect(resources).to include("DomainName") - expect(resources).to include("DnsRecord") - expect(resources).to include("RestApi") - - expect(builder.template_path).to eq "#{Jets.build_root}/templates/api-gateway.yml" - end - end - -end diff --git a/spec/lib/jets/cfn/builder/api/pages/resources_spec.rb b/spec/lib/jets/cfn/builder/api/pages/resources_spec.rb deleted file mode 100644 index 4e93bb1c2..000000000 --- a/spec/lib/jets/cfn/builder/api/pages/resources_spec.rb +++ /dev/null @@ -1,125 +0,0 @@ -# Spec has a few broken examples. -# Commenting out and keeping around for now. -# Will likely remove paginated resources in the future. - -# describe Jets::Cfn::Builder::Api::Pages::Resources do -# let(:builder) do -# ENV['JETS_APIGW_PAGE_LIMIT'] = '3' -# Jets::Cfn::Builder::Api::Pages::Resources -# end - -# def pages(builder) -# pages = builder.pages -# pages.map do |page| -# page.items -# end -# end - -# before(:each) do -# Jets::Cfn::Builder::Api::Pages::Resources.send(:class_variable_set, :@@pages, {}) -# end - -# # Example previously_deployed structure: -# # [ -# # { -# # "items": [ -# # "*catchall", -# # "posts", -# # ], -# # "number": 1, -# # } -# # ] -# def previously_deployed(slices) -# result = [] -# slices.each_with_index do |slice, index| -# data = { -# "items" => slice, -# "page_number" => index, -# } -# result << data -# end -# result -# end - -# describe "PageBuilder" do -# it "same pages after as before" do -# previously_deployed = previously_deployed([ -# %w[a1 a2], -# %w[b1 b2], -# %w[c1 c2], -# ]) -# uids = %w[a1 a2 b1 b2 c1 c2] -# allow(builder).to receive(:previously_deployed).and_return(previously_deployed) -# allow(builder).to receive(:uids).and_return(uids) - -# pages = pages(builder) -# expect(pages).to eq( -# [["a1", "a2"], ["b1", "b2"], ["c1", "c2"]] -# ) -# end - -# it "fill up pages" do -# previously_deployed = previously_deployed([ -# %w[a1 a2], -# %w[b1 b2], -# %w[c1 c2], -# ]) -# uids = %w[a1 a2 a3 b1 b2 c1 c2 d1] -# allow(builder).to receive(:previously_deployed).and_return(previously_deployed) -# allow(builder).to receive(:uids).and_return(uids) - -# pages = pages(builder) -# expect(pages).to eq( -# [["a1", "a2", "a3"], ["b1", "b2", "d1"], ["c1", "c2"]] -# ) -# end - -# it "fill up pages with no nils" do -# previously_deployed = previously_deployed([ -# %w[a1], -# %w[b1], -# %w[c1], -# ]) -# uids = %w[a1 b1 c1 c2 d1 d2 d3 d4] -# allow(builder).to receive(:previously_deployed).and_return(previously_deployed) -# allow(builder).to receive(:uids).and_return(uids) - -# pages = pages(builder) -# expect(pages).to eq( -# [["a1", "c2", "d1"], ["b1", "d2", "d3"], ["c1", "d4"]] -# ) -# end - -# it "build remaining slices" do -# previously_deployed = previously_deployed([ -# %w[a1 a2], -# %w[b1 b2], -# %w[c1 c2], -# ]) -# uids = %w[a1 a2 a3 b1 b2 c1 c2 d1 e1 e2 e3 e4 e5] -# allow(builder).to receive(:previously_deployed).and_return(previously_deployed) -# allow(builder).to receive(:uids).and_return(uids) - -# pages = pages(builder) -# expect(pages).to eq( -# [["a1", "a2", "a3"], -# ["b1", "b2", "d1"], -# ["c1", "c2", "e1"], -# ["e2", "e3", "e4"], -# ["e5"]] -# ) -# end - -# it "no old pages state" do -# previously_deployed = nil -# uids = %w[a1 a2 b1 b2 c1 c2] -# allow(builder).to receive(:previously_deployed).and_return(previously_deployed) -# allow(builder).to receive(:uids).and_return(uids) - -# pages = pages(builder) -# expect(pages).to eq( -# [["a1", "a2", "b1"], ["b2", "c1", "c2"]] -# ) -# end -# end -# end diff --git a/spec/lib/jets/cfn/builder/api/resources_spec.rb b/spec/lib/jets/cfn/builder/api/resources_spec.rb deleted file mode 100644 index 89d3f3de0..000000000 --- a/spec/lib/jets/cfn/builder/api/resources_spec.rb +++ /dev/null @@ -1,30 +0,0 @@ -describe Jets::Cfn::Builder::Api::Resources do - let(:builder) do - Jets::Cfn::Builder::Api::Resources.new(page: page) - end - let(:page) do - Jets::Cfn::Builder::Api::Pages::Page.new( - items: Jets::Router.all_paths, - number: 1, - ) - end - - describe "Api::Resources" do - it "builds a child stack with shared api gateway resources" do - builder.compose - # puts builder.text # uncomment to see template text - - template = builder.template - resources = template["Resources"] - # Probably at least one route or AWS::ApiGateway::Resource is created - resource_types = resources.values.map { |i| i["Type"] } - expect(resource_types).to include("AWS::ApiGateway::Resource") - - # Sanity check. Pretty much all AWS::ApiGateway::Resource resources will have an output - outputs = template["Outputs"] - expect(outputs).not_to be_empty - - expect(builder.template_path).to eq "#{Jets.build_root}/templates/api-resources-1.yml" - end - end -end diff --git a/spec/lib/jets/cfn/builder/authorizer_spec.rb b/spec/lib/jets/cfn/builder/authorizer_spec.rb deleted file mode 100644 index 53cd565b7..000000000 --- a/spec/lib/jets/cfn/builder/authorizer_spec.rb +++ /dev/null @@ -1,26 +0,0 @@ -describe Jets::Cfn::Builder::Authorizer do - let(:builder) do - Jets::Cfn::Builder::Authorizer.new(path) - end - - context "MainAuthorizer" do - let(:path) { "app/authorizers/main_authorizer.rb" } - describe "compose" do - it "builds a child stack with controller resources" do - builder.compose - # puts builder.text # uncomment to see template text - - resources = builder.template["Resources"] - resource_types = resources.values.map { |i| i["Type"] } - expect(resource_types).to include("AWS::Lambda::Function") - expect(resource_types).to include("AWS::Lambda::Permission") - expect(resource_types).to include("AWS::ApiGateway::Authorizer") - - outputs = builder.template["Outputs"] # ProtectAuthorizer, LockAuthorizer, CognitoAuthorizer - expect(outputs).not_to be_empty - - expect(builder.template_path).to eq "#{Jets.build_root}/templates/authorizers-main_authorizer.yml" - end - end - end -end diff --git a/spec/lib/jets/cfn/builder/controller_spec.rb b/spec/lib/jets/cfn/builder/controller_spec.rb deleted file mode 100644 index adb140a78..000000000 --- a/spec/lib/jets/cfn/builder/controller_spec.rb +++ /dev/null @@ -1,96 +0,0 @@ -describe Jets::Cfn::Builder::Controller do - let(:builder) do - Jets::Cfn::Builder::Controller.new(app_class) - end - - def template_resources(template) - template.deep_symbolize_keys[:Resources] - end - - context "PostsController" do - let(:app_class) { PostsController } - describe "compose" do - it "builds a child stack with controller resources" do - builder.compose - # puts builder.text # uncomment to see template text - resources = template_resources(builder.template) - resource_types = resources.values.map { |i| i[:Type] } - expect(resource_types).to include("AWS::Lambda::Function") - - expect(builder.template_path).to eq "#{Jets.build_root}/templates/app-posts_controller.yml" - end - end - end - - context "ChildPostsController" do - let(:app_class) { ChildPostsController } - describe "compose" do - it "builds a child stack with controller resources" do - builder.compose - # puts builder.text # uncomment to see template text - - resources = template_resources(builder.template) - resource_types = resources.values.map { |i| i[:Type] } - expect(resource_types).to include("AWS::Lambda::Function") - # didnt hook up a route in the fixture project so no AWS::ApiGateway::Method expected - - expect(builder.template_path).to eq "#{Jets.build_root}/templates/app-child_posts_controller.yml" - end - end - end - - context "StoresController" do - let(:app_class) { StoresController } - describe "show function properties" do - it "overrides global properties with function properties" do - builder.compose - # puts builder.text # uncomment to see template text - resources = template_resources(builder.template) - properties = resources[:StoresControllerShowLambdaFunction][:Properties] - - expect(properties[:MemorySize]).to eq 1024 - expect(properties[:EphemeralStorage][:Size]).to eq 512 - # should not transform the keys under Variables section - keys = properties[:Environment][:Variables] - # Just testing for some keys since we keep changing .env files - test_keys = %w[ - ENV_KEY - env_key1 - env_key2 - global_app_key1 - global_app_key2 - key1 - key2 - my_test - ] - test_keys.each do |test_key| - expect(keys).to include(test_key.to_sym) - end - end - end - - describe "index function properties" do - it "overrides global properties with function properties" do - builder.compose - # puts builder.text # uncomment to see template text - resources = template_resources(builder.template) - properties = resources[:StoresControllerIndexLambdaFunction][:Properties] - - expect(properties[:MemorySize]).to eq 1000 - expect(properties[:Timeout]).to eq 20 - expect(properties[:EphemeralStorage][:Size]).to eq 1024 - end - end - - describe "new function properties" do - it "overrides global properties with function properties" do - builder.compose - # puts builder.text # uncomment to see template text - resources = template_resources(builder.template) - properties = resources[:StoresControllerNewLambdaFunction][:Properties] - - expect(properties[:MemorySize]).to eq 768 - end - end - end -end diff --git a/spec/lib/jets/cfn/builder/function_spec.rb b/spec/lib/jets/cfn/builder/function_spec.rb deleted file mode 100644 index 6b5c2b224..000000000 --- a/spec/lib/jets/cfn/builder/function_spec.rb +++ /dev/null @@ -1,37 +0,0 @@ -describe Jets::Cfn::Builder::Function do - let(:builder) do - Jets::Cfn::Builder::Function.new(klass) - end - - describe "compose" do - context "function without _function" do - let(:klass) do - Jets::Klass.from_path("app/functions/hello.rb") - end - it "builds a child stack with the scheduled events" do - builder.compose - # puts builder.text # uncomment to see template text - - resources = builder.template["Resources"] - expect(resources).to include("HelloWorldLambdaFunction") - - expect(builder.template_path).to eq "#{Jets.build_root}/templates/app-hello.yml" - end - end - - context "function with _function" do - let(:klass) do - Jets::Klass.from_path("app/functions/simple_function.rb") - end - it "builds a child stack with the scheduled events" do - builder.compose - # puts builder.text # uncomment to see template text - - resources = builder.template["Resources"] - expect(resources).to include("SimpleFunctionHandlerLambdaFunction") - - expect(builder.template_path).to eq "#{Jets.build_root}/templates/app-simple_function.yml" - end - end - end -end diff --git a/spec/lib/jets/cfn/builder/interface_spec.rb b/spec/lib/jets/cfn/builder/interface_spec.rb deleted file mode 100644 index 6360dec41..000000000 --- a/spec/lib/jets/cfn/builder/interface_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -class InterfaceTest - include Jets::Cfn::Builder::Interface -end - -describe Jets::Cfn::Builder::Interface do - let(:interface) do - InterfaceTest.new - end - - describe "Interface" do - # Nice to see a summary of methods here - it "defines method that are common to all template builders" do - # does not yet have the compose method, it is expected to be implemented - # by the classes inheriting Interface - expect(interface).to respond_to(:build) - expect(interface).to respond_to(:write) - expect(interface).to respond_to(:template) - expect(interface).to respond_to(:text) - expect(interface).to respond_to(:add_resource) - expect(interface).to respond_to(:add_parameter) - expect(interface).to respond_to(:add_output) - end - end -end diff --git a/spec/lib/jets/cfn/builder/job_spec.rb b/spec/lib/jets/cfn/builder/job_spec.rb deleted file mode 100644 index 63e92058f..000000000 --- a/spec/lib/jets/cfn/builder/job_spec.rb +++ /dev/null @@ -1,33 +0,0 @@ -describe Jets::Cfn::Builder::Job do - require 'active_support/testing/stream' - include ActiveSupport::Testing::Stream - - let(:builder) do - Jets::Cfn::Builder::Job.new(HardJob) - end - - describe "compose" do - it "builds a child stack with the scheduled events" do - builder.compose - # puts builder.text # uncomment to see template text - - resources = builder.template["Resources"] - expect(resources).to include("HardJobDigLambdaFunction") - expect(resources).to include("HardJobLiftEventsRule") - - expect(builder.template_path).to eq "#{Jets.build_root}/templates/app-hard_job.yml" - end - end - - describe "class_iam_policy" do - it 'does not warn' do - output = capture(:stdout) do - class IamPolicyJob < ApplicationJob - class_iam_policy 'lambda:InvokeFunction' - end - end - - expect(output).not_to include("WARNING: class_iam_policy") - end - end -end diff --git a/spec/lib/jets/cfn/builder/nested_spec.rb b/spec/lib/jets/cfn/builder/nested_spec.rb deleted file mode 100644 index ad15efb9c..000000000 --- a/spec/lib/jets/cfn/builder/nested_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -describe Jets::Cfn::Builder::Nested do - let(:builder) do - Jets::Cfn::Builder::Nested.new(app_class) - end - let(:app_class) { PostsController } - - - describe "Nested" do - it "contains common others for classes inheriting Nested" do - # does not yet have the compose method, it is expected to be implemented - # by the classes inheriting Nested - expect(builder).not_to respond_to(:compose) - - expect(builder).to respond_to(:template_path) - expect(builder).to respond_to(:add_common_parameters) - expect(builder).to respond_to(:add_functions) - expect(builder).to respond_to(:add_function) - end - end -end diff --git a/spec/lib/jets/cfn/builder/parent/stagger_spec.rb b/spec/lib/jets/cfn/builder/parent/stagger_spec.rb deleted file mode 100644 index 3b9e394cb..000000000 --- a/spec/lib/jets/cfn/builder/parent/stagger_spec.rb +++ /dev/null @@ -1,154 +0,0 @@ -module Staggerable - class Stack - attr_reader :stagger_depends_on, :name - def initialize(name, options={}) - @name = name - @stagger_depends_on = [] - end - - def add_stagger_depends_on(*batch) - @stagger_depends_on = batch.flatten.map(&:name) - end - end - - class Parent - include Jets::Cfn::Builder::Parent::Stagger - # override module method `stagger_enabled` in spec to always enable - def stagger_enabled - true - end - - def initialize - @list = [] - end - - def build(*stacks) - stacks.flatten.each do |name| - add_stack(name) - end - @list - end - - def add_stack(name) - current_stack = Stack.new(name) - add_stagger(current_stack) - @list << current_stack - end - end -end - -describe Jets::Cfn::Builder::Parent::Stagger do - let(:builder) do - parent = Staggerable::Parent.new - allow(parent).to receive(:stagger_batch_size).and_return(batch_size) - parent - end - - context "even number of resources in" do - let(:resources) { (1..6).map { |i| "stack#{i}" } } - context "batches of 0" do - let(:batch_size) { 0 } - it "ok" do - list = builder.build(resources) - list.each do |stack| - expect(stack.stagger_depends_on).to eq([]) - end - end - end - - context "batches of 2" do - let(:batch_size) { 2 } - it "ok" do - list = builder.build(resources) - expect(list[0].stagger_depends_on).to eq([]) - expect(list[1].stagger_depends_on).to eq([]) - expect(list[2].stagger_depends_on).to eq(%w[stack1 stack2]) - expect(list[3].stagger_depends_on).to eq(%w[stack1 stack2]) - expect(list[4].stagger_depends_on).to eq(%w[stack3 stack4]) - expect(list[5].stagger_depends_on).to eq(%w[stack3 stack4]) - end - end - - context "batches of 3" do - let(:batch_size) { 3 } - it "ok" do - list = builder.build(resources) - expect(list[0].stagger_depends_on).to eq([]) - expect(list[1].stagger_depends_on).to eq([]) - expect(list[2].stagger_depends_on).to eq([]) - expect(list[3].stagger_depends_on).to eq(%w[stack1 stack2 stack3]) - expect(list[4].stagger_depends_on).to eq(%w[stack1 stack2 stack3]) - expect(list[5].stagger_depends_on).to eq(%w[stack1 stack2 stack3]) - end - end - - context "batches of 4" do - let(:batch_size) { 4 } - it "ok" do - list = builder.build(resources) - expect(list[0].stagger_depends_on).to eq([]) - expect(list[1].stagger_depends_on).to eq([]) - expect(list[2].stagger_depends_on).to eq([]) - expect(list[3].stagger_depends_on).to eq([]) - expect(list[4].stagger_depends_on).to eq(%w[stack1 stack2 stack3 stack4]) - expect(list[5].stagger_depends_on).to eq(%w[stack1 stack2 stack3 stack4]) - end - end - end - - context "odd number of resources in" do - let(:resources) { (1..7).map { |i| "stack#{i}" } } - context "batches of 2" do - let(:batch_size) { 2 } - it "ok" do - list = builder.build(resources) - expect(list[0].stagger_depends_on).to eq([]) - expect(list[1].stagger_depends_on).to eq([]) - expect(list[2].stagger_depends_on).to eq(%w[stack1 stack2]) - expect(list[3].stagger_depends_on).to eq(%w[stack1 stack2]) - expect(list[4].stagger_depends_on).to eq(%w[stack3 stack4]) - expect(list[5].stagger_depends_on).to eq(%w[stack3 stack4]) - expect(list[6].stagger_depends_on).to eq(%w[stack5 stack6]) - end - end - - context "batches of 3" do - let(:batch_size) { 3 } - it "ok" do - list = builder.build(resources) - expect(list[0].stagger_depends_on).to eq([]) - expect(list[1].stagger_depends_on).to eq([]) - expect(list[2].stagger_depends_on).to eq([]) - expect(list[3].stagger_depends_on).to eq(%w[stack1 stack2 stack3]) - expect(list[4].stagger_depends_on).to eq(%w[stack1 stack2 stack3]) - expect(list[5].stagger_depends_on).to eq(%w[stack1 stack2 stack3]) - expect(list[6].stagger_depends_on).to eq(%w[stack4 stack5 stack6]) - end - end - - context "batches of 4" do - let(:batch_size) { 4 } - it "ok" do - list = builder.build(resources) - expect(list[0].stagger_depends_on).to eq([]) - expect(list[1].stagger_depends_on).to eq([]) - expect(list[2].stagger_depends_on).to eq([]) - expect(list[3].stagger_depends_on).to eq([]) - expect(list[4].stagger_depends_on).to eq(%w[stack1 stack2 stack3 stack4]) - expect(list[5].stagger_depends_on).to eq(%w[stack1 stack2 stack3 stack4]) - expect(list[6].stagger_depends_on).to eq(%w[stack1 stack2 stack3 stack4]) - end - end - end - - context "0 number of resources in" do - let(:resources) { [] } - context "batches of 2" do - let(:batch_size) { 2 } - it "ok" do - list = builder.build(resources) - expect(list).to eq([]) - end - end - end -end diff --git a/spec/lib/jets/cfn/builder/parent_spec.rb b/spec/lib/jets/cfn/builder/parent_spec.rb deleted file mode 100644 index 476fe62ac..000000000 --- a/spec/lib/jets/cfn/builder/parent_spec.rb +++ /dev/null @@ -1,55 +0,0 @@ -describe Jets::Cfn::Builder::Parent do - context "first run" do - let(:builder) do - Jets::Cfn::Builder::Parent.new - end - - describe "compose" do - it "builds parent template with mimnimal resources" do - builder.compose - # puts builder.text # uncomment to see template text - - resources = builder.template["Resources"] - expect(resources).to include("S3Bucket") - expect(resources).not_to include("CommentsController") - - resource_types = resources.values.map { |i| i["Type"] } - expect(resource_types).to include("AWS::S3::Bucket") - expect(resource_types).not_to include("AWS::CloudFormation::Stack") - - expect(builder.template_path).to eq "#{Jets.build_root}/templates/parent.yml" - end - end - end - - context "second run" do - let(:builder) do - Jets::Cfn::Builder::Parent.new - end - - describe "compose" do - it "add_shared_resources" do - end - - it "builds a child stack with the scheduled events" do - # builder.compose - # puts builder.text # uncomment to see template text - - # resources = builder.template["Resources"] - # expect(resources).to include("S3Bucket") - # expect(resources).to include("IamRole") - # expect(resources).to include("CommentsController") - # expect(resources).to include("PostsController") - # expect(resources).to include("EasyJob") - # expect(resources).to include("HardJob") - - # resource_types = resources.values.map { |i| i["Type"] } - # expect(resource_types).to include("AWS::S3::Bucket") - # expect(resource_types).to include("AWS::IAM::Role") - # expect(resource_types).to include("AWS::CloudFormation::Stack") # lots of child stacks - - # expect(builder.template_path).to eq "#{Jets.build_root}/templates/parent.yml" - end - end - end -end diff --git a/spec/lib/jets/cfn/builder/post_process_spec.rb b/spec/lib/jets/cfn/builder/post_process_spec.rb deleted file mode 100644 index de05ea839..000000000 --- a/spec/lib/jets/cfn/builder/post_process_spec.rb +++ /dev/null @@ -1,27 +0,0 @@ -describe Jets::Cfn::Builder::PostProcess do - context "one line" do - let(:builder) do - Jets::Cfn::Builder::PostProcess.new(text) - end - let(:text) do - <<~EOL - Resources: - ApiMethods1: - Properties: - Parameters: - IndexLambdaFunction: "!GetAtt PostsController.Outputs.IndexLambdaFunction" - EOL - end - - it "process" do - text = builder.process - expect(text).to eq <<~EOL - Resources: - ApiMethods1: - Properties: - Parameters: - IndexLambdaFunction: !GetAtt PostsController.Outputs.IndexLambdaFunction - EOL - end - end -end diff --git a/spec/lib/jets/cfn/builder/rule_spec.rb b/spec/lib/jets/cfn/builder/rule_spec.rb deleted file mode 100644 index 14dd786a3..000000000 --- a/spec/lib/jets/cfn/builder/rule_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -describe Jets::Cfn::Builder::Rule do - let(:builder) do - Jets::Cfn::Builder::Rule.new(GameRule) - end - - describe "compose" do - it "builds a child stack with the scheduled events" do - builder.compose - # puts builder.text # uncomment to see template text - - resources = builder.template["Resources"] - expect(resources).to include("GameRuleProtectLambdaFunction") - expect(resources).to include("GameRuleProtectPermission") - expect(resources).to include("GameRuleProtectConfigRule") - - expect(builder.template_path).to eq "#{Jets.build_root}/templates/app-game_rule.yml" - end - end -end diff --git a/spec/lib/jets/cfn/builder_spec.rb b/spec/lib/jets/cfn/builder_spec.rb deleted file mode 100644 index 69f83323f..000000000 --- a/spec/lib/jets/cfn/builder_spec.rb +++ /dev/null @@ -1,105 +0,0 @@ -describe Jets::Cfn::Builder do - before(:each) do - # hack to reset subclasses, Stack classes from other specs pollutes it. - # Dont really want to define a reset_subclasses! method because this is only used for specs. - Jets::Stack.instance_variable_set(:@subclasses, []) - end - - context "templates only and fake full" do - let(:builder) do - Jets::Cfn::Builder.new(stack_type: :full) - end - - it "builds templates" do - allow(Jets).to receive(:s3_bucket).and_return("demo-test") - builder.build - # TODO: cfn build only builds templates now. Need to also copy Jets::PublicFilesController? - file_exist = File.exist?("/tmp/jets/demo/templates/jets-controller.yml") - expect(file_exist).to be true - end - - context 'prewarm enabled' do - before { Jets.config.prewarm.enable = true } - - - it 'builds the preheat job template' do - allow(Jets).to receive(:s3_bucket).and_return("demo-test") - builder.build - - preheat_job_file_path = "/tmp/jets/demo/templates/app-jets-preheat_job.yml" - file_exist = File.exist?(preheat_job_file_path) - expect(file_exist).to be true - - template_hsh = YAML.load(File.read(preheat_job_file_path)) - expect(template_hsh["Resources"].keys).to contain_exactly "JetsPreheatJobIamPolicy", - "JetsPreheatJobIamRole", - "JetsPreheatJobWarmEventsRule", - "JetsPreheatJobWarmLambdaFunction", - "JetsPreheatJobWarmPermission" - - rule_schedule = template_hsh.dig("Resources", "JetsPreheatJobWarmEventsRule", "Properties", "ScheduleExpression") - expect(rule_schedule).to eq "rate(#{Jets.config.prewarm.rate})" - end - end - - context 'prewarm disabled' do - before { Jets.config.prewarm.enable = false } - - it 'does not build the preheat job template' do - allow(Jets).to receive(:s3_bucket).and_return("demo-test") - builder.build - - preheat_job_file_path = "/tmp/jets/demo/templates/app-jets-preheat_job.yml" - file_exist = File.exist?(preheat_job_file_path) - expect(file_exist).to be_falsey - end - end - end - - context "methods" do - let(:builder) do - Jets::Cfn::Builder.new(noop: true) - end - - it "app_file?" do - yes = Jets::Cfn::Builder.app_file?("app/controllers/posts_controller.rb") - expect(yes).to be true - - yes = Jets::Cfn::Builder.app_file?("app/jobs/hard_job.rb") - expect(yes).to be true - - yes = Jets::Cfn::Builder.app_file?("app/functions/hello.rb") - expect(yes).to be true - - yes = Jets::Cfn::Builder.app_file?("app/models/post.rb") - expect(yes).to be false - end - - it "app_files" do - expect(Jets::Cfn::Builder.app_files).to include("app/controllers/posts_controller.rb") - expect(Jets::Cfn::Builder.app_files).to include("app/jobs/hard_job.rb") - expect(Jets::Cfn::Builder.app_files).to include("app/functions/hello.rb") - - expect(Jets::Cfn::Builder.app_files).not_to include("app/models/post.rb") - end - - context "prewarming enabled" do - before { Jets.config.prewarm.enable = true } - - it "app_files includes preheat job" do - expect(Jets::Cfn::Builder.app_files).to include(a_string_ending_with("preheat_job.rb")) - end - end - - context "prewarming disabled" do - before { Jets.config.prewarm.enable = false } - - it "app_files does not include preheat job" do - expect(Jets.config.prewarm.enabled).to be_falsey - expect(Jets::Cfn::Builder.app_files).not_to include(a_string_ending_with("preheat_job.rb")) - end - end - end - -end - diff --git a/spec/lib/jets/cfn/resource/api_gateway/domain_name_spec.rb b/spec/lib/jets/cfn/resource/api_gateway/domain_name_spec.rb deleted file mode 100644 index 99b5a0597..000000000 --- a/spec/lib/jets/cfn/resource/api_gateway/domain_name_spec.rb +++ /dev/null @@ -1,17 +0,0 @@ -describe Jets::Cfn::Resource::ApiGateway::DomainName do - - context 'default' do - let(:domain_name) do - Jets::Cfn::Resource::ApiGateway::DomainName.new - end - - it "domain_name" do - allow(Jets.config.domain).to receive(:name).and_return("test.com") - expect(domain_name.logical_id).to eq "DomainName" - properties = domain_name.properties - # pp properties # uncomment to debug - expect(properties[:DomainName]).to eq "test.com" - end - end - -end diff --git a/spec/lib/jets/cfn/resource/api_gateway/method_spec.rb b/spec/lib/jets/cfn/resource/api_gateway/method_spec.rb deleted file mode 100644 index fa991f666..000000000 --- a/spec/lib/jets/cfn/resource/api_gateway/method_spec.rb +++ /dev/null @@ -1,119 +0,0 @@ -describe Jets::Cfn::Resource::ApiGateway::Method do - let(:resource) { Jets::Cfn::Resource::ApiGateway::Method.new(route) } - - context "post#index" do - let(:route) do - Jets::Router::Route.new(path: "posts", http_method: :get, to: "posts#index") - end - - it "resource" do - expect(resource.logical_id).to eq "GetPostsApiMethod" - properties = resource.properties - expect(properties[:RestApiId]).to eq "!Ref RestApi" - expect(properties[:ResourceId]).to eq "!Ref PostsApiResource" - expect(properties[:HttpMethod]).to eq "GET" - end - - it 'defaults to no authorization' do - expect(resource.properties[:AuthorizationType]).to eq 'NONE' - end - - it 'defaults to no api_key_required' do - expect(resource.properties[:ApiKeyRequired]).to eq 'false' - end - end - - context "long route" do - let(:route) do - Jets::Router::Route.new(path: "posts/:post_id/comments/:comment_id/images/:images/source_urls/:source_urls", http_method: :get, to: "posts#index") - end - - it "long route" do - expect(resource.logical_id).to eq "GetPostsPostIdCommentsCommentIdImagesImagesSourceUrlsSourceUrlsApiMethod" - expect(resource.logical_id.size).to eq 72 - end - - it "resource" do - expect(resource.logical_id).to eq "GetPostsPostIdCommentsCommentIdImagesImagesSourceUrlsSourceUrlsApiMethod" - properties = resource.properties - # pp properties # uncomment to debug - expect(properties[:RestApiId]).to eq "!Ref RestApi" - puts "properties[:ResourceId] #{properties[:ResourceId]}" - expect(properties[:ResourceId]).to eq "!Ref PostsPostIdCommentsCommentIdImagesImagesSourceUrlsSourceUrlsApiResource" - expect(properties[:HttpMethod]).to eq "GET" - end - - # Old jets v4 behavior would use truncated logical id for all logical ids, including - # APIGW Method resource. Don't think is needed since the logical id limit is really - # 256 charts. Leaving these tests here as comments in case we want to revert back. - # it "long route" do - # expect(resource.logical_id).to eq "GetPostsPostIdCommentsCommentIdImagesImagesSourceUrlsSou3a92fb" - # expect(resource.logical_id.size).to eq 62 - # end - - # it "resource" do - # expect(resource.logical_id).to eq "GetPostsPostIdCommentsCommentIdImagesImagesSourceUrlsSou3a92fb" - # properties = resource.properties - # # pp properties # uncomment to debug - # expect(properties[:RestApiId]).to eq "!Ref RestApi" - # expect(properties[:ResourceId]).to eq "!Ref PostsPostIdCommentsCommentIdImagesImagesSourcaa1e8bApiResource" - # expect(properties[:HttpMethod]).to eq "GET" - # end - end - - context "route contains dot" do - let(:route) do - Jets::Router::Route.new(path: "v1.2/posts/:post_id/", http_method: :get, to: "posts#index") - end - - it "route contains dot" do - expect(resource.logical_id).to eq "GetV12PostsPostIdApiMethod" - end - - it "resource" do - expect(resource.logical_id).to eq "GetV12PostsPostIdApiMethod" - properties = resource.properties - # pp properties # uncomment to debug - expect(properties[:RestApiId]).to eq "!Ref RestApi" - expect(properties[:ResourceId]).to eq "!Ref V12PostsPostIdApiResource" - expect(properties[:HttpMethod]).to eq "GET" - end - end - - context "authorization" do - let(:route) do - Jets::Router::Route.new(path: "posts", http_method: :get, to: "posts#index", authorization_type: 'AWS_IAM') - end - it "can specify an authorization type" do - expect(resource.properties[:AuthorizationType]).to eq 'AWS_IAM' - end - end - - context "api key" do - let(:route) do - Jets::Router::Route.new(path: "posts", http_method: :get, to: "posts#index", api_key_required: true) - end - - it "can specify an api_key_required" do - expect(resource.properties[:ApiKeyRequired]).to eq 'true' - end - end - - context "authorization scopes on rotes" do - let(:route) do - Jets::Router::Route.new(path: "posts", http_method: :get, to: "posts#index", authorization_scopes: %w[create delete]) - end - it "can specify an authorization scopes" do - expect(resource.properties[:AuthorizationScopes]).to eq ["create", "delete"] - end - end - - context "authorization scopes on controller" do - let(:route) do - Jets::Router::Route.new(path: "toys", http_method: :get, to: "toys#index") - end - it "can specify an authorization scopes" do - expect(resource.properties[:AuthorizationScopes]).to eq ["create", "delete"] - end - end -end diff --git a/spec/lib/jets/cfn/resource/api_gateway/resource_spec.rb b/spec/lib/jets/cfn/resource/api_gateway/resource_spec.rb deleted file mode 100644 index e334d0587..000000000 --- a/spec/lib/jets/cfn/resource/api_gateway/resource_spec.rb +++ /dev/null @@ -1,80 +0,0 @@ -describe Jets::Cfn::Resource::ApiGateway::Resource do - let(:resource) { Jets::Cfn::Resource::ApiGateway::Resource.new(path) } - - context "posts/:id/edit" do - let(:path) { "posts/:id/edit" } - it "resource" do - expect(resource.logical_id).to eq "PostsIdEditApiResource" - properties = resource.properties - # pp properties # uncomment to debug - expect(properties[:ParentId]).to eq "!Ref PostsIdApiResource" - expect(properties[:PathPart]).to eq "edit" - end - end - - context("*catchall") do - let(:path) { "*catchall" } - it "uses valid characters for logical id" do - expect(resource.logical_id).to eq "CatchallApiResource" - properties = resource.properties - expect(properties[:PathPart]).to eq "{catchall+}" - end - end - - context("show path with path_part that has the capture") do - let(:path) { "posts/:id" } - it "contains info for CloudFormation API Gateway Resources" do - expect(resource.logical_id).to eq "PostsIdApiResource" - properties = resource.properties - expect(properties[:PathPart]).to eq "{id}" - expect(properties[:ParentId]).to eq "!Ref PostsApiResource" - end - end - - context("posts index is a root level path") do - let(:path) { "posts" } - it "contains info for CloudFormation API Gateway Resources" do - expect(resource.logical_id).to eq "PostsApiResource" - properties = resource.properties - expect(properties[:PathPart]).to eq "posts" - expect(properties[:ParentId]).to eq "!Ref RootResourceId" - end - end - - # So a resource for the root path is never really created but we have this - # spec because the definitions get initialized right away and we dont - # want initialization to crash. - # Controllers use a an '' path route to add a parameter: - # scoped_routes.each do |route| - # resource = Jets::Cfn::Resource::ApiGateway::Resource.new(route.path) - # add_parameter(resource.logical_id, Description: resource.desc) - # end - context("top most root level path") do - let(:path) { "" } - it "contains info for CloudFormation API Gateway Resources" do - expect(resource.logical_id).to eq "RootResourceId" - end - end - - context("url with dash") do - let(:path) { "url-with-dash" } - it "contains info for CloudFormation API Gateway Resources" do - expect(resource.logical_id).to eq "UrlWithDashApiResource" - properties = resource.properties - expect(properties[:PathPart]).to eq "url-with-dash" - expect(properties[:ParentId]).to eq "!Ref RootResourceId" - end - end - - context("url.with.dot") do - let(:path) { "url.with.dot" } - it "contains info for CloudFormation API Gateway Resources" do - expect(resource.logical_id).to eq "UrlWithDotApiResource" - properties = resource.properties - expect(properties[:PathPart]).to eq "url.with.dot" - expect(properties[:ParentId]).to eq "!Ref RootResourceId" - end - end - -end - diff --git a/spec/lib/jets/cfn/resource/api_gateway/rest_api/logical_id_spec.rb b/spec/lib/jets/cfn/resource/api_gateway/rest_api/logical_id_spec.rb deleted file mode 100644 index a718bd4dc..000000000 --- a/spec/lib/jets/cfn/resource/api_gateway/rest_api/logical_id_spec.rb +++ /dev/null @@ -1,23 +0,0 @@ -describe Jets::Cfn::Resource::ApiGateway::RestApi do - let(:logical_id) do - logical_id = Jets::Cfn::Resource::ApiGateway::RestApi::LogicalId.new - allow(logical_id).to receive(:current).and_return("RestApi") - logical_id - end - - context "changes detected" do - it "get" do - allow(logical_id).to receive(:changed?).and_return(true) - allow(logical_id).to receive(:stack_exists?).and_return(true) - allow(logical_id).to receive(:api_gateway_exists?).and_return(true) - expect(logical_id.get).to eq "RestApi1" - end - end - - context "no changes detected" do - it "get" do - allow(logical_id).to receive(:changed?).and_return(false) - expect(logical_id.get).to eq "RestApi" - end - end -end diff --git a/spec/lib/jets/cfn/resource/api_gateway/rest_api/routes/change/page_spec.rb b/spec/lib/jets/cfn/resource/api_gateway/rest_api/routes/change/page_spec.rb deleted file mode 100644 index def21d406..000000000 --- a/spec/lib/jets/cfn/resource/api_gateway/rest_api/routes/change/page_spec.rb +++ /dev/null @@ -1,103 +0,0 @@ -require 'recursive-open-struct' - -describe Jets::Cfn::Resource::ApiGateway::RestApi::Routes::Change::Page do - let(:page) do - Jets::Cfn::Resource::ApiGateway::RestApi::Routes::Change::Page.new - end - let(:path) { 'spec/fixtures/resource_pages/demo-test-api-resources-1.yml' } - let(:page_number) { 1 } - - context "moved?" do - it "same routes" do - new_pages = { - HiApiResource: 1, - Hi1ApiResource: 1, - Hi2ApiResource: 2, - Hi3ApiResource: 2, - Hi4ApiResource: 3, - } - deployed_pages = { - HiApiResource: 1, - Hi1ApiResource: 1, - Hi2ApiResource: 2, - Hi3ApiResource: 2, - Hi4ApiResource: 3, - } - moved = page.moved?(new_pages, deployed_pages) - expect(moved).to be false - end - - it "new routes" do - new_pages = { - HiApiResource: 1, - Hi1ApiResource: 1, - Hi2ApiResource: 2, - Hi3ApiResource: 2, - Hi4ApiResource: 3, - } - deployed_pages = { - HiApiResource: 1, - } - moved = page.moved?(new_pages, deployed_pages) - expect(moved).to be false - end - - it "route moved page" do - new_pages = { - HiApiResource: 1, - Hi1ApiResource: 1, - Hi2ApiResource: 2, # <= moved - Hi3ApiResource: 2, - Hi4ApiResource: 3, - } - deployed_pages = { - HiApiResource: 1, - Hi1ApiResource: 1, - Hi2ApiResource: 1, # <= moved - Hi3ApiResource: 2, - Hi4ApiResource: 3, - } - moved = page.moved?(new_pages, deployed_pages) - expect(moved).to be true - end - end - - context "local_logical_ids_map" do - it "load map from generated templates" do - logical_ids = page.local_logical_ids_map("spec/fixtures/resource_pages/demo-test-api-resources-*.yml") - expected = { - HiApiResource: "1", - Hi1ApiResource: "1", - Hi2ApiResource: "2", - Hi3ApiResource: "2", - Hi4ApiResource: "3", - } - expect(logical_ids).to eq(expected) - end - end - - context "remote_logical_ids_map" do - it "load map from deployed cloudformation stack" do - cfn = double(:cfn).as_null_object - child_stacks = OpenStruct.new( - stack_resources: [ - OpenStruct.new(physical_resource_id: "demo-test-ApiResources1-") - ] - ) - api_resources = OpenStruct.new( - stack_resources: [ - OpenStruct.new(logical_resource_id: "HiApiResource"), - OpenStruct.new(logical_resource_id: "Hi1ApiResource"), - ] - ) - - allow(cfn).to receive(:describe_stack_resources).and_return(child_stacks) - allow(cfn).to receive(:describe_stack_resources).with(stack_name: "demo-test-ApiResources1-").and_return(api_resources) - allow(page).to receive(:cfn).and_return(cfn) - - logical_ids = page.remote_logical_ids_map - expected = {"HiApiResource"=>"1", "Hi1ApiResource"=>"1"} - expect(logical_ids).to eq(expected) - end - end -end diff --git a/spec/lib/jets/cfn/resource/api_gateway/rest_api/routes/change/to_spec.rb b/spec/lib/jets/cfn/resource/api_gateway/rest_api/routes/change/to_spec.rb deleted file mode 100644 index 6768fb594..000000000 --- a/spec/lib/jets/cfn/resource/api_gateway/rest_api/routes/change/to_spec.rb +++ /dev/null @@ -1,36 +0,0 @@ -describe Jets::Cfn::Resource::ApiGateway::RestApi::Routes::Change::To do - let(:to) do - Jets::Cfn::Resource::ApiGateway::RestApi::Routes::Change::To.new - end - - context "no changes detected" do - it "changed" do - # Use new routes as the "deployed" routes. One way to mimic and test. - # no routes have been changed - new_routes = Jets::Router.routes - deployed_routes = new_routes.clone - allow(to).to receive(:deployed_routes).and_return(deployed_routes) - - changed = to.changed? - - expect(changed).to be false - end - end - - context "yes changes detected" do - it "changed" do - new_routes = Jets::Router.routes - deployed_routes = new_routes.clone - # current to value is posts#index , change it to trigger a change - new_routes[0] = Jets::Router::Route.new(:path=>"/", :to=>"toys#index", :http_method=>:get, :root=>true) - - - allow(to).to receive(:deployed_routes).and_return(deployed_routes) - allow(to).to receive(:new_routes).and_return(new_routes) - - changed = to.changed? - - expect(changed).to be true - end - end -end diff --git a/spec/lib/jets/cfn/resource/api_gateway/rest_api/routes/change/variable_spec.rb b/spec/lib/jets/cfn/resource/api_gateway/rest_api/routes/change/variable_spec.rb deleted file mode 100644 index 0792890ae..000000000 --- a/spec/lib/jets/cfn/resource/api_gateway/rest_api/routes/change/variable_spec.rb +++ /dev/null @@ -1,37 +0,0 @@ -describe Jets::Cfn::Resource::ApiGateway::RestApi::Routes::Change::Variable do - let(:variable) do - Jets::Cfn::Resource::ApiGateway::RestApi::Routes::Change::Variable.new - end - let(:new_routes) do - new_routes = Jets::Router.routes - new_routes.select { |r| r.path.include?(':') } - end - - context "no changes detected" do - it "changed" do - # Use new routes as the "deployed" routes. One way to mimic and test. - # no routes have been changed - new_routes = Jets::Router.routes - deployed_routes = new_routes.clone - allow(variable).to receive(:deployed_routes).and_return(deployed_routes) - - changed = variable.changed? - - expect(changed).to be false - end - end - - context "yes changes detected" do - it "changed" do - new_routes = [Jets::Router::Route.new(path: "posts/:id", to: "posts#show", method: :get)] - deployed_routes = [Jets::Router::Route.new(path: "posts/:post_id", to: "posts#show", method: :get)] - - allow(variable).to receive(:deployed_routes).and_return(deployed_routes) - allow(variable).to receive(:new_routes).and_return(new_routes) - - changed = variable.changed? - - expect(changed).to be true - end - end -end diff --git a/spec/lib/jets/cfn/resource/api_gateway/rest_api/routes/change_detection.rb b/spec/lib/jets/cfn/resource/api_gateway/rest_api/routes/change_detection.rb deleted file mode 100644 index 68ef34bf0..000000000 --- a/spec/lib/jets/cfn/resource/api_gateway/rest_api/routes/change_detection.rb +++ /dev/null @@ -1,21 +0,0 @@ -describe Jets::Cfn::Resource::ApiGateway::RestApi do - let(:detection) { Jets::Cfn::Resource::ApiGateway::RestApi::ChangeDetection.new } - - context "general" do - it "default binary media type" do - expect(detection.new_binary_media_types).to eq ["multipart/form-data"] - end - end - - # TODO: have a spec in place right now for basic syntax checking. Eventually add - # more specs in the future. - # context "changes detected" do - # it "reuses the existing rest api logical id" do - # end - # end - - # context "no changes detected" do - # it "updates the rest api logical id" do - # end - # end -end diff --git a/spec/lib/jets/cfn/resource/api_gateway/rest_api/routes/collsion_spec.rb b/spec/lib/jets/cfn/resource/api_gateway/rest_api/routes/collsion_spec.rb deleted file mode 100644 index 8c117d5e6..000000000 --- a/spec/lib/jets/cfn/resource/api_gateway/rest_api/routes/collsion_spec.rb +++ /dev/null @@ -1,176 +0,0 @@ -describe Jets::Cfn::Resource::ApiGateway::RestApi do - let(:collision) do - Jets::Cfn::Resource::ApiGateway::RestApi::Routes::Collision.new - end - - context "collides" do - it "variable_collision_exists?" do - parent = "users" - paths = %w[ - users/:user_id/posts/:id/edit - users/:id - posts/:id - admin - ] - collide = collision.variable_collision_exists?(parent, paths) - expect(collide).to be true - end - - it "register collisions for later display" do - parent = "users" - paths = %w[ - users/:user_id/posts/:id/edit - users/:id - posts/:id - admin - ] - collide = collision.variable_collision_exists?(parent, paths) - expect(collide).to be true - end - end - - context "no collisions" do - it "collision?" do - paths = Jets.application.routes.routes.map(&:path) - collide = collision.collision?(paths) - expect(collide).to be false - end - - it "variable_collision_exists?" do - parent_path = "users" - paths = %w[ - users/:user_id/posts/:id/edit - users/:user_id - posts/:id - admin - ] - collide = collision.variable_collision_exists?(parent_path, paths) - expect(collide).to be false - end - - it "variable_collision_exists long path?" do - parent_path = "users/:user_id/posts" - paths = %w[ - users/:user_id - users/:user_id/posts - users/:user_id/posts/:id - users/:user_id/posts/:id/edit - users/:user_id/apps - posts/:id - admin - ] - collide = collision.variable_collision_exists?(parent_path, paths) - expect(collide).to be false - end - - # actual data - it "variable_collision_exists? post crud" do - parent_path = "posts" - paths = ["posts", "posts/new", "posts/:id", "posts/:id/edit", "", "*catchall"] - collide = collision.variable_collision_exists?(parent_path, paths) - expect(collide).to be false - end - - it "posts comments nested resources" do - parent_path = "posts/:post_id/comments" - paths = ["posts/:post_id/comments/:id", "posts/:post_id/comments/new"] - collide = collision.variable_collision_exists?(parent_path, paths) - expect(collide).to be false - end - end - - context "general" do - it "variable_parent" do - leaf = collision.variable_parent("users/:user_id/posts/:id/edit") - expect(leaf).to eq "users/:user_id/posts" - - leaf = collision.variable_parent("users/:user_id") - expect(leaf).to eq "users" - - leaf = collision.variable_parent("posts/:id") - expect(leaf).to eq "posts" - end - - it "variable_parents" do - paths = %w[ - users/:user_id/posts/:id/edit - users/:user_id - posts/:id - admin - ] - parents = collision.variable_parents(paths) - expect(parents).to eq ["posts", "users", "users/:user_id/posts"] - end - - it "parent_variables" do - parent = "users" - paths = %w[ - users/:user_id/posts/:post_id/edit - users/:id - posts/:id/users - admin - ] - variables = collision.parent_variables(parent, paths) - expect(variables).to eq [":id", ":user_id"] - end - - it "direct_parent?" do - parent = "users" - path = "users/:id" - is_parent = collision.direct_parent?(parent, path) - expect(is_parent).to be true - - parent = "users" - path = "posts/:id/users" - is_parent = collision.direct_parent?(parent, path) - expect(is_parent).to be false - - parent = "users" - path = "users/:id/posts/:post_id" - is_parent = collision.direct_parent?(parent, path) - expect(is_parent).to be false - - parent = "users/:id/posts" - path = "users/:id/posts/:post_id" - is_parent = collision.direct_parent?(parent, path) - expect(is_parent).to be true - - parent = "users" - leaf = "users/:user_id/posts/:id" - direct_parent = collision.direct_parent?(parent, leaf) - expect(direct_parent).to be false - - parent = "users/:user_id/posts" - leaf = "users/:user_id/posts/:id" - direct_parent = collision.direct_parent?(parent, leaf) - expect(direct_parent).to be true - - parent = "users" - leaf = "users/:user_id" - direct_parent = collision.direct_parent?(parent, leaf) - expect(direct_parent).to be true - end - - it "parent? does not have to be direct" do - parent = "users" - path = "users/:id" - is_parent = collision.parent?(parent, path) - expect(is_parent).to be true - - parent = "users" - path = "posts/:id/users" - is_parent = collision.parent?(parent, path) - expect(is_parent).to be false - - parent = "users" - path = "users/:id/posts/:post_id" - is_parent = collision.parent?(parent, path) - expect(is_parent).to be true - - parent = "users/:id/posts" - path = "users/:id/posts/:post_id" - is_parent = collision.parent?(parent, path) - expect(is_parent).to be true - end - end -end diff --git a/spec/lib/jets/cfn/resource/api_gateway/rest_api_spec.rb b/spec/lib/jets/cfn/resource/api_gateway/rest_api_spec.rb deleted file mode 100644 index e172c915b..000000000 --- a/spec/lib/jets/cfn/resource/api_gateway/rest_api_spec.rb +++ /dev/null @@ -1,50 +0,0 @@ -describe Jets::Cfn::Resource::ApiGateway::RestApi do - - context 'endpoint configuration' do - let(:endpoint_config) do - Jets::Cfn::Resource::ApiGateway::RestApi.new.properties[:EndpointConfiguration] - end - - context 'type' do - let(:endpoint_types) { endpoint_config[:Types] } - - it 'defaults to edge-optimized' do - allow(Jets.config.api).to receive(:endpoint_type).and_return('EDGE') - expect(endpoint_types).to eq ['EDGE'] - end - - it 'can be set explicitly' do - allow(Jets.config.api).to receive(:endpoint_type).and_return('PRIVATE') - expect(endpoint_types).to eq ['PRIVATE'] - end - end - - context 'vpc endpoint ids' do - let(:vpc_endpoint_ids) { endpoint_config[:VpcEndpointIds] } - - it 'defaults to nil' do - expect(vpc_endpoint_ids).to be_nil - end - - it 'can be set explicitly' do - allow(Jets.config.api).to receive(:vpc_endpoint_ids).and_return(['vpce-1234']) - expect(vpc_endpoint_ids).to eq ['vpce-1234'] - end - end - end - - context "policy configuration" do - let(:endpoint_policy) do - Jets::Cfn::Resource::ApiGateway::RestApi.new.properties[:Policy] - end - - it 'defaults to nil' do - expect(endpoint_policy).to be_nil - end - - it 'can be set explicitly' do - allow(Jets.config.api).to receive(:endpoint_policy).and_return(version: "2012-10-17") - expect(endpoint_policy).to a_hash_including(Version: "2012-10-17") - end - end -end diff --git a/spec/lib/jets/cfn/resource/associated_spec.rb b/spec/lib/jets/cfn/resource/associated_spec.rb deleted file mode 100644 index 7ab485f21..000000000 --- a/spec/lib/jets/cfn/resource/associated_spec.rb +++ /dev/null @@ -1,28 +0,0 @@ -describe Jets::Cfn::Resource::Associated do - let(:associated) { Jets::Cfn::Resource::Associated.new(definition) } - let(:definition) do - { - "{namespace}EventsRule1": { - type: "AWS::Events::Rule", - properties: { - schedule_expression: "rate(10 hours)", - state: "ENABLED", - targets: [{ - arn: "!GetAtt {namespace}LambdaFunction.Arn", - id: "{namespace}RuleTarget" - }] - } # closes properties - } - } - end - - context "long form" do - it "standardized format" do - expect(associated.logical_id).to eq :"{namespace}EventsRule1" - attributes = associated.attributes - # pp associated # uncomment to see and debug - # pp attributes # uncomment to see and debug - expect(attributes[:Type]).to eq "AWS::Events::Rule" - end - end -end diff --git a/spec/lib/jets/cfn/resource/iam/class_role_spec.rb b/spec/lib/jets/cfn/resource/iam/class_role_spec.rb deleted file mode 100644 index d50aad390..000000000 --- a/spec/lib/jets/cfn/resource/iam/class_role_spec.rb +++ /dev/null @@ -1,26 +0,0 @@ -describe Jets::Cfn::Resource::Iam::ClassRole do - let(:role) do - reset_application_config_iam! - Jets::Cfn::Resource::Iam::ClassRole.new(PostsController) - end - - context "iam policy" do - it "inherits from the application wide iam policy" do - puts "role.policy_document".color(:purple) - pp role.policy_document - expect(role.policy_document).to eq( - {:Version=>"2012-10-17", - :Statement=> - [{:Action=>["logs:*"], - :Effect=>"Allow", - :Resource=>"arn:aws:logs:us-east-1:123456789:log-group:/aws/lambda/demo-test-*"}, - {:Action=>["s3:Get*", "s3:List*", "s3:HeadBucket"], - :Effect=>"Allow", - :Resource=>"arn:aws:s3:::fake-test-s3-bucket*"}, - {:Action=>["cloudformation:DescribeStacks", "cloudformation:DescribeStackResources"], - :Effect=>"Allow", - :Resource=>"arn:aws:cloudformation:us-east-1:123456789:stack/demo-test*"}]} - ) - end - end -end \ No newline at end of file diff --git a/spec/lib/jets/cfn/resource/iam/function_role_spec.rb b/spec/lib/jets/cfn/resource/iam/function_role_spec.rb deleted file mode 100644 index 04d6df4e7..000000000 --- a/spec/lib/jets/cfn/resource/iam/function_role_spec.rb +++ /dev/null @@ -1,18 +0,0 @@ -describe Jets::Cfn::Resource::Iam::FunctionRole do - let(:role) do - Jets::Cfn::Resource::Iam::FunctionRole.new(task) - end - let(:task) do - PostsController.all_public_definitions[:new] - end - - context "iam policy" do - it "inherits from the application and class wide iam policy" do - # Since one_lambda_for_all_controllers is default this IAM policy is empty - # because the Lambda function points to the main ApplicationController IAM policy - expect(role.policy_document).to eq( - {:Version=>"2012-10-17", :Statement=>[]} - ) - end - end -end \ No newline at end of file diff --git a/spec/lib/jets/cfn/resource/iam/managed_policy_spec.rb b/spec/lib/jets/cfn/resource/iam/managed_policy_spec.rb deleted file mode 100644 index e90cc5efc..000000000 --- a/spec/lib/jets/cfn/resource/iam/managed_policy_spec.rb +++ /dev/null @@ -1,31 +0,0 @@ -describe Jets::Cfn::Resource::Iam::ManagedPolicy do - let(:managed_policy) do - Jets::Cfn::Resource::Iam::ManagedPolicy.new(definitions) - end - - context "single string" do - let(:definitions) { "AmazonEC2ReadOnlyAccess" } - it "builds the resource definition" do - expect(managed_policy.arns).to eq ["arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess"] - end - end - - context "multiple strings" do - let(:definitions) { ["AmazonEC2ReadOnlyAccess", "service-role/AWSConfigRulesExecutionRole"] } - it "provides the iam managed policy arn" do - expect(managed_policy.arns).to eq [ - "arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess", - "arn:aws:iam::aws:policy/service-role/AWSConfigRulesExecutionRole", - ] - end - end - - context "full arn provided" do - let(:definitions) { ["arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess"] } - it "provides the iam managed policy arn" do - expect(managed_policy.arns).to eq [ - "arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess", - ] - end - end -end \ No newline at end of file diff --git a/spec/lib/jets/cfn/resource/iam/policy_document_spec.rb b/spec/lib/jets/cfn/resource/iam/policy_document_spec.rb deleted file mode 100644 index 49c12fdf6..000000000 --- a/spec/lib/jets/cfn/resource/iam/policy_document_spec.rb +++ /dev/null @@ -1,214 +0,0 @@ -describe Jets::Cfn::Resource::Iam::PolicyDocument do - let(:document) do - Jets::Cfn::Resource::Iam::PolicyDocument.new(definitions) - end - - context "single string with no wildcard" do - let(:definitions) { "logs" } - it "builds the resource definition" do - iam_policy_document = <<~EOL - Version: '2012-10-17' - Statement: - - Action: - - logs:* - Effect: Allow - Resource: "*" - EOL - expected_policy = YAML.load(iam_policy_document).deep_symbolize_keys - expect(document.policy_document).to eq expected_policy - end - end - - context "single string" do - let(:definitions) { "logs:*" } - it "builds the resource definition" do - iam_policy_document = <<~EOL - Version: '2012-10-17' - Statement: - - Action: - - logs:* - Effect: Allow - Resource: "*" - EOL - expected_policy = YAML.load(iam_policy_document).deep_symbolize_keys - expect(document.policy_document).to eq expected_policy - end - end - - context "array with single string" do - let(:definitions) { ["logs:*"] } - it "builds the resource definition" do - iam_policy_document = <<~EOL - Version: '2012-10-17' - Statement: - - Action: - - logs:* - Effect: Allow - Resource: "*" - EOL - expected_policy = YAML.load(iam_policy_document).deep_symbolize_keys - expect(document.policy_document).to eq expected_policy - end - end - - context "multiple strings" do - let(:definitions) { ["ec2:*", "logs:*"] } - it "provides the resource definition" do - iam_policy_document = <<~EOL - Version: '2012-10-17' - Statement: - - Action: - - ec2:* - Effect: Allow - Resource: "*" - - Action: - - logs:* - Effect: Allow - Resource: "*" - EOL - expected_policy_document = YAML.load(iam_policy_document).deep_symbolize_keys - expect(document.policy_document).to eq expected_policy_document - end - end - - context "single hash" do - context "string keys" do - let(:definitions) do - [{ - "Sid" => "MyStmt1", - "Action" => ["lambda:*"], - "Effect" => "Allow", - "Resource" => "arn:my-arn", - }] - end - it "provides the resource definition" do - iam_policy_document = <<~EOL - Version: '2012-10-17' - Statement: - - Sid: MyStmt1 - Action: - - lambda:* - Effect: Allow - Resource: arn:my-arn - EOL - expected_policy_document = YAML.load(iam_policy_document).deep_symbolize_keys - expect(document.policy_document).to eq expected_policy_document - end - end - - context "symbol keys" do - let(:definitions) do - [{ - Sid: "MyStmt1", - Action: ["lambda:*"], - Effect: "Allow", - Resource: "arn:my-arn", - }] - end - it "provides the resource definition" do - iam_policy_document = <<~EOL - Version: '2012-10-17' - Statement: - - Sid: MyStmt1 - Action: - - lambda:* - Effect: Allow - Resource: arn:my-arn - EOL - expected_policy_document = YAML.load(iam_policy_document).deep_symbolize_keys - expect(document.policy_document).to eq expected_policy_document - end - end - - context "symbol keys with lowercase" do - let(:definitions) do - [{ - sid: "MyStmt1", - action: ["lambda:*"], - effect: "Allow", - resource: "arn:my-arn", - }] - end - it "provides the resource definition" do - iam_policy_document = <<~EOL - Version: '2012-10-17' - Statement: - - Sid: MyStmt1 - Action: - - lambda:* - Effect: Allow - Resource: arn:my-arn - EOL - expected_policy_document = YAML.load(iam_policy_document).deep_symbolize_keys - expect(document.policy_document).to eq expected_policy_document - end - end - end - - context "multiple hashes" do - context "symbol keys" do - let(:definitions) do - [{ - Sid: "MyStmt1", - Action: ["lambda:*"], - Effect: "Allow", - Resource: "arn:my-arn", - },{ - Sid: "MyStmt2", - Action: ["logs:*"], - Effect: "Allow", - Resource: "*", - }] - end - it "provides the resource definition" do - iam_policy_document = <<~EOL - Version: '2012-10-17' - Statement: - - Sid: MyStmt1 - Action: - - lambda:* - Effect: Allow - Resource: arn:my-arn - - Sid: MyStmt2 - Action: - - logs:* - Effect: Allow - Resource: "*" - EOL - expected_policy_document = YAML.load(iam_policy_document).deep_symbolize_keys - expect(document.policy_document).to eq expected_policy_document - end - end - end - - - context "special case hash with Version key" do - context "symbol keys" do - let(:definitions) do - [{ - Version: "2012-10-17", # special case, a Version key will replace the entire policy - # assumes that only one policy is passed in - Statement: [{ - Sid: "MyStmt1", - Action: ["lambda:*"], - Effect: "Allow", - Resource: "arn:my-arn", - }] - }] - end - it "provides the resource definition" do - iam_policy_document = <<~EOL - Version: '2012-10-17' - Statement: - - Sid: MyStmt1 - Action: - - lambda:* - Effect: Allow - Resource: arn:my-arn - EOL - expected_policy_document = YAML.load(iam_policy_document).deep_symbolize_keys - expect(document.policy_document).to eq expected_policy_document - end - end - end -end diff --git a/spec/lib/jets/cfn/resource/lambda/function_spec.rb b/spec/lib/jets/cfn/resource/lambda/function_spec.rb deleted file mode 100644 index 0b2dcbe81..000000000 --- a/spec/lib/jets/cfn/resource/lambda/function_spec.rb +++ /dev/null @@ -1,116 +0,0 @@ -describe Jets::Cfn::Resource::Lambda::Function do - let(:resource) { Jets::Cfn::Resource::Lambda::Function.new(task) } - - context "function timeout 18" do - let(:task) do - PostsController.all_public_definitions[:index] - end - it "uses function properties" do - expect(resource.logical_id).to eq "PostsControllerIndexLambdaFunction" - properties = resource.properties - # puts YAML.dump(properties) # uncomment to debug - expect(properties[:FunctionName]).to eq "demo-test-posts_controller-index" - expect(properties[:Handler]).to eq "handlers/controllers/posts_controller.index" - end - end - - context "function with description specified" do - let(:task) do - ArticlesController.all_public_definitions[:index] - end - it "uses function properties" do - expect(resource.logical_id).to eq "ArticlesControllerIndexLambdaFunction" - properties = resource.properties - # puts YAML.dump(properties) # uncomment to debug - expect(properties[:Description]).to eq "All articles" - end - end - - context "controller" do - let(:task) do - PostsController.all_public_definitions[:index] - end - - it "contains info for CloudFormation Controller Function Resources" do - expect(resource.logical_id).to eq "PostsControllerIndexLambdaFunction" - properties = resource.properties - # puts YAML.dump(properties) # uncomment to debug - expect(properties[:FunctionName]).to eq "demo-test-posts_controller-index" - expect(properties[:Description]).to eq "PostsController#index" - expect(properties[:Handler]).to eq "handlers/controllers/posts_controller.index" - expect(properties[:Code][:S3Key]).to include("jets/code") - end - end - - context "deeply namespaced controller" do - module Deep - module Namespace - class TestController < Jets::Controller::Base - def index; end - end - end - end - - let(:task) do - Deep::Namespace::TestController.all_public_definitions[:index] - end - - it "contains info for CloudFormation Controller Function Resources" do - expect(resource.logical_id).to eq "DeepNamespaceTestControllerIndexLambdaFunction" - properties = resource.properties - # puts YAML.dump(properties) # uncomment to debug - expect(properties[:FunctionName]).to eq "demo-test-deep-namespace-test_controller-index" - expect(properties[:Description]).to eq "Deep::Namespace::TestController#index" - expect(properties[:Handler]).to eq "handlers/controllers/deep/namespace/test_controller.index" - expect(properties[:Code][:S3Key]).to include("jets/code") - end - end - - context("job") do - let(:task) do - HardJob.all_public_definitions[:dig] - end - - it "contains info for CloudFormation Job Function Resources" do - expect(resource.logical_id).to eq "HardJobDigLambdaFunction" - properties = resource.properties - # puts YAML.dump(properties) # uncomment to debug - expect(properties[:FunctionName]).to eq "demo-test-hard_job-dig" - expect(properties[:Description]).to eq "HardJob#dig" - expect(properties[:Handler]).to eq "handlers/jobs/hard_job.dig" - expect(properties[:Code][:S3Key]).to include("jets/code") - end - end - - context("function with _function") do - let(:task) do - Jets::Lambda::Definition.new("SimpleFunction", :lambda_handler) - end - - it "contains info for CloudFormation Job Function Resources" do - expect(resource.logical_id).to eq "SimpleFunctionLambdaHandlerLambdaFunction" - properties = resource.properties - # puts YAML.dump(properties) # uncomment to debug - expect(properties[:FunctionName]).to eq "demo-test-simple_function-lambda_handler" - expect(properties[:Description]).to eq "SimpleFunction#lambda_handler" - expect(properties[:Handler]).to eq "handlers/functions/simple_function.lambda_handler" - expect(properties[:Code][:S3Key]).to include("jets/code") - end - end - - context("function without _function") do - let(:task) do - Jets::Lambda::Definition.new("Hello", :world, type: "function") - end - - it "contains info for CloudFormation Job Function Resources" do - expect(resource.logical_id).to eq "HelloWorldLambdaFunction" - properties = resource.properties - # puts YAML.dump(properties) # uncomment to debug - expect(properties[:FunctionName]).to eq "demo-test-hello-world" - expect(properties[:Handler]).to eq "handlers/functions/hello.world" - expect(properties[:Code][:S3Key]).to include("jets/code") - end - end -end - diff --git a/spec/lib/jets/cfn/resource/lambda/gem_layer_spec.rb b/spec/lib/jets/cfn/resource/lambda/gem_layer_spec.rb deleted file mode 100644 index bd29d6d55..000000000 --- a/spec/lib/jets/cfn/resource/lambda/gem_layer_spec.rb +++ /dev/null @@ -1,13 +0,0 @@ -describe Jets::Cfn::Resource::Lambda::GemLayer do - let(:resource) { Jets::Cfn::Resource::Lambda::GemLayer.new } - - context "gems layer version" do - it "builds template" do - expect(resource.logical_id).to eq "GemLayer" - properties = resource.properties - # puts YAML.dump(properties) # uncomment to debug - expect(properties[:LayerName]).to eq "test-demo-gems" - end - end -end - diff --git a/spec/lib/jets/cfn/resource/lambda/permission_spec.rb b/spec/lib/jets/cfn/resource/lambda/permission_spec.rb deleted file mode 100644 index ab50bea7d..000000000 --- a/spec/lib/jets/cfn/resource/lambda/permission_spec.rb +++ /dev/null @@ -1,30 +0,0 @@ -describe Jets::Cfn::Resource::Lambda::Permission do - let(:permission) { Jets::Cfn::Resource::Lambda::Permission.new(replacements, associated_resource) } - let(:associated_resource) do - definition = { - "{namespace}EventsRule1": { - type: "AWS::Events::Rule", - properties: { - schedule_expression: "rate(10 hours)", - state: "ENABLED", - targets: [{ - arn: "!GetAtt {namespace}LambdaFunction.Arn", - id: "{namespace}RuleTarget" - }] - } # closes properties - } - } - Jets::Cfn::Resource.new(definition, replacements) - end - let(:replacements) { {namespace: "HardJobDig"} } - - context "raw cloudformation definition" do - it "permission" do - expect(permission.logical_id).to eq "HardJobDigPermission1" - properties = permission.properties - expect(properties[:Principal]).to eq "events.amazonaws.com" - expect(properties[:SourceArn]).to be nil - end - end -end - diff --git a/spec/lib/jets/cfn/resource/nested/api_deployment_spec.rb b/spec/lib/jets/cfn/resource/nested/api_deployment_spec.rb deleted file mode 100644 index 2ec7dcc1a..000000000 --- a/spec/lib/jets/cfn/resource/nested/api_deployment_spec.rb +++ /dev/null @@ -1,14 +0,0 @@ -describe Jets::Cfn::Resource::Nested::Api::Deployment do - let(:resource) do - Jets::Cfn::Resource::Nested::Api::Deployment.new(s3_bucket: "s3-bucket") - end - - describe "resource" do - it "contains child stack info" do - allow(Jets).to receive(:s3_bucket).and_return("s3-bucket") - expect(resource.logical_id).to match(/ApiDeployment(\d+)/) - properties = resource.properties - expect(properties[:TemplateURL]).to eq "https://s3.amazonaws.com/s3-bucket/jets/cfn-templates/shas//api-deployment.yml" - end - end -end diff --git a/spec/lib/jets/cfn/resource/nested/api_gateway_spec.rb b/spec/lib/jets/cfn/resource/nested/api_gateway_spec.rb deleted file mode 100644 index 55e6290f8..000000000 --- a/spec/lib/jets/cfn/resource/nested/api_gateway_spec.rb +++ /dev/null @@ -1,14 +0,0 @@ -describe Jets::Cfn::Resource::Nested::Api::Gateway do - let(:resource) do - Jets::Cfn::Resource::Nested::Api::Gateway.new(s3_bucket: "s3-bucket") - end - - describe "resource" do - it "contains child stack info" do - allow(Jets).to receive(:s3_bucket).and_return("s3-bucket") - expect(resource.logical_id).to eq "ApiGateway" - properties = resource.properties - expect(properties[:TemplateURL]).to eq "https://s3.amazonaws.com/s3-bucket/jets/cfn-templates/shas//api-gateway.yml" - end - end -end diff --git a/spec/lib/jets/cfn/resource/nested/shared_spec.rb b/spec/lib/jets/cfn/resource/nested/shared_spec.rb deleted file mode 100644 index 21d53032e..000000000 --- a/spec/lib/jets/cfn/resource/nested/shared_spec.rb +++ /dev/null @@ -1,15 +0,0 @@ -describe Jets::Cfn::Resource::Nested::Shared do - let(:resource) do - path = "/tmp/jets/demo/templates/shared-custom.yml" - Jets::Cfn::Resource::Nested::Shared.new(s3_bucket: "s3-bucket", path: path) - end - - describe "resource" do - it "contains child stack info" do - allow(Jets).to receive(:s3_bucket).and_return("s3-bucket") - expect(resource.logical_id).to eq "Custom" - properties = resource.properties - expect(properties[:TemplateURL]).to eq "https://s3.amazonaws.com/s3-bucket/jets/cfn-templates/shas//shared-custom.yml" - end - end -end diff --git a/spec/lib/jets/cfn/resource/replacer_spec.rb b/spec/lib/jets/cfn/resource/replacer_spec.rb deleted file mode 100644 index a13fd38b2..000000000 --- a/spec/lib/jets/cfn/resource/replacer_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -describe Jets::Cfn::Resource::Replacer do - let(:replacer) { Jets::Cfn::Resource::Replacer.new(replacements) } - let(:attributes) do - { - k: "{namespace}-value", - a: { - b: "{namespace}-value" - } - } - end - let(:replacements) { { namespace: "FooBar" } } - - context "raw cloudformation definition" do - it "replace_placeholders" do - result = replacer.replace_placeholders(attributes) - # pp result - expect(result).to eq(a: {b: "FooBar-value"}, k: "FooBar-value") - end - end -end \ No newline at end of file diff --git a/spec/lib/jets/cfn/resource/resource_spec.rb b/spec/lib/jets/cfn/resource/resource_spec.rb deleted file mode 100644 index d35e0abc5..000000000 --- a/spec/lib/jets/cfn/resource/resource_spec.rb +++ /dev/null @@ -1,154 +0,0 @@ -describe Jets::Cfn::Resource do - let(:resource) { Jets::Cfn::Resource.new(definition, replacements) } - - context "long form resource with no replacements" do - let(:replacements) { {} } - let(:definition) do - { - "RestApi": { - type: "AWS::ApiGateway::RestApi", - properties: { - name: "demo-test" - } - } - } - end - - it "cloudformation format" do - # pp resource # uncomment to see and debug - # pp resource.attributes # uncomment to see and debug - # pp resource.properties # uncomment to see and debug - expect(resource.logical_id).to eq "RestApi" - expect(resource.type).to eq "AWS::ApiGateway::RestApi" - properties = resource.properties - expect(properties[:Name]).to eq "demo-test" - end - end - - context "long form resource with replacements" do - let(:replacements) do - { namespace: "SecurityJobCheck" } - end - let(:definition) do - { - "{namespace}EventsRule": { - type: "AWS::Events::Rule", - properties: { - event_pattern: { - detail_type: ["AWS API Call via CloudTrail"], - detail: { - event_source: ["ec2.amazonaws.com"], - event_name: [ - "AuthorizeSecurityGroupIngress", - ] - } - }, - state: "ENABLED", - targets: [{ - arn: "!GetAtt {namespace}LambdaFunction.Arn", - id: "{namespace}RuleTarget" - }] - } # closes properties - } - } - end - - it "cloudformation format" do - # pp resource # uncomment to see and debug - # pp resource.logical_id # uncomment to see and debug - # pp resource.attributes # uncomment to see and debug - # pp resource.properties # uncomment to see and debug - expect(resource.logical_id).to eq "SecurityJobCheckEventsRule" - expect(resource.type).to eq "AWS::Events::Rule" - properties = resource.properties - expect(properties[:State]).to eq "ENABLED" - # properties under EventPattern has special dasherized and pascalized casing - event_pattern = properties[:EventPattern] - expect(event_pattern.key?('detail-type'.to_sym)).to be true - expect(event_pattern[:detail][:eventSource]).to eq ["ec2.amazonaws.com"] - end - end - - context "medium form resource with no replacements" do - let(:replacements) { {} } - let(:definition) do - [:rest_api, - type: "AWS::ApiGateway::RestApi", - properties: { - name: "demo-test" - } - ] - end - - it "cloudformation format" do - # pp resource # uncomment to see and debug - # pp resource.attributes # uncomment to see and debug - # pp resource.properties # uncomment to see and debug - expect(resource.logical_id).to eq "RestApi" - expect(resource.type).to eq "AWS::ApiGateway::RestApi" - properties = resource.properties - expect(properties[:Name]).to eq "demo-test" - end - end - - context "medium form resource with replacements" do - let(:replacements) do - { namespace: "SecurityJobCheck" } - end - let(:definition) do - ["{namespace}EventsRule", - type: "AWS::Events::Rule", - properties: { - event_pattern: { - detail_type: ["AWS API Call via CloudTrail"], - detail: { - event_source: ["ec2.amazonaws.com"], - event_name: [ - "AuthorizeSecurityGroupIngress", - ] - } - }, - state: "ENABLED", - targets: [{ - arn: "!GetAtt {namespace}LambdaFunction.Arn", - id: "{namespace}RuleTarget" - }] - } # closes properties - ] - end - - it "cloudformation format" do - # pp resource # uncomment to see and debug - # pp resource.logical_id # uncomment to see and debug - # pp resource.attributes # uncomment to see and debug - # pp resource.properties # uncomment to see and debug - expect(resource.logical_id).to eq "SecurityJobCheckEventsRule" - expect(resource.type).to eq "AWS::Events::Rule" - properties = resource.properties - expect(properties[:State]).to eq "ENABLED" - # properties under EventPattern has special dasherized and pascalized casing - event_pattern = properties[:EventPattern] - expect(event_pattern.key?('detail-type'.to_sym)).to be true - expect(event_pattern[:detail][:eventSource]).to eq ["ec2.amazonaws.com"] - end - end - - context "short form resource with no replacements" do - let(:replacements) { {} } - let(:definition) do - [:rest_api, "AWS::ApiGateway::RestApi", name: "demo-test" ] - end - - it "cloudformation format" do - # pp resource # uncomment to see and debug - # pp resource.attributes # uncomment to see and debug - # pp resource.properties # uncomment to see and debug - expect(resource.logical_id).to eq "RestApi" - expect(resource.type).to eq "AWS::ApiGateway::RestApi" - properties = resource.properties - expect(properties[:Name]).to eq "demo-test" - end - end - -end - diff --git a/spec/lib/jets/cfn/resource/route53/record_set_spec.rb b/spec/lib/jets/cfn/resource/route53/record_set_spec.rb deleted file mode 100644 index 2602561dd..000000000 --- a/spec/lib/jets/cfn/resource/route53/record_set_spec.rb +++ /dev/null @@ -1,25 +0,0 @@ -describe Jets::Cfn::Resource::Route53::RecordSet do - - context 'default' do - let(:record_set) do - Jets::Cfn::Resource::Route53::RecordSet.new - end - - it "record_set" do - allow(Jets.config.domain).to receive(:name).and_return("demo-test.example.com") - allow(Jets.config.domain).to receive(:hosted_zone_name).and_return("example.com") - allow(Jets::Cfn::Resource::ApiGateway::RestApi).to receive(:internal_logical_id).and_return("fakerestapi") - - expect(record_set.logical_id).to eq "DnsRecord" - properties = record_set.properties - # pp properties # uncomment to debug - expect(properties[:HostedZoneName]).to eq "example.com." - expect(properties[:Comment]).to eq "DNS record managed by Jets" - expect(properties[:Name]).to eq "demo-test.example.com" - expect(properties[:Type]).to eq "CNAME" - expect(properties[:TTL]).to eq "60" # special casing - expect(properties[:ResourceRecords]).to eq ["!GetAtt DomainName.RegionalDomainName"] - end - end - -end diff --git a/spec/lib/jets/cfn/resource/standardizer_spec.rb b/spec/lib/jets/cfn/resource/standardizer_spec.rb deleted file mode 100644 index 44278520f..000000000 --- a/spec/lib/jets/cfn/resource/standardizer_spec.rb +++ /dev/null @@ -1,101 +0,0 @@ -describe Jets::Cfn::Resource::Standardizer do - let(:standardizer) { Jets::Cfn::Resource::Standardizer.new(definition) } - - context "long form" do - let(:definition) do - { - "RestApi": { - type: "AWS::ApiGateway::RestApi", - properties: { - name: "demo-test" - } - } - } - end - it "template" do - template = standardizer.template - expect(template).to eq( - {:RestApi=>{:Type=>"AWS::ApiGateway::RestApi", :Properties=>{:Name=>"demo-test"}}} - ) - end - end - - context "medium form with properties" do - let(:definition) do - [:rest_api, - type: "AWS::ApiGateway::RestApi", - properties: { name: "demo-test" } - ] - end - it "template" do - template = standardizer.template - expect(template).to eq( - {:RestApi=>{:Type=>"AWS::ApiGateway::RestApi", :Properties=>{:Name=>"demo-test"}}} - ) - end - end - - context "medium form with empty properties" do - let(:definition) do - [:rest_api, - type: "AWS::ApiGateway::RestApi", - properties: { } # empty - ] - end - it "template" do - template = standardizer.template - expect(template).to eq( - {:RestApi=>{:Type=>"AWS::ApiGateway::RestApi"}} - ) - end - end - - context "medium form with no properties" do - let(:definition) do - [:rest_api, - type: "AWS::ApiGateway::RestApi" - ] - end - it "template" do - template = standardizer.template - expect(template).to eq( - {:RestApi=>{:Type=>"AWS::ApiGateway::RestApi"}} - ) - end - end - - context "short form with properties" do - let(:definition) do - [:sns_topic, "AWS::SNS::Topic", - display_name: "my name"] - end - it "template" do - expect(standardizer.template).to eq( - {:SnsTopic=>{:Type=>"AWS::SNS::Topic", :Properties=>{:DisplayName=>"my name"}}} - ) - end - end - - context "short form with empty properties" do - let(:definition) do - [:sns_topic, "AWS::SNS::Topic", {}] - end - it "template" do - puts standardizer.template - expect(standardizer.template).to eq( - {:SnsTopic=>{:Type=>"AWS::SNS::Topic"}} - ) - end - end - - context "short form with no properties" do - let(:definition) do - [:sns_topic, "AWS::SNS::Topic"] - end - it "template" do - expect(standardizer.template).to eq( - {:SnsTopic=>{:Type=>"AWS::SNS::Topic"}} - ) - end - end -end diff --git a/spec/lib/jets/cfn/template_spec.rb b/spec/lib/jets/cfn/template_spec.rb deleted file mode 100644 index ffafb7612..000000000 --- a/spec/lib/jets/cfn/template_spec.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -describe Jets::Cfn::Template do - let(:path) { '/tmp/test.txt' } - let(:template) do - described_class.new(path, s3_bucket: s3_bucket) - end - - context 'without s3_bucket' do - let(:s3_bucket) { nil } - let(:body) { 'long file body' } - - it 'should read file from disk' do - expect(template).to receive(:body).and_return(body) - expect(template.send(:from_path)[:template_body]).to eq body - end - end - - context 'with s3_bucket' do - let(:s3_bucket) { 'test' } - let(:url) { "https://s3.amazonaws.com/#{s3_bucket}/test" } - - it 'should upload file to s3' do - expect(template).to receive(:upload_file_to_s3).and_return(url) - expect(template.send(:from_s3)[:template_url]).to eq url - end - end -end diff --git a/spec/lib/jets/cli/curl/request_spec.rb b/spec/lib/jets/cli/curl/request_spec.rb new file mode 100644 index 000000000..25d3c3680 --- /dev/null +++ b/spec/lib/jets/cli/curl/request_spec.rb @@ -0,0 +1,38 @@ +describe Jets::CLI::Curl::Request do + let :request do + described_class.new(options) + end + + describe "jets url /" do + let :options do + # Example: + # { + # function: "controller", + # verbose: true, + # request: "GET", + # headers: {}, + # trim: true, + # data: "@data.json", + # path: "/" + # } + {path: "/", trim: true} + end + + it "convert payload" do + hash = JSON.parse(request.payload) # payload is a JSON string + expect(hash["rawPath"]).to eq "/" + end + + # Sanity check + it "invoke" do + mocked_response = {statusCode: 200, body: "body"} + allow(request).to receive(:invoke).and_return(mocked_response) + # stub methods + allow(request).to receive(:warn) + allow(request).to receive(:function_name) + + response = request.run + expect(response).to eq mocked_response + end + end +end diff --git a/spec/lib/jets/controller/authorization_spec.rb b/spec/lib/jets/controller/authorization_spec.rb deleted file mode 100644 index 4ca3a4b63..000000000 --- a/spec/lib/jets/controller/authorization_spec.rb +++ /dev/null @@ -1,41 +0,0 @@ -class TestAuthController < Jets::Controller::Base - authorizer "main#protect" -end - -class TestAuthOnlyController < Jets::Controller::Base - authorizer "main#protect", only: [:index] -end - -class TestAuthExceptController < Jets::Controller::Base - authorizer "main#protect", only: [:show] -end - -describe Jets::Controller::Decorate::Authorization do - context "no filter options" do - let(:controller) { TestAuthController } - it "logical_id" do - logical_id = controller.authorizer_logical_id_for("index") - expect(logical_id).to eq "MainAuthorizerProtectAuthorizer" - end - end - - context "only filter" do - let(:controller) { TestAuthOnlyController } - it "logical_id" do - logical_id = controller.authorizer_logical_id_for("index") - expect(logical_id).to eq "MainAuthorizerProtectAuthorizer" - logical_id = controller.authorizer_logical_id_for("show") - expect(logical_id).to be nil - end - end - - context "except filter" do - let(:controller) { TestAuthExceptController } - it "logical_id" do - logical_id = controller.authorizer_logical_id_for("index") - expect(logical_id).to be nil - logical_id = controller.authorizer_logical_id_for("show") - expect(logical_id).to eq "MainAuthorizerProtectAuthorizer" - end - end -end diff --git a/spec/lib/jets/controller/base_spec.rb b/spec/lib/jets/controller/base_spec.rb deleted file mode 100644 index 9a1dd3a5b..000000000 --- a/spec/lib/jets/controller/base_spec.rb +++ /dev/null @@ -1,173 +0,0 @@ -class TestSimpleController < Jets::Controller::Base - layout :application - - def handler1; end - def handler2; end -end - -describe Jets::Controller::Base do - before(:each) { silence_loggers! } - after(:each) { restore_loggers! } - - let(:controller) do - rack_env = Jets::Controller::RackAdapter::Env.new(event, context).convert - TestSimpleController.new(event, context, meth, rack_env) - end - let(:context) { nil } - let(:meth) { "index" } - - context "class methods" do - it "responds to rescue_from method" do - expect(Jets::Controller::Base.respond_to?(:rescue_from)).to be true - end - end - - context "general" do - let(:event) { {} } - it "lambda_functions returns public user-defined methods" do - expect(controller.lambda_functions).to eq( - [:handler1, :handler2] - ) - end - - it "layout set to application" do - expect(controller.class._layout).to eq :application - end - end - - context "normal lambda function integration request" do - let(:event) { {"key1" => "value1", "key2" => "value2"} } - end - - context "AWS_PROXY lambda proxy integration request from api gateway" do - let(:event) { json_file("spec/fixtures/dumps/api_gateway/request.json") } - - it "#render returns rack triplet" do - body = controller.send(:render, json: {"my": "data"}) - expect(controller.status).to eq 200 - expect(controller.headers).to be_a(Hash) - expect(body).to eq "{\"my\":\"data\"}" - end - - # Spec is to help understand the AWS_PROXY request format and - # help document out important elements. - it "expects AWS_PROXY request format" do - event = controller.event - # Example of AWS_PROXY compatible request format - # { - # "statusCode": 200, - # "body": "must be a string, even if it is json, it should be a string" - # } - expect(event["resource"]).to eq "/posts" - expect(event["path"]).to eq "/posts" - expect(event["httpMethod"]).to eq "POST" - expect(event["headers"]).to be_a(Hash) - expect(event["queryStringParameters"]).to be_a(Hash) # or nil - expect(event["pathParameters"]).to eq nil # or Hash - expect(event["stageVariables"]).to eq nil # or Hash - expect(event["requestContext"]).to be_a(Hash) - expect(event["body"]).to be_a(String) - expect(event["isBase64Encoded"]).to eq false - end - - it "adds cors headers" do - body = controller.send(:render, json: {"my": "data"}) - expect(controller.headers.keys).to_not include("Access-Control-Allow-Origin") - expect(controller.headers.keys).to_not include("Access-Control-Allow-Credentials") - end - end - - context "json passed in body" do - let(:event) do - { - "queryStringParameters" => {"qs-key" => "qs-value"}, - "pathParameters" => {"path-key" => "path-value"}, - "body" => "{\"body-key1\": \"body-value1\", \"body-key2\": \"body-value2\"}" - } - end - it "params merges all types of parameters together" do - params = controller.send(:params) - expect(params.keys.sort).to eq(%w[action body-key1 body-key2 controller path-key qs-key]) - end - end - - context "invalid json passed in body" do - let(:event) do - { - "body" => "{\"body-key1mm.,,, \"body-key2\": \"body-value2\"}" - } - end - it "not error" do - params = controller.send(:params) - expect(params.keys).to eq(["controller", "action"]) - end - end - - context "post data from form with content-type application/x-www-form-urlencoded" do - let(:meth) { "create" } - let(:event) do - { - "headers" => { - "content-type" => "application/x-www-form-urlencoded", - }, - "body" => "article%5Btitle%5D=test1&article%5Bbody%5D=test2&article%5Bpublished%5D=yes&commit=Submit" - } - end - it "parse application/x-www-form-urlencoded form data" do - params = controller.send(:params) - h = params.to_unsafe_hash - # Another spec is channging the controller action and controller name - # Quick fix for spec. - h.delete("controller") - h.delete("action") - expect(h).to eq({ - "article" => { - "title" => "test1", - "body" => "test2", - "published" => "yes" - }, - "commit" => "Submit", - }) - end - end - - context "stores" do - let(:meth) { "index" } - let(:event) { json_file("spec/fixtures/dumps/api_gateway/stores/index.json") } - - it "headers" do - controller.set_header("Custom", "MyHeader") - expect(controller.response.headers).to eq("Custom" => "MyHeader") - end - - it "process headers" do - resp = StoresController.process(event, {}, :index) - expect(resp["headers"]["Set-Cookie"]).to eq "foo=bar" - end - end - - context "posts index" do - let(:meth) { "index" } - let(:event) { json_file("spec/fixtures/dumps/api_gateway/posts/index.json") } - - it "new adapter" do - resp = PostsController.process(event, {}, :index) - expect(resp['statusCode']).to eq 200 - expect(resp['headers']).to include('X-Runtime') # confirm going through full middleware stack - # expect(resp['headers']['x-jets-base64']).to eq "no" # no longer sending in jets v5 - end - end - - describe "#log_finish" do - let(:event) { json_file("spec/fixtures/dumps/api_gateway/request.json") } - - it 'logs completion' do - status = 200 - took = "1.500" - expected_event_log = "Completed Status Code #{status} in #{took}s" - expect(Jets.logger).to receive(:info).with(expected_event_log) - - controller.log_finish(status: status, took: took) - end - end -end diff --git a/spec/lib/jets/controller/handler/apigw_spec.rb b/spec/lib/jets/controller/handler/apigw_spec.rb deleted file mode 100644 index 6da577439..000000000 --- a/spec/lib/jets/controller/handler/apigw_spec.rb +++ /dev/null @@ -1,180 +0,0 @@ -describe Jets::Controller::Handler::Apigw do - let(:apigw) do - rack_env = Jets::Controller::RackAdapter::Env.new(event, context).convert - Jets::Controller::Handler::Apigw.new( - event, - context, - 'controller', - 'index', - rack_env - ) - end - let(:context) { nil } - let(:event) { json_file("spec/fixtures/dumps/api_gateway/posts/index.json") } - let(:default_status) { 200 } - let(:default_body) { StringIO.new(body_text) } - let(:body_text) { 'Test body' } - let(:default_headers) do - { - "Content-Type"=>"application/json", - "x-jets-base64"=>"no", - "Set-Cookie"=>"rack.session=BAh7B0kiD3Nlc3Npb25faWQGOgZFVG86HVJhY2s6OlNlc3Npb246OlNlc3Npb25JZAY6D0BwdWJsaWNfaWRJIkUxZDA2NTRiMDE1NDJjZGYzM2UzMGM3YmI0NTM1MTQ0MzY2N2I3Y2YwN2ZlZGMxMmE0MTE2MzQ1YWVhYzk2MTJiBjsARkkiCGZvbwY7AEZJIgtiYXJiYXIGOwBU--e6313ca3edc4486e1482180d15273fe1d20750ef; path=/; HttpOnly", - "access-control-allow-origin"=>"*", - "access-control-allow-credentials"=>"true", - "X-Runtime"=>"0.017019" - } - end - let(:multi_value_headers) { - { - "Content-Type"=>"application/json", - "x-jets-base64"=>"no", - "Set-Cookie"=>[ - "cookie_a=first.cookie; path=/; HttpOnly", - "cookie_b=second.cookie; path=/; HttpOnly", - ], - "access-control-allow-origin"=>"*", - "access-control-allow-credentials"=>"true", - "X-Runtime"=>"0.017019" - } - } - - describe '#convert_to_api_gateway' do - let(:convert) { apigw.convert_to_api_gateway(default_status, default_headers, default_body) } - - it 'returns hash' do - expect(convert).to be_a(Hash) - end - - context 'key values' do - it 'statusCode' do - expect(convert['statusCode']).to eq(default_status) - end - - context 'body' do - it 'body is a string' do - result = apigw.convert_to_api_gateway(default_status, default_headers, body_text) - expect(result['body']).to eq(body_text) - end - - it 'body is an object' do - expect(convert['body']).to eq(body_text) - end - - it 'base 64 body' do - default_headers['x-jets-base64'] = 'yes' - result = apigw.convert_to_api_gateway(default_status, default_headers, default_body) - expect(result['body']).to eq(Base64.encode64(body_text)) - end - end - - context 'isBase64Encoded' do - it 'encoded' do - default_headers['x-jets-base64'] = 'yes' - result = apigw.convert_to_api_gateway(default_status, default_headers, default_body) - expect(result['isBase64Encoded']).to eq(true) - end - - it 'not encoded' do - expect(convert['isBase64Encoded']).to eq(false) - end - end - - context 'adjust_for_elb' do - it 'from elb' do - allow(apigw).to receive(:from_elb?).and_return(true) - expect(convert.keys).to include('statusDescription') - end - - it 'not from elb' do - allow(apigw).to receive(:from_elb?).and_return(false) - expect(convert.keys).not_to include('statusDescription') - end - end - end - - it 'invokes #add_response_headers' do - resp_hash = convert.select { |key, _| %w[statusCode body isBase64Encoded].include?(key) } - expect(apigw).to receive(:add_response_headers).with(resp_hash, default_headers) - apigw.convert_to_api_gateway(default_status, default_headers, body_text) - end - - it 'invokes #adjust_for_elb' do - resp_hash = convert.select { |key, _| %w[statusCode body isBase64Encoded headers].include?(key) } - expect(apigw).to receive(:adjust_for_elb).with(resp_hash) - apigw.convert_to_api_gateway(default_status, default_headers, body_text) - end - end - - describe '#add_response_headers' do - let(:result) { Hash.new } - let(:call) { apigw.add_response_headers(result, multi_value_headers) } - - context 'headers' do - before { call } - - it 'adds headers key to hash' do - expect(result.keys).to include('headers') - end - - it 'includes only non multivalue headers' do - headers = result['headers'] - multi_value_headers.each do |key, val| - if val.is_a?(Array) - expect(headers.keys).not_to include(key) - else - expect(headers[key]).to eq(val) - end - end - end - end - - context 'multi value headers' do - before { call } - - context 'has multi value headers' do - it 'adds multiValueHeaders key to hash' do - expect(result.keys).to include('multiValueHeaders') - end - - it 'includes only multivalue headers' do - headers = result['multiValueHeaders'] - multi_value_headers.each do |key, val| - if val.is_a?(Array) - expect(headers[key]).to eq(val) - else - expect(headers.keys).not_to include(key) - end - end - end - end - - context 'does not have multi value headers' do - let(:call) { apigw.add_response_headers(result, default_headers) } - - it 'omits multiValueHeaders from hash' do - expect(result.keys).not_to include('multiValueHeaders') - end - end - end - end - - describe '#adjust_for_elb' do - let(:resp) { { 'statusCode' => 200 } } - let(:call) { apigw.adjust_for_elb(resp) } - - it 'adds status description if from ELB' do - allow(apigw).to receive(:from_elb?).and_return(true) - expected_status = "200 #{Rack::Utils::HTTP_STATUS_CODES[200]}" - call - - expect(resp['statusDescription']).to eq(expected_status) - end - - it 'adds status description if from ELB' do - allow(apigw).to receive(:from_elb?).and_return(false) - call - - expect(resp['statusDescription']).to be_nil - end - end -end diff --git a/spec/lib/jets/controller/middleware/local/apigw_spec.rb b/spec/lib/jets/controller/middleware/local/apigw_spec.rb deleted file mode 100644 index 789bd413a..000000000 --- a/spec/lib/jets/controller/middleware/local/apigw_spec.rb +++ /dev/null @@ -1,14 +0,0 @@ -describe Jets::Controller::Middleware::Mimic::Apigw do - let(:route) do - matcher = Jets::Router::Matcher.new - matcher.find_by_env(env) # simpler version does not check constraints - end - let(:env) do - { "PATH_INFO" => "/posts/tung/edit", "REQUEST_METHOD" => "GET" } - end - - it "call" do - api_gateway = Jets::Controller::Middleware::Mimic::Apigw.new(route, env) - expect(api_gateway.event).to be_a(Hash) - end -end diff --git a/spec/lib/jets/controller/middleware/main_spec.rb b/spec/lib/jets/controller/middleware/main_spec.rb deleted file mode 100644 index 1bef88ea7..000000000 --- a/spec/lib/jets/controller/middleware/main_spec.rb +++ /dev/null @@ -1,39 +0,0 @@ -describe Jets::Controller::Middleware::Main do - before(:each) { silence_loggers! } - after(:each) { restore_loggers! } - - let(:main) { Jets::Controller::Middleware::Main.new(rack_env) } - let(:rack_env) do - rack_env = Jets::Controller::RackAdapter::Env.new(event, context).convert - - route = Jets::Router::Matcher.new.find_by_env(rack_env) - apigw = Jets::Controller::Middleware::Mimic::Apigw.new(route, rack_env) - - rack_env.merge!( - 'jets.controller' => apigw.controller, # mimic controller instance - 'jets.context' => apigw.context, # mimic context - 'jets.event' => apigw.event, # mimic event - 'jets.meth' => apigw.meth, - ) - - apigw.controller.define_singleton_method(:commit_flash) do - # noop hack to avoid commit_flash call. - # Since not full middleware stack is setup in this test - end - - rack_env - end - let(:context) { nil } - - context "posts index" do - let(:event) { json_file("spec/fixtures/dumps/api_gateway/posts/index.json") } - - it "call" do - triplet = Jets::Controller::Middleware::Main.call(rack_env) - status, headers, body = triplet - expect(status).to eq 200 - expect(headers).to be_a(Hash) - expect(body.first).to eq "{\"action\":\"index\",\"posts\":[]}" - end - end -end diff --git a/spec/lib/jets/controller/parameters_filter_spec.rb b/spec/lib/jets/controller/parameters_filter_spec.rb deleted file mode 100644 index 047251754..000000000 --- a/spec/lib/jets/controller/parameters_filter_spec.rb +++ /dev/null @@ -1,49 +0,0 @@ -class ParametersFilterTest - include Jets::Controller::Decorate::Logging - attr_reader :parameter_filter - def initialize(filter_values) - @parameter_filter = ActiveSupport::ParameterFilter.new(filter_values) - end -end - -RSpec.describe "Jets::Controller::Decorate::Logging" do - describe "#filter_json_log" do - let(:result) do - ParametersFilterTest.new(filtered_values).filter_json_log(json_text) - end - - let(:json_text) do - JSON.dump( - password: "private_text", - password_confirmation: 'private_text_wrong', - name: 'Joe', - bio: 'Ruby on jets!' - ) - end - - context 'When filtered_parameters is not empty' do - let(:filtered_values) { %i[password password_confirmation] } - - it 'Should return a new json with filtered_parameters masked as [FILTERED]' do - expect(result).to eq("{\"password\":\"[FILTERED]\",\"password_confirmation\":\"[FILTERED]\",\"name\":\"Joe\",\"bio\":\"Ruby on jets!\"}") - end - end - - context 'When filtered_parameters is empty' do - let(:filtered_values) { [] } - - it 'Should return the original json text' do - expect(result).to eq(json_text) - end - end - - context 'When json_text has wrong format' do - let(:filtered_values) { %i[password password_confirmation] } - let(:json_text) { "just a text" } - - it 'Should return a [FILTERED]' do - expect(result).to eq("[FILTERED]") - end - end - end -end diff --git a/spec/lib/jets/controller/params_spec.rb b/spec/lib/jets/controller/params_spec.rb deleted file mode 100644 index 5f4711de5..000000000 --- a/spec/lib/jets/controller/params_spec.rb +++ /dev/null @@ -1,199 +0,0 @@ -describe Jets::Controller::Request::Compat::Params do - let(:controller) do - context = nil - rack_env = Jets::Controller::RackAdapter::Env.new(event, context).convert - PostsController.new(event, nil, "update", rack_env) - end - - context "with unicode" do - context "body parameter" do - let(:event) do - { - "headers" => { - "content-type" => "application/x-www-form-urlencoded; charset=UTF-8" - }, - "body" => "name=#{CGI.escape("太郎")}&location=#{CGI.escape("東京")}" - } - end - it "decode values" do - params = controller.send(:params) - expect(params["name"]).to eq "太郎" - end - end - - context "path parameter" do - let(:event) do - { - "pathParameters" => { - "name" => CGI.escape("太郎"), - "location" => CGI.escape("東京"), - } - } - end - it "decode values" do - params = controller.send(:params) - expect(params["name"]).to eq "太郎" - end - end - - context "query parameter" do - let(:event) do - { - "queryStringParameters" => { - "name" => [ - CGI.escape("太郎"), - CGI.escape("鈴木"), - ], - "location" => CGI.escape("東京"), - } - } - end - it "decode values" do - params = controller.send(:params) - expect(params["name"][0]).to eq "太郎" - expect(params["name"][1]).to eq "鈴木" - end - end - end - - context "update action called" do - let(:event) do - { - "headers" => { - "content-type" => "application/x-www-form-urlencoded; charset=UTF-8" - }, - "body" => "name=John&location=Boston" - } - end - it "params" do - params = controller.send(:params) - expect(params.keys).to include("name") - end - - end - - context "real put request from api gateway to aws lambda" do - let(:event) do - { - "headers" => { - "Content-Type"=>"application/x-www-form-urlencoded" - }, - "body" => - "utf8=%E2%9C%93&authenticity_token=TRdBlqH9zQ1TW7MBeZ38pb4IeTPf8MJOmtPM6ft8XW2g3IoD3ZBBQ5%2BVa8H0qkeDg%2B%2Bw%2BwueYvkphMH3r3gCgw%3D%3D&post%5Btitle%5D=Test+Post+1&commit=Submit" - } - end - it "params2" do - params = controller.send(:params) - expect(params["post"]["title"]).to eq "Test Post 1" - end - end - - context "multipart form data in body" do - context "simple form" do - let(:event) { multipart_event(:simple_form) } - it "params" do - params = controller.send(:params) - expect(params["name"]).to eq "Tung" - expect(params["title"]).to eq "Mr" - end - end - - context "binary" do - let(:event) { multipart_event(:binary) } - it "params" do - # Example content-type: "multipart/form-data; boundary=----WebKitFormBoundaryB78dBBqs2MSBKMoX", - # pp event - - params = controller.send(:params) - expect(params["submit-name"]).to eq "Larry" - expect(params["files"]).to be_a(ActionDispatch::Http::UploadedFile) - # expect(params["files"]["filename"]).to eq "rack-logo.png" - # expect(params["files"]["type"]).to eq "image/png" - # expect(params["files"]["name"]).to eq "files" - # expect(params["files"]["tempfile"]).to be_a(Tempfile) - end - end - - context "nested" do - let(:event) { multipart_event(:nested) } - it "params" do - params = controller.send(:params) - expect(params["foo"]["submit-name"]).to eq "Larry" - expect(params["foo"]["files"]).to be_a(ActionDispatch::Http::UploadedFile) - # expect(params["foo"]["files"]["filename"]).to eq "file1.txt" - # expect(params["foo"]["files"]["type"]).to eq "text/plain" - # expect(params["foo"]["files"]["name"]).to eq "foo[files]" - # expect(params["foo"]["files"]["tempfile"]).to be_a(Tempfile) - end - end - - context "base64 encoded simple form" do - let(:event) { multipart_event(:simple_form, base64: true) } - it "params" do - params = controller.send(:params) - expect(params["name"]).to eq "Tung" - expect(params["title"]).to eq "Mr" - end - end - end - - describe "#filtered_params" do - let(:event) { {} } - - context "With plain filtered parameters" do - let(:event) { multipart_event(:simple_form) } - - it "Masks provided keys as [FILTERED]" do - controller.request.set_header("action_dispatch.parameter_filter", [:title]) - filtered_params = controller.send(:filtered_parameters) - expect(filtered_params).to eq( - "action" => "index", - "controller" => "posts", - "name" => "Tung", - "title" => "[FILTERED]" - ) - end - end - - context "With nested filtered parameters" do - let(:event) { multipart_event(:nested) } - - it "Masks provided keys as [FILTERED]" do - controller.request.set_header("action_dispatch.parameter_filter", ["foo.submit-name"]) - filtered_params = controller.send(:filtered_parameters) - expect(filtered_params["foo"]["submit-name"]).to eq("[FILTERED]") - end - end - - context "With nested array filtered parameters" do - let(:event) do - { - "headers" => { - "content-type" => "application/x-www-form-urlencoded; charset=UTF-8" - }, - "body" => "users[0][name]=John&users[0][location]=Boston&users[1][name]=Luke&users[1][location]=Chicago" - } - end - - it "Masks provided keys as [FILTERED]" do - controller.request.set_header("action_dispatch.parameter_filter", [/users\.\d+\.name/]) - - filtered_params = controller.send(:filtered_parameters) - expect(filtered_params).to eq( - "action" => "index", - "controller" => "posts", - "users" => { - "0" => { - "name" => "[FILTERED]", - "location" => "Boston" - }, - "1" => { - "name" => "[FILTERED]", - "location" => "Chicago" - } - } - ) - end - end - end -end diff --git a/spec/lib/jets/controller/rack_adapter/env_spec.rb b/spec/lib/jets/controller/rack_adapter/env_spec.rb deleted file mode 100644 index 0ae9bcff6..000000000 --- a/spec/lib/jets/controller/rack_adapter/env_spec.rb +++ /dev/null @@ -1,50 +0,0 @@ -describe Jets::Controller::RackAdapter::Env do - let(:rack_env) { Jets::Controller::RackAdapter::Env.new(event, context) } - let(:context) { nil } - - context "books list" do - let(:event) { json_file("spec/fixtures/dumps/api_gateway/books/list.json") } - it "convert" do - env = rack_env.convert - # pp env # uncomment to debug - expect(env).to be_a(Hash) - expect(env['REQUEST_METHOD']).to eq "GET" - expect(env['SERVER_NAME']).to eq("uhghn8z6t1.execute-api.us-east-1.amazonaws.com") - expect(env['QUERY_STRING']).to eq("a=1&b=2&c%5B%5D=4") - expect(env['PATH_INFO']).to eq("/books/list") - expect(env['REMOTE_ADDR']).to eq("69.42.1.180, 54.239.203.100") - expect(env['REQUEST_URI']).to eq("https://uhghn8z6t1.execute-api.us-east-1.amazonaws.com/books/list?a=1&b=2&c%5B%5D=4") - expect(env['HTTP_USER_AGENT']).to eq("PostmanRuntime/6.4.1") - expect(env["rack.input"]).not_to be nil - end - end - - context "mount" do - let(:event) { json_file("spec/fixtures/dumps/api_gateway/books/mounted_list.json") } - it "convert" do - env = rack_env.convert - # pp env # uncomment to debug - expect(env).to be_a(Hash) - expect(env['REQUEST_METHOD']).to eq "GET" - expect(env['SERVER_NAME']).to eq("uhghn8z6t1.execute-api.us-east-1.amazonaws.com") - expect(env['QUERY_STRING']).to eq("a=1&b=2") - expect(env['PATH_INFO']).to eq("/mount/books/list") - expect(env['REMOTE_ADDR']).to eq("69.42.1.180, 54.239.203.100") - expect(env['REQUEST_URI']).to eq("https://uhghn8z6t1.execute-api.us-east-1.amazonaws.com/mount/books/list?a=1&b=2") - expect(env['HTTP_USER_AGENT']).to eq("PostmanRuntime/6.4.1") - expect(env["rack.input"]).not_to be nil - end - end - - context "empty body" do - let(:event) { json_file("spec/fixtures/dumps/api_gateway/posts/show.json") } - it "convert" do - env = rack_env.convert - # pp env # uncomment to debug - expect(env).to be_a(Hash) - expect(env['REQUEST_METHOD']).to eq "GET" - # Rack always assigns an StringIO to "rack.input" - expect(env["rack.input"]).not_to be nil - end - end -end diff --git a/spec/lib/jets/controller/redirection_spec.rb b/spec/lib/jets/controller/redirection_spec.rb deleted file mode 100644 index d9e418095..000000000 --- a/spec/lib/jets/controller/redirection_spec.rb +++ /dev/null @@ -1,40 +0,0 @@ -class RedirectionController < Jets::Controller::Base -end - -describe RedirectionController do - let(:controller) do - context = Jets::Controller::Middleware::Mimic::LambdaContext.new - rack_env = Jets::Controller::RackAdapter::Env.new(event, context).convert - RedirectionController.new(event, context, meth, rack_env) - end - let(:context) { nil } - let(:meth) { "index" } - - context "localhost" do - context "rack headers with Host and origin" do - # the fixture uses actual data capture from a local request - let(:event) do - json_file("spec/fixtures/dumps/rack/posts/create.json") - end - it "redirect_to" do - body = controller.send(:redirect_to, "/myurl", status: 301) - expect(controller.status).to eq 301 - expect(controller.headers["Location"]).to eq "http://localhost:8888/myurl" - end - end - end - - context "amazonaws.com" do - context "api gateway headers with Host and X-Forwarded-Proto" do - # the fixture uses actual data capture from a api gateay request - let(:event) do - json_file("spec/fixtures/dumps/api_gateway/posts/create.json") - end - it "redirect_to adds the stage name to the url" do - body = controller.send(:redirect_to, "/test/myurl", status: 301) - expect(controller.status).to eq 301 - expect(controller.headers["Location"]).to eq "https://8s1wzivnz4.execute-api.us-east-1.amazonaws.com/test/myurl" - end - end - end -end diff --git a/spec/lib/jets/controller/request_spec.rb b/spec/lib/jets/controller/request_spec.rb deleted file mode 100644 index c712962bf..000000000 --- a/spec/lib/jets/controller/request_spec.rb +++ /dev/null @@ -1,50 +0,0 @@ -describe Jets::Controller::Request do - let(:req) do - rack_env = Jets::Controller::RackAdapter::Env.new(event, {}).convert - Jets::Controller::Request.new(rack_env: rack_env, event: event) - end - - context "ajax request" do - let(:event) { json_file("spec/fixtures/dumps/api_gateway/xhr-delete.json") } - - it "xhr" do - expect(req.xhr?).to be true - end - - it "host" do - expect(req.host).to eq "localhost" - end - - it "path" do - expect(req.path).to eq("/articles/5") - end - - it "headers" do - expect(req.headers).to be_a(Hash) - end - - it "cookies" do - expect(req.cookies).to be_a(Hash) - end - - it "script_name" do - expect(req.script_name).to eq '' - end - end - - context "posts" do - let(:event) { json_file("spec/fixtures/dumps/api_gateway/posts/index.json") } - - it "ssl?" do - expect(req.ssl?).to be true - end - end - - context "request with cookie" do - let(:event) { json_file("spec/fixtures/dumps/api_gateway/request-with-cookies.json") } - - it "ssl?" do - expect(req.cookies).to be_a(Hash) - end - end -end diff --git a/spec/lib/jets/controller/rescuable_spec.rb b/spec/lib/jets/controller/rescuable_spec.rb deleted file mode 100644 index 6f8753759..000000000 --- a/spec/lib/jets/controller/rescuable_spec.rb +++ /dev/null @@ -1,34 +0,0 @@ -class RescuableController < Jets::Controller::Base - rescue_from StandardError, with: :error_handler - - def index - raise StandardError - end - - private - - def error_handler - render json: { error: "there was an error" }, status: 500 - end -end - -describe Jets::Controller::Base do - context RescuableController do - before(:each) { silence_loggers! } - after(:each) { restore_loggers! } - - let(:controller) do - RescuableController.new({}, nil, :index, {}) - end - - it "rescue_handlers includes error_handler only" do - expect(controller.class.rescue_handlers).to eq [["StandardError", :error_handler]] - end - - it "rescues the error raised in index" do - response = controller.dispatch! - expect(response[0]).to eq 500 - expect(response[2].first).to eq({error: "there was an error"}.to_json) - end - end -end diff --git a/spec/lib/jets/controller/response_spec.rb b/spec/lib/jets/controller/response_spec.rb deleted file mode 100644 index 17299a5bb..000000000 --- a/spec/lib/jets/controller/response_spec.rb +++ /dev/null @@ -1,21 +0,0 @@ -describe Jets::Controller::Response do - let(:resp) { Jets::Controller::Response.new } - - context "resp" do - it "headers" do - expect(resp.headers).to eq({}) - resp.headers["Set-Cookie"] = "foo=bar" - resp.set_header("AnotherHeader", "MyValue") - expect(resp.headers).to eq({"Set-Cookie" => "foo=bar", "AnotherHeader" => "MyValue"}) - end - - it "cookies" do - resp.set_cookie(:favorite_food, "chocolate cookie") - resp.set_cookie(:favorite_color, "yellow") - resp.delete_cookie(:favorite_food) - cookie = resp.headers["Set-Cookie"] - expect(cookie).to include("favorite_color=yellow") - expect(cookie).to include("max-age=0; expires=Thu, 01 Jan 1970 00:00:00") - end - end -end diff --git a/spec/lib/jets/dotenv_spec.rb b/spec/lib/jets/dotenv_spec.rb deleted file mode 100644 index ef3a933c5..000000000 --- a/spec/lib/jets/dotenv_spec.rb +++ /dev/null @@ -1,71 +0,0 @@ -describe Jets::Dotenv do - describe "#load!" do - it "ignores *.remote files unless JETS_ENV_REMOTE=1" do - env = Jets::Dotenv.new(false).load! - expect(env["ONLY_REMOTE"]).to eq nil - expect(env["ONLY_LOCAL"]).to eq "1" - end - it "ignores *.local files when JETS_ENV_REMOTE=1" do - env = Jets::Dotenv.new(true).load! - expect(env["ONLY_REMOTE"]).to eq "1" - expect(env["ONLY_LOCAL"]).to eq nil - end - it "replaces ssm: with SSM parameters prefixed with ///" do - relative_path = "authenticated-url" - value = "https://foo:bar@example.com" - - expect(::Dotenv).to receive(:load).and_return( - "AUTENTICATED_URL" => "ssm:#{relative_path}", - ) - - username_response_double = double( - Aws::SSM::Types::GetParameterResult, - parameter: Aws::SSM::Types::Parameter.new(value: value), - ) - expect_any_instance_of(Aws::SSM::Client).to receive(:get_parameter) - .with(name: "/demo/test/#{relative_path}", with_decryption: true) - .and_return(username_response_double) - - env = Jets::Dotenv.new.load! - - expect(env.fetch("AUTENTICATED_URL")).to eq value - expect(ENV.fetch("AUTENTICATED_URL")).to eq value - end - - it "replaces ssm:/ with SSM parameters from the provided path" do - absolute_path = "/absolute/path" - value = "foo-bar" - - expect(::Dotenv).to receive(:load).and_return( - "ABSOLUTE_VARIABLE" => "ssm:#{absolute_path}", - ) - - absolute_response_double = double( - Aws::SSM::Types::GetParameterResult, - parameter: Aws::SSM::Types::Parameter.new(value: value), - ) - expect_any_instance_of(Aws::SSM::Client).to receive(:get_parameter) - .with(name: absolute_path, with_decryption: true) - .and_return(absolute_response_double) - - - env = Jets::Dotenv.new.load! - - expect(env.fetch("ABSOLUTE_VARIABLE")).to eq value - expect(ENV.fetch("ABSOLUTE_VARIABLE")).to eq value - end - - it "aborts the process with a helpful error if an SSM parameter is not found at AWS" do - expect(::Dotenv).to receive(:load).and_return( - "ABSOLUTE_VARIABLE" => "ssm:/absolute/path", - ) - - expect_any_instance_of(Aws::SSM::Client).to receive(:get_parameter) - .and_raise(Aws::SSM::Errors::ParameterNotFound.new(Seahorse::Client::RequestContext.new, "")) - - expect { Jets::Dotenv.new.load! } - .to raise_error(SystemExit) - .and output(/Error loading/).to_stderr - end - end -end diff --git a/spec/lib/jets/functions/base_path_mapping_spec.rb b/spec/lib/jets/functions/base_path_mapping_spec.rb deleted file mode 100644 index 268d5e705..000000000 --- a/spec/lib/jets/functions/base_path_mapping_spec.rb +++ /dev/null @@ -1,53 +0,0 @@ -require "./engines/internal/app/functions/jets/base_path_mapping.rb" - -describe "base_path_mapping" do - let(:mapping) do - mapping = BasePathMapping.new(event, "fake_stage_name") - allow(mapping).to receive(:deployment_stack).and_return(deployment_stack) - allow(mapping).to receive(:deleting_parent?).and_return(deleting_parent) - allow(mapping).to receive(:delete) - allow(mapping).to receive(:create) - mapping - end - let(:deployment_stack) do - parameters = { - "RestApi" => "fake-rest-api", - "DomainName" => "fake-domain-name", - "BasePath" => base_path, - } - parameters = parameters.map { |k,v| OpenStruct.new(parameter_key: k, parameter_value: v) } - { - parameters: parameters, - root_id: "fake-parent-stack", - } - end - let(:event) do - JSON.load(<<~EOL) - { - "RequestType": "#{request_type}", - "ServiceToken": "arn:aws:lambda:us-west-2:112233445566:function:demo-dev-jets-base-path-20210105230228", - "ResponseURL": "https://cloudformation-custom-resource-response-uswest2.s3-us-west-2.amazonaws.com/arn%3Aaws%3Acloudformation%3Aus-west-2%3A112233445566%3Astack/demo-dev-ApiDeployment20210105230228-1D1N5RX92CRC5/32027ab0-4faa-11eb-aa8e-0a949e794ddf%7CBasePathMapping%7Cd931675a-7230-4820-aca4-dc5e054405af?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20210105T230346Z&X-Amz-SignedHeaders=host&X-Amz-Expires=7200&X-Amz-Credential=AKIA54RCMT6SEP3BXUV5%2F20210105%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Signature=56ad77cdfae5e19d259bcdddf8d53d300354067c021026de2d8f1e34d1b4ac06", - "StackId": "fake-stack-id", - "RequestId": "d931675a-7230-4820-aca4-dc5e054405af", - "LogicalResourceId": "BasePathMapping", - "ResourceType": "Custom::BasePathMapping", - "ResourceProperties": { - "ServiceToken": "arn:aws:lambda:us-west-2:112233445566:function:demo-dev-jets-base-path-20210105230228" - } - } - EOL - end - let(:request_type) { "Create" } - let(:deleting_parent) { false } - let(:base_path) { nil } - - # sanity check for syntax errors - context "typical update" do - it "update" do - expect(mapping).to receive(:rest_api_changed?).and_return(true) - expect(mapping).to receive(:delete) - expect(mapping).to receive(:create) - mapping.update - end - end -end diff --git a/spec/lib/jets/functions/base_path_spec.rb b/spec/lib/jets/functions/base_path_spec.rb deleted file mode 100644 index 0a36e7852..000000000 --- a/spec/lib/jets/functions/base_path_spec.rb +++ /dev/null @@ -1,28 +0,0 @@ -require "ostruct" -require "render_me_pretty" - -code = RenderMePretty.result("./engines/internal/app/functions/jets/base_path.rb", stage_name: "test") -# Hack to mimic lambda -eval %Q{ -class MainScope - #{code} -end -} - -describe "base_path" do - before do - allow(CfnResponse).to receive(:new).and_return(null) - end - let(:event) { null } - let(:context) { null } - let(:null) { double(:null).as_null_object } - - let(:main) do - MainScope.new - end - - # mainly test for silly syntax errors - it "BasePathMapping" do - main.lambda_handler(event: event, context: context) - end -end diff --git a/spec/lib/jets/functions/s3_bucket_config_spec.rb b/spec/lib/jets/functions/s3_bucket_config_spec.rb deleted file mode 100644 index 170003965..000000000 --- a/spec/lib/jets/functions/s3_bucket_config_spec.rb +++ /dev/null @@ -1,52 +0,0 @@ -ENV['CFN_RESPONSE_SEND'] = '1' -ENV['CFN_RESPONSE_VERBOSE'] = '0' -require "ostruct" - -code = IO.read("./engines/internal/app/shared/functions/jets/s3_bucket_config.rb") -# Hack: Seems to be the only way to mimic access the methods by include Cfnresponse like send_response -eval %Q{ - class MainScope - #{code} - end -} - -describe "s3_bucket_config" do - before do - # https://rubyrailroad.com/2014/01/23/how-to-ignore-ruby-puts-in-rspec-tests/ - allow($stdout).to receive(:write) - end - - let(:event) do - { - "RequestType" => "Create", - "ServiceToken" => "arn:aws:lambda:us-west-2:112233445566:function:demo-dev-my-bucket-s3_bucket_config", - "ResponseURL" => "https://cloudformation-custom-resource-response-uswest2.s3-us-west-2.amazonaws.com/arn-blah", - "StackId" => "arn:aws:cloudformation:us-west-2:112233445566:stack/blah", - "RequestId" => "61318e12-6c1a-44c4-bd8f-ce0b34f32026", - "LogicalResourceId" => "MyBucketS3BucketConfiguration", - "ResourceType" => "Custom::S3BucketConfiguration", - "ResourceProperties" => { - "ServiceToken" => "arn:aws:lambda:us-west-2:112233445566:function:demo-dev-my-bucket-s3_bucket_config", - "Bucket" => "my-bucket", - "NotificationConfiguration" => { - "TopicConfigurations" => [ - { - "Events" => [ "s3:ObjectCreated:*"], - "TopicArn" => "sns:arn:topic", - }, - ], - } - } - } - end - let(:context) do - OpenStruct.new(log_group_name: "fake-log-group", log_stream_name: "fake-log-stream") - end - let(:main) do - MainScope.new - end - - it "BucketConfigurator" do - main.lambda_handler(event: event, context: context) - end -end diff --git a/spec/lib/jets/internal/preheat_job_spec.rb b/spec/lib/jets/internal/preheat_job_spec.rb deleted file mode 100644 index 645f6a137..000000000 --- a/spec/lib/jets/internal/preheat_job_spec.rb +++ /dev/null @@ -1,28 +0,0 @@ -describe Jets::PreheatJob do - let(:job) do - Jets::PreheatJob.new({},{},nil) - end - - context "IAM policy" do - it "has a class_iam_policy with lambda::InvokeFunction" do - expect(Jets::PreheatJob.class_iam_policy).to include(hash_including( - :Action=>["lambda:InvokeFunction", "lambda:InvokeAsync"] - )) - end - end - - context "warm" do - it "calls all lambda functions with a warm request" do - expect(Jets::Preheat).to receive(:warm_all) - job.warm - end - end - - context "prewarm!" do - it "calls all lambda functions with a prewarm! request" do - expect(Jets::Preheat).to receive(:warm_all) - Jets::PreheatJob.prewarm! - end - end -end - diff --git a/spec/lib/jets/job/base_spec.rb b/spec/lib/jets/job/base_spec.rb deleted file mode 100644 index 4b8cdbbae..000000000 --- a/spec/lib/jets/job/base_spec.rb +++ /dev/null @@ -1,128 +0,0 @@ -describe Jets::Job::Base do - let(:null) { double(:null).as_null_object } - - # by the time the class is finished loading into memory the rate and - # cron configs will have been loaded so we can use them later to configure - # the lambda functions - context HardJob do - it "definitions" do - definitions = HardJob.all_public_definitions.keys - expect(definitions).to eq [:dig, :drive, :lift] - - dig_definition = HardJob.all_public_definitions[:dig] - expect(dig_definition).to be_a(Jets::Lambda::Definition) - - drive_definition = HardJob.all_public_definitions[:lift] - expect(drive_definition).to be_a(Jets::Lambda::Definition) - end - - it "definitions contains flatten Array structure" do - definitions = HardJob.definitions - expect(definitions.first).to be_a(Jets::Lambda::Definition) - - definition_names = definitions.map(&:name) - expect(definition_names).to eq(HardJob.all_public_definitions.keys) - end - - it "perform_now" do - resp = HardJob.perform_now(:dig, {}) - expect(resp).to eq(done: "digging") - end - - it "perform_later" do - allow(Jets::Commands::Call::Caller).to receive(:new).and_return(null) - allow(HardJob).to receive(:on_lambda?).and_return(true) - HardJob.perform_later(:dig, {}) - expect(Jets::Commands::Call::Caller).to have_received(:new) - end - end - - context EasyJob do - it "definitions" do - definitions = EasyJob.all_public_definitions.keys - expect(definitions).to eq [:sleep] - end - end - - describe "s3_event" do - context 'configuration' do - it "allows custom sns subscription properties" do - class HardJob - s3_event("s3-bucket", sns_subscription_properties: { FilterPolicy: { field: [{ "prefix": "some_value" }] }.to_json }) - def process_s3_event;end - end - - process_s3_event = HardJob.all_public_definitions[:process_s3_event] - definition = process_s3_event.associated_resources.first.definition.detect { |d| d.try(:dig, "{namespace}SnsSubscription", :Type) == "AWS::SNS::Subscription" } - expected_properties = { - :Endpoint=>"!GetAtt {namespace}LambdaFunction.Arn", - :Protocol=>"lambda", - :FilterPolicy=>"{\"field\":[{\"prefix\":\"some_value\"}]}", - :TopicArn=>"!Ref S3BucketSnsTopic" - } - - expect(definition.dig("{namespace}SnsSubscription", :Type)).to eq("AWS::SNS::Subscription") - expect(definition.dig("{namespace}SnsSubscription", :Properties)).to eq(expected_properties) - end - end - - context "s3 upload" do - it "s3_events" do - event = json_file("spec/fixtures/dumps/sns/s3_upload.json") - job = HardJob.new(event, {}, :dig) - expect(job.s3_events.first).to include("eventName") - expect(job.s3_events.first[:eventName]).to eq "ObjectCreated:Put" - expect(job.s3_events?).to eq true - - expect(job.s3_objects.first.key?("key")).to be true - expect(job.s3_objects.first[:key]).to eq "myfolder/subfolder/test.txt" - expect(job.s3_objects?).to eq true - end - end - end - - context 'sns_event' do - it 'sns_events' do - event = json_file("spec/fixtures/dumps/sns/sns_event.json") - job = HardJob.new(event, {}, :dig) - expect(job.sns_events.first.key?("body")).to be true - expect(job.sns_events.first[:body]).to eq "This is a sns hard job" - end - end - - context 'sqs_event' do - it 'sqs_events' do - event = json_file("spec/fixtures/dumps/sqs/sqs_event.json") - job = HardJob.new(event, {}, :dig) - expect(job.sqs_events.first.key?("message")).to be true - expect(job.sqs_events.first[:message]).to eq "This is a hard job" - end - end - - context "cloudwatch log event" do - it "log_event" do - event = json_file("spec/fixtures/dumps/logs/log_event.json") - job = HardJob.new(event, {}, :dig) - # uncomment to debug - # puts JSON.pretty_generate(job.event) - # puts JSON.pretty_generate(job.log_event) - - data = job.log_event - expect(data["messageType"]).to eq "DATA_MESSAGE" - expect(data.key?("logEvents")).to be true - end - end - - context "kinesis log event" do - it "kinesis_data" do - event = json_file("spec/fixtures/dumps/kinesis/records.json") - job = HardJob.new(event, {}, :dig) - # uncomment to debug - # puts JSON.pretty_generate(job.event) - # puts JSON.pretty_generate(job.kinesis_data) - - expect(job.event.key?("Records")).to be true - expect(job.kinesis_data).to eq(["hello world", "hello world", "hello world"]) - end - end -end \ No newline at end of file diff --git a/spec/lib/jets/klass_spec.rb b/spec/lib/jets/klass_spec.rb deleted file mode 100644 index d14d07eca..000000000 --- a/spec/lib/jets/klass_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -describe Jets::Klass do - it "class_name" do - class_name = Jets::Klass.class_name("app/functions/hello_function.rb") - expect(class_name).to eq "HelloFunction" - - class_name = Jets::Klass.class_name("app/functions/hello.rb") - expect(class_name).to eq "Hello" - end - - it "from_path" do - klass = Jets::Klass.from_path("app/functions/hello.rb") - expect(klass).to eq Hello - - klass = Jets::Klass.from_path("app/controllers/posts_controller.rb") - expect(klass).to eq PostsController - end - - it "from_definition" do - definition = Jets::Lambda::Definition.new("HardJob", :dig) - klass = Jets::Klass.from_definition(definition) - expect(klass).to eq HardJob - - definition = Jets::Lambda::Definition.new("Hello", :handler, type: "function") - klass = Jets::Klass.from_definition(definition) - expect(klass).to eq Hello - - definition = Jets::Lambda::Definition.new("SimpleFunction", :handler, type: "function") - klass = Jets::Klass.from_definition(definition) - expect(klass).to eq SimpleFunction - end -end - diff --git a/spec/lib/jets/lambda/definition_spec.rb b/spec/lib/jets/lambda/definition_spec.rb deleted file mode 100644 index 6404f2389..000000000 --- a/spec/lib/jets/lambda/definition_spec.rb +++ /dev/null @@ -1,34 +0,0 @@ -describe Jets::Lambda::Definition do - context "PostsController" do - let(:definition) do - Jets::Lambda::Definition.new("PostsController", :index) - end - - it "type" do - expect(definition.type).to eq "controller" - end - end - - context "HardJob" do - let(:definition) do - Jets::Lambda::Definition.new("HardJob", :dig) - end - - it "type" do - expect(definition.type).to eq "job" - end - end - - context "HelloWorld which is anonyomous class" do - let(:definition) do - # functions are anonymoust classes which have a class_name of "". - # We will fix the class name later when in FunctionConstructor. - # This is tested in function_constructor_spec.rb. - Jets::Lambda::Definition.new("", :world) - end - - it "type" do - expect(definition.type).to be nil - end - end -end diff --git a/spec/lib/jets/lambda/dsl_spec.rb b/spec/lib/jets/lambda/dsl_spec.rb deleted file mode 100644 index f4df539f9..000000000 --- a/spec/lib/jets/lambda/dsl_spec.rb +++ /dev/null @@ -1,81 +0,0 @@ -class TestPropertiesController < ApplicationController - properties( - dead_letter_config: "arn", timeout: 20, role: "myrole", memory_size: 1000 - ) - def index - end -end - -describe Jets::Lambda::Dsl do - context "TestPropertiesController" do - let(:controller) { TestPropertiesController.new({}, nil, "index") } - - it "definitions" do - index_definition = TestPropertiesController.all_public_definitions[:index] - expect(index_definition).to be_a(Jets::Lambda::Definition) - expect(index_definition.properties).to eq( - {:DeadLetterConfig=>"arn", :MemorySize=>1000, :Role=>"myrole", :Timeout=>20} - ) - end - end - - context "StoresController" do - let(:controller) { StoresController.new({}, nil, "new") } - - it "definitions" do - definitions = StoresController.all_public_definitions.keys - expect(definitions).to eq [:index, :new, :show] - - index_definition = StoresController.all_public_definitions[:index] - expect(index_definition).to be_a(Jets::Lambda::Definition) - end - - it "timeout" do - definition = StoresController.all_public_definitions[:new] - expect(definition.properties).to eq(Timeout: 35) - end - - it "environment" do - definition = StoresController.all_public_definitions[:show] - expect(definition.properties[:Environment]).to eq({ - Variables: { - key1: "value1", - key2: "value2", - }}) - end - - it "memory_size" do - definition = StoresController.all_public_definitions[:show] - expect(definition.properties[:MemorySize]).to eq 1024 - end - end - - context "Admin::StoresController" do - let(:controller) { Admin::StoresController.new({}, nil, "new") } - - it "definitions should include definitions from parent class" do - definitions = Admin::StoresController.all_public_definitions.keys - expect(definitions).to eq [:index, :new, :show] - end - end - - context "App extension iot" do - it "creates custom resource" do - definition = TemperatureJob.definitions.first - # pp definition # uncomment to see and debug - logical_id = definition.associated_resources.first.logical_id - expect(logical_id).to eq "room_topic_rule" - end - end - - context "Class with inherited definitions" do - it "contains definitions from parent classes" do - definitions = ChildPostsController.all_public_definitions - meths = definitions.keys - expect(meths).to eq [:index, :new, :show, :create, :edit, :update, :delete, :foobar] - expect(definitions[:index].class_name).to eq "ChildPostsController" - expect(definitions[:foobar].class_name).to eq "ChildPostsController" - expect(definitions[:show].class_name).to eq "ChildPostsController" - end - end -end diff --git a/spec/lib/jets/lambda/function_constructor_spec.rb b/spec/lib/jets/lambda/function_constructor_spec.rb deleted file mode 100644 index afcf7a382..000000000 --- a/spec/lib/jets/lambda/function_constructor_spec.rb +++ /dev/null @@ -1,42 +0,0 @@ -describe Jets::Lambda::FunctionConstructor do - let(:constructor) { Jets::Lambda::FunctionConstructor.new(code_path) } - - context("app function") do - let(:code_path) { "app/functions/hello.rb" } - let(:event) { {"key1" => "value1", "key2" => "value2", "key3" => "value3"} } - - context "with _function" do - it "build returns hello function that has world handler method to call" do - WhateverFunction = constructor.build - whatever_function = WhateverFunction.new - result = whatever_function.world(event, {}) - expect(result).to eq 'hello world: "value1"' - end - end - - context "without _function" do - it "build calls adjust_definitions and adds class_name and type" do - Hello = constructor.build - definition = Hello.definitions.first - expect(definition.class_name).to eq "Hello" - expect(definition.type).to eq "function" - end - end - end - - context("shared function") do - let(:code_path) { "app/shared/functions/whatever.rb" } - let(:event) { {"key1" => "value1", "key2" => "value2", "key3" => "value3"} } - context "assigned to constant" do - it "build calls adjust_definitions and adds class_name and type" do - Whatever = constructor.build - definition = Whatever.definitions.first - expect(definition.class_name).to eq "Whatever" - expect(definition.type).to eq "function" - - result = Whatever.process(event, {}, :handle) - expect(result).to eq 'hello world: "value1"' - end - end - end -end diff --git a/spec/lib/jets/lambda/function_spec.rb b/spec/lib/jets/lambda/function_spec.rb deleted file mode 100644 index fe978abf6..000000000 --- a/spec/lib/jets/lambda/function_spec.rb +++ /dev/null @@ -1,31 +0,0 @@ -class TestHelloFunction < Jets::Lambda::Function - # first method is automatically used as the handler - def handler(event, context={}) - event["key1"] - end - - # other methods to help - def another_function - end - - def some_other_helper - end -end - -describe Jets::Lambda::Function do - context TestHelloFunction do - it "first method used as the handler" do - definition = TestHelloFunction.handler_definition - expect(definition.meth).to eq :handler - end - end - - context "created function" do - let(:hello_function) { TestHelloFunction.new } - - it "handler function returns the right result" do - result = hello_function.handler("key1" => "value1") - expect(result).to eq "value1" - end - end -end diff --git a/spec/lib/jets/names_spec.rb b/spec/lib/jets/names_spec.rb deleted file mode 100644 index af81bc721..000000000 --- a/spec/lib/jets/names_spec.rb +++ /dev/null @@ -1,6 +0,0 @@ -describe Jets::Names do - it "provides some names used throughout jets" do - names = Jets::Names - expect(names.parent_stack_name).to eq "demo-test" - end -end diff --git a/spec/lib/jets/namespace_spec.rb b/spec/lib/jets/namespace_spec.rb deleted file mode 100644 index 5a7d39f76..000000000 --- a/spec/lib/jets/namespace_spec.rb +++ /dev/null @@ -1,9 +0,0 @@ -describe "Namespace" do - it "autovivification" do - Dir.chdir("spec/fixtures/demo") do - # Sanity check: should not raise an error - # If it raises an error then module autovivification is not working - Ns::A - end - end -end diff --git a/spec/lib/jets/poly/node_executor_spec.rb b/spec/lib/jets/poly/node_executor_spec.rb deleted file mode 100644 index bff486112..000000000 --- a/spec/lib/jets/poly/node_executor_spec.rb +++ /dev/null @@ -1,14 +0,0 @@ -describe Jets::Poly::NodeExecutor do - let(:fun) { Jets::Poly::NodeExecutor.new(task) } - let(:task) { BooksController.all_public_definitions[:list] } - - context("python") do - context("lets see the script") do - let(:action) { :node } - it "generates code" do - code = fun.code - expect(code).to include("var app = require") - end - end - end -end diff --git a/spec/lib/jets/poly/python_executor_spec.rb b/spec/lib/jets/poly/python_executor_spec.rb deleted file mode 100644 index e018894f8..000000000 --- a/spec/lib/jets/poly/python_executor_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -describe Jets::Poly::PythonExecutor do - let(:executor) { Jets::Poly::PythonExecutor.new(task) } - let(:task) { double(:null).as_null_object } - - context("failed python command") do - let(:event) { json_file("spec/fixtures/dumps/api_gateway/books/show.json") } - it "produces lambda format error response" do - stdout = "" - stderr = stderr_data - status = double(:null).as_null_object - allow(status).to receive(:success?).and_return(false) - allow(Open3).to receive(:capture3).and_return([stdout, stderr, status]) - - text = executor.run_lambda_executor(event, {}) - expect(text).to include("stackTrace") - end - - - def stderr_data - <<-EOL - Traceback (most recent call last): - File "/tmp/jets/lite/executor/20180804-10727-mcs6qk/lambda_executor.py", line 6, in - resp = handle(event, context) - File "/tmp/jets/lite/executor/20180804-10727-mcs6qk/index.py", line 22, in handle - return response({'message': e.message}, 400) - File "/tmp/jets/lite/executor/20180804-10727-mcs6qk/index.py", line 5, in response - badcode - NameError: global name 'badcode' is not defined - EOL - end - end -end diff --git a/spec/lib/jets/poly_spec.rb b/spec/lib/jets/poly_spec.rb deleted file mode 100644 index 2d26d1eba..000000000 --- a/spec/lib/jets/poly_spec.rb +++ /dev/null @@ -1,54 +0,0 @@ -describe Jets::Poly do - let(:fun) { Jets::Poly.new(BooksController, action) } - - context("python") do - context("successful command") do - let(:action) { :show } - let(:event) { json_file("spec/fixtures/dumps/api_gateway/books/show.json") } - it "produces lambda response payload" do - resp = fun.run(event) - expect(resp["statusCode"]).to eq "200" - end - end - - context("failed command") do - let(:action) { :error_test } - let(:event) { json_file("spec/fixtures/dumps/api_gateway/books/show.json") } - it "raises an custom PythonError exception" do - expect { fun.run(event) }.to raise_error(Jets::Poly::PythonError) - end - end - end - - context("node callback syntax") do - context("successful command") do - let(:action) { :list } - let(:event) { json_file("spec/fixtures/dumps/api_gateway/books/list.json") } - it "produces lambda response payload" do - resp = fun.run(event) - expect(resp["statusCode"]).to eq "200" - end - end - - context("failed command") do - let(:action) { :node_error_test } - let(:event) { json_file("spec/fixtures/dumps/api_gateway/books/list.json") } - it "raises an custom NodeError exception" do - expect { fun.run(event) }.to raise_error(Jets::Poly::NodeError) - end - end - end - - # Note the specs work with node v8.10.0 - context("node async syntax") do - context("successful command") do - let(:action) { :node_async } - # event doesnt really matter - let(:event) { json_file("spec/fixtures/dumps/api_gateway/books/list.json") } - it "produces lambda response payload" do - resp = fun.run(event) - expect(resp["statusCode"]).to eq "200" - end - end - end -end diff --git a/spec/lib/jets/processors/deducer_spec.rb b/spec/lib/jets/processors/deducer_spec.rb deleted file mode 100644 index ab4e621a7..000000000 --- a/spec/lib/jets/processors/deducer_spec.rb +++ /dev/null @@ -1,61 +0,0 @@ -describe "Deducer" do - context "controller" do - let(:deducer) do - Jets::Processors::Deducer.new("handlers/controllers/posts_controller.create") - end - - it "deduces info to run the ruby code" do - expect(deducer.process_type).to include("controller") - expect(deducer.path).to include("app/controllers/posts_controller.rb") - expect(deducer.code).to eq %Q|PostsController.process(event, context, "create")| - end - end - - context "job" do - let(:deducer) do - Jets::Processors::Deducer.new("handlers/jobs/hard_job.dig") - end - - it "deduces info to run the ruby code" do - expect(deducer.process_type).to include("job") - expect(deducer.path).to include("app/jobs/hard_job.rb") - expect(deducer.code).to eq %Q|HardJob.process(event, context, "dig")| - end - end - - context "function with _function" do - let(:deducer) do - Jets::Processors::Deducer.new("handlers/functions/hello_function.world") - end - - it "deduces info to run the ruby code" do - expect(deducer.process_type).to include("function") - expect(deducer.path).to include("app/functions/hello_function.rb") - expect(deducer.code).to eq %Q|HelloFunction.process(event, context, "world")| - end - end - - context "function without _function" do - let(:deducer) do - Jets::Processors::Deducer.new("handlers/functions/hello.world") - end - - it "deduces info to run the ruby code" do - expect(deducer.process_type).to include("function") - expect(deducer.path).to include("app/functions/hello.rb") - expect(deducer.code).to eq %Q|Hello.process(event, context, "world")| - end - end - - context "shared function" do - let(:deducer) do - Jets::Processors::Deducer.new("handlers/shared/functions/whatever.handle") - end - - it "deduces info to run the ruby code" do - expect(deducer.process_type).to include("function") - expect(deducer.path).to include("app/shared/functions/whatever.rb") - expect(deducer.code).to eq %Q|Whatever.process(event, context, "handle")| - end - end -end diff --git a/spec/lib/jets/processors/main_processor_spec.rb b/spec/lib/jets/processors/main_processor_spec.rb deleted file mode 100644 index ebd21bdd2..000000000 --- a/spec/lib/jets/processors/main_processor_spec.rb +++ /dev/null @@ -1,69 +0,0 @@ -describe Jets::Processors::MainProcessor do - before(:each) { silence_loggers! } - after(:each) { restore_loggers! } - - let(:main) do - Jets::Processors::MainProcessor.new( - event, - context, - handler - ) - end - let(:event) { {} } - let(:context) { {} } - - context "controller create" do - let(:handler) { 'handlers/controllers/posts_controller.create' } - it "returns data" do - data = main.run - expect(data["statusCode"]).to eq 200 - expect(data["body"]).to be_a(String) - end - end - - context "controller new" do - let(:handler) { 'handlers/controllers/posts_controller.new' } - let(:event) { { "path" => "/posts/new"} } - it "process:controller event context handler" do - data = main.run - expect(data["statusCode"]).to eq 200 - expect(data["body"]).to eq('{"controller":"posts","action":"new"}') # body is JSON encoded String - end - end - - context "job" do - let(:handler) { 'handlers/jobs/hard_job.dig' } - it "returns data" do - data = main.run - expect(data[:done]).to eq "digging" - expect(data["done"]).to eq "digging" # testing HashWithIndifferentAccess - end - end - - context "error job" do - let(:handler) { 'handlers/jobs/error_job.break' } - it "throws error" do - expect { - main.run - }.to raise_error(RuntimeError) - end - end - - context "function" do - let(:handler) { 'handlers/functions/hello.world' } - let(:event) { {"key1" => "value1"} } - it "returns data" do - data = main.run - expect(data).to eq 'hello world: "value1"' - end - end - - context "shared function" do - let(:handler) { 'handlers/shared/functions/whatever.handle' } - let(:event) { {"key1" => "value1"} } - it "returns data" do - data = main.run - expect(data).to eq 'hello world: "value1"' - end - end -end diff --git a/spec/lib/jets/router/dsl/demo_spec.rb b/spec/lib/jets/router/dsl/demo_spec.rb deleted file mode 100644 index 92972cf8c..000000000 --- a/spec/lib/jets/router/dsl/demo_spec.rb +++ /dev/null @@ -1,25 +0,0 @@ -describe Jets::Router do - let(:route_set) { Jets.application.routes } - let(:app) { RouterTestApp.new } - - describe "Router" do - it "demo" do - output = draw do - # only = [:new, :edit] - # resources :comments, shallow: true, only: [] do - # resources :likes, only: [:index, :create, :new, :edit] - # end - - # resources :posts, shallow: true do - # resources :comments, shallow: true do - # resources :likes - # end - # end - # resources :posts, only: only do - # resources :comments, only: only - # end - end - # puts output - end - end -end diff --git a/spec/lib/jets/router/dsl/kingsman_spec.rb b/spec/lib/jets/router/dsl/kingsman_spec.rb deleted file mode 100644 index fe170f88f..000000000 --- a/spec/lib/jets/router/dsl/kingsman_spec.rb +++ /dev/null @@ -1,89 +0,0 @@ -describe Jets::Router do - let(:route_set) { Jets.application.routes } - let(:app) { RouterTestApp.new } - - describe "Router" do - it "match with as for new action" do - output = draw do - match "posts/:id/edit", as: "change", to: "posts#edit", via: [:get] - end - # A little annoying that as is change instead of change_post. - # That's how Rails behaves though and it's tricky to get change_post right now. - text = <<~EOL - change GET /posts/:id/edit posts#edit - EOL - expect(output).to eq(text) - end - - it "match with as" do - output = draw do - resource :session, only: [], controller: "kingsman/sessions", path: "" do - match :destroy, path: "sign_out", as: "destroy", via: :delete - end - end - text = <<~EOL - destroy_session DELETE /sign_out kingsman/sessions#destroy - EOL - expect(output).to eq(text) - end - - it "resource sessions" do - output = draw do - resource :session, only: [], controller: "kingsman/sessions", path: "" do - get :new, path: "sign_in", as: "new" - post :create, path: "sign_in" - match :destroy, path: "sign_out", as: "destroy", via: :delete - end - end - text = <<~EOL - new_session GET /sign_in kingsman/sessions#new - session POST /sign_in kingsman/sessions#create - destroy_session DELETE /sign_out kingsman/sessions#destroy - EOL - expect(output).to eq(text) - end - - it "full scope with user sessions" do - output = draw do - scope(:as=>:user, :path=>"/users", :module=>nil) do - resource :session, only: [], controller: "kingsman/sessions", path: "" do - get :new, path: "sign_in", as: "new" - post :create, path: "sign_in" - match :destroy, path: "sign_out", as: "destroy", via: :delete - end - end - end - text = <<~EOL - new_user_session GET /users/sign_in kingsman/sessions#new - user_session POST /users/sign_in kingsman/sessions#create - destroy_user_session DELETE /users/sign_out kingsman/sessions#destroy - EOL - expect(output).to eq(text) - # destroy_user_destroy_session DELETE users/sign_out kingsman/sessions#destroy - end - - it "full scope with user registrations" do - output = draw do - scope(as: :user, path: '/users') do - resource(:registration, - only: [:new, :create, :edit, :update, :destroy], - path: "", - path_names: {new: "sign_up", edit: "edit", cancel: "cancel"}, - controller: "kingsman/registrations") do - get :cancel - end - end - end - text = <<~EOL - user_registration POST /users kingsman/registrations#create - new_user_registration GET /users/sign_up kingsman/registrations#new - edit_user_registration GET /users/edit kingsman/registrations#edit - user_registration PUT /users kingsman/registrations#update - user_registration PATCH /users kingsman/registrations#update - user_registration DELETE /users kingsman/registrations#destroy - cancel_user_registration GET /users/cancel kingsman/registrations#cancel - EOL - expect(output).to eq(text) - end - end -end diff --git a/spec/lib/jets/router/dsl/member_and_collection_spec.rb b/spec/lib/jets/router/dsl/member_and_collection_spec.rb deleted file mode 100644 index 4db176aeb..000000000 --- a/spec/lib/jets/router/dsl/member_and_collection_spec.rb +++ /dev/null @@ -1,53 +0,0 @@ -describe Jets::Router do - let(:route_set) { Jets.application.routes } - let(:app) { RouterTestApp.new } - - describe "Router member and collection" do - it "direct option" do - output = draw do - resources :posts, only: [] do - get "preview", on: :member - get "list", on: :collection - end - end - text = <<~EOL - preview_post GET /posts/:id/preview posts#preview - list_posts GET /posts/list posts#list - EOL - expect(output).to eq(text) - - expect(app.preview_post_path(1)).to eq("/posts/1/preview") - expect(app.list_posts_path).to eq("/posts/list") - end - - it "nested resources member" do - output = draw do - resources :posts, only: [] do - member do - get "preview" - end - collection do - get "list" - end - end - end - text = <<~EOL - preview_post GET /posts/:id/preview posts#preview - list_posts GET /posts/list posts#list - EOL - expect(output).to eq(text) - - expect(app.preview_post_path(1)).to eq("/posts/1/preview") - expect(app.list_posts_path).to eq("/posts/list") - end - - it "no parent resources block" do - expect { - output = draw do - get "preview", on: :member - get "list", on: :collection - end - }.to raise_error(Jets::Router::Error) - end - end -end \ No newline at end of file diff --git a/spec/lib/jets/router/dsl/module_spec.rb b/spec/lib/jets/router/dsl/module_spec.rb deleted file mode 100644 index fdc8d757f..000000000 --- a/spec/lib/jets/router/dsl/module_spec.rb +++ /dev/null @@ -1,30 +0,0 @@ -describe Jets::Router do - let(:route_set) { Jets.application.routes } - let(:app) { RouterTestApp.new } - - describe "Router scope" do - it "resources or option module" do - output = draw do - scope module: "api" do - scope module: "v1" do - resources :posts, only: [:edit], module: "draft" - end - end - end - text = <<~EOL - edit_post GET /posts/:id/edit api/v1/draft/posts#edit - EOL - expect(output).to eq(text) - - route_set.clear! - output = draw do - scope module: "api" do - scope module: "v1" do - get "posts/:id/edit", to: "posts#edit", module: "draft" - end - end - end - expect(output).to eq(text) - end - end -end diff --git a/spec/lib/jets/router/dsl/namespace_spec.rb b/spec/lib/jets/router/dsl/namespace_spec.rb deleted file mode 100644 index f7d54e386..000000000 --- a/spec/lib/jets/router/dsl/namespace_spec.rb +++ /dev/null @@ -1,154 +0,0 @@ -describe Jets::Router do - let(:route_set) { Jets.application.routes } - let(:app) { RouterTestApp.new } - - describe "Router namespace" do - it "admin resources posts" do - captured_scope = nil - output = draw do - namespace :admin do - resources :posts - end - end - text = <<~EOL - admin_posts GET /admin/posts admin/posts#index - admin_posts POST /admin/posts admin/posts#create - new_admin_post GET /admin/posts/new admin/posts#new - edit_admin_post GET /admin/posts/:id/edit admin/posts#edit - admin_post GET /admin/posts/:id admin/posts#show - admin_post PUT /admin/posts/:id admin/posts#update - admin_post PATCH /admin/posts/:id admin/posts#update - admin_post DELETE /admin/posts/:id admin/posts#destroy - EOL - expect(output).to eq(text) - - expect(app.admin_posts_path).to eq("/admin/posts") - expect(app.new_admin_post_path).to eq("/admin/posts/new") - expect(app.admin_post_path(1)).to eq("/admin/posts/1") - expect(app.edit_admin_post_path(1)).to eq("/admin/posts/1/edit") - end - - it "namespace v1 namespace admin resources posts resources comments multiple lines" do - output = draw do - namespace :v1 do - namespace :admin do - resources :posts do - resources :comments - end - end - end - end - text = <<~EOL - v1_admin_posts GET /v1/admin/posts v1/admin/posts#index - v1_admin_posts POST /v1/admin/posts v1/admin/posts#create - new_v1_admin_post GET /v1/admin/posts/new v1/admin/posts#new - edit_v1_admin_post GET /v1/admin/posts/:id/edit v1/admin/posts#edit - v1_admin_post GET /v1/admin/posts/:id v1/admin/posts#show - v1_admin_post PUT /v1/admin/posts/:id v1/admin/posts#update - v1_admin_post PATCH /v1/admin/posts/:id v1/admin/posts#update - v1_admin_post DELETE /v1/admin/posts/:id v1/admin/posts#destroy - v1_admin_post_comments GET /v1/admin/posts/:post_id/comments v1/admin/comments#index - v1_admin_post_comments POST /v1/admin/posts/:post_id/comments v1/admin/comments#create - new_v1_admin_post_comment GET /v1/admin/posts/:post_id/comments/new v1/admin/comments#new - edit_v1_admin_post_comment GET /v1/admin/posts/:post_id/comments/:id/edit v1/admin/comments#edit - v1_admin_post_comment GET /v1/admin/posts/:post_id/comments/:id v1/admin/comments#show - v1_admin_post_comment PUT /v1/admin/posts/:post_id/comments/:id v1/admin/comments#update - v1_admin_post_comment PATCH /v1/admin/posts/:post_id/comments/:id v1/admin/comments#update - v1_admin_post_comment DELETE /v1/admin/posts/:post_id/comments/:id v1/admin/comments#destroy - EOL - expect(output).to eq(text) - - expect(app.v1_admin_posts_path).to eq("/v1/admin/posts") - expect(app.new_v1_admin_post_path).to eq("/v1/admin/posts/new") - expect(app.v1_admin_post_path(1)).to eq("/v1/admin/posts/1") - expect(app.edit_v1_admin_post_path(1)).to eq("/v1/admin/posts/1/edit") - - expect(app.v1_admin_post_comments_path(1)).to eq("/v1/admin/posts/1/comments") - expect(app.new_v1_admin_post_comment_path(1)).to eq("/v1/admin/posts/1/comments/new") - expect(app.v1_admin_post_comment_path(1,2)).to eq("/v1/admin/posts/1/comments/2") - expect(app.edit_v1_admin_post_comment_path(1,2)).to eq("/v1/admin/posts/1/comments/2/edit") - end - - it "namespace v1/admin resources posts resources comments" do - output = draw do - namespace "v1/admin" do - resources :posts do - resources :comments - end - end - end - text = <<~EOL - v1_admin_posts GET /v1/admin/posts v1/admin/posts#index - v1_admin_posts POST /v1/admin/posts v1/admin/posts#create - new_v1_admin_post GET /v1/admin/posts/new v1/admin/posts#new - edit_v1_admin_post GET /v1/admin/posts/:id/edit v1/admin/posts#edit - v1_admin_post GET /v1/admin/posts/:id v1/admin/posts#show - v1_admin_post PUT /v1/admin/posts/:id v1/admin/posts#update - v1_admin_post PATCH /v1/admin/posts/:id v1/admin/posts#update - v1_admin_post DELETE /v1/admin/posts/:id v1/admin/posts#destroy - v1_admin_post_comments GET /v1/admin/posts/:post_id/comments v1/admin/comments#index - v1_admin_post_comments POST /v1/admin/posts/:post_id/comments v1/admin/comments#create - new_v1_admin_post_comment GET /v1/admin/posts/:post_id/comments/new v1/admin/comments#new - edit_v1_admin_post_comment GET /v1/admin/posts/:post_id/comments/:id/edit v1/admin/comments#edit - v1_admin_post_comment GET /v1/admin/posts/:post_id/comments/:id v1/admin/comments#show - v1_admin_post_comment PUT /v1/admin/posts/:post_id/comments/:id v1/admin/comments#update - v1_admin_post_comment PATCH /v1/admin/posts/:post_id/comments/:id v1/admin/comments#update - v1_admin_post_comment DELETE /v1/admin/posts/:post_id/comments/:id v1/admin/comments#destroy - EOL - expect(output).to eq(text) - - expect(app.v1_admin_posts_path).to eq("/v1/admin/posts") - expect(app.new_v1_admin_post_path).to eq("/v1/admin/posts/new") - expect(app.v1_admin_post_path(1)).to eq("/v1/admin/posts/1") - expect(app.edit_v1_admin_post_path(1)).to eq("/v1/admin/posts/1/edit") - - expect(app.v1_admin_post_comments_path(1)).to eq("/v1/admin/posts/1/comments") - expect(app.new_v1_admin_post_comment_path(1)).to eq("/v1/admin/posts/1/comments/new") - expect(app.v1_admin_post_comment_path(1,2)).to eq("/v1/admin/posts/1/comments/2") - expect(app.edit_v1_admin_post_comment_path(1,2)).to eq("/v1/admin/posts/1/comments/2/edit") - end - - it "regular create route methods" do - output = draw do - namespace "admin" do - get "posts", to: "posts#index" - get "posts/:id", to: "posts#show" - end - end - text = <<~EOL - admin_posts GET /admin/posts admin/posts#index - admin_post GET /admin/posts/:id admin/posts#show - EOL - expect(output).to eq(text) - - expect(app.admin_posts_path).to eq("/admin/posts") - expect(app.admin_post_path(1)).to eq("/admin/posts/1") - end - - # prettier namespace method - it "api/v2 namespace" do - output = draw do - namespace "api/v2" do - get "posts", to: "posts#index" - end - end - route = route_set.routes.first - expect(route.path).to eq "/api/v2/posts" - end - - it "absolute controller path" do - output = draw do - namespace :admin do - resources :posts, only: [:edit] do - get :photo, on: :member, controller: "/photos" - end - end - end - text = <<~EOL - edit_admin_post GET /admin/posts/:id/edit admin/posts#edit - photo_admin_post GET /admin/posts/:id/photo photos#photo - EOL - expect(output).to eq(text) - end - end -end \ No newline at end of file diff --git a/spec/lib/jets/router/dsl/nested_resources_spec.rb b/spec/lib/jets/router/dsl/nested_resources_spec.rb deleted file mode 100644 index 45206552b..000000000 --- a/spec/lib/jets/router/dsl/nested_resources_spec.rb +++ /dev/null @@ -1,147 +0,0 @@ -describe Jets::Router do - let(:route_set) { Jets.application.routes } - let(:app) { RouterTestApp.new } - - describe "Router nested resources" do - it "plural to plural" do - output = draw do - resources :posts do - resources :comments - end - end - text = <<~EOL - posts GET /posts posts#index - posts POST /posts posts#create - new_post GET /posts/new posts#new - edit_post GET /posts/:id/edit posts#edit - post GET /posts/:id posts#show - post PUT /posts/:id posts#update - post PATCH /posts/:id posts#update - post DELETE /posts/:id posts#destroy - post_comments GET /posts/:post_id/comments comments#index - post_comments POST /posts/:post_id/comments comments#create - new_post_comment GET /posts/:post_id/comments/new comments#new - edit_post_comment GET /posts/:post_id/comments/:id/edit comments#edit - post_comment GET /posts/:post_id/comments/:id comments#show - post_comment PUT /posts/:post_id/comments/:id comments#update - post_comment PATCH /posts/:post_id/comments/:id comments#update - post_comment DELETE /posts/:post_id/comments/:id comments#destroy - EOL - expect(output).to eq(text) - - expect(app.posts_path).to eq("/posts") - expect(app.new_post_path).to eq("/posts/new") - expect(app.post_path(1)).to eq("/posts/1") - expect(app.edit_post_path(1)).to eq("/posts/1/edit") - - expect(app.post_comments_path(1)).to eq("/posts/1/comments") - expect(app.new_post_comment_path(1)).to eq("/posts/1/comments/new") - expect(app.post_comment_path(1,2)).to eq("/posts/1/comments/2") - expect(app.edit_post_comment_path(1,2)).to eq("/posts/1/comments/2/edit") - end - - it "singular to plural" do - output = draw do - resource :account do - resources :api_keys - end - end - text = <<~EOL - account POST /account accounts#create - new_account GET /account/new accounts#new - edit_account GET /account/edit accounts#edit - account GET /account accounts#show - account PUT /account accounts#update - account PATCH /account accounts#update - account DELETE /account accounts#destroy - account_api_keys GET /account/api_keys api_keys#index - account_api_keys POST /account/api_keys api_keys#create - new_account_api_key GET /account/api_keys/new api_keys#new - edit_account_api_key GET /account/api_keys/:id/edit api_keys#edit - account_api_key GET /account/api_keys/:id api_keys#show - account_api_key PUT /account/api_keys/:id api_keys#update - account_api_key PATCH /account/api_keys/:id api_keys#update - account_api_key DELETE /account/api_keys/:id api_keys#destroy - EOL - expect(output).to eq(text) - - expect(app.new_account_path).to eq("/account/new") - expect(app.account_path).to eq("/account") - expect(app.edit_account_path).to eq("/account/edit") - - expect(app.account_api_keys_path).to eq("/account/api_keys") - expect(app.new_account_api_key_path).to eq("/account/api_keys/new") - expect(app.account_api_key_path(1)).to eq("/account/api_keys/1") - expect(app.edit_account_api_key_path(1)).to eq("/account/api_keys/1/edit") - end - - it "plural to singular" do - output = draw do - resources :books do - resource :cover - end - end - text = <<~EOL - books GET /books books#index - books POST /books books#create - new_book GET /books/new books#new - edit_book GET /books/:id/edit books#edit - book GET /books/:id books#show - book PUT /books/:id books#update - book PATCH /books/:id books#update - book DELETE /books/:id books#destroy - book_cover POST /books/:book_id/cover covers#create - new_book_cover GET /books/:book_id/cover/new covers#new - edit_book_cover GET /books/:book_id/cover/edit covers#edit - book_cover GET /books/:book_id/cover covers#show - book_cover PUT /books/:book_id/cover covers#update - book_cover PATCH /books/:book_id/cover covers#update - book_cover DELETE /books/:book_id/cover covers#destroy - EOL - expect(output).to eq(text) - - expect(app.books_path).to eq("/books") - expect(app.new_book_path).to eq("/books/new") - expect(app.book_path(1)).to eq("/books/1") - expect(app.edit_book_path(1)).to eq("/books/1/edit") - - expect(app.new_book_cover_path(1)).to eq("/books/1/cover/new") - expect(app.book_cover_path(1)).to eq("/books/1/cover") - expect(app.edit_book_cover_path(1)).to eq("/books/1/cover/edit") - end - - it "singular to singular" do - output = draw do - resource :account do - resource :avatar - end - end - text = <<~EOL - account POST /account accounts#create - new_account GET /account/new accounts#new - edit_account GET /account/edit accounts#edit - account GET /account accounts#show - account PUT /account accounts#update - account PATCH /account accounts#update - account DELETE /account accounts#destroy - account_avatar POST /account/avatar avatars#create - new_account_avatar GET /account/avatar/new avatars#new - edit_account_avatar GET /account/avatar/edit avatars#edit - account_avatar GET /account/avatar avatars#show - account_avatar PUT /account/avatar avatars#update - account_avatar PATCH /account/avatar avatars#update - account_avatar DELETE /account/avatar avatars#destroy - EOL - expect(output).to eq(text) - - expect(app.new_account_path).to eq("/account/new") - expect(app.account_path).to eq("/account") - expect(app.edit_account_path).to eq("/account/edit") - - expect(app.new_account_avatar_path).to eq("/account/avatar/new") - expect(app.account_avatar_path).to eq("/account/avatar") - expect(app.edit_account_avatar_path).to eq("/account/avatar/edit") - end - end -end - diff --git a/spec/lib/jets/router/dsl/no_scope_spec.rb b/spec/lib/jets/router/dsl/no_scope_spec.rb deleted file mode 100644 index 2df3167d2..000000000 --- a/spec/lib/jets/router/dsl/no_scope_spec.rb +++ /dev/null @@ -1,134 +0,0 @@ -describe Jets::Router do - let(:route_set) { Jets::Router::RouteSet.new } - let(:app) { RouterTestApp.new } - - describe "Router no scope" do - it "get posts resources" do - output = draw do - resources :posts - end - text = <<~EOL - posts GET /posts posts#index - posts POST /posts posts#create - new_post GET /posts/new posts#new - edit_post GET /posts/:id/edit posts#edit - post GET /posts/:id posts#show - post PUT /posts/:id posts#update - post PATCH /posts/:id posts#update - post DELETE /posts/:id posts#destroy - EOL - expect(output).to eq(text) - end - - it "get posts create_route methods" do - output = draw do - get "posts", to: "posts#index" - post "posts", to: "posts#create" - get "posts/new", to: "posts#new" - get "posts/:id/edit", to: "posts#edit" - get "posts/:id", to: "posts#show" - put "posts/:id", to: "posts#update" - patch "posts/:id", to: "posts#update" - delete "posts/:id", to: "posts#destroy" - end - text = <<~EOL - posts GET /posts posts#index - posts POST /posts posts#create - new_post GET /posts/new posts#new - edit_post GET /posts/:id/edit posts#edit - post GET /posts/:id posts#show - post PUT /posts/:id posts#update - post PATCH /posts/:id posts#update - post DELETE /posts/:id posts#destroy - EOL - expect(output).to eq(text) - - expect(app.posts_path).to eq("/posts") - expect(app.new_post_path).to eq("/posts/new") - expect(app.post_path(1)).to eq("/posts/1") - expect(app.edit_post_path(1)).to eq("/posts/1/edit") - end - - it "any with get" do - output = draw do - any "comments/hot", to: "comments#hot" - get "landing/foo/bar", to: "posts#index" - get "admin/pages", to: "admin/pages#index" - get "related_posts/:id", to: "related_posts#show" - any "others/*proxy", to: "others#catchall" - end - # Named routes helpers are not generated with any - text = <<~EOL - ANY /comments/hot comments#hot - landing_foo_bar GET /landing/foo/bar posts#index - admin_pages GET /admin/pages admin/pages#index - related_post GET /related_posts/:id related_posts#show - ANY /others/*proxy others#catchall - EOL - expect(output).to eq(text) - end - - it "resources and no scope together" do - output = draw do - resources :articles - resources :posts - any "comments/hot", to: "comments#hot" - get "landing/posts", to: "posts#index" - get "admin/pages", to: "admin/pages#index" - get "related_posts/:id", to: "related_posts#show" - any "others/*proxy", to: "others#catchall" - end - text = <<~EOL - articles GET /articles articles#index - articles POST /articles articles#create - new_article GET /articles/new articles#new - edit_article GET /articles/:id/edit articles#edit - article GET /articles/:id articles#show - article PUT /articles/:id articles#update - article PATCH /articles/:id articles#update - article DELETE /articles/:id articles#destroy - posts GET /posts posts#index - posts POST /posts posts#create - new_post GET /posts/new posts#new - edit_post GET /posts/:id/edit posts#edit - post GET /posts/:id posts#show - post PUT /posts/:id posts#update - post PATCH /posts/:id posts#update - post DELETE /posts/:id posts#destroy - ANY /comments/hot comments#hot - landing_posts GET /landing/posts posts#index - admin_pages GET /admin/pages admin/pages#index - related_post GET /related_posts/:id related_posts#show - ANY /others/*proxy others#catchall - EOL - expect(output).to eq(text) - end - - it "root" do - output = draw do - root "posts#index" - end - text = <<~EOL - root GET / posts#index - EOL - expect(output).to eq(text) - - route = route_set.routes.first - expect(route).to be_a(Jets::Router::Route) - expect(route.homepage?).to be true - expect(route.to).to eq "posts#index" - expect(route.path).to eq '/' - expect(route.http_method).to eq "GET" - end - - it "path as option" do - output = draw do - get path: "posts", to: "posts#index", path: "posts" - end - text = <<~EOL - posts GET /posts posts#index - EOL - expect(output).to eq(text) - end - end -end diff --git a/spec/lib/jets/router/dsl/path_spec.rb b/spec/lib/jets/router/dsl/path_spec.rb deleted file mode 100644 index ab26ad2bb..000000000 --- a/spec/lib/jets/router/dsl/path_spec.rb +++ /dev/null @@ -1,30 +0,0 @@ -describe Jets::Router do - let(:route_set) { Jets.application.routes } - let(:app) { RouterTestApp.new } - - describe "Router path" do - it "admin resources posts" do - output = draw do - path :admin do - resources :posts - end - end - text = <<~EOL - posts GET /admin/posts posts#index - posts POST /admin/posts posts#create - new_post GET /admin/posts/new posts#new - edit_post GET /admin/posts/:id/edit posts#edit - post GET /admin/posts/:id posts#show - post PUT /admin/posts/:id posts#update - post PATCH /admin/posts/:id posts#update - post DELETE /admin/posts/:id posts#destroy - EOL - expect(output).to eq(text) - - expect(app.posts_path).to eq("/admin/posts") - expect(app.new_post_path).to eq("/admin/posts/new") - expect(app.post_path(1)).to eq("/admin/posts/1") - expect(app.edit_post_path(1)).to eq("/admin/posts/1/edit") - end - end -end diff --git a/spec/lib/jets/router/dsl/rails_guide/1-connecting_urls_spec.rb b/spec/lib/jets/router/dsl/rails_guide/1-connecting_urls_spec.rb deleted file mode 100644 index aa54b5542..000000000 --- a/spec/lib/jets/router/dsl/rails_guide/1-connecting_urls_spec.rb +++ /dev/null @@ -1,49 +0,0 @@ -describe Jets::Router do - let(:route_set) { Jets.application.routes } - let(:app) { RouterTestApp.new } - - # Demostrates that the Jets routing engine is close to the Rails routing engine. - # Shows the slight differences, some due to APIGW and some due to Jets design decisions. - describe "Router Rails Guide" do - it "Connecting URLs to Code" do - output = draw do - get '/patients/:id', to: 'patients#show' - end - text = <<~EOL - patient GET /patients/:id patients#show - EOL - expect(output).to eq(text) - end - - it "Generating Paths and URLs from Code" do - output = draw do - get '/patients/:id', to: 'patients#show', as: 'patient' - end - text = <<~EOL - patient GET /patients/:id patients#show - EOL - expect(output).to eq(text) - end - - it "Configuring the Rails Router" do - output = draw do - resources :brands, only: [:index, :show] do - resources :products, only: [:index, :show] - end - resource :basket, only: [:show, :update, :destroy] - end - # Shows must be :brand_id for brands#show due to APIGW sibling limitation - text = <<~EOL - brands GET /brands brands#index - brand GET /brands/:id brands#show - brand_products GET /brands/:brand_id/products products#index - brand_product GET /brands/:brand_id/products/:id products#show - basket GET /basket baskets#show - basket PUT /basket baskets#update - basket PATCH /basket baskets#update - basket DELETE /basket baskets#destroy - EOL - expect(output).to eq(text) - end - end -end diff --git a/spec/lib/jets/router/dsl/rails_guide/2-resource_routing_spec.rb b/spec/lib/jets/router/dsl/rails_guide/2-resource_routing_spec.rb deleted file mode 100644 index f106c273b..000000000 --- a/spec/lib/jets/router/dsl/rails_guide/2-resource_routing_spec.rb +++ /dev/null @@ -1,508 +0,0 @@ -describe Jets::Router do - let(:route_set) { Jets.application.routes } - let(:app) { RouterTestApp.new } - - describe "Router Rails Guide" do - it "CRUD, Verbs, and Actions" do - output = draw do - resources :photos - end - text = <<~EOL - photos GET /photos photos#index - photos POST /photos photos#create - new_photo GET /photos/new photos#new - edit_photo GET /photos/:id/edit photos#edit - photo GET /photos/:id photos#show - photo PUT /photos/:id photos#update - photo PATCH /photos/:id photos#update - photo DELETE /photos/:id photos#destroy - EOL - expect(output).to eq(text) - end - - it "Defining Multiple Resources at the Same Time" do - output = draw do - resources :photos, :books, :videos - end - text = <<~EOL - photos GET /photos photos#index - photos POST /photos photos#create - new_photo GET /photos/new photos#new - edit_photo GET /photos/:id/edit photos#edit - photo GET /photos/:id photos#show - photo PUT /photos/:id photos#update - photo PATCH /photos/:id photos#update - photo DELETE /photos/:id photos#destroy - books GET /books books#index - books POST /books books#create - new_book GET /books/new books#new - edit_book GET /books/:id/edit books#edit - book GET /books/:id books#show - book PUT /books/:id books#update - book PATCH /books/:id books#update - book DELETE /books/:id books#destroy - videos GET /videos videos#index - videos POST /videos videos#create - new_video GET /videos/new videos#new - edit_video GET /videos/:id/edit videos#edit - video GET /videos/:id videos#show - video PUT /videos/:id videos#update - video PATCH /videos/:id videos#update - video DELETE /videos/:id videos#destroy - EOL - expect(output).to eq(text) - - route_set.clear! - output = draw do - resources :photos - resources :books - resources :videos - end - expect(output).to eq(text) - end - - it "Singular Resources profile users show" do - output = draw do - get 'profile', to: 'users#show' - end - text = <<~EOL - profile GET /profile users#show - EOL - expect(output).to eq(text) - end - - it "Singular Resources profile controller users" do - output = draw do - get 'profile', action: :show, controller: 'users' - end - text = <<~EOL - profile GET /profile users#show - EOL - expect(output).to eq(text) - end - - it "Singular Resources geocoder" do - output = draw do - resource :geocoder - end - text = <<~EOL - geocoder POST /geocoder geocoders#create - new_geocoder GET /geocoder/new geocoders#new - edit_geocoder GET /geocoder/edit geocoders#edit - geocoder GET /geocoder geocoders#show - geocoder PUT /geocoder geocoders#update - geocoder PATCH /geocoder geocoders#update - geocoder DELETE /geocoder geocoders#destroy - EOL - expect(output).to eq(text) - end - - it "Controller Namespaces and Routing" do - output = draw do - namespace :admin do - resources :articles, :comments - end - end - text = <<~EOL - admin_articles GET /admin/articles admin/articles#index - admin_articles POST /admin/articles admin/articles#create - new_admin_article GET /admin/articles/new admin/articles#new - edit_admin_article GET /admin/articles/:id/edit admin/articles#edit - admin_article GET /admin/articles/:id admin/articles#show - admin_article PUT /admin/articles/:id admin/articles#update - admin_article PATCH /admin/articles/:id admin/articles#update - admin_article DELETE /admin/articles/:id admin/articles#destroy - admin_comments GET /admin/comments admin/comments#index - admin_comments POST /admin/comments admin/comments#create - new_admin_comment GET /admin/comments/new admin/comments#new - edit_admin_comment GET /admin/comments/:id/edit admin/comments#edit - admin_comment GET /admin/comments/:id admin/comments#show - admin_comment PUT /admin/comments/:id admin/comments#update - admin_comment PATCH /admin/comments/:id admin/comments#update - admin_comment DELETE /admin/comments/:id admin/comments#destroy - EOL - expect(output).to eq(text) - end - - it "Controller Namespaces and Routing scope block" do - output = draw do - scope module: 'admin' do - resources :articles, :comments - end - end - text = <<~EOL - articles GET /articles admin/articles#index - articles POST /articles admin/articles#create - new_article GET /articles/new admin/articles#new - edit_article GET /articles/:id/edit admin/articles#edit - article GET /articles/:id admin/articles#show - article PUT /articles/:id admin/articles#update - article PATCH /articles/:id admin/articles#update - article DELETE /articles/:id admin/articles#destroy - comments GET /comments admin/comments#index - comments POST /comments admin/comments#create - new_comment GET /comments/new admin/comments#new - edit_comment GET /comments/:id/edit admin/comments#edit - comment GET /comments/:id admin/comments#show - comment PUT /comments/:id admin/comments#update - comment PATCH /comments/:id admin/comments#update - comment DELETE /comments/:id admin/comments#destroy - EOL - expect(output).to eq(text) - - route_set.clear! - output = draw do - resources :articles, module: 'admin' - resources :comments, module: 'admin' - end - expect(output).to eq(text) - end - - it "Controller Namespaces and Routing scope block with path" do - output = draw do - scope '/admin' do - resources :articles, :comments - end - end - text = <<~EOL - articles GET /admin/articles articles#index - articles POST /admin/articles articles#create - new_article GET /admin/articles/new articles#new - edit_article GET /admin/articles/:id/edit articles#edit - article GET /admin/articles/:id articles#show - article PUT /admin/articles/:id articles#update - article PATCH /admin/articles/:id articles#update - article DELETE /admin/articles/:id articles#destroy - comments GET /admin/comments comments#index - comments POST /admin/comments comments#create - new_comment GET /admin/comments/new comments#new - edit_comment GET /admin/comments/:id/edit comments#edit - comment GET /admin/comments/:id comments#show - comment PUT /admin/comments/:id comments#update - comment PATCH /admin/comments/:id comments#update - comment DELETE /admin/comments/:id comments#destroy - EOL - expect(output).to eq(text) - - route_set.clear! - output = draw do - resources :articles, path: '/admin/articles' - resources :comments, path: '/admin/comments' - end - expect(output).to eq(text) - end - - it "Nested Resources" do - output = draw do - resources :magazines do - resources :ads - end - end - text = <<~EOL - magazines GET /magazines magazines#index - magazines POST /magazines magazines#create - new_magazine GET /magazines/new magazines#new - edit_magazine GET /magazines/:id/edit magazines#edit - magazine GET /magazines/:id magazines#show - magazine PUT /magazines/:id magazines#update - magazine PATCH /magazines/:id magazines#update - magazine DELETE /magazines/:id magazines#destroy - magazine_ads GET /magazines/:magazine_id/ads ads#index - magazine_ads POST /magazines/:magazine_id/ads ads#create - new_magazine_ad GET /magazines/:magazine_id/ads/new ads#new - edit_magazine_ad GET /magazines/:magazine_id/ads/:id/edit ads#edit - magazine_ad GET /magazines/:magazine_id/ads/:id ads#show - magazine_ad PUT /magazines/:magazine_id/ads/:id ads#update - magazine_ad PATCH /magazines/:magazine_id/ads/:id ads#update - magazine_ad DELETE /magazines/:magazine_id/ads/:id ads#destroy - EOL - expect(output).to eq(text) - end - - it "Nested Resources Limits to Nesting" do - output = draw do - resources :publishers do - resources :magazines do - resources :photos - end - end - end - text = <<~EOL - publishers GET /publishers publishers#index - publishers POST /publishers publishers#create - new_publisher GET /publishers/new publishers#new - edit_publisher GET /publishers/:id/edit publishers#edit - publisher GET /publishers/:id publishers#show - publisher PUT /publishers/:id publishers#update - publisher PATCH /publishers/:id publishers#update - publisher DELETE /publishers/:id publishers#destroy - publisher_magazines GET /publishers/:publisher_id/magazines magazines#index - publisher_magazines POST /publishers/:publisher_id/magazines magazines#create - new_publisher_magazine GET /publishers/:publisher_id/magazines/new magazines#new - edit_publisher_magazine GET /publishers/:publisher_id/magazines/:id/edit magazines#edit - publisher_magazine GET /publishers/:publisher_id/magazines/:id magazines#show - publisher_magazine PUT /publishers/:publisher_id/magazines/:id magazines#update - publisher_magazine PATCH /publishers/:publisher_id/magazines/:id magazines#update - publisher_magazine DELETE /publishers/:publisher_id/magazines/:id magazines#destroy - publisher_magazine_photos GET /publishers/:publisher_id/magazines/:magazine_id/photos photos#index - publisher_magazine_photos POST /publishers/:publisher_id/magazines/:magazine_id/photos photos#create - new_publisher_magazine_photo GET /publishers/:publisher_id/magazines/:magazine_id/photos/new photos#new - edit_publisher_magazine_photo GET /publishers/:publisher_id/magazines/:magazine_id/photos/:id/edit photos#edit - publisher_magazine_photo GET /publishers/:publisher_id/magazines/:magazine_id/photos/:id photos#show - publisher_magazine_photo PUT /publishers/:publisher_id/magazines/:magazine_id/photos/:id photos#update - publisher_magazine_photo PATCH /publishers/:publisher_id/magazines/:magazine_id/photos/:id photos#update - publisher_magazine_photo DELETE /publishers/:publisher_id/magazines/:magazine_id/photos/:id photos#destroy - EOL - expect(output).to eq(text) - end - - it "Shallow Nesting comments shallow true" do - output = draw do - resources :articles do - resources :comments, shallow: true - end - end - text = <<~EOL - articles GET /articles articles#index - articles POST /articles articles#create - new_article GET /articles/new articles#new - edit_article GET /articles/:id/edit articles#edit - article GET /articles/:id articles#show - article PUT /articles/:id articles#update - article PATCH /articles/:id articles#update - article DELETE /articles/:id articles#destroy - article_comments GET /articles/:article_id/comments comments#index - article_comments POST /articles/:article_id/comments comments#create - new_article_comment GET /articles/:article_id/comments/new comments#new - edit_comment GET /comments/:id/edit comments#edit - comment GET /comments/:id comments#show - comment PUT /comments/:id comments#update - comment PATCH /comments/:id comments#update - comment DELETE /comments/:id comments#destroy - EOL - expect(output).to eq(text) - end - - it "Shallow Nesting articles shallow true" do - output = draw do - resources :articles, shallow: true do - resources :comments - resources :quotes - resources :drafts - end - end - text = <<~EOL - articles GET /articles articles#index - articles POST /articles articles#create - new_article GET /articles/new articles#new - edit_article GET /articles/:id/edit articles#edit - article GET /articles/:id articles#show - article PUT /articles/:id articles#update - article PATCH /articles/:id articles#update - article DELETE /articles/:id articles#destroy - article_comments GET /articles/:article_id/comments comments#index - article_comments POST /articles/:article_id/comments comments#create - new_article_comment GET /articles/:article_id/comments/new comments#new - edit_comment GET /comments/:id/edit comments#edit - comment GET /comments/:id comments#show - comment PUT /comments/:id comments#update - comment PATCH /comments/:id comments#update - comment DELETE /comments/:id comments#destroy - article_quotes GET /articles/:article_id/quotes quotes#index - article_quotes POST /articles/:article_id/quotes quotes#create - new_article_quote GET /articles/:article_id/quotes/new quotes#new - edit_quote GET /quotes/:id/edit quotes#edit - quote GET /quotes/:id quotes#show - quote PUT /quotes/:id quotes#update - quote PATCH /quotes/:id quotes#update - quote DELETE /quotes/:id quotes#destroy - article_drafts GET /articles/:article_id/drafts drafts#index - article_drafts POST /articles/:article_id/drafts drafts#create - new_article_draft GET /articles/:article_id/drafts/new drafts#new - edit_draft GET /drafts/:id/edit drafts#edit - draft GET /drafts/:id drafts#show - draft PUT /drafts/:id drafts#update - draft PATCH /drafts/:id drafts#update - draft DELETE /drafts/:id drafts#destroy - EOL - expect(output).to eq(text) - end - - it "Shallow Nesting shallow block" do - output = draw do - shallow do - resources :articles do - resources :comments - resources :quotes - resources :drafts - end - end - end - text = <<~EOL - articles GET /articles articles#index - articles POST /articles articles#create - new_article GET /articles/new articles#new - edit_article GET /articles/:id/edit articles#edit - article GET /articles/:id articles#show - article PUT /articles/:id articles#update - article PATCH /articles/:id articles#update - article DELETE /articles/:id articles#destroy - article_comments GET /articles/:article_id/comments comments#index - article_comments POST /articles/:article_id/comments comments#create - new_article_comment GET /articles/:article_id/comments/new comments#new - edit_comment GET /comments/:id/edit comments#edit - comment GET /comments/:id comments#show - comment PUT /comments/:id comments#update - comment PATCH /comments/:id comments#update - comment DELETE /comments/:id comments#destroy - article_quotes GET /articles/:article_id/quotes quotes#index - article_quotes POST /articles/:article_id/quotes quotes#create - new_article_quote GET /articles/:article_id/quotes/new quotes#new - edit_quote GET /quotes/:id/edit quotes#edit - quote GET /quotes/:id quotes#show - quote PUT /quotes/:id quotes#update - quote PATCH /quotes/:id quotes#update - quote DELETE /quotes/:id quotes#destroy - article_drafts GET /articles/:article_id/drafts drafts#index - article_drafts POST /articles/:article_id/drafts drafts#create - new_article_draft GET /articles/:article_id/drafts/new drafts#new - edit_draft GET /drafts/:id/edit drafts#edit - draft GET /drafts/:id drafts#show - draft PUT /drafts/:id drafts#update - draft PATCH /drafts/:id drafts#update - draft DELETE /drafts/:id drafts#destroy - EOL - expect(output).to eq(text) - end - - # TODO: shallow_path support - # it "Shallow Nesting shallow_path sekret" do - # output = draw do - # scope shallow_path: "sekret" do - # resources :articles do - # resources :comments, shallow: true - # end - # end - # end - # text = <<~EOL - # articles GET /articles articles#index - # articles POST /articles articles#create - # new_article GET /articles/new articles#new - # edit_article GET /articles/:id/edit articles#edit - # article GET /articles/:id articles#show - # article PUT /articles/:id articles#update - # article PATCH /articles/:id articles#update - # article DELETE /articles/:id articles#destroy - # article_comments GET /articles/:article_id/comments comments#index - # article_comments POST /articles/:article_id/comments comments#create - # new_article_comment GET /articles/:article_id/comments/new comments#new - # edit_comment GET /sekret/comments/:id/edit comments#edit - # comment GET /sekret/comments/:id comments#show - # comment PUT /sekret/comments/:id comments#update - # comment PATCH /sekret/comments/:id comments#update - # comment DELETE /sekret/comments/:id comments#destroy - # EOL - # expect(output).to eq(text) - # end - - # it "Shallow Nesting shallow_path sekret resources articles" do - # output = draw do - # scope shallow_path: "sekret" do - # resources :articles, shallow: true do - # resources :comments - # end - # end - # end - # text = <<~EOL - # EOL - # expect(output).to eq(text) - # end - - # TODOs - # Routing Concerns - # Creating Paths and URLs from Objects - # url_for([@magazine, @ad]) - # Array notation: link_to 'Ad details', [@magazine, @ad] - - it "Adding More RESTful Actions Adding Member Routes" do - output = draw do - resources :photos do - member do - get 'preview' - end - end - end - text = <<~EOL - photos GET /photos photos#index - photos POST /photos photos#create - new_photo GET /photos/new photos#new - edit_photo GET /photos/:id/edit photos#edit - photo GET /photos/:id photos#show - photo PUT /photos/:id photos#update - photo PATCH /photos/:id photos#update - photo DELETE /photos/:id photos#destroy - preview_photo GET /photos/:id/preview photos#preview - EOL - expect(output).to eq(text) - - route_set.clear! - output = draw do - resources :photos do - get 'preview', on: :member - end - end - - expect(output).to eq(text) - end - - it "Adding More RESTful Actions Adding Collection Routes" do - output = draw do - resources :photos do - collection do - get 'search' - end - end - end - text = <<~EOL - photos GET /photos photos#index - photos POST /photos photos#create - new_photo GET /photos/new photos#new - edit_photo GET /photos/:id/edit photos#edit - photo GET /photos/:id photos#show - photo PUT /photos/:id photos#update - photo PATCH /photos/:id photos#update - photo DELETE /photos/:id photos#destroy - search_photos GET /photos/search photos#search - EOL - expect(output).to eq(text) - - route_set.clear! - output = draw do - resources :photos do - get 'search', on: :collection - end - end - expect(output).to eq(text) - end - - it "Adding More RESTful Actions Adding Routes for Additional New Actions" do - output = draw do - resources :photos do - get 'preview', on: :new - end - end - text = <<~EOL - photos GET /photos photos#index - photos POST /photos photos#create - new_photo GET /photos/new photos#new - edit_photo GET /photos/:id/edit photos#edit - photo GET /photos/:id photos#show - photo PUT /photos/:id photos#update - photo PATCH /photos/:id photos#update - photo DELETE /photos/:id photos#destroy - new_photo GET /photos/new/preview photos#new - EOL - expect(output).to eq(text) - end - end -end diff --git a/spec/lib/jets/router/dsl/rails_guide/3-non_resource_routing.rb b/spec/lib/jets/router/dsl/rails_guide/3-non_resource_routing.rb deleted file mode 100644 index a663a3379..000000000 --- a/spec/lib/jets/router/dsl/rails_guide/3-non_resource_routing.rb +++ /dev/null @@ -1,292 +0,0 @@ -class RestrictedListConstraint - def initialize - @ips = [] # RestrictedList.retrieve_ips - end - - def matches?(request) - @ips.include?(request.remote_ip) - end -end - -describe Jets::Router do - let(:route_set) { Jets.application.routes } - let(:app) { RouterTestApp.new } - - describe "Router Rails Guide" do - it "Bound Parameters" do - output = draw do - # Note: ( ... ) not supported - # get 'photos(/:id)', to: 'photos#display' - get 'photos/:id', to: 'photos#display' - end - text = <<~EOL - photos GET /photos/:id photos#display - EOL - expect(output).to eq(text) - end - - it "Dynamic Segments" do - output = draw do - get 'photos/:id/:user_id', to: 'photos#show' - end - text = <<~EOL - photo GET /photos/:id/:user_id photos#show - EOL - expect(output).to eq(text) - end - - it "Static Segments" do - output = draw do - get 'photos/:id/with_user/:user_id', to: 'photos#show' - end - text = <<~EOL - photo GET /photos/:id/with_user/:user_id photos#show - EOL - expect(output).to eq(text) - end - - it "Defining Defaults http method options" do - output = draw do - get 'photos/:id', to: 'photos#show', defaults: { format: 'jpg' } - end - - text = <<~EOL - photo GET /photos/:id photos#show - EOL - expect(output).to eq(text) - - route = find_route("/photos/1") - expect(route.resolved_defaults).to eq({:format=>'jpg'}) - end - - it "Defining Defaults block" do - output = draw do - defaults format: :json do - resources :photos - end - end - text = <<~EOL - photos GET /photos photos#index - photos POST /photos photos#create - new_photo GET /photos/new photos#new - edit_photo GET /photos/:id/edit photos#edit - photo GET /photos/:id photos#show - photo PUT /photos/:id photos#update - photo PATCH /photos/:id photos#update - photo DELETE /photos/:id photos#destroy - EOL - expect(output).to eq(text) - - route = find_route("/photos") - expect(route.resolved_defaults).to eq({:format=>:json}) - end - - it "Naming Routes as option" do - output = draw do - get 'exit', to: 'sessions#destroy', as: :logout - end - text = <<~EOL - logout GET /exit sessions#destroy - EOL - expect(output).to eq(text) - end - - it "Naming Routes overriding method by defining first" do - output = draw do - get ':username', to: 'users#show', as: :user - resources :users - end - text = <<~EOL - user GET /:username users#show - users GET /users users#index - users POST /users users#create - new_user GET /users/new users#new - edit_user GET /users/:id/edit users#edit - user GET /users/:id users#show - user PUT /users/:id users#update - user PATCH /users/:id users#update - user DELETE /users/:id users#destroy - EOL - expect(output).to eq(text) - end - - it "HTTP Verb Constraints via get post" do - output = draw do - match 'photos/:id', to: 'photos#show', via: [:get, :post] - end - text = <<~EOL - photo GET /photos/:id photos#show - photo POST /photos/:id photos#show - EOL - expect(output).to eq(text) - end - - it "HTTP Verb Constraints via all" do - output = draw do - match 'photos', to: 'photos#show', via: :all - end - text = <<~EOL - ANY /photos photos#show - EOL - expect(output).to eq(text) - end - - it "Segment Constraints" do - output = draw do - get 'photos/:id', to: 'photos#show', constraints: { id: /[A-Z]\d{5}/ } - end - text = <<~EOL - photo GET /photos/:id photos#show - EOL - expect(output).to eq(text) - - route = find_route("/photos/1") - expect(route.constraints).to eq({ id: /[A-Z]\d{5}/ }) - end - - it "Segment Constraints id" do - output = draw do - get 'photos/:id', to: 'photos#show', id: /[A-Z]\d{5}/ - end - text = <<~EOL - photo GET /photos/:id photos#show - EOL - expect(output).to eq(text) - - route = find_route("/photos/1") - expect(route.constraints).to eq({ id: /[A-Z]\d{5}/ }) - end - - it "Request-Based Constraints subdomain" do - output = draw do - get 'photos/:id', to: 'photos#show', constraints: { subdomain: 'admin' } - end - text = <<~EOL - photo GET /photos/:id photos#show - EOL - expect(output).to eq(text) - - route = find_route("/photos/1") - expect(route.constraints).to eq({ subdomain: 'admin' }) - end - - it "Request-Based Constraints block form" do - output = draw do - namespace :admin do - constraints subdomain: 'admin' do - resources :photos - end - end - end - text = <<~EOL - admin_photos GET /admin/photos admin/photos#index - admin_photos POST /admin/photos admin/photos#create - new_admin_photo GET /admin/photos/new admin/photos#new - edit_admin_photo GET /admin/photos/:id/edit admin/photos#edit - admin_photo GET /admin/photos/:id admin/photos#show - admin_photo PUT /admin/photos/:id admin/photos#update - admin_photo PATCH /admin/photos/:id admin/photos#update - admin_photo DELETE /admin/photos/:id admin/photos#destroy - EOL - expect(output).to eq(text) - - route = find_route("/admin/photos") - expect(route.constraints).to eq({ subdomain: 'admin' }) - end - - it "Advanced Constraints object that responds to matches" do - output = draw do - get '*path', to: 'restricted_list#index', constraints: RestrictedListConstraint.new - end - text = <<~EOL - GET /*path restricted_list#index - EOL - expect(output).to eq(text) - - route = find_route("/photos/1") - expect(route.constraints).to be_a(RestrictedListConstraint) - end - - it "Advanced Constraints lambda" do - output = draw do - get '*path', to: 'restricted_list#index', constraints: lambda { |request| RestrictedList.retrieve_ips.include?(request.remote_ip) } - end - text = <<~EOL - GET /*path restricted_list#index - EOL - expect(output).to eq(text) - - route = find_route("/photos/1") - expect(route.constraints).to be_a(Proc) - end - - it "Route Globbing and Wildcard Segments" do - output = draw do - get 'photos/*other', to: 'photos#unknown' - end - text = <<~EOL - GET /photos/*other photos#unknown - EOL - expect(output).to eq(text) - end - - it "Route Globbing and Wildcard Segments anywhere" do - output = draw do - get 'books/*section/:title', to: 'books#show' - end - text = <<~EOL - GET /books/*section/:title books#show - EOL - expect(output).to eq(text) - end - - # Note: Unsure if this works for APIGW though - it "Route Globbing and Wildcard Segments more than one wildcard" do - output = draw do - get '*a/foo/*b', to: 'test#index' - end - text = <<~EOL - GET /*a/foo/*b test#index - EOL - expect(output).to eq(text) - end - - # TODOs - # 3.12 Redirection - # 3.13 Routing to Rack Applications - - it "Using root" do - output = draw do - root to: 'pages#main' - end - text = <<~EOL - root GET / pages#main - EOL - expect(output).to eq(text) - - route_set.clear! - output = draw do - root 'pages#main' - end - expect(output).to eq(text) - - expect(app.root_path).to eq("/") - end - - it "Unicode Character Routes" do - output = draw do - get '/こんにちは', to: 'welcome#index' - end - text = <<~EOL - GET /こんにちは welcome#index - EOL - expect(output).to eq(text) - end - - # TODOs - # 3.16 Direct Routes - # 3.17 Using resolve - - end -end - diff --git a/spec/lib/jets/router/dsl/rails_guide/4-customizing_resource_routes_spec.rb b/spec/lib/jets/router/dsl/rails_guide/4-customizing_resource_routes_spec.rb deleted file mode 100644 index afc56cb0f..000000000 --- a/spec/lib/jets/router/dsl/rails_guide/4-customizing_resource_routes_spec.rb +++ /dev/null @@ -1,274 +0,0 @@ -describe Jets::Router do - let(:route_set) { Jets.application.routes } - let(:app) { RouterTestApp.new } - - describe "Router Rails Guide" do - it "Specifying a Controller to Use" do - output = draw do - resources :photos, controller: 'images' - end - text = <<~EOL - photos GET /photos images#index - photos POST /photos images#create - new_photo GET /photos/new images#new - edit_photo GET /photos/:id/edit images#edit - photo GET /photos/:id images#show - photo PUT /photos/:id images#update - photo PATCH /photos/:id images#update - photo DELETE /photos/:id images#destroy - EOL - expect(output).to eq(text) - end - - it "namespaced controllers" do - output = draw do - resources :user_permissions, controller: 'admin/user_permissions' - end - text = <<~EOL - user_permissions GET /user_permissions admin/user_permissions#index - user_permissions POST /user_permissions admin/user_permissions#create - new_user_permission GET /user_permissions/new admin/user_permissions#new - edit_user_permission GET /user_permissions/:id/edit admin/user_permissions#edit - user_permission GET /user_permissions/:id admin/user_permissions#show - user_permission PUT /user_permissions/:id admin/user_permissions#update - user_permission PATCH /user_permissions/:id admin/user_permissions#update - user_permission DELETE /user_permissions/:id admin/user_permissions#destroy - EOL - expect(output).to eq(text) - end - - it "Specifying Constraints" do - output = draw do - resources :photos, constraints: { id: /[A-Z][A-Z][0-9]+/ } - end - text = <<~EOL - photos GET /photos photos#index - photos POST /photos photos#create - new_photo GET /photos/new photos#new - edit_photo GET /photos/:id/edit photos#edit - photo GET /photos/:id photos#show - photo PUT /photos/:id photos#update - photo PATCH /photos/:id photos#update - photo DELETE /photos/:id photos#destroy - EOL - expect(output).to eq(text) - - route = find_route("/photos/1") - expect(route.constraints).to eq({ id: /[A-Z][A-Z][0-9]+/ }) - end - - it "Overriding the Named Route Helpers" do - output = draw do - resources :photos, as: 'images' - end - text = <<~EOL - images GET /photos photos#index - images POST /photos photos#create - new_image GET /photos/new photos#new - edit_image GET /photos/:id/edit photos#edit - image GET /photos/:id photos#show - image PUT /photos/:id photos#update - image PATCH /photos/:id photos#update - image DELETE /photos/:id photos#destroy - EOL - expect(output).to eq(text) - end - - it "Overriding the new and edit Segments" do - output = draw do - resources :photos, path_names: { new: 'make', edit: 'change' } - end - text = <<~EOL - photos GET /photos photos#index - photos POST /photos photos#create - new_photo GET /photos/make photos#new - edit_photo GET /photos/:id/change photos#edit - photo GET /photos/:id photos#show - photo PUT /photos/:id photos#update - photo PATCH /photos/:id photos#update - photo DELETE /photos/:id photos#destroy - EOL - expect(output).to eq(text) - end - - it "Change path_names uniformily" do - output = draw do - scope path_names: { new: 'make' } do - resources :posts - end - end - text = <<~EOL - posts GET /posts posts#index - posts POST /posts posts#create - new_post GET /posts/make posts#new - edit_post GET /posts/:id/edit posts#edit - post GET /posts/:id posts#show - post PUT /posts/:id posts#update - post PATCH /posts/:id posts#update - post DELETE /posts/:id posts#destroy - EOL - expect(output).to eq(text) - end - - it "Prefixing the Named Route Helpers" do - output = draw do - scope 'admin' do - resources :photos, as: 'admin_photos' - end - resources :photos - end - text = <<~EOL - admin_photos GET /admin/photos photos#index - admin_photos POST /admin/photos photos#create - new_admin_photo GET /admin/photos/new photos#new - edit_admin_photo GET /admin/photos/:id/edit photos#edit - admin_photo GET /admin/photos/:id photos#show - admin_photo PUT /admin/photos/:id photos#update - admin_photo PATCH /admin/photos/:id photos#update - admin_photo DELETE /admin/photos/:id photos#destroy - photos GET /photos photos#index - photos POST /photos photos#create - new_photo GET /photos/new photos#new - edit_photo GET /photos/:id/edit photos#edit - photo GET /photos/:id photos#show - photo PUT /photos/:id photos#update - photo PATCH /photos/:id photos#update - photo DELETE /photos/:id photos#destroy - EOL - expect(output).to eq(text) - end - - it "Parametric Scopes" do - output = draw do - scope ':account_id', as: 'account', constraints: { account_id: /\d+/ } do - resources :articles - end - end - text = <<~EOL - account_articles GET /:account_id/articles articles#index - account_articles POST /:account_id/articles articles#create - new_account_article GET /:account_id/articles/new articles#new - edit_account_article GET /:account_id/articles/:id/edit articles#edit - account_article GET /:account_id/articles/:id articles#show - account_article PUT /:account_id/articles/:id articles#update - account_article PATCH /:account_id/articles/:id articles#update - account_article DELETE /:account_id/articles/:id articles#destroy - EOL - expect(output).to eq(text) - - expect(app.account_articles_path(1)).to eq("/1/articles") - expect(app.edit_account_article_path(1,2)).to eq("/1/articles/2/edit") - end - - it "Restricting the Routes Created" do - output = draw do - resources :photos, only: [:index, :show] - end - text = <<~EOL - photos GET /photos photos#index - photo GET /photos/:id photos#show - EOL - expect(output).to eq(text) - end - - it "Restricting the Routes Created except" do - output = draw do - resources :photos, except: :destroy - end - text = <<~EOL - photos GET /photos photos#index - photos POST /photos photos#create - new_photo GET /photos/new photos#new - edit_photo GET /photos/:id/edit photos#edit - photo GET /photos/:id photos#show - photo PUT /photos/:id photos#update - photo PATCH /photos/:id photos#update - EOL - expect(output).to eq(text) - end - - it "Translated Paths" do - output = draw do - scope(path_names: { new: 'neu', edit: 'bearbeiten' }) do - resources :categories, path: 'kategorien' - end - end - text = <<~EOL - categories GET /kategorien categories#index - categories POST /kategorien categories#create - new_category GET /kategorien/neu categories#new - edit_category GET /kategorien/:id/bearbeiten categories#edit - category GET /kategorien/:id categories#show - category PUT /kategorien/:id categories#update - category PATCH /kategorien/:id categories#update - category DELETE /kategorien/:id categories#destroy - EOL - expect(output).to eq(text) - end - - it "Overriding the Singular Form" do - ActiveSupport::Inflector.inflections do |inflect| - inflect.irregular 'tooth', 'teeth' - end - output = draw do - resources :teeth - end - text = <<~EOL - teeth GET /teeth teeth#index - teeth POST /teeth teeth#create - new_tooth GET /teeth/new teeth#new - edit_tooth GET /teeth/:id/edit teeth#edit - tooth GET /teeth/:id teeth#show - tooth PUT /teeth/:id teeth#update - tooth PATCH /teeth/:id teeth#update - tooth DELETE /teeth/:id teeth#destroy - EOL - expect(output).to eq(text) - end - - it "Using :as in Nested Resources" do - output = draw do - resources :magazines do - resources :ads, as: 'periodical_ads' - end - end - text = <<~EOL - magazines GET /magazines magazines#index - magazines POST /magazines magazines#create - new_magazine GET /magazines/new magazines#new - edit_magazine GET /magazines/:id/edit magazines#edit - magazine GET /magazines/:id magazines#show - magazine PUT /magazines/:id magazines#update - magazine PATCH /magazines/:id magazines#update - magazine DELETE /magazines/:id magazines#destroy - magazine_periodical_ads GET /magazines/:magazine_id/ads ads#index - magazine_periodical_ads POST /magazines/:magazine_id/ads ads#create - new_magazine_periodical_ad GET /magazines/:magazine_id/ads/new ads#new - edit_magazine_periodical_ad GET /magazines/:magazine_id/ads/:id/edit ads#edit - magazine_periodical_ad GET /magazines/:magazine_id/ads/:id ads#show - magazine_periodical_ad PUT /magazines/:magazine_id/ads/:id ads#update - magazine_periodical_ad PATCH /magazines/:magazine_id/ads/:id ads#update - magazine_periodical_ad DELETE /magazines/:magazine_id/ads/:id ads#destroy - EOL - expect(output).to eq(text) - end - - it "Overriding Named Route Parameters" do - output = draw do - resources :videos, param: :identifier - end - text = <<~EOL - videos GET /videos videos#index - videos POST /videos videos#create - new_video GET /videos/new videos#new - edit_video GET /videos/:identifier/edit videos#edit - video GET /videos/:identifier videos#show - video PUT /videos/:identifier videos#update - video PATCH /videos/:identifier videos#update - video DELETE /videos/:identifier videos#destroy - EOL - expect(output).to eq(text) - end - end -end - diff --git a/spec/lib/jets/router/dsl/resource_spec.rb b/spec/lib/jets/router/dsl/resource_spec.rb deleted file mode 100644 index 82bc1b896..000000000 --- a/spec/lib/jets/router/dsl/resource_spec.rb +++ /dev/null @@ -1,151 +0,0 @@ -describe Jets::Router do - let(:route_set) { Jets.application.routes } - let(:app) { RouterTestApp.new } - - describe "Router resource" do - it "profile" do - output = draw do - resource :profile - end - # There is no index route for the singular resource - text = <<~EOL - profile POST /profile profiles#create - new_profile GET /profile/new profiles#new - edit_profile GET /profile/edit profiles#edit - profile GET /profile profiles#show - profile PUT /profile profiles#update - profile PATCH /profile profiles#update - profile DELETE /profile profiles#destroy - EOL - expect(output).to eq(text) - - expect(app.new_profile_path).to eq("/profile/new") - expect(app.profile_path).to eq("/profile") - expect(app.edit_profile_path).to eq("/profile/edit") - end - - it "nested resources profile" do - output = draw do - resources :users do - resource :profile - end - end - # There is no index route for the singular resource - text = <<~EOL - users GET /users users#index - users POST /users users#create - new_user GET /users/new users#new - edit_user GET /users/:id/edit users#edit - user GET /users/:id users#show - user PUT /users/:id users#update - user PATCH /users/:id users#update - user DELETE /users/:id users#destroy - user_profile POST /users/:user_id/profile profiles#create - new_user_profile GET /users/:user_id/profile/new profiles#new - edit_user_profile GET /users/:user_id/profile/edit profiles#edit - user_profile GET /users/:user_id/profile profiles#show - user_profile PUT /users/:user_id/profile profiles#update - user_profile PATCH /users/:user_id/profile profiles#update - user_profile DELETE /users/:user_id/profile profiles#destroy - EOL - expect(output).to eq(text) - - expect(app.users_path).to eq("/users") - expect(app.new_user_path).to eq("/users/new") - expect(app.user_path(1)).to eq("/users/1") - expect(app.edit_user_path(1)).to eq("/users/1/edit") - - expect(app.new_user_profile_path(1)).to eq("/users/1/profile/new") - expect(app.user_profile_path(1)).to eq("/users/1/profile") - expect(app.edit_user_profile_path(1)).to eq("/users/1/profile/edit") - end - - it "nested namespace profile" do - output = draw do - namespace :admin do - resource :profile - end - end - text = <<~EOL - admin_profile POST /admin/profile admin/profiles#create - new_admin_profile GET /admin/profile/new admin/profiles#new - edit_admin_profile GET /admin/profile/edit admin/profiles#edit - admin_profile GET /admin/profile admin/profiles#show - admin_profile PUT /admin/profile admin/profiles#update - admin_profile PATCH /admin/profile admin/profiles#update - admin_profile DELETE /admin/profile admin/profiles#destroy - EOL - expect(output).to eq(text) - - expect(app.new_admin_profile_path).to eq("/admin/profile/new") - expect(app.admin_profile_path).to eq("/admin/profile") - expect(app.edit_admin_profile_path).to eq("/admin/profile/edit") - end - - it "member and collection" do - output = draw do - resource :profile do - get "photo", on: :member - get "comments", on: :collection - end - end - text = <<~EOL - profile POST /profile profiles#create - new_profile GET /profile/new profiles#new - edit_profile GET /profile/edit profiles#edit - profile GET /profile profiles#show - profile PUT /profile profiles#update - profile PATCH /profile profiles#update - profile DELETE /profile profiles#destroy - photo_profile GET /profile/photo profiles#photo - comments_profile GET /profile/comments profiles#comments - EOL - expect(output).to eq(text) - - expect(app.new_profile_path).to eq("/profile/new") - expect(app.profile_path).to eq("/profile") - expect(app.edit_profile_path).to eq("/profile/edit") - expect(app.photo_profile_path).to eq("/profile/photo") - expect(app.comments_profile_path).to eq("/profile/comments") - end - - it "as option" do - output = draw do - resource :profile, as: :account - end - text = <<~EOL - account POST /profile profiles#create - new_account GET /profile/new profiles#new - edit_account GET /profile/edit profiles#edit - account GET /profile profiles#show - account PUT /profile profiles#update - account PATCH /profile profiles#update - account DELETE /profile profiles#destroy - EOL - expect(output).to eq(text) - - expect(app.new_account_path).to eq("/profile/new") - expect(app.account_path).to eq("/profile") - expect(app.edit_account_path).to eq("/profile/edit") - end - - # Sanity check that empty Hash does break. In case try to use ruby syntax - # def resource(*resource_names, **options) - # With that syntax, would need to double splat it: resource(:profile, **{}) - it "resource with empty options" do - output = draw do - resource(:profile, {}) - end - text = <<~EOL - profile POST /profile profiles#create - new_profile GET /profile/new profiles#new - edit_profile GET /profile/edit profiles#edit - profile GET /profile profiles#show - profile PUT /profile profiles#update - profile PATCH /profile profiles#update - profile DELETE /profile profiles#destroy - EOL - expect(output).to eq(text) - end - end -end diff --git a/spec/lib/jets/router/dsl/resources_spec.rb b/spec/lib/jets/router/dsl/resources_spec.rb deleted file mode 100644 index e9333bd02..000000000 --- a/spec/lib/jets/router/dsl/resources_spec.rb +++ /dev/null @@ -1,328 +0,0 @@ -describe Jets::Router do - let(:route_set) { Jets.application.routes } - let(:app) { RouterTestApp.new } - - describe "Router resources" do - it ":posts" do - output = draw do - resources :posts - end - text = <<~EOL - posts GET /posts posts#index - posts POST /posts posts#create - new_post GET /posts/new posts#new - edit_post GET /posts/:id/edit posts#edit - post GET /posts/:id posts#show - post PUT /posts/:id posts#update - post PATCH /posts/:id posts#update - post DELETE /posts/:id posts#destroy - EOL - expect(output).to eq(text) - - expect(app.posts_path).to eq("/posts") - end - - it "only option posts comments" do - output = draw do - resources :posts, only: :new do - resources :comments, only: [:edit] - end - end - text = <<~EOL - new_post GET /posts/new posts#new - edit_post_comment GET /posts/:post_id/comments/:id/edit comments#edit - EOL - expect(output).to eq(text) - expect(route_set.routes).to be_a(Array) - expect(route_set.routes.first).to be_a(Jets::Router::Route) - end - - it "nested with another resources posts comments" do - output = draw do - resources :posts do - resources :comments - end - end - text = <<~EOL - posts GET /posts posts#index - posts POST /posts posts#create - new_post GET /posts/new posts#new - edit_post GET /posts/:id/edit posts#edit - post GET /posts/:id posts#show - post PUT /posts/:id posts#update - post PATCH /posts/:id posts#update - post DELETE /posts/:id posts#destroy - post_comments GET /posts/:post_id/comments comments#index - post_comments POST /posts/:post_id/comments comments#create - new_post_comment GET /posts/:post_id/comments/new comments#new - edit_post_comment GET /posts/:post_id/comments/:id/edit comments#edit - post_comment GET /posts/:post_id/comments/:id comments#show - post_comment PUT /posts/:post_id/comments/:id comments#update - post_comment PATCH /posts/:post_id/comments/:id comments#update - post_comment DELETE /posts/:post_id/comments/:id comments#destroy - EOL - expect(output).to eq(text) - - expect(app.posts_path).to eq("/posts") - expect(app.new_post_path).to eq("/posts/new") - expect(app.post_path(1)).to eq("/posts/1") - expect(app.edit_post_path(1)).to eq("/posts/1/edit") - - expect(app.post_comments_path(1)).to eq("/posts/1/comments") - expect(app.new_post_comment_path(1)).to eq("/posts/1/comments/new") - expect(app.post_comment_path(1, 2)).to eq("/posts/1/comments/2") - expect(app.edit_post_comment_path(1, 2)).to eq("/posts/1/comments/2/edit") - end - - it "member and collection" do - output = draw do - resources :accounts, only: [] do - get :photo, on: :member - get :comments, on: :collection - end - end - text = <<~EOL - photo_account GET /accounts/:id/photo accounts#photo - comments_accounts GET /accounts/comments accounts#comments - EOL - expect(output).to eq(text) - - expect(app.photo_account_path(1)).to eq("/accounts/1/photo") - expect(app.comments_accounts_path).to eq("/accounts/comments") - end - - it "as articles" do - output = draw do - resources :posts, as: "articles" - end - text = <<~EOL - articles GET /posts posts#index - articles POST /posts posts#create - new_article GET /posts/new posts#new - edit_article GET /posts/:id/edit posts#edit - article GET /posts/:id posts#show - article PUT /posts/:id posts#update - article PATCH /posts/:id posts#update - article DELETE /posts/:id posts#destroy - EOL - expect(output).to eq(text) - - expect(app.articles_path).to eq("/posts") - expect(app.new_article_path).to eq("/posts/new") - expect(app.article_path(1)).to eq("/posts/1") - expect(app.edit_article_path(1)).to eq("/posts/1/edit") - end - - it "module admin" do - output = draw do - resources :posts, module: "admin" - end - text = <<~EOL - posts GET /posts admin/posts#index - posts POST /posts admin/posts#create - new_post GET /posts/new admin/posts#new - edit_post GET /posts/:id/edit admin/posts#edit - post GET /posts/:id admin/posts#show - post PUT /posts/:id admin/posts#update - post PATCH /posts/:id admin/posts#update - post DELETE /posts/:id admin/posts#destroy - EOL - expect(output).to eq(text) - - expect(app.posts_path).to eq("/posts") - expect(app.new_post_path).to eq("/posts/new") - expect(app.post_path(1)).to eq("/posts/1") - expect(app.edit_post_path(1)).to eq("/posts/1/edit") - end - - it "path admin" do - output = draw do - resources :posts, path: "admin/posts" - end - text = <<~EOL - posts GET /admin/posts posts#index - posts POST /admin/posts posts#create - new_post GET /admin/posts/new posts#new - edit_post GET /admin/posts/:id/edit posts#edit - post GET /admin/posts/:id posts#show - post PUT /admin/posts/:id posts#update - post PATCH /admin/posts/:id posts#update - post DELETE /admin/posts/:id posts#destroy - EOL - expect(output).to eq(text) - - expect(app.posts_path).to eq("/admin/posts") - expect(app.new_post_path).to eq("/admin/posts/new") - expect(app.post_path(1)).to eq("/admin/posts/1") - expect(app.edit_post_path(1)).to eq("/admin/posts/1/edit") - end - - it "prefix with nested resources comments" do - output = draw do - resources :posts, path: "admin/posts" do - resources :comments - end - end - text = <<~EOL - posts GET /admin/posts posts#index - posts POST /admin/posts posts#create - new_post GET /admin/posts/new posts#new - edit_post GET /admin/posts/:id/edit posts#edit - post GET /admin/posts/:id posts#show - post PUT /admin/posts/:id posts#update - post PATCH /admin/posts/:id posts#update - post DELETE /admin/posts/:id posts#destroy - post_comments GET /admin/posts/:post_id/comments comments#index - post_comments POST /admin/posts/:post_id/comments comments#create - new_post_comment GET /admin/posts/:post_id/comments/new comments#new - edit_post_comment GET /admin/posts/:post_id/comments/:id/edit comments#edit - post_comment GET /admin/posts/:post_id/comments/:id comments#show - post_comment PUT /admin/posts/:post_id/comments/:id comments#update - post_comment PATCH /admin/posts/:post_id/comments/:id comments#update - post_comment DELETE /admin/posts/:post_id/comments/:id comments#destroy - EOL - - expect(output).to eq(text) - - expect(app.posts_path).to eq("/admin/posts") - expect(app.new_post_path).to eq("/admin/posts/new") - expect(app.post_path(1)).to eq("/admin/posts/1") - expect(app.edit_post_path(1)).to eq("/admin/posts/1/edit") - - expect(app.post_comments_path(1)).to eq("/admin/posts/1/comments") - expect(app.new_post_comment_path(1)).to eq("/admin/posts/1/comments/new") - expect(app.post_comment_path(1, 2)).to eq("/admin/posts/1/comments/2") - expect(app.edit_post_comment_path(1, 2)).to eq("/admin/posts/1/comments/2/edit") - end - - it "controller articles" do - output = draw do - resources :posts, controller: "articles" - end - text = <<~EOL - posts GET /posts articles#index - posts POST /posts articles#create - new_post GET /posts/new articles#new - edit_post GET /posts/:id/edit articles#edit - post GET /posts/:id articles#show - post PUT /posts/:id articles#update - post PATCH /posts/:id articles#update - post DELETE /posts/:id articles#destroy - EOL - - expect(output).to eq(text) - - expect(app.posts_path).to eq("/posts") - expect(app.new_post_path).to eq("/posts/new") - expect(app.post_path(1)).to eq("/posts/1") - expect(app.edit_post_path(1)).to eq("/posts/1/edit") - end - - it "controller with namespace admin/posts" do - output = draw do - resources :posts, controller: "admin/posts" - end - text = <<~EOL - posts GET /posts admin/posts#index - posts POST /posts admin/posts#create - new_post GET /posts/new admin/posts#new - edit_post GET /posts/:id/edit admin/posts#edit - post GET /posts/:id admin/posts#show - post PUT /posts/:id admin/posts#update - post PATCH /posts/:id admin/posts#update - post DELETE /posts/:id admin/posts#destroy - EOL - expect(output).to eq(text) - - expect(app.posts_path).to eq("/posts") - expect(app.new_post_path).to eq("/posts/new") - expect(app.post_path(1)).to eq("/posts/1") - expect(app.edit_post_path(1)).to eq("/posts/1/edit") - end - - it "param custom my_comment_id" do - output = draw do - resources :posts do - resources :comments, param: :my_comment_id - end - resources :users, param: :my_user_id - end - text = <<~EOL - posts GET /posts posts#index - posts POST /posts posts#create - new_post GET /posts/new posts#new - edit_post GET /posts/:id/edit posts#edit - post GET /posts/:id posts#show - post PUT /posts/:id posts#update - post PATCH /posts/:id posts#update - post DELETE /posts/:id posts#destroy - post_comments GET /posts/:post_id/comments comments#index - post_comments POST /posts/:post_id/comments comments#create - new_post_comment GET /posts/:post_id/comments/new comments#new - edit_post_comment GET /posts/:post_id/comments/:my_comment_id/edit comments#edit - post_comment GET /posts/:post_id/comments/:my_comment_id comments#show - post_comment PUT /posts/:post_id/comments/:my_comment_id comments#update - post_comment PATCH /posts/:post_id/comments/:my_comment_id comments#update - post_comment DELETE /posts/:post_id/comments/:my_comment_id comments#destroy - users GET /users users#index - users POST /users users#create - new_user GET /users/new users#new - edit_user GET /users/:my_user_id/edit users#edit - user GET /users/:my_user_id users#show - user PUT /users/:my_user_id users#update - user PATCH /users/:my_user_id users#update - user DELETE /users/:my_user_id users#destroy - EOL - expect(output).to eq(text) - - expect(app.users_path).to eq("/users") - expect(app.new_user_path).to eq("/users/new") - expect(app.user_path(1)).to eq("/users/1") - expect(app.edit_user_path(1)).to eq("/users/1/edit") - end - - it "param custom my_comment_id with block" do - output = draw do - resources :posts do - resources :comments, param: :my_comment_id, only: [:create] do - get :test, on: :member - end - end - - resources :cars, param: :my_parent_id do - resources :ratings, only: [:create, :show], param: :my_child_id do - # nothing - end - end - - resources :users, param: :my_user_id, only: [] do - get :test, on: :member - end - end - text = <<~EOL - posts GET /posts posts#index - posts POST /posts posts#create - new_post GET /posts/new posts#new - edit_post GET /posts/:id/edit posts#edit - post GET /posts/:id posts#show - post PUT /posts/:id posts#update - post PATCH /posts/:id posts#update - post DELETE /posts/:id posts#destroy - post_comments POST /posts/:post_id/comments comments#create - test_post_comment GET /posts/:post_id/comments/:my_comment_id/test comments#test - cars GET /cars cars#index - cars POST /cars cars#create - new_car GET /cars/new cars#new - edit_car GET /cars/:my_parent_id/edit cars#edit - car GET /cars/:my_parent_id cars#show - car PUT /cars/:my_parent_id cars#update - car PATCH /cars/:my_parent_id cars#update - car DELETE /cars/:my_parent_id cars#destroy - car_ratings POST /cars/:my_parent_id/ratings ratings#create - car_rating GET /cars/:my_parent_id/ratings/:my_child_id ratings#show - test_user GET /users/:my_user_id/test users#test - EOL - expect(output).to eq(text) - end - end -end diff --git a/spec/lib/jets/router/dsl/scope_spec.rb b/spec/lib/jets/router/dsl/scope_spec.rb deleted file mode 100644 index bd034ea62..000000000 --- a/spec/lib/jets/router/dsl/scope_spec.rb +++ /dev/null @@ -1,200 +0,0 @@ -describe Jets::Router do - let(:route_set) { Jets.application.routes } - let(:app) { RouterTestApp.new } - - describe "Router scope" do - it "single admin path" do - output = draw do - scope(path: :admin) do - get "posts", to: "posts#index" - end - end - text = <<~EOL - posts GET /admin/posts posts#index - EOL - end - - it "nested admin path on multiple lines" do - output = draw do - scope(path: :v1) do - scope(path: :admin) do - get "posts", to: "posts#index" - end - end - end - text = <<~EOL - posts GET /v1/admin/posts posts#index - EOL - expect(output).to eq(text) - end - - it "nested admin path on oneline" do - output = draw do - scope(path: :v1) do - scope(path: :admin) do - get "posts", to: "posts#index" - end - end - end - text = <<~EOL - posts GET /v1/admin/posts posts#index - EOL - expect(output).to eq(text) - end - - it "nested admin path as string" do - output = draw do - scope "v1/admin" do - get "posts", to: "posts#index" - end - end - text = <<~EOL - posts GET /v1/admin/posts posts#index - EOL - expect(output).to eq(text) - end - - it "nested admin path as symbol" do - output = draw do - scope :admin do - get "posts", to: "posts#index" - end - end - text = <<~EOL - posts GET /admin/posts posts#index - EOL - expect(output).to eq(text) - end - - it "single admin as with individual routes" do - output = draw do - scope(as: :admin) do - get "posts", to: "posts#index" - get "posts/new", to: "posts#new" - get "posts/:id", to: "posts#show" - post "posts", to: "posts#create" - get "posts/:id/edit", to: "posts#edit" - put "posts/:id", to: "posts#update" - post "posts/:id", to: "posts#update" - patch "posts/:id", to: "posts#update" - delete "posts/:id", to: "posts#delete" - end - end - text = <<~EOL - admin_posts GET /posts posts#index - new_admin_post GET /posts/new posts#new - admin_post GET /posts/:id posts#show - admin_posts POST /posts posts#create - edit_admin_post GET /posts/:id/edit posts#edit - admin_post PUT /posts/:id posts#update - admin_post POST /posts/:id posts#update - admin_post PATCH /posts/:id posts#update - admin_posts DELETE /posts/:id posts#delete - EOL - expect(output).to eq(text) - end - - it "single admin as with resources" do - output = draw do - scope(as: :admin) do - resources :posts - end - end - text = <<~EOL - admin_posts GET /posts posts#index - admin_posts POST /posts posts#create - new_admin_post GET /posts/new posts#new - edit_admin_post GET /posts/:id/edit posts#edit - admin_post GET /posts/:id posts#show - admin_post PUT /posts/:id posts#update - admin_post PATCH /posts/:id posts#update - admin_post DELETE /posts/:id posts#destroy - EOL - expect(output).to eq(text) - end - - # more general scope method - it "admin module single method" do - output = draw do - scope(module: :admin) do - get "posts", to: "posts#index" - end - end - text = <<~EOL - posts GET /posts admin/posts#index - EOL - expect(output).to eq(text) - end - - it "admin module all methods" do - output = draw do - scope(module: :admin) do - resources "posts" - end - end - text = <<~EOL - posts GET /posts admin/posts#index - posts POST /posts admin/posts#create - new_post GET /posts/new admin/posts#new - edit_post GET /posts/:id/edit admin/posts#edit - post GET /posts/:id admin/posts#show - post PUT /posts/:id admin/posts#update - post PATCH /posts/:id admin/posts#update - post DELETE /posts/:id admin/posts#destroy - EOL - - end - - it "api/v1 module nested single method" do - output = draw do - scope(module: :api) do - scope(module: :v1) do - get "posts", to: "posts#index" - end - end - end - text = <<~EOL - posts GET /posts api/v1/posts#index - EOL - expect(output).to eq(text) - end - - it "api/v1 module nested all resources methods" do - output = draw do - scope(module: :api) do - scope(module: :v1) do - resources :posts - end - end - end - text = <<~EOL - posts GET /posts api/v1/posts#index - posts POST /posts api/v1/posts#create - new_post GET /posts/new api/v1/posts#new - edit_post GET /posts/:id/edit api/v1/posts#edit - post GET /posts/:id api/v1/posts#show - post PUT /posts/:id api/v1/posts#update - post PATCH /posts/:id api/v1/posts#update - post DELETE /posts/:id api/v1/posts#destroy - EOL - expect(output).to eq(text) - - expect(app.posts_path).to eq("/posts") - expect(app.new_post_path).to eq("/posts/new") - expect(app.post_path(1)).to eq("/posts/1") - expect(app.edit_post_path(1)).to eq("/posts/1/edit") - end - - it "api/v1 module oneline" do - output = draw do - scope(module: "api/v1") do - get "posts", to: "posts#index" - end - end - text = <<~EOL - posts GET /posts api/v1/posts#index - EOL - expect(output).to eq(text) - end - end -end diff --git a/spec/lib/jets/router/dsl/shallow_spec.rb b/spec/lib/jets/router/dsl/shallow_spec.rb deleted file mode 100644 index d73f8b6a3..000000000 --- a/spec/lib/jets/router/dsl/shallow_spec.rb +++ /dev/null @@ -1,117 +0,0 @@ -describe Jets::Router do - let(:route_set) { Jets.application.routes } - let(:app) { RouterTestApp.new } - - describe "Router shallow" do - it "triple nested shallow at the top on resources posts" do - output = draw do - resources :posts, shallow: true do - resources :comments do - resources :likes - end - end - end - text = <<~EOL - posts GET /posts posts#index - posts POST /posts posts#create - new_post GET /posts/new posts#new - edit_post GET /posts/:id/edit posts#edit - post GET /posts/:id posts#show - post PUT /posts/:id posts#update - post PATCH /posts/:id posts#update - post DELETE /posts/:id posts#destroy - post_comments GET /posts/:post_id/comments comments#index - post_comments POST /posts/:post_id/comments comments#create - new_post_comment GET /posts/:post_id/comments/new comments#new - edit_comment GET /comments/:id/edit comments#edit - comment GET /comments/:id comments#show - comment PUT /comments/:id comments#update - comment PATCH /comments/:id comments#update - comment DELETE /comments/:id comments#destroy - comment_likes GET /comments/:comment_id/likes likes#index - comment_likes POST /comments/:comment_id/likes likes#create - new_comment_like GET /comments/:comment_id/likes/new likes#new - edit_like GET /likes/:id/edit likes#edit - like GET /likes/:id likes#show - like PUT /likes/:id likes#update - like PATCH /likes/:id likes#update - like DELETE /likes/:id likes#destroy - EOL - expect(output).to eq(text) - end - - it "triple nested shallow at the middle on resources comments" do - output = draw do - resources :posts do - resources :comments, shallow: true do - resources :likes - end - end - end - text = <<~EOL - posts GET /posts posts#index - posts POST /posts posts#create - new_post GET /posts/new posts#new - edit_post GET /posts/:id/edit posts#edit - post GET /posts/:id posts#show - post PUT /posts/:id posts#update - post PATCH /posts/:id posts#update - post DELETE /posts/:id posts#destroy - post_comments GET /posts/:post_id/comments comments#index - post_comments POST /posts/:post_id/comments comments#create - new_post_comment GET /posts/:post_id/comments/new comments#new - edit_comment GET /comments/:id/edit comments#edit - comment GET /comments/:id comments#show - comment PUT /comments/:id comments#update - comment PATCH /comments/:id comments#update - comment DELETE /comments/:id comments#destroy - comment_likes GET /comments/:comment_id/likes likes#index - comment_likes POST /comments/:comment_id/likes likes#create - new_comment_like GET /comments/:comment_id/likes/new likes#new - edit_like GET /likes/:id/edit likes#edit - like GET /likes/:id likes#show - like PUT /likes/:id likes#update - like PATCH /likes/:id likes#update - like DELETE /likes/:id likes#destroy - EOL - expect(output).to eq(text) - end - - it "triple nested shallow at the bottom on resources likes" do - output = draw do - resources :posts do - resources :comments do - resources :likes, shallow: true - end - end - end - text = <<~EOL - posts GET /posts posts#index - posts POST /posts posts#create - new_post GET /posts/new posts#new - edit_post GET /posts/:id/edit posts#edit - post GET /posts/:id posts#show - post PUT /posts/:id posts#update - post PATCH /posts/:id posts#update - post DELETE /posts/:id posts#destroy - post_comments GET /posts/:post_id/comments comments#index - post_comments POST /posts/:post_id/comments comments#create - new_post_comment GET /posts/:post_id/comments/new comments#new - edit_post_comment GET /posts/:post_id/comments/:id/edit comments#edit - post_comment GET /posts/:post_id/comments/:id comments#show - post_comment PUT /posts/:post_id/comments/:id comments#update - post_comment PATCH /posts/:post_id/comments/:id comments#update - post_comment DELETE /posts/:post_id/comments/:id comments#destroy - post_comment_likes GET /posts/:post_id/comments/:comment_id/likes likes#index - post_comment_likes POST /posts/:post_id/comments/:comment_id/likes likes#create - new_post_comment_like GET /posts/:post_id/comments/:comment_id/likes/new likes#new - edit_like GET /likes/:id/edit likes#edit - like GET /likes/:id likes#show - like PUT /likes/:id likes#update - like PATCH /likes/:id likes#update - like DELETE /likes/:id likes#destroy - EOL - expect(output).to eq(text) - end - end -end diff --git a/spec/lib/jets/router/matcher_spec.rb b/spec/lib/jets/router/matcher_spec.rb deleted file mode 100644 index 717f47580..000000000 --- a/spec/lib/jets/router/matcher_spec.rb +++ /dev/null @@ -1,143 +0,0 @@ -describe Jets::Router::Matcher do - let(:matcher) do - matcher = Jets::Router::Matcher.new(route_set) - matcher.instance_variable_set(:@request_path, request_path) if request_path - matcher.instance_variable_set(:@request_method, request_method) if request_method - matcher - end - let(:route_set) do - double(:route_set, ordered_routes: ordered_routes) - end - # Defaults: specs will override and use accordingly - let(:ordered_routes) { [] } - let(:request_path) { nil } - let(:request_method) { "GET" } - - def build_env(path, method) - { - "PATH_INFO" => path, - "REQUEST_METHOD" => method, - } - end - - def route(options) - options = {path: options} if options.is_a?(String) - defaults = { - http_method: "GET", - # path: "/", - # to: "posts#new", # dont really have to set to for specs - } - options.reverse_merge!(defaults) - Jets::Router::Route.new(options) - end - - context "get /" do - let(:ordered_routes) do - [route("/")] - end - - it "match? finds root route" do - env = build_env("/", "GET") - route = matcher.find_by_env(env) - expect(route).not_to be(nil) - end - end - - context "get posts/:id/edit" do - let(:request_path) { "/posts/tung/edit" } - - it "match? finds highest precedence route" do - # In this case the catchall and the capture route matches - # But the matcher finds the route with the highest precedence - route1 = route("*catchall") - found = matcher.match?(route1) - expect(found).to be true - - route2 = route("posts/:id/edit") - - found = matcher.match?(route2) - expect(found).to be true - end - end - - context "get everything/else catchall route" do - let(:request_path) { "/everything/else" } - - it "match?" do - route = route("*catchall") - found = matcher.match?(route) - expect(found).to be true - end - end - - context "get posts/:id/edit" do - let(:request_path) { "/posts/tung/edit" } - - it "match?" do - route = route("posts/:id/edit") - found = matcher.match?(route) - expect(found).to be true - end - end - - context "any comments/hot with get" do - let(:request_path) { "/comments/hot" } - - it "route_found?" do - route = route("comments/hot") - found = matcher.match?(route) - expect(found).to be true - end - end - - context "any comments/hot with post" do - let(:request_path) { "/comments/hot" } - let(:request_method) { "POST" } - - it "match?" do - route = route(path: "comments/hot", http_method: "POST") - found = matcher.match?(route) - expect(found).to be true - end - end - - context "any comments/hot with non-matching path" do - let(:request_path) { "/some/other/path" } - - it "match?" do - route = route("comments/hot") - found = matcher.match?(route) - expect(found).to be false - end - end - - context "get admin/pages" do - let(:request_path) { "/admin/pages" } - - it "route_found?" do - route = route("admin/pages") - found = matcher.match?(route) - expect(found).to be true - end - end - - context "get others/my/long/path - proxy path route" do - let(:request_path) { "others/my/long/path" } - - it "match?" do - route = route("others/*proxy") - found = matcher.match?(route) - expect(found).to be true - end - end - - context "get others/my/long/path - proxy path route" do - let(:request_path) { "others2/my/long/path" } - - it "not match?" do - route = route("others/*proxy") - found = matcher.match?(route) - expect(found).to be false - end - end -end diff --git a/spec/lib/jets/router/route_spec.rb b/spec/lib/jets/router/route_spec.rb deleted file mode 100644 index efdb04a48..000000000 --- a/spec/lib/jets/router/route_spec.rb +++ /dev/null @@ -1,164 +0,0 @@ -describe "Route" do - it "evaluates route info" do - route = Jets::Router::Route.new(path: "posts", http_method: :get, to: "posts#index") - expect(route.path).to eq "/posts" - expect(route.to).to eq "posts#index" - expect(route.http_method).to eq "GET" - expect(route.controller_name).to eq "PostsController" - end - - context "route with namespace" do - it "evaluates route info" do - route = Jets::Router::Route.new(path: "admin/pages", http_method: :get, to: "admin/pages#index") - expect(route.path).to eq "/admin/pages" - expect(route.to).to eq "admin/pages#index" - expect(route.http_method).to eq "GET" - expect(route.controller_name).to eq "Admin::PagesController" - end - end - - context "route with captures" do - let(:route) do - Jets::Router::Route.new(path: "posts/:id/edit", http_method: :get, to: "posts#edit") - end - it "extract_parameters" do - parameters = route.extract_parameters("posts/tung/edit") - expect(parameters).to eq("id" => "tung") - end - - it "extract_parameters with dash" do - parameters = route.extract_parameters("posts/tung-nguyen/edit") - expect(parameters).to eq("id" => "tung-nguyen") - end - - it "api_gateway_format" do - api_gateway_path = route.send(:api_gateway_format, route.path) - expect(api_gateway_path).to eq "/posts/{id}/edit" - end - end - - context "route with multiple captures" do - let(:route) do - Jets::Router::Route.new(path: "posts/:type/:id", http_method: :get, to: "posts#edit") - end - it "extract_parameters" do - parameters = route.extract_parameters("posts/sometype/someid") - expect(parameters).to eq({ - "id" => "someid", - "type" => "sometype", - }) - end - - it "api_gateway_format" do - api_gateway_path = route.send(:api_gateway_format, route.path) - expect(api_gateway_path).to eq "/posts/{type}/{id}" - end - end - - context "route with captures and extension" do - let(:route) do - Jets::Router::Route.new(path: "posts/:id", http_method: :get, to: "posts#show") - end - - it "extract_parameters" do - parameters = route.extract_parameters("posts/tung.png") - expect(parameters).to eq("id" => "tung.png") - end - end - - context "route with catchall/globbing/wildcard" do - let(:route) do - Jets::Router::Route.new(path: "others/*proxy", http_method: :any, to: "others#all") - end - - it "api_gateway_format" do - api_gateway_path = route.send(:api_gateway_format, route.path) - expect(api_gateway_path).to eq "/others/{proxy+}" - end - - it "extract_parameters" do - parameters = route.extract_parameters("others/my/long/path") - expect(parameters).to eq("proxy" => "my/long/path") - end - end - - context "route with toplevel catchall/globbing/wildcard" do - let(:route) do - Jets::Router::Route.new(path: "*catchall", http_method: :any, to: "catch#all") - end - - it "api_gateway_format" do - api_gateway_path = route.send(:api_gateway_format, route.path) - expect(api_gateway_path).to eq "/{catchall+}" - end - - it "extract_parameters for path with slashes" do - parameters = route.extract_parameters("my/long/path") - expect(parameters).to eq("catchall" => "my/long/path") - end - - it "extract_parameters for path with no slashes" do - parameters = route.extract_parameters("whatever") - expect(parameters).to eq("catchall" => "whatever") - end - end - - context "route with nested catchall/globbing/wildcard" do - let(:route) do - Jets::Router::Route.new(path: "nested/others/*proxy", http_method: :any, to: "others#all") - end - - it "api_gateway_format" do - api_gateway_path = route.send(:api_gateway_format, route.path) - expect(api_gateway_path).to eq "/nested/others/{proxy+}" - end - - it "extract_parameters" do - parameters = route.extract_parameters("nested/others/my/long/path") - expect(parameters).to eq("proxy" => "my/long/path") - end - end - - context "route provided in aws api gateway format" do - let(:route) do - Jets::Router::Route.new(path: "posts/{id}/edit", http_method: :any, to: "posts#edit") - end - - it "extract_parameters" do - parameters = route.extract_parameters("posts/tung/edit") - expect(parameters).to eq("id" => "tung") - end - - it "api_gateway_format" do - api_gateway_path = route.send(:api_gateway_format, route.path) - expect(api_gateway_path).to eq "/posts/{id}/edit" - end - - it "path" do - jets_format = route.path - expect(jets_format).to eq "/posts/:id/edit" - end - - it "ensure_jets_format" do - jets_format = route.send(:ensure_jets_format, 'posts/{id}/edit') - expect(jets_format).to eq "posts/:id/edit" - - jets_format = route.send(:ensure_jets_format, 'others/{proxy+}') - expect(jets_format).to eq "others/*proxy" - end - end - - context "route with authorization controls" do - let(:route) do - Jets::Router::Route.new(path: "posts", http_method: :get, to: "posts#index", authorization_type: 'AWS_IAM') - end - - it 'authorization can be specified' do - expect(route.authorization_type).to eq 'AWS_IAM' - end - - it 'authorization can be nil' do - expect(Jets::Router::Route.new(path: "posts", http_method: :get, to: "posts#index").authorization_type).to be_nil - end - end -end diff --git a/spec/lib/jets/router/scope_spec.rb b/spec/lib/jets/router/scope_spec.rb deleted file mode 100644 index 8cf72a0ac..000000000 --- a/spec/lib/jets/router/scope_spec.rb +++ /dev/null @@ -1,33 +0,0 @@ -describe Jets::Router::Scope do - context "root level" do - let(:scope) do - Jets::Router::Scope.new - end - it "scope is has level 1" do - expect(scope.level).to eq 1 - end - end - - context "root level with namespace" do - let(:scope) do - Jets::Router::Scope.new(namespace: :admin) - end - it "scope is has level 1" do - expect(scope.level).to eq 1 - expect(scope.options[:namespace]).to eq :admin - end - end - - context "nested 2nd level" do - let(:root) do - Jets::Router::Scope.new - end - let(:scope) do - root.new - end - it "scope is has level 2" do - expect(scope.level).to eq 2 - expect(scope.parent).to eq root - end - end -end \ No newline at end of file diff --git a/spec/lib/jets/router_spec.rb b/spec/lib/jets/router_spec.rb deleted file mode 100644 index 9eb75728d..000000000 --- a/spec/lib/jets/router_spec.rb +++ /dev/null @@ -1,197 +0,0 @@ -class RouterTestApp - include Jets::Router::Helpers::NamedRoutes -end - -describe Jets::Router do - let(:route_set) { Jets::Router::RouteSet.new } - let(:app) { RouterTestApp.new } - - describe "Router" do - context "routes with resources macro" do - it "expands macro to all the REST routes" do - output = draw do - resources :posts - end - tos = route_set.routes.map(&:to).sort.uniq - expect(tos).to eq( - ["posts#create", "posts#destroy", "posts#edit", "posts#index", "posts#new", "posts#show", "posts#update"].sort - ) - end - - it "all_paths list all subpaths" do - output = draw do - resources :posts - end - # pp route_set.routes # uncomment to debug - expect(route_set.all_paths).to eq( - ["/posts", "/posts/:id", "/posts/:id/edit", "/posts/new"] - ) - end - - it "ordered_routes should sort by precedence" do - output = draw do - resources :posts - any "*catchall", to: "catch#all" - end - paths = route_set.ordered_routes.map(&:path).uniq - expect(paths).to eq( - ["/posts/new", "/posts", "/posts/:id/edit", "/posts/:id", "/*catchall"]) - end - - it "ordered_routes should sort nested resources new before show" do - output = draw do - resources :posts do - resources :comments - end - any "*catchall", to: "catch#all" - end - paths = route_set.ordered_routes.map(&:path).uniq - expect(paths.index("/posts/:post_id/comments/new")).to be < paths.index("/posts/:post_id/comments/:id") - end - end - - context "direct as" do - it "logout" do - output = draw do - get "exit", to: "sessions#destroy", as: :logout - end - text = <<~EOL - logout GET /exit sessions#destroy - EOL - expect(output).to eq(text) - end - - it "namespace logout" do - output = draw do - namespace :users do - get "exit", to: "sessions#destroy", as: :logout - end - end - text = <<~EOL - logout_users_sessions GET /users/exit users/sessions#destroy - EOL - expect(output).to eq(text) - end - end - - context "regular create_route methods" do - it "resources users posts" do - output = draw do - resources :users, only: [] do - get "posts", to: "posts#index" - get "posts/new", to: "posts#new" - get "posts/:id", to: "posts#show" - post "posts", to: "posts#create" - get "posts/:id/edit", to: "posts#edit" - put "posts/:id", to: "posts#update" - patch "posts/:id", to: "posts#update" - delete "posts/:id", to: "posts#destroy" - end - end - text = <<~EOL - users GET /users/posts posts#index - new_user GET /posts/users/new posts#new - user GET /users/:id/posts/:id posts#show - users POST /users/posts posts#create - edit_user GET /posts/:id/users/:id/edit posts#edit - user PUT /users/:id/posts/:id posts#update - user PATCH /users/:id/posts/:id posts#update - user DELETE /users/:id/posts/:id posts#destroy - EOL - expect(output).to eq(text) - end - - it "posts as articles" do - output = draw do - get "posts", to: "posts#index", as: "articles" - get "posts", to: "posts#list", as: "articles2" - get "posts/new", to: "posts#new", as: "new_article" - get "posts/:id", to: "posts#show", as: "article" - get "posts/:id/edit", to: "posts#edit", as: "edit_article" - get "posts", to: "posts#no_as" # should not create route - end - text = <<~EOL - articles GET /posts posts#index - articles2 GET /posts posts#list - new_article GET /posts/new posts#new - article GET /posts/:id posts#show - edit_article GET /posts/:id/edit posts#edit - posts GET /posts posts#no_as - EOL - expect(output).to eq(text) - end - end - - context "singular resource nested with plural resources" do - it "profile posts" do - output = draw do - resource :profile do - resources :posts - end - end - text = <<~EOL - profile POST /profile profiles#create - new_profile GET /profile/new profiles#new - edit_profile GET /profile/edit profiles#edit - profile GET /profile profiles#show - profile PUT /profile profiles#update - profile PATCH /profile profiles#update - profile DELETE /profile profiles#destroy - profile_posts GET /profile/posts posts#index - profile_posts POST /profile/posts posts#create - new_profile_post GET /profile/posts/new posts#new - edit_profile_post GET /profile/posts/:id/edit posts#edit - profile_post GET /profile/posts/:id posts#show - profile_post PUT /profile/posts/:id posts#update - profile_post PATCH /profile/posts/:id posts#update - profile_post DELETE /profile/posts/:id posts#destroy - EOL - expect(output).to eq(text) - end - - it "posts profile" do - output = draw do - resources :posts do - resource :profile - end - end - text = <<~EOL - posts GET /posts posts#index - posts POST /posts posts#create - new_post GET /posts/new posts#new - edit_post GET /posts/:id/edit posts#edit - post GET /posts/:id posts#show - post PUT /posts/:id posts#update - post PATCH /posts/:id posts#update - post DELETE /posts/:id posts#destroy - post_profile POST /posts/:post_id/profile profiles#create - new_post_profile GET /posts/:post_id/profile/new profiles#new - edit_post_profile GET /posts/:post_id/profile/edit profiles#edit - post_profile GET /posts/:post_id/profile profiles#show - post_profile PUT /posts/:post_id/profile profiles#update - post_profile PATCH /posts/:post_id/profile profiles#update - post_profile DELETE /posts/:post_id/profile profiles#destroy - EOL - expect(output).to eq(text) - end - end - - context "infer to option" do - it "credit cards" do - output = draw do - get "credit_cards/open" - get "credit_cards/debit" - get "credit_cards/credit" - get "credit_cards/close" - end - text = <<~EOL - credit_cards_open GET /credit_cards/open credit_cards#open - credit_cards_debit GET /credit_cards/debit credit_cards#debit - credit_cards_credit GET /credit_cards/credit credit_cards#credit - credit_cards_close GET /credit_cards/close credit_cards#close - EOL - expect(output).to eq(text) - end - end - end -end diff --git a/spec/lib/jets/rule/base_spec.rb b/spec/lib/jets/rule/base_spec.rb deleted file mode 100644 index 43dc35bc3..000000000 --- a/spec/lib/jets/rule/base_spec.rb +++ /dev/null @@ -1,63 +0,0 @@ -describe Jets::Rule::Base do - let(:null) { double(:null).as_null_object } - - # by the time the class is finished loading into memory the properties have - # been load loaded so we can use them later to configure the lambda functions - context GameRule do - it "definitions" do - definitions = GameRule.all_public_definitions.keys - expect(definitions).to eq [:protect] - - protect_definition = GameRule.all_public_definitions[:protect] - expect(protect_definition).to be_a(Jets::Lambda::Definition) - end - - it "definitions contains flatten Array structure" do - definitions = GameRule.definitions - expect(definitions.first).to be_a(Jets::Lambda::Definition) - - definition_names = definitions.map(&:name) - expect(definition_names).to eq(GameRule.all_public_definitions.keys) - end - end -end - -class Example1Rule < Jets::Rule::Base -end - -class Example2Rule < Example1Rule - rule_namespace false -end - -class Example3Rule < Example2Rule -end - -describe "example rules" do - it "rule_namespace" do - expect(Example1Rule.rule_namespace).to be nil - expect(Example2Rule.rule_namespace).to be false - expect(Example3Rule.rule_namespace).to be nil - end - - let(:rule) { Jets::Cfn::Resource::Config::ConfigRule.new(klass.to_s, "meth") } - context "inherited rule namespace" do - let(:klass) { Example3Rule } - it "config_rule_name" do - expect(rule.config_rule_name).to eq "example3-meth" - end - end - - context "false namespace" do - let(:klass) { Example2Rule } - it "config_rule_name" do - expect(rule.config_rule_name).to eq "example2-meth" - end - end - - context "nil namespace" do - let(:klass) { Example1Rule } - it "config_rule_name" do - expect(rule.config_rule_name).to eq "demo-test-example1-meth" - end - end -end \ No newline at end of file diff --git a/spec/lib/jets/rule/dsl_spec.rb b/spec/lib/jets/rule/dsl_spec.rb deleted file mode 100644 index 6e10e6cad..000000000 --- a/spec/lib/jets/rule/dsl_spec.rb +++ /dev/null @@ -1,45 +0,0 @@ -# Example with the full config_rule syntax -class FullPropertiesRule < Jets::Rule::Base - resource(config_rule_definition(:protect)) - def protect - puts "protect" - end -end - -class PrettyPropertiesRule < Jets::Rule::Base - scope "AWS::EC2::SecurityGroup" - def protect - puts "protect" - end -end - -describe Jets::Rule::Dsl do - context "FullPropertiesRule" do - let(:rule) { FullPropertiesRule.new({}, nil, "protect") } - - it "associated_resources" do - protect_task = FullPropertiesRule.all_public_definitions[:protect] - expect(protect_task).to be_a(Jets::Lambda::Definition) - resources = protect_task.associated_resources - associated_resource = resources.first - expect(associated_resource.logical_id).to eq "{namespace}ConfigRule".to_sym - attributes = associated_resource.attributes - props = attributes[:Properties] - expect(props[:ConfigRuleName]).to eq "demo-test-full-properties-protect" - end - end - - context "PrettyPropertiesRule" do - let(:rule) { PrettyPropertiesRule.new({}, nil, "protect") } - - it "scope expands to full ComplianceResourceTypes with AWS::EC2::SecurityGroup" do - protect_task = PrettyPropertiesRule.all_public_definitions[:protect] - expect(protect_task).to be_a(Jets::Lambda::Definition) - resources = protect_task.associated_resources - associated_resource = resources.first - attributes = associated_resource.attributes - props = attributes[:Properties] - expect(props[:Scope][:ComplianceResourceTypes]).to eq(["AWS::EC2::SecurityGroup"]) - end - end -end diff --git a/spec/lib/jets/shim/adapter/alb_spec.rb b/spec/lib/jets/shim/adapter/alb_spec.rb new file mode 100644 index 000000000..bb927ac58 --- /dev/null +++ b/spec/lib/jets/shim/adapter/alb_spec.rb @@ -0,0 +1,17 @@ +describe Jets::Shim::Adapter::Alb do + describe "alb" do + let :adapter do + described_class.new(event) + end + let :event do + JSON.load(IO.read("spec/fixtures/shim/events/alb.json")) + end + + it "transforms event to rack env" do + env = adapter.to_rack_env + expect(env["REQUEST_METHOD"]).to eq "POST" + expect(env["PATH_INFO"]).to eq "/path/to/resource" + expect(env["QUERY_STRING"]).to eq "query=1234ABCD" + end + end +end diff --git a/spec/lib/jets/shim/adapter/apigw_spec.rb b/spec/lib/jets/shim/adapter/apigw_spec.rb new file mode 100644 index 000000000..25a3c5e9c --- /dev/null +++ b/spec/lib/jets/shim/adapter/apigw_spec.rb @@ -0,0 +1,55 @@ +describe Jets::Shim::Adapter::Apigw do + let :adapter do + described_class.new(event) + end + + describe "apigw" do + let :event do + JSON.load(IO.read("spec/fixtures/shim/events/apigw.json")) + end + + it "transforms event to rack env" do + env = adapter.to_rack_env + expect(env["REQUEST_METHOD"]).to eq "POST" + expect(env["PATH_INFO"]).to eq "/path/to/resource" + expect(env["QUERY_STRING"]).to eq "foo=bar" + + expect(env["HTTP_HOST"]).to eq "1234567890.execute-api.us-east-1.amazonaws.com" + end + end + + # Note: I tested it and content-length is not available in apigw event + # See: https://stackoverflow.com/questions/56693981/how-do-i-get-http-header-content-length-in-api-gateway-lambda-proxy-integratio + describe "content-type" do + let :event do + # curl -H "Content-Type: application/json" => produces this: + { + "headers" => { + "content-type" => "application/json" + } + } + end + + it "should set CONTENT_TYPE" do + env = adapter.to_rack_env + expect(env["CONTENT_TYPE"]).to eq "application/json" + expect(env["HTTP_CONTENT_TYPE"]).to be nil + end + end + + describe "request-uri" do + let :event do + { + "path" => "/path/to/resource", + "queryStringParameters" => { + "foo" => "bar" + } + } + end + + it "should set REQUEST_URI" do + env = adapter.to_rack_env + expect(env["REQUEST_URI"]).to eq "/path/to/resource?foo=bar" + end + end +end diff --git a/spec/lib/jets/shim/adapter/lambda_spec.rb b/spec/lib/jets/shim/adapter/lambda_spec.rb new file mode 100644 index 000000000..b86f71924 --- /dev/null +++ b/spec/lib/jets/shim/adapter/lambda_spec.rb @@ -0,0 +1,86 @@ +describe Jets::Shim::Adapter::Lambda do + let :adapter do + described_class.new(event) + end + describe "lambda" do + let :event do + JSON.load(IO.read("spec/fixtures/shim/events/lambda.json")) + end + + it "transforms event to rack env" do + env = adapter.to_rack_env + expect(env["REQUEST_METHOD"]).to eq "GET" + expect(env["PATH_INFO"]).to eq "/path/to/resource" + expect(env["QUERY_STRING"]).to eq "foo=bar" + end + end + + describe "content-type" do + let :event do + # curl -H "Content-Type: application/json" => produces this: + { + "headers" => { + "content-type" => "application/json" + } + } + end + + it "should set CONTENT_TYPE" do + env = adapter.to_rack_env + expect(env["CONTENT_TYPE"]).to eq "application/json" + expect(env["HTTP_CONTENT_TYPE"]).to be nil + end + end + + # APIGW does not provide content-length :( see apigw_spec.rb + # But Lambda URL does :) + describe "content-length" do + let :event do + # curl -H "Content-Type: application/json" => produces this: + { + "headers" => { + "content-length" => "9" + } + } + end + + it "should set CONTENT_LENGTH" do + env = adapter.to_rack_env + expect(env["CONTENT_LENGTH"]).to eq "9" + expect(env["HTTP_CONTENT_LENGTH"]).to be nil + end + end + + describe "request-uri" do + let :event do + { + "version" => "2.0", + "rawPath" => "/posts", + "rawQueryString" => "foo=bar" + } + end + + it "should set REQUEST_URI" do + env = adapter.to_rack_env + expect(env["REQUEST_URI"]).to eq "/posts?foo=bar" + end + end + + describe "cookies" do + let :event do + { + "version" => "2.0", + "rawPath" => "/posts", + "cookies" => [ + "yummy1=value1", + "yummy2=value2" + ] + } + end + + it "should set REQUEST_URI" do + env = adapter.to_rack_env + expect(env["HTTP_COOKIE"]).to eq "yummy1=value1; yummy2=value2" + end + end +end diff --git a/spec/lib/jets/shim/adapter/response/alb_spec.rb b/spec/lib/jets/shim/adapter/response/alb_spec.rb new file mode 100644 index 000000000..14d18587e --- /dev/null +++ b/spec/lib/jets/shim/adapter/response/alb_spec.rb @@ -0,0 +1,22 @@ +describe Jets::Shim::Response::Alb do + let :response do + described_class.new(triplet) + end + + describe "apigw" do + let :triplet do + [ + 200, + {"Content-Type" => "application/json"}, + ["body"] + ] + end + + it "has alb structure" do + h = response.translate + expect(h.keys.size).to eq 5 + expect(h.keys.sort).to eq [:body, :headers, :isBase64Encoded, :statusCode, :statusDescription] + expect(h[:statusDescription]).to eq "200 OK" + end + end +end diff --git a/spec/lib/jets/shim/adapter/response/lambda_spec.rb b/spec/lib/jets/shim/adapter/response/lambda_spec.rb new file mode 100644 index 000000000..e6e9cd193 --- /dev/null +++ b/spec/lib/jets/shim/adapter/response/lambda_spec.rb @@ -0,0 +1,111 @@ +require "action_dispatch/http/mime_type" + +describe Jets::Shim::Response::Lambda do + let :response do + described_class.new(triplet) + end + + describe "apigw" do + let :triplet do + [ + 200, + {"Content-Type" => "application/json; charset=utf-8"}, + ["body"] + ] + end + + it "has apigw structure" do + h = response.translate + expect(h.keys.size).to eq 4 + expect(h.keys.sort).to eq [:body, :headers, :isBase64Encoded, :statusCode] + end + + # "response set-cookies" + # https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html#urls-cookies + context "cookies" do + let :triplet do + [ + 200, + headers, + ["body"] + ] + end + let :headers do + { + "Content-Type" => "text/html; charset=utf-8", + "Set-Cookie" => set_cookie + } + end + + context "set_cookie is String" do + let :set_cookie do + "yummy1=value1" + end + + it "for single cookie" do + h = response.translate + expect(h[:statusCode]).to eq 200 + expect(h[:headers]).to include("Content-Type" => "text/html; charset=utf-8") + expect(h[:headers]).to_not have_key("Set-Cookie") + expect(h[:body]).to eq Base64.strict_encode64("body") + expect(h[:cookies]).to eq ["yummy1=value1"] + end + end + + context "set_cookie is Array" do + let :set_cookie do + [ + "yummy1=value1", + "yummy2=value2" + ] + end + + it "for multiple cookies" do + h = response.translate + expect(h[:statusCode]).to eq 200 + expect(h[:headers]).to include("Content-Type" => "text/html; charset=utf-8") + expect(h[:headers]).to_not have_key("Set-Cookie") + expect(h[:body]).to eq Base64.strict_encode64("body") + expect(h[:cookies]).to eq ["yummy1=value1", "yummy2=value2"] + end + end + end + + # Note: Rails returns MimeType objects. APIGW requires strings. + context "Rails" do + let :triplet do + [ + 200, + {"Content-Type" => content_type}, + ["body"] + ] + end + + context "content_type is application/json object" do + let :content_type do + Mime::Type.lookup "application/json" # => object + end + + it "translate to String" do + h = response.translate + expect(h[:statusCode]).to eq 200 + expect(h[:headers]).to include("Content-Type" => "application/json") + expect(h[:body]).to eq Base64.strict_encode64("body") + end + end + + context "content_type is text/html object" do + let :content_type do + Mime::Type.lookup "text/html" # => object + end + + it "translate to String" do + h = response.translate + expect(h[:statusCode]).to eq 200 + expect(h[:headers]).to include("Content-Type" => "text/html") + expect(h[:body]).to eq Base64.strict_encode64("body") + end + end + end + end +end diff --git a/spec/lib/jets/shim/config_spec.rb b/spec/lib/jets/shim/config_spec.rb new file mode 100644 index 000000000..fe0624d08 --- /dev/null +++ b/spec/lib/jets/shim/config_spec.rb @@ -0,0 +1,16 @@ +describe Jets::Shim::Config do + let :config do + described_class.instance + end + before(:each) do + config.flush_cache # unmemoize :framework? + end + + describe "config" do + it "Rails" do + Dir.chdir("spec/fixtures/shim/frameworks/rails") do + expect(config.rails?).to be true + end + end + end +end diff --git a/spec/lib/jets/spec_helpers_spec.rb b/spec/lib/jets/spec_helpers_spec.rb deleted file mode 100644 index e467ba6c7..000000000 --- a/spec/lib/jets/spec_helpers_spec.rb +++ /dev/null @@ -1,234 +0,0 @@ -# frozen_string_literal: true - -require "jets/spec_helpers" - -class SimpleController < Jets::Controller::Base - layout :application - - def index - render json: {} - end - - def show - if params[:id] == '404' - render json: {}, status: :not_found - else - render json: { id: params[:id], filter: params[:filter] } - end - end - - def create - render json: { id: params[:id] }, status: :created - end - - def update - render json: { id: params[:id], name: params[:name] } - end - - def destroy - render json: {}, status: :no_content - end - - def echo_body - render plain: request.body.string - end - - def echo_headers - render json: request.headers - end -end - -describe Jets::SpecHelpers do - before do - Jets::Application::RoutesReloader.disable! - Jets.application.routes.clear! - Jets.application.routes.draw do - get '/spec_helper_test', to: 'simple#index' - get '/ほげ', to: 'simple#index' - - get '/spec_helper_test/:id', to: 'simple#show' - get '/ほげ/:id', to: 'simple#show' - - post '/spec_helper_test', to: 'simple#create' - - put '/spec_helper_test/:id', to: 'simple#update' - - delete '/spec_helper_test/:id', to: 'simple#destroy' - - post '/spec_helper_test/echo_body', to: 'simple#echo_body' - - post '/spec_helper_test/echo_headers', to: 'simple#echo_headers' - end - end - - after do - Jets::Application::RoutesReloader.enable! - end - - context "get" do - let(:nested_params) do - { - level_1: { - level_2: { - level_3: "value" - } - } - }.with_indifferent_access - end - - it "gets 200" do - get '/spec_helper_test' - expect(response.status).to eq 200 - end - - it "gets 200 with id" do - get '/spec_helper_test/123' - expect(response.status).to eq 200 - expect(JSON.parse(response.body)['id']).to eq '123' - end - - it "gets 200 with unicode" do - get '/ほげ' - expect(response.status).to eq 200 - end - - it "gets 200 with id and unicode" do - get '/ほげ/ふが' - expect(response.status).to eq 200 - expect(JSON.parse(response.body)['id']).to eq 'ふが' - end - - it "gets 200 with query params" do - get '/spec_helper_test/123?filter=abc' - expect(response.status).to eq 200 - expect(JSON.parse(response.body)['filter']).to eq 'abc' - end - - it "gets 200 with nested query params" do - query_string = Rack::Utils.build_nested_query(filter: nested_params) - get "/spec_helper_test/123?#{query_string}" - - expect(response.status).to eq 200 - expect(JSON.parse(response.body)['filter']).to eq nested_params - end - - it "gets 200 with array query params" do - query_string = Rack::Utils.build_nested_query(filter: ['abc', 'def']) - get "/spec_helper_test/123?#{query_string}" - expect(response.status).to eq 200 - expect(JSON.parse(response.body)['filter']).to eq ['abc', 'def'] - end - - it "gets 200 with query params with params keyword" do - query_string = Rack::Utils.build_nested_query(filter: 'abc') - get "/spec_helper_test/123?#{query_string}" - expect(response.status).to eq 200 - expect(JSON.parse(response.body)['filter']).to eq 'abc' - end - - it "gets 200 with unicode query params" do - query_string = Rack::Utils.build_nested_query(filter: 'ふが') - get "/spec_helper_test/123?#{query_string}" - expect(response.status).to eq 200 - expect(JSON.parse(response.body)['filter']).to eq 'ふが' - end - - it "gets 200 with query params no query keyword" do - query_string = Rack::Utils.build_nested_query(filter: 'abc') - get "/spec_helper_test/123?#{query_string}" - expect(response.status).to eq 200 - expect(JSON.parse(response.body)['filter']).to eq 'abc' - end - - it "gets 200 with route params" do - get '/spec_helper_test/123' - expect(response.status).to eq 200 - expect(JSON.parse(response.body)['id']).to eq '123' - end - - it "gets 404 with id" do - get '/spec_helper_test/404' - expect(response.status).to eq 404 - end - end - - context "post" do - it "posts 201" do - post '/spec_helper_test', params: { id: 123 } # params also works - expect(response.status).to eq 201 - expect(JSON.parse(response.body)['id']).to eq '123' - end - - context "with body" do - let(:body) { { a: 1, b: 2 }.to_json } - - context "when using body string as :params" do - it "echoes body" do - post '/spec_helper_test/echo_body', params: body - expect(response.status).to eq 200 - expect(response.body).to eq body - end - - it "sets the valid content length" do - post '/spec_helper_test/echo_headers', params: body - expect(response.status).to eq 200 - expect(JSON.parse(response.body)['Content-Length'].to_i).to eq body.size - end - end - - context "when using body string as :body" do - it "echoes body" do - post '/spec_helper_test/echo_body', body: body - expect(response.status).to eq 200 - expect(response.body).to eq body - end - - it "sets the valid content length" do - post '/spec_helper_test/echo_headers', body: body - expect(response.status).to eq 200 - expect(JSON.parse(response.body)['Content-Length'].to_i).to eq body.size - end - end - - context "with custom content type" do - let(:headers) { { 'Content-Type' => 'application/json' } } - - it 'sets the content-type header' do - post '/spec_helper_test/echo_headers', body: body, headers: headers - expect(response.status).to eq 200 - expect(JSON.parse(response.body)['Content-Type']).to eq headers['Content-Type'] - end - end - - context "without custom content type" do - it 'uses the default content type' do - post '/spec_helper_test/echo_headers', body: body - expect(response.status).to eq 200 - expect(JSON.parse(response.body)['Content-Type']).to eq 'application/x-www-form-urlencoded' - end - end - end - end - - context "put" do - it "puts" do - put '/spec_helper_test/1', name: 'Tom' - expect(response.status).to eq 200 - expect(JSON.parse(response.body)['id']).to eq '1' - expect(JSON.parse(response.body)['name']).to eq 'Tom' - end - end - - context "delete" do - it "destroys" do - delete '/spec_helper_test/:id', id: 1 - expect(response.status).to eq 204 - end - end - - context "fixtures" do - it "gets valid fixture path" do - expect(fixture_path('abc')).to eq "#{Jets.root}/spec/fixtures/abc" - end - end -end diff --git a/spec/lib/jets/stack/builder_spec.rb b/spec/lib/jets/stack/builder_spec.rb deleted file mode 100644 index ef9498f9c..000000000 --- a/spec/lib/jets/stack/builder_spec.rb +++ /dev/null @@ -1,83 +0,0 @@ -class BuilderExampleStack < Jets::Stack - ### Parameters - # long form - parameter(instance_type: { - default: "t2.micro" , - description: "instance type" , - }) - # medium form - parameter :company, default: "boltops" - # short form - parameter :ami_id, "ami-123" - - ### Outputs - # long form - output(vpc_id: { - description: "vpc id", - value: ref("vpc_id"), - }) - # medium form - output :stack_name, value: "!Ref AWS::StackName" - # short form - output :elb, "!Ref Elb" - output :elb # short form - - ### Resources - # long form - resource(sns_topic: { - type: "AWS::SNS::Topic", - properties: { - description: "my desc", - display_name: "my name", - } - }) - # medium form - resource(:sns_topic2, - type: "AWS::SNS::Topic", - properties: { - display_name: "my name 2", - } - ) - # short form - resource(:sns_topic3, "AWS::SNS::Topic", - display_name: "my name 3", - ) -end - -class BuilderExampleAlarm < Jets::Stack - depends_on :builder_example_alert -end - -class BuilderExampleAlert < Jets::Stack -end - -describe "Stack builder" do - let(:builder) { Jets::Stack::Builder.new(stack) } - - context "full template" do - let(:stack) { BuilderExampleStack.new } - - it "template" do - template = builder.template - # puts YAML.dump(template) # uncomment to see and debug - # Check just a few keys for sanity and keep spec at a reasonable length - - expect(template['Parameters']['InstanceType']['Default']).to eq 't2.micro' - expect(template['Parameters']['Company']['Default']).to eq 'boltops' - expect(template['Resources']['SnsTopic']['Type']).to eq 'AWS::SNS::Topic' - expect(template['Resources']['SnsTopic2']['Type']).to eq 'AWS::SNS::Topic' - expect(template['Outputs']['VpcId']['Description']).to eq 'vpc id' - expect(template['Outputs']['StackName']['Value']).to eq '!Ref AWS::StackName' - end - end - - # context "two stacks with depends_on" do - # let(:stack) { ExampleAlarm.new } - - # it "adds parameters" do - # template = builder.template - # puts YAML.dump(template) # uncomment to see and debug - # expect(template['Parameters']['ExampleAlert']['Type']).to eq 'String' - # end - # end -end diff --git a/spec/lib/jets/stack/depends/item_spec.rb b/spec/lib/jets/stack/depends/item_spec.rb deleted file mode 100644 index e69de29bb..000000000 diff --git a/spec/lib/jets/stack/depends_spec.rb b/spec/lib/jets/stack/depends_spec.rb deleted file mode 100644 index d15e78003..000000000 --- a/spec/lib/jets/stack/depends_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -describe "Stack Depends" do - before(:each) { Alert ; Custom } - let(:depends) { Jets::Stack::Depends.new(depends_on) } - - context "single item" do - context "no class prefix" do - let(:depends_on) do - [Jets::Stack::Depends::Item.new(:alert)] - end - it "params" do - expect(depends.params).to eq({:Delivered=>"!GetAtt Alert.Outputs.Delivered"}) - end - end - - context "class prefix" do - let(:depends_on) do - [Jets::Stack::Depends::Item.new(:alert, class_prefix: true)] - end - it "params has prefix added to the key but not the value" do - expect(depends.params).to eq({"AlertDelivered"=>"!GetAtt Alert.Outputs.Delivered"}) - end - end - end - - context "multiple items" do - context "no class prefix" do - let(:depends_on) do - [ - Jets::Stack::Depends::Item.new(:alert), - Jets::Stack::Depends::Item.new(:custom), - ] - end - it "params" do - expect(depends.params).to eq({:Delivered=>"!GetAtt Alert.Outputs.Delivered", :Test=>"!GetAtt Custom.Outputs.Test"}) - end - end - end -end diff --git a/spec/lib/jets/stack/function_spec.rb b/spec/lib/jets/stack/function_spec.rb deleted file mode 100644 index c77cb5142..000000000 --- a/spec/lib/jets/stack/function_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -class FunctionExampleStack < Jets::Stack - ruby_function(:hello) - ruby_function("admin/send_message") - python_function(:kevin) -end - -describe "Stack builder" do - let(:function) { Jets::Stack::Function.new(template) } - - context "ruby function" do - let(:template) { FunctionExampleStack.new.resources.map(&:template).first } - it "lang is ruby" do - expect(function.lang).to eq :ruby - end - - it "meth" do - expect(function.meth).to eq "lambda_handler" - end - end - - context "function with namespace" do - let(:template) { FunctionExampleStack.new.resources.map(&:template)[1] } - it "lang is ruby" do - props = template[:AdminSendMessage][:Properties] - expect(props[:FunctionName]).to eq "demo-test-function_example_stack-admin-send_message" - expect(props[:Handler]).to eq "handlers/shared/functions/admin/send_message.lambda_handler" - expect(function.lang).to eq :ruby - end - end - - context "python function" do - let(:template) { FunctionExampleStack.new.resources.map(&:template).last } - it "lang is python" do - expect(function.lang).to eq :python - end - end - -end diff --git a/spec/lib/jets/stack/main_spec.rb b/spec/lib/jets/stack/main_spec.rb deleted file mode 100644 index d7245e573..000000000 --- a/spec/lib/jets/stack/main_spec.rb +++ /dev/null @@ -1,51 +0,0 @@ -class AllInOne < Jets::Stack - cloudwatch_alarm(:billing_alarm, - depends_on: logical_id(:billing_notification), - properties: { - alarm_description: "Alarm if AWS spending is too much", - namespace: "AWS/Billing", - metric_name: "EstimatedCharges", - dimensions: [{name: "Currency", value: "USD"}], - statistic: "Maximum", - period: "21600", - evaluation_periods: "1", - threshold: 100, - comparison_operator: "GreaterThanThreshold", - alarm_actions: ["!Ref BillingNotification"], - } - ) - sns_topic(:billing_notification) -end - -describe "Stack templates" do - let(:stack) { AllInOne.new } - it "outputs" do - templates = stack.outputs.map(&:template) - expect(templates).to eq( - [{:BillingAlarm=>{:Value=>"!Ref BillingAlarm"}}, - {:BillingNotification=>{:Value=>"!Ref BillingNotification"}}] - ) - end - - it "resources" do - templates = stack.resources.map(&:template) - expect(templates).to eq( - [{:BillingAlarm=> - {:Type=>"AWS::CloudWatch::Alarm", - :Properties=> - {:DependsOn=>"BillingNotification", - :Properties=> - {:AlarmDescription=>"Alarm if AWS spending is too much", - :Namespace=>"AWS/Billing", - :MetricName=>"EstimatedCharges", - :Dimensions=>[{:Name=>"Currency", :Value=>"USD"}], - :Statistic=>"Maximum", - :Period=>"21600", - :EvaluationPeriods=>"1", - :Threshold=>100, - :ComparisonOperator=>"GreaterThanThreshold", - :AlarmActions=>["!Ref BillingNotification"]}}}}, - {:BillingNotification=>{:Type=>"AWS::SNS::Topic"}}] - ) - end -end diff --git a/spec/lib/jets/stack/output/lookup_spec.rb b/spec/lib/jets/stack/output/lookup_spec.rb deleted file mode 100644 index d11b2ff11..000000000 --- a/spec/lib/jets/stack/output/lookup_spec.rb +++ /dev/null @@ -1,73 +0,0 @@ -require 'recursive-open-struct' - -class Alert2 < Jets::Stack - # the definition doesnt matters because it's no used in the spec but added for clarity - sns_topic(:my_sns_topic, display_name: "cool topic") -end - -describe "shared resource" do - let(:lookup) do - lookup = Jets::Stack::Output::Lookup.new(Alert2) - allow(lookup).to receive(:cfn).and_return(cfn) - lookup - end - let(:cfn) do - cfn = double(:cfn) - allow(cfn).to receive(:describe_stacks).with(stack_name: "demo-test").and_return(parent_response) - allow(cfn).to receive(:describe_stacks).with(stack_name: shared_stack_arn).and_return(child_response) - cfn - end - let(:parent_response) do - RecursiveOpenStruct.new({ - stacks: [ - outputs: [{ - output_key: "S3Bucket", - output_value: "demo-test-s3bucket-1evq5kzp1an0m", - },{ - output_key: "Alert2", - output_value: shared_stack_arn, - }] - ] - }, recurse_over_arrays: true) - end - let(:child_response) do - RecursiveOpenStruct.new({ - stacks: [ - outputs: [{ - output_key: "MySnsTopic", - output_value: sns_child_arn, - }] - ] - }, recurse_over_arrays: true) - end - - let(:shared_stack_arn) do - 'arn:aws:cloudformation:us-west-2:111111111111:stack/demo-test-Alert2-TBJ19S6JPXPD/e8d1ec20-b5c0-11e8-a781-503ac9ec2461' - end - let(:sns_child_arn) do - "arn:aws:sns:us-west-2:111111111111:config-dev-Alert2-TBJ19S6JPXPD-MySnsTopic-3EW5BWDH5L1Z" - end - - it "output" do - arn = lookup.output(:my_sns_topic) - expect(arn).to eq sns_child_arn - end - - it "shared_stack_arn" do - arn = lookup.shared_stack_arn("Alert2") - expect(arn).to eq shared_stack_arn - end - - # Test Stack subclass using the lookup in here because the fixtures are already here - let(:shared_resource_class) do - allow(Alert2).to receive(:cfn).and_return(cfn) - Alert2 - end - - it "Alert2.lookup" do - allow(Alert2).to receive(:looker).and_return(lookup) - arn = Alert2.lookup(:my_sns_topic) - expect(arn).to eq sns_child_arn - end - -end \ No newline at end of file diff --git a/spec/lib/jets/stack/output_spec.rb b/spec/lib/jets/stack/output_spec.rb deleted file mode 100644 index 2c164da1f..000000000 --- a/spec/lib/jets/stack/output_spec.rb +++ /dev/null @@ -1,52 +0,0 @@ -describe "Stack output" do - let(:output) { Jets::Stack::Output.new("ExampleStack", definition) } - - context "long form" do - let(:definition) do - { - vpc_id: { - description: "vpc id", - value: "!Ref VpcId", - } - } - end - it "template" do - expect(output.template).to eq( - {:VpcId=>{:Description=>"vpc id", :Value=>"!Ref VpcId"}} - ) - end - end - - context "medium form" do - let(:definition) do - [:stack_name, value: "!Ref AWS::StackName"] - end - it "template" do - expect(output.template).to eq( - {:StackName=>{:Value=>"!Ref AWS::StackName"}} - ) - end - end - - context "short form with value" do - let(:definition) do - [:elb, "!Ref Elb"] - end - it "template" do - expect(output.template).to eq( - {:Elb=>{:Value=>"!Ref Elb"}} - ) - end - end - - context "short form without value" do - let(:definition) do - [:elb] - end - it "template" do - expect(output.template).to eq( - {:Elb=>{:Value=>"!Ref Elb"}} - ) - end - end -end diff --git a/spec/lib/jets/stack/parameter_spec.rb b/spec/lib/jets/stack/parameter_spec.rb deleted file mode 100644 index 09f32723e..000000000 --- a/spec/lib/jets/stack/parameter_spec.rb +++ /dev/null @@ -1,52 +0,0 @@ -describe "Stack parameter" do - let(:parameter) { Jets::Stack::Parameter.new("ExampleStack", definition) } - - context "long form" do - let(:definition) do - { - instance_type: { - default: "t2.micro" , - description: "instance type" , - } - } - end - it "template" do - expect(parameter.template).to eq( - {:InstanceType=>{:Default=>"t2.micro", :Description=>"instance type", :Type=>"String"}} - ) - end - end - - context "medium form" do - let(:definition) do - [:company, default: "boltops"] - end - it "template" do - expect(parameter.template).to eq( - {:Company=>{:Default=>"boltops", :Type=>"String"}} - ) - end - end - - context "short form default value" do - let(:definition) do - [:ami_id, "ami-123"] - end - it "template" do - expect(parameter.template).to eq( - {:AmiId=>{:Default=>"ami-123", :Type=>"String"}} - ) - end - end - - context "short form no default value" do - let(:definition) do - [:ami_id] - end - it "template" do - expect(parameter.template).to eq( - {:AmiId=>{:Type=>"String"}} - ) - end - end -end diff --git a/spec/lib/jets/stack/resource_spec.rb b/spec/lib/jets/stack/resource_spec.rb deleted file mode 100644 index 5b049a505..000000000 --- a/spec/lib/jets/stack/resource_spec.rb +++ /dev/null @@ -1,96 +0,0 @@ -describe "Stack resource" do - let(:resource) { Jets::Stack::Resource.new("ExampleStack", definition) } - - context "long form" do - let(:definition) do - { - sns_topic: { - type: "AWS::SNS::Topic", - properties: { - description: "my desc", - display_name: "my name", - } - } - } - end - it "template" do - expect(resource.template).to eq( - {:SnsTopic=>{:Type=>"AWS::SNS::Topic", :Properties=>{:Description=>"my desc", :DisplayName=>"my name"}}} - ) - end - end - - context "medium form with properties" do - let(:definition) do - [:sns_topic, - type: "AWS::SNS::Topic", - properties: { - display_name: "my name", - }] - end - it "template" do - expect(resource.template).to eq( - {:SnsTopic=>{:Type=>"AWS::SNS::Topic", :Properties=>{:DisplayName=>"my name"}}} - ) - end - end - - context "medium form with empty properties" do - let(:definition) do - [:sns_topic, - type: "AWS::SNS::Topic", - properties: {}] - end - it "template" do - expect(resource.template).to eq( - {:SnsTopic=>{:Type=>"AWS::SNS::Topic"}} - ) - end - end - - context "medium form without properties" do - let(:definition) do - [:sns_topic, - type: "AWS::SNS::Topic"] - end - it "template" do - expect(resource.template).to eq( - {:SnsTopic=>{:Type=>"AWS::SNS::Topic"}} - ) - end - end - - context "short form with properties" do - let(:definition) do - [:sns_topic, "AWS::SNS::Topic", - display_name: "my name"] - end - it "template" do - expect(resource.template).to eq( - {:SnsTopic=>{:Type=>"AWS::SNS::Topic", :Properties=>{:DisplayName=>"my name"}}} - ) - end - end - - context "short form without properties" do - let(:definition) do - [:sns_topic, "AWS::SNS::Topic"] - end - it "template" do - expect(resource.template).to eq( - {:SnsTopic=>{:Type=>"AWS::SNS::Topic"}} - ) - end - end - - context "short form with empty properties" do - let(:definition) do - [:sns_topic, "AWS::SNS::Topic", {}] - end - it "template" do - expect(resource.template).to eq( - {:SnsTopic=>{:Type=>"AWS::SNS::Topic"}} - ) - end - end -end diff --git a/spec/lib/jets/stack_spec.rb b/spec/lib/jets/stack_spec.rb deleted file mode 100644 index 087637c8e..000000000 --- a/spec/lib/jets/stack_spec.rb +++ /dev/null @@ -1,142 +0,0 @@ -class ExampleStack2 < Jets::Stack - ### Parameters - # long form - parameter(instance_type: { - default: "t2.micro" , - description: "instance type" , - }) - # medium form - parameter :company, default: "boltops" - # short form - parameter :ami_id, "ami-123" - - ### Outputs - # long form - output(vpc_id: { - description: "vpc id", - value: ref("vpc_id"), - }) - # medium form - output :stack_name, value: "!Ref AWS::StackName" - # short form - output :elb, "!Ref Elb" - output :elb2 # short form - - ### Resources - # long form - resource(sns_topic: { - type: "AWS::SNS::Topic", - properties: { - description: "my desc", - display_name: "my name", - } - }) - # medium form - resource(:sns_topic2, - type: "AWS::SNS::Topic", - properties: { - display_name: "my name 2", - } - ) - # short form - resource(:sns_topic3, "AWS::SNS::Topic", - display_name: "my name 3", - ) -end - -class ExampleAlarm < Jets::Stack - depends_on :example_alert -end - -class ExampleAlert < Jets::Stack -end - -class ExampleCustom < Jets::Stack - resource(:hello, - type: "AWS::Lambda::Function", - properties: { - function_name: "hello", - code: { - s3_bucket: "!Ref S3Bucket", - s3_key: code_s3_key - }, - description: "Hello world", - handler: handler("hello.lambda_handler"), - memory_size: 128, - role: "!Ref IamRole", - runtime: "python3.6", - timeout: 20, - } - ) -end - -# SecurityJob: -# Type: AWS::CloudFormation::Stack -# Properties: -# TemplateURL: https://s3.amazonaws.com//jets/cfn-templates/config-dev-security_job.yml -# Parameters: -# IamRole: !GetAtt IamRole.Arn -# S3Bucket: !Ref S3Bucket - -# # Belongs in parent_builder_spec.rb -# Alarm: -# Type: AWS::CloudFormation::Stack -# Properties: -# TemplateURL: https://s3.amazonaws.com//jets/cfn-templates/config-dev-shared-custom.yml -# Parameters: -# Alert: !Ref Alert -# DependsOn: -# - Alert -# Alert: -# Type: AWS::CloudFormation::Stack -# Properties: -# TemplateURL: https://s3.amazonaws.com//jets/cfn-templates/config-dev-shared-custom.yml -# Epen - -describe "Stack templates" do - let(:stack) { ExampleStack2.new } - it "parameters" do - templates = stack.parameters.map(&:template) - expect(templates).to eq( - [{:InstanceType=>{:Default=>"t2.micro", :Description=>"instance type", :Type=>"String"}}, - {:Company=>{:Default=>"boltops", :Type=>"String"}}, - {:AmiId=>{:Default=>"ami-123", :Type=>"String"}}, - {:IamRole=>{:Type=>"String"}}, - {:S3Bucket=>{:Type=>"String"}}] - ) - end - - it "outputs" do - templates = stack.outputs.map(&:template) - expect(templates).to eq( - [{:VpcId=>{:Description=>"vpc id", :Value=>"!Ref VpcId"}}, - {:StackName=>{:Value=>"!Ref AWS::StackName"}}, - {:Elb=>{:Value=>"!Ref Elb"}}, - {:Elb2=>{:Value=>"!Ref Elb2"}}] - ) - end - - it "resources" do - templates = stack.resources.map(&:template) - expect(templates).to eq( - [{:SnsTopic=>{:Type=>"AWS::SNS::Topic", :Properties=>{:Description=>"my desc", :DisplayName=>"my name"}}}, - {:SnsTopic2=>{:Type=>"AWS::SNS::Topic", :Properties=>{:DisplayName=>"my name 2"}}}, - {:SnsTopic3=>{:Type=>"AWS::SNS::Topic", :Properties=>{:DisplayName=>"my name 3"}}}] - ) - end - - context "depends_on" do - it "works" do - expect(ExampleAlarm.depends_on).to eq [:example_alert] - expect(ExampleAlert.depends_on).to be nil - end - end - - context "functions" do - it "selects function resources only" do - expect(ExampleCustom.functions.size).to eq 1 - expect(ExampleAlarm.functions.size).to eq 0 - expect(ExampleAlert.functions.size).to eq 0 - end - end -end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 209cbfb0f..64727ad1a 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,96 +1,25 @@ -ENV["JETS_TEST"] = "1" ENV["JETS_ENV"] = "test" +ENV["JETS_TEST"] = "1" +ENV["AWS_MFA_SECURE_TEST"] = "1" # Ensures aws api never called. Fixture home folder does not contain ~/.aws/credentials -ENV['HOME'] = File.join(Dir.pwd,'spec/fixtures/home') -ENV['SECRET_KEY_BASE'] = 'fake' -ENV['AWS_MFA_SECURE_TEST'] = '1' +ENV["HOME"] = File.join(Dir.pwd, "spec/fixtures/shim/home") +require "aws-sdk-core" require "byebug" require "fileutils" require "memoist" require "pp" -module Helpers - autoload :Multipart, "./spec/spec_helper/multipart" - include Multipart - - def execute(cmd) - puts "Running: TEST=1 JETS_ROOT=#{ENV['JETS_ROOT']} #{cmd}" if ENV['JETS_DEBUG'] - out = `#{cmd}` - puts out if ENV['JETS_DEBUG'] - out - end - - def json_file(path) - JSON.load(IO.read(path)) - end - - def yaml_file(path) - YAML.load_file(path) - end - - # Only needed for specs since the application_spec calls iam_policy and internally - # override Jets.application singleton. This affects other tests. The effected test need - # to call this to reload config.iam settings. - def reset_application_config_iam! - Jets.application.config.iam_policy = nil - Jets.application.config.default_iam_policy = nil - end - - def draw(&block) - route_set.clear! # from previous ran specs - route_set.draw(&block) - route_set.url_helpers.add_methods! - routes = route_set.routes - help = Jets::Router::Help.new(format: "space", header: false) - allow(help).to receive(:routes).and_return(routes) - text = help.text - lines = text.split("\n") - # Find the minimum number of leading spaces - min_spaces = lines.reject { |line| line.strip.empty? }.map { |line| line.match(/^\s*/)[0].length }.min - # Remove leading spaces while keeping alignment - formatted_text = lines.map { |line| line[min_spaces..-1] }.join("\n") + "\n" - end - - def find_route(path, http_method="GET") - matcher = Jets::Router::Matcher.new - allow(matcher).to receive(:routes).and_return(route_set.routes) - matcher.find_by_env( - "REQUEST_METHOD" => http_method, - "PATH_INFO" => path, - ) - end - - def silence_loggers! - @old_logger = Jets.logger - Jets.logger = ActionView::Base.logger = Logger.new("/dev/null") - end +root = File.expand_path("..", __dir__) +require "#{root}/lib/jets" - def restore_loggers! - Jets.logger = @old_logger - end +module Helpers end RSpec.configure do |c| c.before(:suite) do Aws.config.update(stub_responses: true) - FileUtils.rm_rf("spec/fixtures/project/handlers") end c.include Helpers end - -root = File.expand_path("../../", __FILE__) -require "#{root}/lib/jets" -Dir.chdir("#{root}/spec/fixtures/demo") do - ENV['JETS_SKIP_ROUTES_LOAD'] = '1' - Jets.boot - # Pretty confusing to set JETS_ROOT after the boot, but it works for the specs - # IE: stack/function_spec.rb - ENV['JETS_ROOT'] = "spec/fixtures/demo" -end -require "aws-sdk-lambda" # for Aws.config.update - -class RouterTestApp - include Jets.application.routes.url_helpers -end diff --git a/spec/spec_helper/multipart.rb b/spec/spec_helper/multipart.rb deleted file mode 100644 index 1e0f4f5b5..000000000 --- a/spec/spec_helper/multipart.rb +++ /dev/null @@ -1,31 +0,0 @@ -require 'base64' - -module Helpers - module Multipart - extend Memoist - - def multipart_event(name, base64: false) - boundary = multipart_boundary(name) - # Example content-type: "multipart/form-data; boundary=----WebKitFormBoundaryB78dBBqs2MSBKMoX", - content_type = "multipart/form-data; boundary=#{boundary}" - body = multipart_fixture(name) - body = Base64.encode64(body) if base64 - { - "body" => body, - "headers" => { - "Content-Type"=> content_type, - }, - "isBase64Encoded" => base64, - } - end - - def multipart_boundary(name) - multipart_fixture(name).split("\n").first.strip[2..-1] # also remove firs two chars "--" - end - - def multipart_fixture(name) - File.open("spec/fixtures/multipart/#{name}", "rb").read - end - memoize :multipart_fixture - end -end