├── .cody ├── README.md ├── acceptance │ ├── bin │ │ └── build.sh │ ├── buildspec.yml │ ├── project.rb │ └── role.rb └── shared │ └── script │ ├── install.sh │ └── install │ └── lono.sh ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── documentation.md │ ├── feature_request.md │ └── question.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .rspec ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── exe └── lono ├── lib ├── lono.rb ├── lono │ ├── api │ │ ├── client.rb │ │ ├── proxy.rb │ │ ├── repos.rb │ │ └── verify.rb │ ├── app.rb │ ├── app │ │ ├── callable_option.rb │ │ ├── callable_option │ │ │ └── concern.rb │ │ └── inits.rb │ ├── autoloader.rb │ ├── aws_services.rb │ ├── aws_services │ │ └── helper.rb │ ├── blueprint.rb │ ├── booter.rb │ ├── builder.rb │ ├── builder │ │ ├── allow.rb │ │ ├── allow │ │ │ ├── base.rb │ │ │ ├── env.rb │ │ │ └── region.rb │ │ ├── configset │ │ │ ├── combiner.rb │ │ │ ├── definition.rb │ │ │ ├── definition │ │ │ │ ├── base.rb │ │ │ │ ├── context.rb │ │ │ │ ├── dsl.rb │ │ │ │ ├── dsl │ │ │ │ │ ├── syntax.rb │ │ │ │ │ └── syntax │ │ │ │ │ │ ├── auth.rb │ │ │ │ │ │ ├── content.rb │ │ │ │ │ │ ├── core.rb │ │ │ │ │ │ └── package.rb │ │ │ │ └── erb.rb │ │ │ ├── evaluator.rb │ │ │ └── registration.rb │ │ ├── context.rb │ │ ├── dsl.rb │ │ ├── dsl │ │ │ ├── evaluator.rb │ │ │ ├── finalizer.rb │ │ │ ├── finalizer │ │ │ │ ├── base.rb │ │ │ │ ├── configsets.rb │ │ │ │ ├── files.rb │ │ │ │ ├── files │ │ │ │ │ ├── base.rb │ │ │ │ │ ├── build.rb │ │ │ │ │ └── replace.rb │ │ │ │ └── parameter_groups.rb │ │ │ ├── helpers.rb │ │ │ ├── helpers │ │ │ │ ├── ec2.rb │ │ │ │ ├── files.rb │ │ │ │ ├── partials.rb │ │ │ │ ├── s3.rb │ │ │ │ ├── ssm.rb │ │ │ │ ├── ssm │ │ │ │ │ └── fetcher.rb │ │ │ │ ├── stack.rb │ │ │ │ ├── tags.rb │ │ │ │ └── template_file.rb │ │ │ ├── syntax.rb │ │ │ └── syntax │ │ │ │ ├── core.rb │ │ │ │ ├── core │ │ │ │ ├── base.rb │ │ │ │ ├── condition.rb │ │ │ │ ├── mapping.rb │ │ │ │ ├── output.rb │ │ │ │ ├── parameter.rb │ │ │ │ ├── resource.rb │ │ │ │ ├── resource │ │ │ │ │ └── property_mover.rb │ │ │ │ ├── section.rb │ │ │ │ └── squeezer.rb │ │ │ │ ├── fn.rb │ │ │ │ └── parameter_group.rb │ │ ├── param.rb │ │ ├── template.rb │ │ ├── template │ │ │ ├── aws_service.rb │ │ │ ├── bashify.rb │ │ │ ├── output.rb │ │ │ └── upload.rb │ │ └── util │ │ │ └── stringify.rb │ ├── bundler.rb │ ├── bundler │ │ ├── cli.rb │ │ ├── cli │ │ │ ├── base.rb │ │ │ ├── bundle.rb │ │ │ ├── clean.rb │ │ │ ├── help.rb │ │ │ └── help │ │ │ │ └── bundle.md │ │ ├── component.rb │ │ ├── component │ │ │ ├── concerns │ │ │ │ ├── local_concern.rb │ │ │ │ ├── notation_concern.rb │ │ │ │ ├── path_concern.rb │ │ │ │ └── stack_concern.rb │ │ │ ├── fetcher.rb │ │ │ ├── fetcher │ │ │ │ ├── base.rb │ │ │ │ ├── gcs.rb │ │ │ │ ├── git.rb │ │ │ │ ├── local.rb │ │ │ │ └── s3.rb │ │ │ ├── http │ │ │ │ ├── concern.rb │ │ │ │ └── source.rb │ │ │ ├── org_repo.rb │ │ │ ├── props.rb │ │ │ ├── props │ │ │ │ ├── extension.rb │ │ │ │ └── typer.rb │ │ │ └── registry.rb │ │ ├── config.rb │ │ ├── dsl.rb │ │ ├── dsl │ │ │ └── syntax.rb │ │ ├── exporter.rb │ │ ├── exporter │ │ │ ├── base.rb │ │ │ └── copy.rb │ │ ├── extract │ │ │ ├── tar.rb │ │ │ └── zip.rb │ │ ├── info.rb │ │ ├── list.rb │ │ ├── lockfile.rb │ │ ├── lockfile │ │ │ ├── version_comparer.rb │ │ │ └── yamler.rb │ │ ├── lonofile.rb │ │ ├── runner.rb │ │ ├── syncer.rb │ │ └── util │ │ │ ├── git.rb │ │ │ ├── logging.rb │ │ │ └── sure.rb │ ├── cfn │ │ ├── base.rb │ │ ├── cancel.rb │ │ ├── concerns.rb │ │ ├── concerns │ │ │ ├── build.rb │ │ │ └── template_output.rb │ │ ├── delete.rb │ │ ├── deploy.rb │ │ ├── deploy │ │ │ ├── base.rb │ │ │ ├── iam.rb │ │ │ ├── notification.rb │ │ │ ├── operable.rb │ │ │ ├── opts.rb │ │ │ ├── rollback.rb │ │ │ └── tags.rb │ │ ├── download.rb │ │ ├── output.rb │ │ ├── plan.rb │ │ ├── plan │ │ │ ├── base.rb │ │ │ ├── changeset.rb │ │ │ ├── changeset │ │ │ │ ├── base.rb │ │ │ │ ├── notifications.rb │ │ │ │ ├── outputs.rb │ │ │ │ ├── resources.rb │ │ │ │ └── tags.rb │ │ │ ├── concerns.rb │ │ │ ├── delete.rb │ │ │ ├── diff │ │ │ │ ├── base.rb │ │ │ │ ├── data.rb │ │ │ │ └── file.rb │ │ │ ├── new.rb │ │ │ ├── param.rb │ │ │ ├── summary.rb │ │ │ └── template.rb │ │ ├── show.rb │ │ └── status.rb │ ├── cli.rb │ ├── cli │ │ ├── abstract.rb │ │ ├── base.rb │ │ ├── bundle.rb │ │ ├── cfn.rb │ │ ├── cfn │ │ │ └── opts.rb │ │ ├── clean.rb │ │ ├── completion.rb │ │ ├── help.rb │ │ ├── help │ │ │ ├── build.md │ │ │ ├── cfn.md │ │ │ ├── cfn │ │ │ │ ├── cancel.md │ │ │ │ ├── download.md │ │ │ │ ├── preview.md │ │ │ │ └── status.md │ │ │ ├── code │ │ │ │ ├── convert.md │ │ │ │ └── import.md │ │ │ ├── completion.md │ │ │ ├── completion_script.md │ │ │ ├── configsets.md │ │ │ ├── down.md │ │ │ ├── new │ │ │ │ ├── helper.md │ │ │ │ ├── hook.md │ │ │ │ └── project.md │ │ │ ├── param.md │ │ │ ├── param │ │ │ │ └── generate.md │ │ │ ├── pro │ │ │ │ ├── blueprints.md │ │ │ │ └── configsets.md │ │ │ ├── script │ │ │ │ ├── build.md │ │ │ │ └── upload.md │ │ │ ├── seed.md │ │ │ ├── summary.md │ │ │ ├── template.md │ │ │ ├── template │ │ │ │ ├── bashify.md │ │ │ │ └── generate.md │ │ │ ├── up.md │ │ │ └── user_data.md │ │ ├── iam.rb │ │ ├── list.rb │ │ ├── new.rb │ │ ├── new │ │ │ ├── blueprint.rb │ │ │ ├── concerns.rb │ │ │ ├── configset.rb │ │ │ ├── helper.rb │ │ │ ├── hook.rb │ │ │ ├── project.rb │ │ │ ├── sequence.rb │ │ │ ├── shim.rb │ │ │ ├── test.rb │ │ │ └── test │ │ │ │ ├── blueprint.rb │ │ │ │ └── sequence.rb │ │ ├── opts.rb │ │ ├── s3.rb │ │ ├── script.rb │ │ ├── script │ │ │ ├── base.rb │ │ │ ├── build.rb │ │ │ └── upload.rb │ │ ├── seed.rb │ │ ├── status.rb │ │ ├── summary.rb │ │ └── test.rb │ ├── command.rb │ ├── completer.rb │ ├── completer │ │ ├── script.rb │ │ └── script.sh │ ├── component.rb │ ├── concerns │ │ ├── aws_info.rb │ │ └── names.rb │ ├── configset.rb │ ├── core.rb │ ├── ext.rb │ ├── ext │ │ ├── bundler.rb │ │ └── core │ │ │ ├── module.rb │ │ │ ├── object.rb │ │ │ └── string.rb │ ├── files.rb │ ├── files │ │ ├── base.rb │ │ ├── builder.rb │ │ ├── builder │ │ │ ├── lambda_layer.rb │ │ │ └── lambda_layer │ │ │ │ ├── rsync.rb │ │ │ │ ├── ruby_packager.rb │ │ │ │ └── ruby_version.rb │ │ ├── compressor.rb │ │ ├── concerns │ │ │ ├── post_processing.rb │ │ │ └── registration.rb │ │ └── registry.rb │ ├── hooks │ │ ├── builder.rb │ │ ├── concern.rb │ │ ├── dsl.rb │ │ └── runner.rb │ ├── importer.rb │ ├── layering.rb │ ├── layering │ │ └── layer.rb │ ├── logger.rb │ ├── logger │ │ └── formatter.rb │ ├── md5.rb │ ├── names.rb │ ├── plugin.rb │ ├── plugin │ │ ├── meta.rb │ │ └── tester.rb │ ├── registration.rb │ ├── s3 │ │ ├── aws_setup.rb │ │ ├── bucket.rb │ │ ├── rollback.rb │ │ └── uploader.rb │ ├── script │ │ ├── base.rb │ │ ├── build.rb │ │ └── upload.rb │ ├── seeder.rb │ ├── user_data.rb │ ├── utils.rb │ ├── utils │ │ ├── call_line.rb │ │ ├── logging.rb │ │ ├── pretty.rb │ │ ├── quit.rb │ │ ├── sh.rb │ │ └── sure.rb │ ├── version.rb │ └── yamler │ │ ├── loader.rb │ │ └── validator.rb └── templates │ ├── blueprint │ └── template.rb │ ├── configset │ └── configset.rb │ ├── examples │ ├── blueprint │ │ └── template.rb │ └── configset │ │ └── configset.rb │ ├── helper │ └── %underscore_name%_helper.rb.tt │ ├── hook │ └── config │ │ └── hooks.rb.tt │ ├── project │ ├── .gitignore │ ├── Gemfile.tt │ ├── README.md │ └── config │ │ └── app.rb │ └── shim │ └── lono ├── lono.gemspec └── spec ├── fixtures ├── configsets │ ├── dsl │ │ └── httpd │ │ │ └── configset.rb │ ├── erb │ │ └── httpd │ │ │ ├── configset.yml │ │ │ └── vars.rb │ ├── snippets │ │ ├── config1.json │ │ ├── config2.json │ │ └── single.yml │ └── templates │ │ ├── ec2-and-sg-metadata.yml │ │ ├── ec2-and-sg.yml │ │ ├── ec2-multiple.yml │ │ ├── ec2-no-metadata.yml │ │ └── ec2-single.yml ├── md5 │ ├── a.rb │ ├── folder │ │ └── a.rb │ └── folder1 │ │ └── folder2 │ │ └── a.rb └── validator │ └── bad.yml ├── lono ├── builder │ └── configset │ │ ├── combiner_spec.rb │ │ └── definition │ │ ├── dsl_spec.rb │ │ └── erb_spec.rb ├── cfn │ └── plan │ │ └── diff │ │ └── data_spec.rb ├── cli_spec.rb ├── completion_spec.rb ├── md5_spec.rb └── yamler │ └── validator_spec.rb ├── spec_helper.rb └── spec_helper ├── execute.rb └── logging.rb /.cody/README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | CodeBuild is used to run **acceptance-level tests**. 4 | 5 | ## Deploy Project 6 | 7 | To update the CodeBuild project that handles deployment: 8 | 9 | cody deploy lono -t acceptance 10 | 11 | ## Start Build 12 | 13 | To start a CodeBuild build: 14 | 15 | cody start lono -t acceptance 16 | 17 | To specify a branch: 18 | 19 | cody start lono -t acceptance -b feature 20 | -------------------------------------------------------------------------------- /.cody/acceptance/bin/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | final_status=0 4 | function capture_status { 5 | if [ "$?" -ne "0" ] && [ $final_status -ne 1 ] ; then 6 | final_status=1 7 | fi 8 | } 9 | 10 | set -eu 11 | # will build from /tmp because terraspace/Gemfile may interfere 12 | cd /tmp 13 | export PATH=~/bin:$PATH # ~/bin/lono wrapper 14 | 15 | lono new project infra 16 | cd infra 17 | 18 | # Rewrite the Gemfile to use the local lono gem for testing 19 | cat << EOF > Gemfile 20 | source "https://rubygems.org" 21 | gem "lono", path: "$CODEBUILD_SRC_DIR", submodules: true 22 | gem "rspec-lono", git: "https://github.com/boltops-tools/rspec-lono", branch: "master" 23 | EOF 24 | cat Gemfile 25 | 26 | bundle # install lono gem in the infra project 27 | 28 | export LONO_ENV="test-$(date +%Y%m%d%H%M%S)" 29 | lono new blueprint demo --examples 30 | 31 | # Continue on error and capture final exit status so lono down always and cleans up stack 32 | set +e 33 | # Test new stack creation 34 | lono up demo -y 35 | capture_status 36 | 37 | lono seed demo # just to test it. will overwrite file 38 | capture_status 39 | 40 | # Test stack update 41 | cat << EOF > config/blueprints/demo/params/$LONO_ENV.txt 42 | AccessControl=PublicRead 43 | EOF 44 | lono up demo -y 45 | capture_status 46 | 47 | # Clean up resources 48 | lono down demo -y 49 | capture_status 50 | set -e 51 | 52 | exit $final_status 53 | -------------------------------------------------------------------------------- /.cody/acceptance/buildspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | phases: 4 | install: 5 | runtime-versions: 6 | ruby: latest 7 | build: 8 | commands: 9 | - .cody/shared/script/install.sh 10 | - .cody/acceptance/bin/build.sh 11 | -------------------------------------------------------------------------------- /.cody/acceptance/project.rb: -------------------------------------------------------------------------------- 1 | github_url("https://github.com/boltops-tools/lono") 2 | linux_image("aws/codebuild/amazonlinux2-x86_64-standard:3.0") 3 | # triggers( 4 | # webhook: true, 5 | # filter_groups: [[{type: "EVENT", pattern: "PUSH"}]] 6 | # ) 7 | -------------------------------------------------------------------------------- /.cody/acceptance/role.rb: -------------------------------------------------------------------------------- 1 | iam_policy( 2 | "cloudformation", 3 | "ec2", 4 | "logs", 5 | "s3", 6 | "ssm", 7 | ) -------------------------------------------------------------------------------- /.cody/shared/script/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eu 4 | 5 | .cody/shared/script/install/lono.sh 6 | -------------------------------------------------------------------------------- /.cody/shared/script/install/lono.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eux 4 | 5 | export PATH=~/bin:$PATH 6 | 7 | cat << 'EOF' > ~/.gemrc 8 | --- 9 | :backtrace: false 10 | :bulk_threshold: 1000 11 | :sources: 12 | - https://rubygems.org 13 | :update_sources: true 14 | :verbose: true 15 | benchmark: false 16 | install: "--no-ri --no-rdoc --no-document" 17 | update: "--no-ri --no-rdoc --no-document" 18 | EOF 19 | 20 | gem install bundler # upgrade bundler 21 | 22 | # In original lono source and install lono 23 | cd $CODEBUILD_SRC_DIR # lono folder - in case code is added later above this that uses cd 24 | bundle install 25 | bundle exec rake install 26 | 27 | mkdir -p ~/bin 28 | cat << EOF > ~/bin/lono 29 | #!/bin/bash 30 | # If there's a Gemfile, assume we're in a lono project with a Gemfile for lono 31 | if [ -f Gemfile ]; then 32 | exec bundle exec $CODEBUILD_SRC_DIR/exe/lono "\$@" 33 | else 34 | exec $CODEBUILD_SRC_DIR/exe/lono "\$@" 35 | fi 36 | EOF 37 | 38 | cat ~/bin/lono 39 | 40 | chmod a+x ~/bin/lono 41 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: boltops-tools 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Please fill out one of the templates on https://github.com/boltops-tools/lono/issues/new/choose 2 | 3 | If you want to ask a question please do so in the Lono category in the BoltOps Community forum: https://community.boltops.com 4 | 5 | To be sensitive to everyone's time, we may close issues asking questions without comment. Posting your questions in the Lono community forum is the best place. It also benefits others by making the questions easier to find. Here are some additional options also https://lono.cloud/support/ 👌 6 | 7 | Thank you! 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Documentation 3 | about: Found a typo or something that isn't crystal clear in the docs? 4 | title: '' 5 | labels: docs 6 | assignees: '' 7 | 8 | --- 9 | 10 | The Lono Docs are in the [lono-docs repo](https://github.com/boltops-tools/lono-docs). Please submit a PR there. Thanks! 11 | 12 | For documentation changes to the lono code base itself, like code comments. Please submit a PR here. Thanks! -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: New Feature Suggestion 3 | about: Want to add a feature to Lono? 4 | title: '' 5 | labels: feature 6 | assignees: '' 7 | 8 | --- 9 | 10 | 15 | 16 | ## Summary 17 | 18 | 21 | 22 | ## Motivation 23 | 24 | 29 | 30 | ## Guide-level explanation 31 | 32 | 41 | 42 | ## Reference-level explanation 43 | 44 | 53 | 54 | ## Drawbacks 55 | 56 | 59 | 60 | ## Unresolved Questions 61 | 62 | 65 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Have any questions about how Lono works? 4 | title: '' 5 | labels: 'question' 6 | assignees: '' 7 | 8 | --- 9 | 10 | The Lono issue tracker IS NOT for usage questions! Please post your question on our dedicated forum at https://community.boltops.com 11 | 12 | To be sensitive to everyone's time, we may close issues asking questions without comment. If you repeatedly post questions in the issues tracker, you may be blocked from ever submitting issues to Lono again. Please use your best judgment. 👍 13 | 14 | Posting your questions in the Lono community forum benefits others by grouping questions in a dedicated place. Here are some additional options also https://lono.cloud/support/ 😁 15 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 12 | 13 | 14 | 15 | 20 | 21 | - [ ] I've added tests (if it's a bug, feature or enhancement) 22 | - [ ] I've adjusted the documentation (if it's a feature or enhancement) 23 | - [ ] The test suite passes (run `bundle exec rspec` to verify this) 24 | 25 | ## Summary 26 | 27 | 30 | 31 | ## Context 32 | 33 | 36 | 37 | ## How to Test 38 | 39 | 42 | 43 | 44 | ## Version Changes 45 | 46 | 50 | 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .DS_Store 6 | .yardoc 7 | _yardoc 8 | coverage 9 | doc/ 10 | Gemfile.lock 11 | InstalledFiles 12 | lib/bundler/man 13 | pkg 14 | rdoc 15 | spec/fixtures/my_project/templates/aws-waf-security-automations.yml 16 | spec/project 17 | spec/reports 18 | test/tmp 19 | test/version_tmp 20 | 21 | /infra 22 | /output 23 | /tmp 24 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format documentation 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Specify your gem dependencies in lono.gemspec 4 | gemspec 5 | 6 | group :test do 7 | gem "rspec-lono", git: "https://github.com/boltops-tools/rspec-lono", branch: "master" 8 | end 9 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "./lib/lono" 2 | require "bundler/gem_tasks" 3 | require "rspec/core/rake_task" 4 | 5 | task :default => :spec 6 | 7 | RSpec::Core::RakeTask.new 8 | 9 | require_relative "lib/lono" 10 | require "cli_markdown" 11 | desc "Generates cli reference docs as markdown" 12 | task :docs do 13 | CliMarkdown::Creator.create_all(cli_class: Lono::CLI, cli_name: "lono") 14 | end 15 | -------------------------------------------------------------------------------- /exe/lono: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Trap ^C 4 | Signal.trap("INT") { 5 | puts "\nCtrl-C detected. Exiting..." 6 | sleep 0.1 7 | exit 8 | } 9 | 10 | require_relative "../lib/lono" 11 | Lono::CLI.start 12 | -------------------------------------------------------------------------------- /lib/lono.rb: -------------------------------------------------------------------------------- 1 | $stdout.sync = true unless ENV["LONO_STDOUT_SYNC"] == "0" 2 | 3 | require 'active_support' 4 | require 'active_support/core_ext/class' 5 | require 'active_support/core_ext/hash' 6 | require 'active_support/core_ext/string' 7 | require 'aws_data' 8 | require 'cfn_camelizer' 9 | require 'cfn_status' 10 | require 'cli-format' 11 | require 'dsl_evaluator' 12 | require 'fileutils' 13 | require 'json' 14 | require 'lono/ext' 15 | require 'memoist' 16 | require 'plissken' 17 | require 'rainbow/ext/string' 18 | require 'render_me_pretty' 19 | require 'singleton' 20 | require 'yaml' 21 | 22 | require "lono/autoloader" 23 | Lono::Autoloader.setup 24 | 25 | module Lono 26 | API_DEFAULT = 'https://api.lono.cloud/v1' 27 | API = ENV['LONO_API'] || API_DEFAULT 28 | extend Core 29 | end 30 | 31 | DslEvaluator.configure do |config| 32 | config.backtrace.select_pattern = Lono.root.to_s 33 | config.logger = Lono.logger 34 | config.on_exception = :exit 35 | config.root = Lono.root 36 | end 37 | 38 | Lono::Booter.boot 39 | -------------------------------------------------------------------------------- /lib/lono/api/client.rb: -------------------------------------------------------------------------------- 1 | module Lono::Api 2 | class Client 3 | extend Memoist 4 | include Verify 5 | include Repos 6 | include Lono::Utils::Logging 7 | 8 | def http 9 | Proxy.new 10 | end 11 | memoize :http 12 | 13 | def load_json(res) 14 | if res.code == "200" 15 | data = JSON.load(res.body) 16 | case data 17 | when Array 18 | data.map(&:deep_symbolize_keys) 19 | when Hash 20 | data.deep_symbolize_keys 21 | end 22 | else 23 | if ENV['LONO_DEBUG_API'] 24 | logger.info "Error: Non-successful http response status code: #{res.code}" 25 | logger.info "headers: #{res.each_header.to_h.inspect}" 26 | end 27 | nil 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/lono/api/proxy.rb: -------------------------------------------------------------------------------- 1 | # To allow a pretty interface: 2 | # 3 | # res = http.get("blueprints") 4 | # 5 | module Lono::Api 6 | class Proxy 7 | extend Memoist 8 | 9 | def get(path) 10 | request(Net::HTTP::Get, path) 11 | end 12 | 13 | def post(path, data={}) 14 | request(Net::HTTP::Post, path, data) 15 | end 16 | 17 | def http(url) 18 | uri = URI(url) 19 | http = Net::HTTP.new(uri.host, uri.port) 20 | http.open_timeout = http.read_timeout = 30 21 | http.use_ssl = true if uri.scheme == 'https' 22 | http 23 | end 24 | memoize :http 25 | 26 | def request(klass, path, data={}) 27 | url = url(path) 28 | http = http(url) 29 | req = send_request(klass, url, data) 30 | http.request(req) # send request 31 | end 32 | 33 | def send_request(klass, url, data={}) 34 | data.merge!( 35 | lono_version: Lono::VERSION, 36 | lono_command: lono_command, 37 | ) 38 | req = klass.new(url) # url includes query string and uri.path does not, must used url 39 | if [Net::HTTP::Post, Net::HTTP::Put].include?(klass) 40 | text = JSON.dump(data) 41 | req.body = text 42 | req.content_length = text.bytesize 43 | end 44 | req 45 | end 46 | 47 | # Lono::API does not include the /. IE: localhost:8888 48 | # path includes the /. IE: "/blueprints" 49 | def url(path) 50 | "#{Lono::API}/#{path}" 51 | end 52 | 53 | private 54 | def lono_command 55 | "#{$0} #{ARGV.join(' ')}" 56 | end 57 | end 58 | end -------------------------------------------------------------------------------- /lib/lono/api/repos.rb: -------------------------------------------------------------------------------- 1 | module Lono::Api 2 | module Repos 3 | def repos(type) 4 | res = http.get("repos?type=#{type}") 5 | load_json(res) 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/lono/api/verify.rb: -------------------------------------------------------------------------------- 1 | module Lono::Api 2 | module Verify 3 | def verify(data) 4 | res = http.post("verify", data) 5 | load_json(res) 6 | end 7 | 8 | def temp_key 9 | res = http.post("temp_keys") 10 | load_json(res) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/lono/app/callable_option/concern.rb: -------------------------------------------------------------------------------- 1 | class Lono::App::CallableOption 2 | module Concern 3 | def callable_option(options={}) 4 | callable_option = Lono::App::CallableOption.new( 5 | config_name: options[:config_name], 6 | config_value: options[:config_value], 7 | passed_args: options[:passed_args], 8 | ) 9 | callable_option.object 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/lono/app/inits.rb: -------------------------------------------------------------------------------- 1 | class Lono::App 2 | class Inits 3 | class << self 4 | include DslEvaluator 5 | 6 | def run_all 7 | Dir.glob("#{Lono.root}/config/inits/*.rb").each do |path| 8 | evaluate_file(path) 9 | end 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/lono/autoloader.rb: -------------------------------------------------------------------------------- 1 | require "lono/bundler" 2 | Lono::Bundler.setup 3 | require "zeitwerk" 4 | 5 | module Lono 6 | class Autoloader 7 | class Inflector < Zeitwerk::Inflector 8 | def camelize(basename, _abspath) 9 | map = { cli: "CLI", version: "VERSION" } 10 | map[basename.to_sym] || super 11 | end 12 | end 13 | 14 | class << self 15 | def setup 16 | loader = Zeitwerk::Loader.new 17 | loader.inflector = Inflector.new 18 | loader.push_dir(File.dirname(__dir__)) # lib 19 | loader.log! if ENV["LONO_AUTOLOAD_LOG"] 20 | loader.ignore("#{__dir__}/ext.rb") 21 | loader.setup 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/lono/blueprint.rb: -------------------------------------------------------------------------------- 1 | module Lono 2 | class Blueprint < Component 3 | def output_path 4 | "#{Lono.root}/output/#{@name}/template.yml" 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/lono/booter.rb: -------------------------------------------------------------------------------- 1 | module Lono 2 | module Booter 3 | def boot 4 | run_hooks 5 | Lono::Bundler.require # load plugins 6 | end 7 | 8 | # Special boot hooks run super early. 9 | # Useful for setting env vars and other early things. 10 | # 11 | # config/boot.rb 12 | # config/boot/dev.rb 13 | # 14 | def run_hooks 15 | run_hook 16 | run_hook(Lono.env) 17 | Lono::App::Inits.run_all 18 | end 19 | 20 | def run_hook(env=nil) 21 | name = env ? "boot/#{env}" : "boot" 22 | path = "#{Lono.root}/config/#{name}.rb" 23 | require path if File.exist?(path) 24 | end 25 | 26 | extend self 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/lono/builder.rb: -------------------------------------------------------------------------------- 1 | module Lono 2 | class Builder < Lono::CLI::Base 3 | include Lono::Hooks::Concern 4 | 5 | def all 6 | check_allow! 7 | 8 | parameters = nil 9 | run_hooks("build") do 10 | clean 11 | template_builder.run # build with placeholders IE: LONO://app/files/index.rb 12 | parameters = param_builder.build # Writes the json file in CamelCase keys format 13 | end 14 | 15 | logger.info "" # newline 16 | parameters 17 | end 18 | memoize :all 19 | alias_method :parameters, :all 20 | 21 | def clean 22 | Lono::CLI::Clean.new(@options.merge(mute: true)).run 23 | end 24 | 25 | def check_allow! 26 | Lono::Builder::Allow.new(@options).check! 27 | end 28 | 29 | def param_builder 30 | Lono::Builder::Param.new(@options) 31 | end 32 | memoize :param_builder 33 | 34 | def template_builder 35 | Lono::Builder::Template.new(@options) # write templates to disk 36 | end 37 | memoize :template_builder 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/lono/builder/allow.rb: -------------------------------------------------------------------------------- 1 | class Lono::Builder 2 | class Allow < Lono::CLI::Base 3 | def check! 4 | Env.new(@options).check! 5 | Region.new(@options).check! 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/lono/builder/allow/base.rb: -------------------------------------------------------------------------------- 1 | class Lono::Builder::Allow 2 | class Base < Lono::CLI::Base 3 | include Lono::App::CallableOption::Concern 4 | 5 | def check! 6 | messages = [] 7 | unless allowed? 8 | messages << message # message is interface method 9 | end 10 | unless messages.empty? 11 | puts "ERROR: The configs do not allow this.".color(:red) 12 | puts messages 13 | exit 1 14 | end 15 | end 16 | 17 | def allowed? 18 | if allows.nil? && denys.nil? 19 | true 20 | elsif denys.nil? 21 | allows.include?(check_value) 22 | elsif allows.nil? 23 | !denys.include?(check_value) 24 | else 25 | allows.include?(check_value) && !denys.include?(check_value) 26 | end 27 | end 28 | 29 | def allows 30 | callable_option( 31 | config_name: "config.allow.#{config_name}", 32 | config_value: config.dig(:allow, config_name), 33 | passed_args: [@blueprint], 34 | ) 35 | end 36 | 37 | def denys 38 | callable_option( 39 | config_name: "config.deny.#{config_name}", 40 | config_value: config.dig(:deny, config_name), 41 | passed_args: [@blueprint], 42 | ) 43 | end 44 | 45 | private 46 | def config 47 | Lono.config 48 | end 49 | 50 | def config_name 51 | self.class.to_s.split('::').last.underscore.pluralize.to_sym # ActiveSuport::HashWithIndifferentAccess#dig requires symbol 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/lono/builder/allow/env.rb: -------------------------------------------------------------------------------- 1 | class Lono::Builder::Allow 2 | class Env < Base 3 | # interface method 4 | def message 5 | messages = [] 6 | messages << "This env is not allowed to be used: LONO_ENV=#{Lono.env}" 7 | messages << "Allow envs: #{allows.join(', ')}" if allows 8 | messages << "Deny envs: #{denys.join(', ')}" if denys 9 | messages.join("\n") 10 | end 11 | 12 | # interface method 13 | def check_value 14 | Lono.env 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/lono/builder/allow/region.rb: -------------------------------------------------------------------------------- 1 | class Lono::Builder::Allow 2 | class Region < Base 3 | include Lono::Concerns::AwsInfo 4 | 5 | # interface method 6 | def message 7 | messages = [] 8 | word = config_name.to_s # IE: regions or locations 9 | messages << "This #{word.singularize} is not allowed to be used: Detected current #{word.singularize}=#{current_region}" 10 | messages << "Allow #{word}: #{allows.join(', ')}" if allows 11 | messages << "Deny #{word}: #{denys.join(', ')}" if denys 12 | messages.join("\n") 13 | end 14 | 15 | # interface method 16 | def check_value 17 | region 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/lono/builder/configset/definition.rb: -------------------------------------------------------------------------------- 1 | module Lono::Builder::Configset 2 | class Definition < Lono::CLI::Base 3 | attr_reader :configset 4 | def initialize(options={}) 5 | super 6 | @meta = options[:meta] 7 | @configset = Lono::Configset.new(@meta) 8 | end 9 | 10 | def evaluate 11 | strategy_class = configset.path.include?('.rb') ? Dsl : Erb 12 | strategy = strategy_class.new(@options.merge(path: configset.path)) 13 | metadata = strategy.evaluate 14 | @configset.metadata = metadata 15 | @configset 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/lono/builder/configset/definition/base.rb: -------------------------------------------------------------------------------- 1 | class Lono::Builder::Configset::Definition 2 | class Base < Lono::CLI::Base 3 | include DslEvaluator 4 | include Lono::Builder::Dsl::Syntax 5 | include Lono::Utils::Pretty 6 | include Context 7 | 8 | # Really only use @path in Configset DSL and ERB evaluation. 9 | # However, configsets are built within the CloudFormation template and can use 10 | # things instrinic functions like `ref` like would normally have access to. 11 | # So configsets need the same context 12 | # 13 | # Configset::Definition::Base < Lono::CLI::Base 14 | # 15 | # for 16 | # 17 | # include Lono::Builder::Dsl::Syntax 18 | # 19 | def initialize(options={}) 20 | super 21 | @meta = @options[:meta] 22 | @configset = Lono::Configset.new(@meta) 23 | expose_instance_variables 24 | end 25 | 26 | # This context is used by the DSL evaluation. Expose variables so user can use them in configset definitions. 27 | # Example: 28 | # 29 | # "/etc/cfn/hooks.d/cfn-auto-reloader.conf": 30 | # content: 31 | # Fn::Sub: 32 | # ... 33 | # path=Resources.<%= @resource %>.Metadata.AWS::CloudFormation::Init 34 | # 35 | def expose_instance_variables 36 | @name = @meta[:name] 37 | @resource = @meta[:resource] 38 | end 39 | 40 | def wrap_with_metadata(cloudformation_init) 41 | full = {"Metadata" => {}} 42 | full["Metadata"]["AWS::CloudFormation::Init"] = cloudformation_init["AWS::CloudFormation::Init"] 43 | full["Metadata"]["AWS::CloudFormation::Authentication"] = authentication if authentication # only on dsl 44 | full.deep_stringify_keys.dup # metadata 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/lono/builder/configset/definition/dsl.rb: -------------------------------------------------------------------------------- 1 | class Lono::Builder::Configset::Definition 2 | class Dsl < Base 3 | include Syntax 4 | 5 | def initialize(options={}) 6 | super 7 | @current = "main" # current configset 8 | @tracked = [] 9 | @structure = {} # holds in memory the configset hash structure to build AWS::CloudFormation::Init 10 | @command_counts = Hash.new(0) 11 | # Also support ability to add AWS::CloudFormation::Authentication 12 | @authentication = nil # holds IAM policy info to build AWS::CloudFormation::Authentication 13 | end 14 | 15 | def evaluate 16 | load_context 17 | evaluate_file(@configset.path) 18 | configsets = configsets || @tracked.uniq 19 | configsets = ["main"] if configsets.empty? 20 | configsets_structure = {"configSets" => {"default" => configsets}}.merge(@structure) 21 | cloudformation_init = { "AWS::CloudFormation::Init" => configsets_structure } 22 | wrap_with_metadata(cloudformation_init) 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/lono/builder/configset/definition/dsl/syntax.rb: -------------------------------------------------------------------------------- 1 | class Lono::Builder::Configset::Definition::Dsl 2 | module Syntax 3 | include_modules "syntax" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/lono/builder/configset/definition/dsl/syntax/auth.rb: -------------------------------------------------------------------------------- 1 | module Lono::Builder::Configset::Definition::Dsl::Syntax 2 | module Auth 3 | def authentication(data=nil, force: false) 4 | if data.nil? 5 | authentication_reader 6 | else 7 | authentication_setter(data, force) 8 | end 9 | end 10 | 11 | # data can be either: 12 | # 13 | # 1. logical id - String 14 | # 2. Full AWS::CloudFormation::Authentication value structure 15 | # 16 | def authentication_reader 17 | # AWS::CloudFormation::Authentication 18 | case @authentication 19 | when String 20 | logical_id = @authentication 21 | { 22 | rolebased: { 23 | type: "S3", 24 | buckets: [lono_bucket_name], 25 | roleName: {Ref: logical_id}, # currently ref meth is not available 26 | } 27 | } 28 | when Hash 29 | @authentication 30 | end 31 | end 32 | 33 | def authentication_setter(data, force=false) 34 | @authentication = data unless @authentication || force 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/lono/builder/configset/definition/dsl/syntax/content.rb: -------------------------------------------------------------------------------- 1 | module Lono::Builder::Configset::Definition::Dsl::Syntax 2 | module Content 3 | def content_file(path) 4 | content_path = "#{content_file_root}/content" 5 | file = "#{content_path}/#{path}" 6 | if File.exist?(file) 7 | IO.read(file) 8 | else 9 | "File not found: #{file}" 10 | end 11 | end 12 | 13 | private 14 | def content_file_root 15 | if @configset 16 | @configset.root 17 | else 18 | @blueprint.root 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/lono/builder/configset/definition/dsl/syntax/package.rb: -------------------------------------------------------------------------------- 1 | module Lono::Builder::Configset::Definition::Dsl::Syntax 2 | module Package 3 | # In recent versions of the AmazonLinux2 cfnbootstrap, , the package command doesnt work with updated versions of rubygems. 4 | # Get this error 5 | # 6 | # invalid option: --no-ri 7 | # 8 | # More details: https://gist.github.com/tongueroo/569878afdc7eb904490b9ee8b03f304f 9 | # 10 | # Found the cfnbootstrap version by looking at the source on 2020-03-21 in 11 | # 12 | # $ cat /usr/lib/python2.7/site-packages/cfnbootstrap/public_constants.py 13 | # _release = '31' 14 | # _version = '1.4-' + _release 15 | # 16 | # There is no way to get the version from the /opt/aws/bin/cfn-init command. 17 | # 18 | # We work around this be using the command instruction and use the gem install and list commands. 19 | # 20 | # $ gem list tilt -e -i -v 1.4.0 21 | # false # also $? is 1 22 | # $ gem list tilt -e -i -v 1.4.1 23 | # true # also $? is 0 24 | # $ 25 | # 26 | def gem_package(name, version=nil) 27 | unless_clause = "gem list #{name} -e -i " 28 | unless_clause += "-v #{version}" if version 29 | command("#{name}-gem-install", 30 | command: "gem install #{name} #{version}", 31 | unless: unless_clause 32 | ) 33 | end 34 | 35 | def yum_package(name, version=nil) 36 | versions = [version].compact 37 | package("yum", name => versions) 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/lono/builder/configset/definition/erb.rb: -------------------------------------------------------------------------------- 1 | class Lono::Builder::Configset::Definition 2 | class Erb < Base 3 | def evaluate 4 | load_context 5 | cloudformation_init = build 6 | wrap_with_metadata(cloudformation_init) 7 | end 8 | 9 | def build 10 | content = RenderMePretty.result(@configset.path, context: self) 11 | if File.extname(@configset.path) == ".yml" 12 | load_yaml(content) 13 | else 14 | JSON.load(content) 15 | end 16 | end 17 | 18 | def load_yaml(content) 19 | # init structure 20 | # Write to file so can use Yamler::Validator 21 | path = "/tmp/lono/configset.yml" 22 | FileUtils.mkdir_p(File.dirname(path)) 23 | IO.write(path, content) 24 | Lono::Yamler::Validator.new(path).validate! 25 | Lono::Yamler::Loader.new(content).load 26 | end 27 | 28 | def authentication 29 | # noop 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/lono/builder/configset/evaluator.rb: -------------------------------------------------------------------------------- 1 | # Dsl or Erb 2 | module Lono::Builder::Configset 3 | class Evaluator < Lono::CLI::Base 4 | def evaluate 5 | Registration.new(@blueprint).evaluate 6 | combiner = Combiner.new(@options.merge(metas: Registration.metas)) 7 | combiner.combine 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/lono/builder/configset/registration.rb: -------------------------------------------------------------------------------- 1 | module Lono::Builder::Configset 2 | class Registration 3 | include DslEvaluator 4 | 5 | def initialize(blueprint) 6 | @blueprint = blueprint 7 | end 8 | 9 | cattr_accessor :metas, default: [] 10 | def metas 11 | self.class.metas 12 | end 13 | 14 | def evaluate 15 | path = "#{@blueprint.root}/configsets.rb" # plural 16 | evaluate_file(path) 17 | end 18 | 19 | # Only one syntax method so not in separate module. 20 | # 21 | # Register configset for later processing. 22 | # By registering them all up front, can aggregate errors and show them together 23 | # for a user-friendly experience. 24 | # 25 | # The configset method is different with configset registration vs configset definition 26 | # 27 | # definition: app/configsets/httpd/configset.rb 28 | # registration: app/blueprints/demo/configsets.rb 29 | # 30 | def configset(name, options={}) 31 | found = metas.detect { |i| i[:name] == name && i[:resource] == options[:resource] } 32 | metas << options.merge(name: name) unless found 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/lono/builder/context.rb: -------------------------------------------------------------------------------- 1 | class Lono::Builder 2 | module Context 3 | include DslEvaluator 4 | 5 | def load_context 6 | load_helpers # load helpers before variable so user custom helpers are available in vars files 7 | load_variables 8 | end 9 | 10 | # Variables in base.rb are overridden by their environment specific variables 11 | # file. Example, file LONO_ENV=dev: 12 | # 13 | # config/vars/base.rb 14 | # config/vars/dev.rb - will override any variables in base.rb 15 | # config/vars/base.rb 16 | # config/vars/dev.rb - will override any variables in base.rb 17 | # 18 | def load_variables 19 | return if seed? 20 | layers = Lono::Layering::Layer.new(@blueprint, "vars").paths 21 | layers.each do |layer| 22 | evaluate_file(layer) 23 | end 24 | end 25 | 26 | # Load blueprint helpers 27 | # blueprint helpers override extension helpers 28 | def load_helpers 29 | load_helper_files("#{Lono.root}/vendor/helpers", type: :project) 30 | load_helper_files("#{Lono.root}/app/helpers", type: :project) 31 | load_helper_files("#{@blueprint.root}/helpers", type: :blueprint) # takes higher precedence 32 | end 33 | 34 | # Dont want any existing files to prevent building the blueprint. 35 | # This means that parameters cannot be based on vars. It's a trade-off. 36 | def seed? 37 | ARGV[0] == "seed" 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/lono/builder/dsl.rb: -------------------------------------------------------------------------------- 1 | class Lono::Builder 2 | class Dsl < Lono::CLI::Base 3 | attr_reader :results 4 | def run 5 | logger.info "Building template" unless @options[:quiet] 6 | build_template 7 | end 8 | 9 | def build_template 10 | evaluator = Evaluator.new(@options) 11 | evaluator.evaluate 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/lono/builder/dsl/evaluator.rb: -------------------------------------------------------------------------------- 1 | class Lono::Builder::Dsl 2 | class Evaluator < Lono::CLI::Base 3 | include DslEvaluator 4 | include Lono::Builder::Context 5 | include Lono::Builder::Dsl::Syntax 6 | include Lono::Utils::Pretty 7 | 8 | def initialize(options={}) 9 | super 10 | @template_path = "#{@blueprint.root}/template.rb" 11 | @parameters = [] # built by parameter_groups.rb 12 | @cfn = {} 13 | end 14 | 15 | def evaluate 16 | load_context 17 | evaluate_template_paths(@template_path) # modifies @cfn 18 | finalize 19 | to_yaml 20 | @cfn 21 | end 22 | 23 | def finalize 24 | o = @options.merge(parameters: @parameters) 25 | @cfn = Finalizer.new(@cfn, o).run 26 | end 27 | 28 | def to_yaml 29 | # https://stackoverflow.com/questions/24508364/how-to-emit-yaml-in-ruby-expanding-aliases 30 | # Trick to prevent YAML from emitting aliases 31 | @cfn = YAML.load(@cfn.to_json) 32 | end 33 | 34 | # Example path: /full/path/to/project/app/blueprints/demo/template.rb 35 | def evaluate_template_paths(path) 36 | ext = File.extname(path) 37 | folder = path.sub(ext, '') 38 | expr = "#{folder}/**/*.rb" 39 | evaluate_file(path) # process top-level template.rb first 40 | Dir.glob(expr).each do |path| 41 | evaluate_file(path) 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/lono/builder/dsl/finalizer.rb: -------------------------------------------------------------------------------- 1 | class Lono::Builder::Dsl 2 | class Finalizer 3 | def initialize(cfn, options={}) 4 | @cfn, @options = cfn, options 5 | end 6 | 7 | def run 8 | o = @options.merge(cfn: @cfn) 9 | @cfn = ParameterGroups.new(o).run 10 | @cfn = Configsets.new(o).run 11 | @cfn = Files.new(o).run 12 | @cfn 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/lono/builder/dsl/finalizer/base.rb: -------------------------------------------------------------------------------- 1 | class Lono::Builder::Dsl::Finalizer 2 | class Base < Lono::CLI::Base 3 | def initialize(options={}) 4 | super 5 | @cfn = options[:cfn] 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/lono/builder/dsl/finalizer/configsets.rb: -------------------------------------------------------------------------------- 1 | class Lono::Builder::Dsl::Finalizer 2 | class Configsets < Base 3 | # Replaces metadata under each logical id resource. 4 | def run 5 | dsl = Lono::Builder::Configset::Evaluator.new(@options.merge(cfn: @cfn)) 6 | metadata_map = dsl.evaluate 7 | metadata_map.each do |logical_id, cs| 8 | resource = @cfn["Resources"][logical_id] 9 | unless resource 10 | puts "WARN: Resources.#{logical_id} not found in the template. Are you sure you specified the correct resource logical id in your configsets.rb?".color(:yellow) 11 | next 12 | end 13 | 14 | resource["Metadata"] = cs["Metadata"] 15 | end 16 | 17 | @cfn 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/lono/builder/dsl/finalizer/files.rb: -------------------------------------------------------------------------------- 1 | class Lono::Builder::Dsl::Finalizer 2 | class Files < Base 3 | # Replaces metadata under each logical id resource. 4 | def run 5 | Build.new(@options).run 6 | @cfn = Replace.new(@options).run 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/lono/builder/dsl/finalizer/files/base.rb: -------------------------------------------------------------------------------- 1 | class Lono::Builder::Dsl::Finalizer::Files 2 | class Base < Lono::Builder::Dsl::Finalizer::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /lib/lono/builder/dsl/finalizer/files/build.rb: -------------------------------------------------------------------------------- 1 | class Lono::Builder::Dsl::Finalizer::Files 2 | class Build < Base 3 | def initialize(options={}) 4 | super 5 | @output_path = "#{Lono.root}/.output/files" 6 | end 7 | 8 | def run 9 | return if Lono::Files.empty? 10 | logger.debug "Building files" 11 | clean 12 | validate! 13 | build_files 14 | end 15 | 16 | def build_files 17 | Lono::Files.files.each do |file| # using singular file, but is like a "file_list" 18 | file.build 19 | file.build_lambda_layer(@cfn) 20 | file.compress 21 | # UploadNote: Instead of part of the build process here, uploading happen as part 22 | # of create_stack or execute_change_set after user confirms action. IE: 23 | # Cfn::Deploy#create and Cfn::Deploy#update 24 | end 25 | end 26 | 27 | def clean 28 | FileUtils.rm_rf(@output_path) 29 | end 30 | 31 | def validate! 32 | missing = Lono::Files.files.select do |file| 33 | !File.exist?(file.full_path) 34 | end 35 | return if missing.empty? 36 | 37 | logger.info "ERROR: These files are missing".color(:red) 38 | logger.info <<~EOL 39 | The file helper is calling them. 40 | They need to exist to be uploaded to s3. 41 | EOL 42 | missing.each do |file| 43 | logger.info " #{pretty_path(file.full_path)}" 44 | logger.info " Called at #{file.call_line}" 45 | end 46 | logger.info "Please double check that they exist." 47 | exit 1 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/lono/builder/dsl/finalizer/files/replace.rb: -------------------------------------------------------------------------------- 1 | class Lono::Builder::Dsl::Finalizer::Files 2 | class Replace < Base 3 | def run 4 | replacements.each do |placeholder, replacement| 5 | update_template!(@cfn) 6 | end 7 | @cfn 8 | end 9 | 10 | def replacements 11 | data = Lono::Files.files.inject({}) do |result, file| 12 | result.merge(file.marker => file.s3_key) 13 | end 14 | # Edge case when bucket is created for the first time and files_bucket is 15 | # not available yet. So we call it at this point. 16 | Lono::S3::Bucket.ensure_exist 17 | data["LONO://S3_BUCKET"] = Lono::S3::Bucket.name 18 | data 19 | end 20 | 21 | def update_template!(hash) 22 | hash.each do |k, v| 23 | if v.is_a?(String) 24 | if v =~ %r{^LONO://} 25 | v.replace(replacements[v]) # replace the placeholder 26 | end 27 | elsif v.is_a?(Hash) 28 | update_template!(v) # recurse 29 | elsif v.is_a?(Array) 30 | v.each { |x| update_template!(x) if x.is_a?(Hash) } 31 | end 32 | end 33 | hash 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/lono/builder/dsl/finalizer/parameter_groups.rb: -------------------------------------------------------------------------------- 1 | class Lono::Builder::Dsl::Finalizer 2 | class ParameterGroups < Base 3 | extend Memoist 4 | include Lono::Builder::Util::Stringify 5 | 6 | def initialize(options={}) 7 | super 8 | @parameters = options[:parameters] 9 | end 10 | 11 | def run 12 | return @cfn if parameter_groups.empty? 13 | @cfn["Metadata"] ||= {} 14 | @cfn["Metadata"]["AWS::CloudFormation::Interface"] = stringify!(interface) 15 | @cfn 16 | end 17 | 18 | def interface 19 | interface = {} 20 | parameter_groups.each do |group_label, parameters| 21 | group = { 22 | Label: { default: group_label}, 23 | Parameters: parameters, 24 | } 25 | interface[:ParameterGroups] ||= [] 26 | interface[:ParameterGroups] << group 27 | end 28 | parameter_labels.each do |name, label| 29 | l = { name => { default: label } } 30 | interface[:ParameterLabels] ||= {} 31 | interface[:ParameterLabels].merge!(l) 32 | end 33 | interface 34 | end 35 | 36 | def parameter_groups 37 | parameter_groups = {} 38 | @parameters.each do |p| 39 | next unless p.group_label 40 | parameter_groups[p.group_label] ||= [] 41 | parameter_groups[p.group_label] << p.name 42 | end 43 | parameter_groups 44 | end 45 | memoize :parameter_groups 46 | 47 | def parameter_labels 48 | parameter_labels = {} 49 | @parameters.each do |p| 50 | next unless p.label 51 | parameter_labels[p.name] = p.label 52 | end 53 | parameter_labels 54 | end 55 | memoize :parameter_labels 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/lono/builder/dsl/helpers.rb: -------------------------------------------------------------------------------- 1 | class Lono::Builder::Dsl 2 | module Helpers 3 | include_modules("helpers") 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/lono/builder/dsl/helpers/ec2.rb: -------------------------------------------------------------------------------- 1 | module Lono::Builder::Dsl::Helpers 2 | module Ec2 3 | extend Memoist 4 | 5 | # Returns vpc object 6 | def vpc(name) 7 | filters = name == "default" ? 8 | [name: "isDefault", values: ["true"]] : 9 | [name: "tag:Name", values: [name]] 10 | resp = ec2.describe_vpcs(filters: filters) 11 | resp.vpcs.first 12 | end 13 | memoize :vpc 14 | 15 | def default_vpc 16 | vpc = vpc("default") 17 | vpc ? vpc.vpc_id : "no default vpc found" 18 | end 19 | 20 | def default_vpc_cidr 21 | vpc = vpc("default") 22 | vpc.cidr_block 23 | end 24 | 25 | def default_subnets 26 | return "no default subnets because no default vpc found" if default_vpc == "no default vpc found" 27 | resp = ec2.describe_subnets(filters: [name: "vpc-id", values: [default_vpc]]) 28 | subnets = resp.subnets 29 | subnets.map(&:subnet_id) 30 | end 31 | memoize :default_subnets 32 | 33 | def key_pairs(regexp=nil) 34 | resp = ec2.describe_key_pairs 35 | key_names = resp.key_pairs.map(&:key_name) 36 | key_names.select! { |k| k =~ regexp } if regexp 37 | key_names 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/lono/builder/dsl/helpers/files.rb: -------------------------------------------------------------------------------- 1 | module Lono::Builder::Dsl::Helpers 2 | module Files 3 | def files(path, o={}) 4 | Lono::Files.register(@options.merge(path: path).merge(o)) 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/lono/builder/dsl/helpers/s3.rb: -------------------------------------------------------------------------------- 1 | module Lono::Builder::Dsl::Helpers 2 | module S3 3 | def s3_bucket 4 | Lono::S3::Bucket.name 5 | end 6 | alias_method :lono_bucket, :s3_bucket 7 | 8 | # Edge case when bucket is created for the first time and files_bucket is 9 | # not available yet to be use by Finalizer::Files::Replace#replacements 10 | def files_bucket 11 | "LONO://S3_BUCKET" 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/lono/builder/dsl/helpers/ssm.rb: -------------------------------------------------------------------------------- 1 | module Lono::Builder::Dsl::Helpers 2 | module Ssm 3 | extend Memoist 4 | 5 | def ssm(name) 6 | ssm_fetcher.get(name) 7 | end 8 | 9 | def ssm_fetcher 10 | Fetcher.new 11 | end 12 | memoize :ssm_fetcher 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/lono/builder/dsl/helpers/ssm/fetcher.rb: -------------------------------------------------------------------------------- 1 | require 'aws-sdk-ssm' 2 | 3 | module Lono::Builder::Dsl::Helpers::Ssm 4 | class Fetcher 5 | extend Memoist 6 | include Lono::Utils::Logging 7 | 8 | def get(name) 9 | fetch_ssm_value(name) 10 | end 11 | 12 | def fetch_ssm_value(name) 13 | resp = ssm.get_parameter(name: name, with_decryption: true) 14 | resp.parameter.value 15 | rescue Aws::SSM::Errors::ParameterNotFound 16 | logger.warn 'WARN: SSM-PARAM-NOT-FOUND' 17 | nil 18 | end 19 | 20 | def ssm 21 | Aws::SSM::Client.new 22 | end 23 | memoize :ssm 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/lono/builder/dsl/helpers/stack.rb: -------------------------------------------------------------------------------- 1 | module Lono::Builder::Dsl::Helpers 2 | module Stack 3 | extend Memoist 4 | include Lono::AwsServices 5 | 6 | def stack_output(name) 7 | stack_name, key = name.split(".") 8 | stack_name = stack_name(stack_name) 9 | resp = describe_stacks(stack_name: stack_name) 10 | stack = resp.stacks.first 11 | if stack 12 | o = stack.outputs.detect { |h| h.output_key == key } 13 | end 14 | 15 | if o 16 | o.output_value 17 | else 18 | logger.info "WARN: STACK OUTPUT NOT FOUND: output #{key} for stack #{stack_name}" 19 | nil 20 | end 21 | end 22 | 23 | def stack_resource(name) 24 | stack_name, logical_id = name.split(".") 25 | stack_name = stack_name(stack_name) 26 | resp = describe_stack_resources(stack_name: stack_name) 27 | resources = resp.stack_resources 28 | resource = resources.find { |r| r.logical_resource_id == logical_id } 29 | if resource 30 | resource.physical_resource_id 31 | else 32 | logger.info "WARN: NOT FOUND: logical_id #{logical_id} for stack #{stack_name}" 33 | nil 34 | end 35 | end 36 | 37 | def stack_name(blueprint) 38 | names = Lono::Names.new(blueprint: blueprint) 39 | pattern = blueprint # blueprint can be a pattern provided by user. IE: :BLUEPRINT-:ENV 40 | names.expansion(pattern) 41 | end 42 | 43 | private 44 | def describe_stacks(options={}) 45 | cfn.describe_stacks(options) 46 | end 47 | memoize :describe_stacks 48 | 49 | def describe_stack_resources(options={}) 50 | cfn.describe_stack_resources(options) 51 | end 52 | memoize :describe_stack_resources 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/lono/builder/dsl/helpers/tags.rb: -------------------------------------------------------------------------------- 1 | module Lono::Builder::Dsl::Helpers 2 | module Tags 3 | def tags(hash={}) 4 | if hash.empty? 5 | tag_list(@tags) if @tags # when hash is empty, use @tags variable. If not set then return nil 6 | else 7 | tag_list(hash) 8 | end 9 | end 10 | 11 | def tag_list(hash) 12 | raise "tags hash cannot be empty" if hash == nil 13 | 14 | if hash.is_a?(Array) 15 | hash = hash.inject({}) do |h,i| 16 | i.symbolize_keys! 17 | h[i[:Key]] = i[:Value] 18 | h 19 | end 20 | return tag_list(hash) # recursive call tag_list to normalized the argument with a Hash 21 | end 22 | 23 | propagate = hash[:PropagateAtLaunch] # special treatment 24 | list = hash.map do |k,v| 25 | h = {Key: k.to_s, Value: v} 26 | h[:PropagateAtLaunch] = propagate unless propagate.nil? 27 | h 28 | end 29 | list.reject { |h| h[:Key] == "PropagateAtLaunch" } 30 | end 31 | 32 | def dimensions(hash) 33 | tag_list(hash).map { |h| 34 | h[:Name] = h.delete(:Key) || h.delete(:key) 35 | h 36 | } 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/lono/builder/dsl/syntax.rb: -------------------------------------------------------------------------------- 1 | # Encapsulates syntax methods so they can be included in both the Evaluator and Context scope 2 | class Lono::Builder::Dsl 3 | module Syntax 4 | include Fn 5 | include Core 6 | include ParameterGroup 7 | include Helpers 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/lono/builder/dsl/syntax/core/base.rb: -------------------------------------------------------------------------------- 1 | module Lono::Builder::Dsl::Syntax::Core 2 | class Base 3 | include Lono::Builder::Util::Stringify 4 | include Lono::Builder::Dsl::Syntax::Fn 5 | 6 | def initialize(blueprint, *definition) 7 | @blueprint = blueprint 8 | @definition = definition.flatten 9 | end 10 | 11 | private 12 | def camelize(attributes) 13 | data = stringify!(attributes) 14 | clean(data) 15 | end 16 | 17 | # Remove items with nil value automatically 18 | def clean(data) 19 | Squeezer.new(data).squeeze 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/lono/builder/dsl/syntax/core/condition.rb: -------------------------------------------------------------------------------- 1 | # Implements: 2 | # 3 | # template - uses @definition to build a CloudFormation template section 4 | # 5 | module Lono::Builder::Dsl::Syntax::Core 6 | class Condition < Base 7 | def template 8 | camelize(standarize(@definition)) 9 | end 10 | 11 | # There are only 2 forms for condition: long and medium 12 | # 13 | # Type is the only required property: https://amzn.to/2x8W5aD 14 | def standarize(definition) 15 | first, second, _ = definition 16 | if definition.size == 1 && first.is_a?(Hash) # long form 17 | first # pass through 18 | elsif definition.size == 2 && second.is_a?(Hash) # medium form 19 | logical_id, properties = first, second 20 | { logical_id => properties } 21 | else # I dont know what form 22 | raise "Invalid form provided. definition #{definition.inspect}" 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/lono/builder/dsl/syntax/core/mapping.rb: -------------------------------------------------------------------------------- 1 | # Implements: 2 | # 3 | # template - uses @definition to build a CloudFormation template section 4 | # 5 | module Lono::Builder::Dsl::Syntax::Core 6 | class Mapping < Base 7 | def template 8 | camelize(standarize(@definition)) 9 | end 10 | 11 | # Type is the only required property: https://amzn.to/2x8W5aD 12 | def standarize(definition) 13 | first, second, _ = definition 14 | if definition.size == 1 && first.is_a?(Hash) # long form 15 | first # pass through 16 | elsif definition.size == 2 && second.is_a?(Hash) # medium form 17 | logical_id, maps = first, second 18 | { logical_id => maps } 19 | else # I dont know what form 20 | raise "Invalid form provided. definition #{definition.inspect}" 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/lono/builder/dsl/syntax/core/output.rb: -------------------------------------------------------------------------------- 1 | # Implements: 2 | # 3 | # template - uses @definition to build a CloudFormation template section 4 | # 5 | module Lono::Builder::Dsl::Syntax::Core 6 | class Output < Base 7 | def template 8 | camelize(standarize(@definition)) 9 | end 10 | 11 | # Value is the only required property: https://amzn.to/2xbhmk3 12 | def standarize(definition) 13 | first, second, _ = definition 14 | if definition.size == 1 && first.is_a?(Hash) # long form 15 | first # pass through 16 | elsif definition.size == 2 && second.is_a?(Hash) # medium form 17 | if second.key?(:value) || second.key?("Value") 18 | logical_id, properties = first, second 19 | else 20 | logical_id = first 21 | properties = {Value: second} 22 | end 23 | { logical_id => properties } 24 | elsif definition.size == 2 && (second.is_a?(String) || second.nil?) # short form 25 | logical_id = first 26 | properties = second.is_a?(String) ? { Value: second } : {} 27 | { logical_id => properties } 28 | elsif definition.size == 1 29 | logical_id = first.to_s 30 | properties = { Value: ref(logical_id) } 31 | { logical_id => properties } 32 | else # I dont know what form 33 | raise "Invalid form provided. definition #{definition.inspect}" 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/lono/builder/dsl/syntax/core/resource.rb: -------------------------------------------------------------------------------- 1 | # Implements: 2 | # 3 | # template - uses @definition to build a CloudFormation template section 4 | # 5 | module Lono::Builder::Dsl::Syntax::Core 6 | class Resource < Base 7 | def template 8 | camelize(standarize(@definition)) 9 | end 10 | 11 | # Type is the only required property: https://amzn.to/2x8W5aD 12 | def standarize(definition) 13 | first, second, third, _ = definition 14 | if definition.size == 1 && first.is_a?(Hash) # long form 15 | first # pass through 16 | elsif definition.size == 2 && second.is_a?(Hash) # medium form 17 | logical_id, attributes = first, second 18 | attributes.delete(:properties) if attributes[:properties].blank? 19 | attributes.delete("Properties") if attributes["Properties"].blank? 20 | { logical_id => attributes } 21 | elsif definition.size == 2 && second.is_a?(String) # short form with no properties 22 | logical_id, type = first, second 23 | { logical_id => { 24 | Type: type 25 | }} 26 | elsif definition.size == 3 && (second.is_a?(String) || second.is_a?(NilClass)) # short form 27 | logical_id, type, properties = first, second, third 28 | resource = { logical_id => { 29 | Type: type 30 | }} 31 | 32 | attributes = resource.values.first 33 | 34 | property_mover = PropertyMover.new(resource, logical_id, properties) 35 | property_mover.move! 36 | 37 | attributes["Properties"] = properties unless properties.empty? 38 | resource 39 | else # Dont understand this form 40 | raise "Invalid form provided. definition #{definition.inspect}" 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/lono/builder/dsl/syntax/core/resource/property_mover.rb: -------------------------------------------------------------------------------- 1 | class Lono::Builder::Dsl::Syntax::Core::Resource 2 | # Moves the property to the top-level attributes *destructively* 3 | class PropertyMover 4 | def initialize(resource, logical_id, properties) 5 | @resource, @logical_id, @properties = resource, logical_id, properties 6 | end 7 | 8 | # AWS Docs: Resource attribute reference 9 | # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-product-attribute-reference.html 10 | def move! 11 | %w[ 12 | Condition 13 | CreationPolicy 14 | DeletionPolicy 15 | DependsOn 16 | Metadata 17 | UpdatePolicy 18 | UpdateReplacePolicy 19 | ].each do |attribute_name| 20 | # Account for camelize, underscore, String, and Symbol 21 | move(attribute_name.to_sym) 22 | move(attribute_name.camelize.to_sym) 23 | move(attribute_name) 24 | move(attribute_name.camelize) 25 | end 26 | end 27 | 28 | def move(attribute_name) 29 | attribute_value = @properties.delete(attribute_name) 30 | @resource[@logical_id][attribute_name] = attribute_value if attribute_value 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/lono/builder/dsl/syntax/core/section.rb: -------------------------------------------------------------------------------- 1 | # Implements: 2 | # 3 | # template - uses @definition to build a CloudFormation template section 4 | # 5 | module Lono::Builder::Dsl::Syntax::Core 6 | class Section < Base 7 | def template 8 | hash, _ = @definition 9 | camelize(hash) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/lono/builder/dsl/syntax/core/squeezer.rb: -------------------------------------------------------------------------------- 1 | module Lono::Builder::Dsl::Syntax::Core 2 | class Squeezer 3 | def initialize(data) 4 | @data = data 5 | end 6 | 7 | def squeeze(new_data=nil, previous_key=nil) 8 | data = new_data.nil? ? @data : new_data 9 | 10 | case data 11 | when Array 12 | # .compact prevents infinite loop when data = [nil] on accident 13 | # IE: data[:key] = [nil] 14 | data.compact.map! { |v| squeeze(v) } 15 | when Hash 16 | data.each_with_object({}) do |(k,v), squeezed| 17 | # only remove nil and empty Array values within Hash structures 18 | squeezed[k] = squeeze(v, k) unless v.nil? # || empty_array?(v, previous_key) 19 | squeezed 20 | end 21 | else 22 | data # do not transform 23 | end 24 | end 25 | 26 | # Special case do not squeeze empty Array when previous_key is "yum". Handles: 27 | # 28 | # Metadata: 29 | # AWS::CloudFormation::Init: 30 | # config: 31 | # packages: 32 | # yum: 33 | # httpd: [] 34 | # 35 | def empty_array?(v, previous_key) 36 | return false if previous_key.to_s == "yum" 37 | v.is_a?(Array) && v.empty? 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/lono/builder/dsl/syntax/parameter_group.rb: -------------------------------------------------------------------------------- 1 | module Lono::Builder::Dsl::Syntax 2 | module ParameterGroup 3 | def parameter_group(label) 4 | @group_label = label 5 | yield 6 | @group_label = nil 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/lono/builder/template.rb: -------------------------------------------------------------------------------- 1 | class Lono::Builder 2 | class Template < Lono::CLI::Base 3 | extend Memoist 4 | 5 | def run 6 | check_blueprint_exist! 7 | data = dsl.run # The generator strategy classes write templates to disk 8 | write_output(data) 9 | end 10 | 11 | private 12 | def check_blueprint_exist! 13 | return if @blueprint.exist? 14 | logger.error "ERROR: Blueprint #{@blueprint.name} not found.".color(:red) 15 | logger.error <<~EOL 16 | Searched: 17 | 18 | app/blueprints 19 | vendor/blueprints 20 | 21 | Are you sure it exists? To check, you can run: 22 | 23 | lono list 24 | 25 | EOL 26 | exit 1 27 | end 28 | 29 | def write_output(data) 30 | path = @blueprint.output_path 31 | ensure_parent_dir(path) 32 | text = YAML.dump(data) 33 | IO.write(path, text) 34 | 35 | Lono::Yamler::Validator.new(path).validate! 36 | 37 | unless @options[:quiet] 38 | pretty_path = path.sub("#{Lono.root}/",'') 39 | logger.info " #{pretty_path}" 40 | end 41 | end 42 | 43 | def ensure_parent_dir(path) 44 | dir = File.dirname(path) 45 | FileUtils.mkdir_p(dir) unless File.exist?(dir) 46 | end 47 | 48 | def dsl 49 | Dsl.new(@options) 50 | end 51 | memoize :dsl 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/lono/builder/template/aws_service.rb: -------------------------------------------------------------------------------- 1 | require "aws-sdk-s3" 2 | 3 | class Lono::Builder::Template::AwsService 4 | def s3 5 | return @s3 if @s3 6 | 7 | options = {} 8 | endpoint = ENV['S3_ENDPOINT'] if ENV['S3_ENDPOINT'] 9 | options[:endpoint] = endpoint if endpoint 10 | if options[:endpoint] 11 | options[:region] = options[:endpoint].split('.')[1] 12 | end 13 | @s3 = Aws::S3::Client.new(options) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/lono/builder/template/bashify.rb: -------------------------------------------------------------------------------- 1 | require 'open-uri' 2 | 3 | class Lono::Builder::Template 4 | class Bashify 5 | include Lono::Utils::Logging 6 | 7 | def initialize(options={}) 8 | @options = options 9 | @path = options[:path] 10 | end 11 | 12 | def user_data_paths(data,path="") 13 | paths = [] 14 | paths << path 15 | data.each do |key,value| 16 | if value.is_a?(Hash) 17 | paths += user_data_paths(value,"#{path}/#{key}") 18 | else 19 | paths += ["#{path}/#{key}"] 20 | end 21 | end 22 | paths.select {|p| p =~ /UserData/ && p =~ /Fn::Join/ } 23 | end 24 | 25 | def run 26 | raw = open(@path).read 27 | json = JSON.load(raw) 28 | paths = user_data_paths(json) 29 | if paths.empty? 30 | logger.info "No UserData script found" 31 | return 32 | end 33 | paths.each do |path| 34 | logger.info "UserData script for #{path}:" 35 | key = path.sub('/','').split("/").map {|x| "['#{x}']"}.join('') 36 | user_data = eval("json#{key}") 37 | delimiter = user_data[0] 38 | script = user_data[1] 39 | logger.info script.join(delimiter) 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/lono/builder/template/upload.rb: -------------------------------------------------------------------------------- 1 | class Lono::Builder::Template 2 | class Upload < Lono::CLI::Base 3 | def run 4 | path = "#{Lono.root}/output/#{@blueprint.name}/template.yml" 5 | Lono::S3::Uploader.new(path).upload 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/lono/builder/util/stringify.rb: -------------------------------------------------------------------------------- 1 | module Lono::Builder::Util 2 | module Stringify 3 | # Accounts for Arrays also. ActiveSupport only works for Hashes. 4 | def stringify!(data) 5 | case data 6 | when Array 7 | data.map! { |i| stringify!(i) } 8 | when Hash 9 | data.deep_transform_keys! { |k| k.to_s } 10 | else 11 | data # do not transform 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/lono/bundler/cli.rb: -------------------------------------------------------------------------------- 1 | module Lono::Bundler 2 | class CLI < Lono::Command 3 | desc "bundle SUBCOMMAND", "bundle subcommands" 4 | long_desc Help.text(:bundle) 5 | subcommand "bundle", Bundle 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/lono/bundler/cli/base.rb: -------------------------------------------------------------------------------- 1 | class Lono::Bundler::CLI 2 | class Base 3 | include LB::Util::Logging 4 | include LB::Util::Sure 5 | 6 | def initialize(options={}) 7 | @options = options 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/lono/bundler/cli/bundle.rb: -------------------------------------------------------------------------------- 1 | class Lono::Bundler::CLI 2 | class Bundle < Lono::Command 3 | lonofile_option = Proc.new { 4 | option :lonofile, default: ENV['LONOFILE'] || "Lonofile", desc: "Lonofile to use" 5 | } 6 | 7 | desc "list", "List bundled blueprints included by Lonofile" 8 | long_desc Help.text("bundle/list") 9 | lonofile_option.call 10 | def list 11 | Lono::Bundler::List.new(options).run 12 | end 13 | 14 | desc "info COMPONENT", "Provide info about a bundled component" 15 | long_desc Help.text("bundle/info") 16 | lonofile_option.call 17 | option :type, aliases: :t, desc: "Type. IE: blueprint, configset, extension" 18 | def info(component) 19 | Lono::Bundler::Info.new(options.merge(component: component)).run 20 | end 21 | 22 | desc "install", "Install blueprints from the Lonofile" 23 | long_desc Help.text("bundle/install") 24 | lonofile_option.call 25 | def install 26 | Lono::Bundler::Runner.new(options).run 27 | end 28 | 29 | desc "clean", "Clean cache" 30 | long_desc Help.text("bundle/clean") 31 | option :yes, aliases: :y, type: :boolean, desc: "bypass are you sure prompt" 32 | def clean 33 | Clean.new(options).run 34 | end 35 | 36 | desc "update [COMPONENT]", "Update bundled blueprints" 37 | long_desc Help.text("bundle/update") 38 | lonofile_option.call 39 | def update(*components) 40 | Lono::Bundler.update_mode = true 41 | Lono::Bundler::Runner.new(options.merge(components: components)).run 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/lono/bundler/cli/clean.rb: -------------------------------------------------------------------------------- 1 | class Lono::Bundler::CLI 2 | class Clean < Base 3 | include LB::Component::Concerns::PathConcern 4 | 5 | def run 6 | paths = [tmp_root] 7 | are_you_sure?(paths) 8 | paths.each do |path| 9 | FileUtils.rm_rf(path) 10 | logger.info "Removed #{path}" 11 | end 12 | 13 | end 14 | 15 | def are_you_sure?(paths) 16 | pretty_paths = paths.map { |p| " #{p}" }.join("\n") 17 | message = <<~EOL.chomp 18 | Will remove these folders and all their files: 19 | 20 | #{pretty_paths} 21 | 22 | Are you sure? 23 | EOL 24 | sure?(message) # from Util::Sure 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/lono/bundler/cli/help.rb: -------------------------------------------------------------------------------- 1 | class Lono::Bundler::CLI 2 | module Help 3 | class << self 4 | def text(namespaced_command) 5 | path = namespaced_command.to_s.gsub(':','/') 6 | path = File.expand_path("../help/#{path}.md", __FILE__) 7 | IO.read(path) if File.exist?(path) 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/lono/bundler/cli/help/bundle.md: -------------------------------------------------------------------------------- 1 | ## Examples 2 | 3 | lono bundle 4 | -------------------------------------------------------------------------------- /lib/lono/bundler/component.rb: -------------------------------------------------------------------------------- 1 | module Lono::Bundler 2 | class Component 3 | extend Memoist 4 | extend Props::Extension 5 | props :export_to, :name, :sha, :source, :subfolder, :source_type, :type, :url, :clone_with 6 | delegate :repo, :org, :repo_folder, :org_folder, to: :org_repo 7 | 8 | include Concerns::StackConcern 9 | include Concerns::LocalConcern 10 | 11 | attr_reader :props, :version, :ref, :tag, :branch 12 | def initialize(options={}) 13 | @props = Component::Props.new(options.deep_symbolize_keys).build 14 | # These props are used for version comparing by VersionComparer 15 | @version, @ref, @tag, @branch = @props[:version], @props[:ref], @props[:tag], @props[:branch] 16 | end 17 | 18 | # support variety of options, prefer version 19 | def checkout_version 20 | @version || @ref || @tag || @branch 21 | end 22 | 23 | def latest_sha 24 | fetcher = Fetcher.new(self).instance 25 | fetcher.run 26 | fetcher.sha 27 | end 28 | 29 | def vcs_provider 30 | if url.include?('http') 31 | # "https://github.com/org/repo" => github.com 32 | url.match(%r{http[s]?://(.*?)/})[1] 33 | elsif url.include?('http') # git@ 34 | # "git@github.com:org/repo" => github.com 35 | url.match(%r{git@(.*?):})[1] 36 | else # ssh://user@domain.com/path/to/repo 37 | 'none' 38 | end 39 | end 40 | 41 | def export_path 42 | export_to = self.export_to || LB.config.export_to 43 | "#{export_to}/#{type.pluralize}/#{name}" 44 | end 45 | 46 | private 47 | def org_repo 48 | OrgRepo.new(url) 49 | end 50 | memoize :org_repo 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/lono/bundler/component/concerns/local_concern.rb: -------------------------------------------------------------------------------- 1 | module Lono::Bundler::Component::Concerns 2 | module LocalConcern 3 | def local? 4 | source.starts_with?("/") || 5 | source.starts_with?(".") || 6 | source.starts_with?("..") || 7 | source.starts_with?("~") 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/lono/bundler/component/concerns/notation_concern.rb: -------------------------------------------------------------------------------- 1 | require 'uri' 2 | 3 | module Lono::Bundler::Component::Concerns 4 | module NotationConcern 5 | def remove_notations(source) 6 | remove_subfolder_notation(remove_ref_notation(source)) 7 | end 8 | 9 | def remove_ref_notation(source) 10 | source.sub(/\?.*/,'') 11 | end 12 | 13 | def remove_subfolder_notation(source) 14 | clean = clean_notation(source) 15 | parts = clean.split('//') 16 | if parts.size == 2 # has subfolder 17 | source.split('//')[0..-2].join('//') # remove only subfolder, keep rest of original source 18 | else 19 | source 20 | end 21 | end 22 | 23 | def subfolder(source) 24 | clean = clean_notation(source) 25 | parts = clean.split('//') 26 | if parts.size == 2 # has subfolder 27 | remove_ref_notation(parts.last) 28 | end 29 | end 30 | 31 | def ref(source) 32 | url = clean_notation(source) 33 | uri = URI(url) 34 | if uri.query 35 | params = URI::decode_www_form(uri.query).to_h # if you are in 2.1 or later version of Ruby 36 | params['ref'] 37 | end 38 | end 39 | 40 | 41 | def clean_notation(source) 42 | clean = source 43 | .sub(/.*::/,'') # remove git:: 44 | .sub(%r{http[s?]://},'') # remove https:// 45 | .sub(%r{git@(.*?):},'') # remove git@word 46 | remove_host(clean) 47 | end 48 | 49 | def remove_host(clean) 50 | return clean unless clean.include?('ssh://') 51 | if clean.count(':') == 1 52 | uri = URI(clean) 53 | uri.path 54 | else 55 | clean.split(':').last 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/lono/bundler/component/fetcher.rb: -------------------------------------------------------------------------------- 1 | # Delegates to: 2 | # 3 | # 1. Local 4 | # 2. Git 5 | # 6 | class Lono::Bundler::Component 7 | class Fetcher 8 | def initialize(component) 9 | @component = component 10 | end 11 | 12 | def instance 13 | type = @component.source_type == "registry" ? "git" : @component.source_type 14 | klass = "Lono::Bundler::Component::Fetcher::#{type.camelize}".constantize 15 | klass.new(@component) # IE: Local.new(@component), Git.new(@component), S3.new(@component), etc 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/lono/bundler/component/fetcher/base.rb: -------------------------------------------------------------------------------- 1 | # Interface of subclasses should implement 2 | # 3 | # run 4 | # switch_version(component.sha) 5 | # sha 6 | # 7 | class Lono::Bundler::Component::Fetcher 8 | class Base 9 | include LB::Util::Git 10 | include LB::Util::Logging 11 | include LB::Component::Concerns::PathConcern 12 | 13 | attr_reader :sha # returns nil for Local 14 | def initialize(component) 15 | @component = component 16 | end 17 | 18 | def switch_version(*) 19 | # noop 20 | end 21 | 22 | def extract(archive, dest) 23 | Lono::Bundler::Extract.extract(archive, dest) 24 | end 25 | 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/lono/bundler/component/fetcher/local.rb: -------------------------------------------------------------------------------- 1 | class Lono::Bundler::Component::Fetcher 2 | class Local < Base 3 | def run 4 | stage_path = stage_path(rel_dest_dir) 5 | source = @component.source 6 | src = source.sub(/^~/, ENV['HOME']) # allow ~/ notation 7 | FileUtils.rm_rf(stage_path) 8 | FileUtils.mkdir_p(File.dirname(stage_path)) 9 | logger.debug "Local: cp -r #{src} #{stage_path}" 10 | # copy from stage area to vendor/blueprints/NAME 11 | # IE: cp -r /tmp/lono/bundler/stage/local/local1 vendor/blueprints/local1 12 | FileUtils.cp_r(src, stage_path) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/lono/bundler/component/http/concern.rb: -------------------------------------------------------------------------------- 1 | require 'net/http' 2 | require 'open-uri' 3 | 4 | module Lono::Bundler::Component::Http 5 | module Concern 6 | include LB::Util::Logging 7 | 8 | def http_request(url, auth_domain: nil) 9 | uri = URI(url) 10 | http = Net::HTTP.new(uri.host, uri.port) 11 | http.use_ssl = uri.scheme == "https" 12 | # Total time will be 40s = 20 x 2 13 | http.max_retries = 1 # Default is already 1, just being explicit 14 | http.read_timeout = 20 # Sites that dont return in 20 seconds are considered down 15 | request = Net::HTTP::Get.new(uri) 16 | 17 | if auth_domain 18 | path = "#{ENV['HOME']}/.terraform.d/credentials.tfrc.json" 19 | if File.exist?(path) 20 | data = JSON.load(IO.read(path)) 21 | token = data['credentials'][auth_domain]['token'] 22 | request.add_field 'Authorization', "Bearer #{token}" 23 | else 24 | auth_error_exit! 25 | end 26 | end 27 | 28 | begin 29 | http.request(request) # response 30 | rescue Net::OpenTimeout => e # internal ELB but VPC is not configured for Lambda function 31 | http_request_error_message(e) 32 | rescue Exception => e 33 | # Net::ReadTimeout - too slow 34 | # Errno::ECONNREFUSED - completely down 35 | # SocketError - improper url "dsfjsl" instead of example.com 36 | http_request_error_message(e) 37 | end 38 | end 39 | 40 | def http_request_error_message(e) 41 | logger.info "ERROR: #{e.message}\n#{e.message}".color(:red) 42 | exit 1 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/lono/bundler/component/http/source.rb: -------------------------------------------------------------------------------- 1 | require 'net/http' 2 | require 'open-uri' 3 | 4 | module Lono::Bundler::Component::Http 5 | class Source 6 | include LB::Util::Logging 7 | include Concern 8 | 9 | def initialize(params) 10 | @params = params 11 | @options = params[:options] 12 | @source = @options[:source] 13 | end 14 | 15 | def url 16 | @source 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/lono/bundler/component/props/extension.rb: -------------------------------------------------------------------------------- 1 | class Lono::Bundler::Component::Props 2 | module Extension 3 | def props(*names) 4 | names.each { |n| prop(n) } 5 | end 6 | 7 | def prop(name) 8 | name = name.to_sym 9 | define_method(name) do 10 | @props[name] 11 | end 12 | 13 | define_method("#{name}=") do |v| 14 | @props[name] = v 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/lono/bundler/component/props/typer.rb: -------------------------------------------------------------------------------- 1 | class Lono::Bundler::Component::Props 2 | class Typer 3 | include Lono::Bundler::Component::Concerns::LocalConcern 4 | include Lono::Bundler::Component::Concerns::NotationConcern 5 | 6 | delegate :source, to: :props 7 | 8 | attr_reader :props 9 | def initialize(props) 10 | @props = props # Props.new object 11 | end 12 | 13 | # IE: git or registry 14 | def source_type 15 | if source.include?('ssh://') 16 | "git" 17 | elsif source.include?('::') 18 | source.split('::').first # IE: git:: s3:: gcs:: 19 | elsif local? 20 | "local" 21 | elsif registry? 22 | "registry" 23 | else 24 | "git" 25 | end 26 | end 27 | 28 | private 29 | # dont use registry? externally. instead use type since it can miss local detection 30 | def registry? 31 | if source.nil? || 32 | source.starts_with?('git@') || # git@github.com:tongueroo/pet 33 | source.starts_with?('http') || # https://github.com/tongueroo/pet 34 | source.include?('::') # git::https:://git.example.com/pet 35 | return false 36 | end 37 | s = remove_notations(@props.source) 38 | s.split('/').size == 3 || s.split('/').size == 4 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/lono/bundler/config.rb: -------------------------------------------------------------------------------- 1 | module Lono::Bundler 2 | class Config 3 | extend Memoist 4 | include Singleton 5 | 6 | def config 7 | config = ActiveSupport::OrderedOptions.new 8 | config.base_clone_url = "https://github.com/" 9 | config.export_to = ENV['LB_EXPORT_TO'] || "vendor" 10 | config.export_purge = ENV['LB_EXPORT_PRUNE'] == '0' ? false : true 11 | config.lonofile = ENV['LB_LONOFILE'] || "Lonofile" 12 | config.lockfile = "#{config.lonofile}.lock" 13 | config.logger = Lono.config.logger 14 | config 15 | end 16 | memoize :config 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/lono/bundler/dsl.rb: -------------------------------------------------------------------------------- 1 | module Lono::Bundler 2 | class Dsl 3 | include DslEvaluator 4 | include Syntax 5 | 6 | class_attribute :meta, default: {global: {}, components: []} 7 | 8 | def run 9 | evaluate_file(LB.config.lonofile) 10 | self 11 | end 12 | 13 | def meta 14 | self.class.meta 15 | end 16 | 17 | def global 18 | meta[:global] 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/lono/bundler/dsl/syntax.rb: -------------------------------------------------------------------------------- 1 | class Lono::Bundler::Dsl 2 | module Syntax 3 | def org(value) 4 | config.org = value 5 | end 6 | alias_method :user, :org 7 | 8 | def base_clone_url(value) 9 | config.base_clone_url = value 10 | end 11 | 12 | def export_to(path) 13 | config.export_to = path 14 | end 15 | 16 | def export_purge(value) 17 | config.export_purge = value 18 | end 19 | 20 | def stack_options(value={}) 21 | config.stack_options.merge!(value) 22 | end 23 | 24 | def clone_with(value) 25 | config.clone_with = value 26 | end 27 | 28 | def config 29 | LB.config 30 | end 31 | 32 | def component(name, options={}) 33 | meta[:components] << options.merge(name: name) 34 | end 35 | 36 | def self.friendly_method(meth) 37 | define_method meth do |name, options={}| 38 | component(name, options.merge(type: meth.to_s)) 39 | end 40 | end 41 | 42 | friendly_method :blueprint 43 | friendly_method :configset 44 | friendly_method :extension 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/lono/bundler/exporter.rb: -------------------------------------------------------------------------------- 1 | module Lono::Bundler 2 | class Exporter 3 | include LB::Component::Concerns::PathConcern 4 | include LB::Util::Logging 5 | 6 | def initialize(options={}) 7 | @options = options 8 | end 9 | 10 | def run 11 | purge_all 12 | components.each do |component| 13 | logger.info "Exporting #{component.export_path}" 14 | purge(component) 15 | export(component) 16 | end 17 | end 18 | 19 | def components 20 | components = lockfile.components 21 | if LB.update_mode? && !@options[:components].empty? 22 | components.select! { |component| @options[:components].include?(component.name) } 23 | end 24 | components 25 | end 26 | 27 | def export(component) 28 | fetcher = Component::Fetcher.new(component).instance 29 | fetcher.run 30 | fetcher.switch_version(component.sha) 31 | copy = Copy.new(component) 32 | copy.component 33 | end 34 | 35 | private 36 | def purge_all 37 | return if LB.update_mode? 38 | return unless LB.config.export_purge 39 | FileUtils.rm_rf(LB.config.export_to) 40 | end 41 | 42 | def purge(component) 43 | FileUtils.rm_rf(component.export_path) 44 | end 45 | 46 | def lockfile 47 | Lockfile.instance 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/lono/bundler/exporter/base.rb: -------------------------------------------------------------------------------- 1 | class Lono::Bundler::Exporter 2 | class Base 3 | include LB::Component::Concerns::PathConcern 4 | include LB::Util::Logging 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/lono/bundler/exporter/copy.rb: -------------------------------------------------------------------------------- 1 | class Lono::Bundler::Exporter 2 | class Copy < Base 3 | def initialize(component) 4 | @component = component 5 | end 6 | 7 | def component 8 | FileUtils.rm_rf(@component.export_path) 9 | FileUtils.mkdir_p(File.dirname(@component.export_path)) 10 | logger.debug "Copy: cp -r #{src_path} #{@component.export_path}" 11 | FileUtils.cp_r(src_path, @component.export_path) 12 | FileUtils.rm_rf("#{@component.export_path}/.git") 13 | end 14 | 15 | # src path is from the stage area 16 | def src_path 17 | path = stage_path(rel_dest_dir) 18 | path = "#{path}/#{@component.subfolder}" if @component.subfolder 19 | path 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/lono/bundler/extract/tar.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems/package' 2 | require 'zlib' 3 | require 'fileutils' 4 | 5 | # Thanks: https://gist.github.com/ForeverZer0/2adbba36fd452738e7cca6a63aee2f30 6 | class Lono::Bundler::Extract 7 | class Tar 8 | # tar_gz_archive = '/tmp/lono/bundler/stage/s3-us-west-2.amazonaws.com/demo-terraform-test/blueprints/example-module.tgz' 9 | # destination = '/tmp/lono/where/extract/to' 10 | def self.extract(tar_gz_archive, destination) 11 | reader = Zlib::GzipReader.open(tar_gz_archive) 12 | Gem::Package::TarReader.new(reader) do |tar| 13 | dest = nil 14 | tar.each do |entry| 15 | dest ||= File.join destination, entry.full_name 16 | if entry.directory? 17 | FileUtils.rm_rf dest unless File.directory? dest 18 | FileUtils.mkdir_p dest, :mode => entry.header.mode, :verbose => false 19 | elsif entry.file? 20 | FileUtils.rm_rf dest unless File.file? dest 21 | File.open dest, "wb" do |f| 22 | f.print entry.read 23 | end 24 | FileUtils.chmod entry.header.mode, dest, :verbose => false 25 | elsif entry.header.typeflag == '2' #Symlink! 26 | File.symlink entry.header.linkname, dest 27 | end 28 | dest = nil 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/lono/bundler/extract/zip.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'zip' 3 | 4 | class Lono::Bundler::Extract 5 | class Zip 6 | def self.extract(file, dest) 7 | ::Zip::File.open(file) { |zip_file| 8 | zip_file.each { |f| 9 | f_path=File.join(dest, f.name) 10 | FileUtils.mkdir_p(File.dirname(f_path)) 11 | zip_file.extract(f, f_path) unless File.exist?(f_path) 12 | } 13 | } 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/lono/bundler/info.rb: -------------------------------------------------------------------------------- 1 | module Lono::Bundler 2 | class Info < CLI::Base 3 | def run 4 | file = LB.config.lockfile 5 | unless File.exist?(file) 6 | logger.info "No #{file} found".color(:red) 7 | logger.info "Maybe run: lono bundle" 8 | return 9 | end 10 | 11 | name = @options[:component] 12 | all = lockfile.components.find_all { |c| c.name == @options[:component] } 13 | all = all.select { |c| c.type == @options[:type] } if @options[:type] 14 | if all.size > 1 15 | logger.info "Multiple components found with different types" 16 | end 17 | 18 | all.each do |found| 19 | show(found) 20 | end 21 | unless all.size > 0 22 | logger.info "Could not find component in #{LB.config.lockfile}: #{name}".color(:red) 23 | end 24 | end 25 | 26 | def show(component) 27 | props = component.props.reject { |k,v| k == :name }.stringify_keys # for sort 28 | puts "#{component.name}:" 29 | props.keys.sort.each do |k| 30 | v = props[k] 31 | puts " #{k}: #{v}" unless v.blank? 32 | end 33 | end 34 | 35 | def lockfile 36 | Lockfile.instance 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/lono/bundler/list.rb: -------------------------------------------------------------------------------- 1 | module Lono::Bundler 2 | class List < CLI::Base 3 | def run 4 | file = LB.config.lockfile 5 | unless File.exist?(file) 6 | logger.info "No #{file} found".color(:red) 7 | logger.info "Maybe run: lono bundle" 8 | return 9 | end 10 | 11 | logger.info "Components included by #{file}\n\n" 12 | presenter = CliFormat::Presenter.new(@options.merge(format: "table")) 13 | presenter.header = ["Name", "Type"] 14 | lockfile.components.each do |component| 15 | row = [component.name, component.type] 16 | presenter.rows << row 17 | end 18 | presenter.show 19 | logger.info "\nUse `lono bundle info` to print more detailed information about a component" 20 | end 21 | 22 | def lockfile 23 | Lockfile.instance 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/lono/bundler/lockfile/yamler.rb: -------------------------------------------------------------------------------- 1 | require "yaml" 2 | 3 | class Lono::Bundler::Lockfile 4 | class Yamler 5 | def initialize(components) 6 | @components = components.sort_by(&:name) 7 | end 8 | 9 | def dump 10 | YAML.dump(data) 11 | end 12 | 13 | def data 14 | @components.map do |component| 15 | item(component) 16 | end 17 | end 18 | 19 | def item(component) 20 | props = component.props.dup # passthrough: name, url, version, ref, tag, branch etc 21 | props[:sha] ||= component.latest_sha 22 | props.delete_if { |k,v| v.nil? } 23 | props.deep_stringify_keys 24 | end 25 | 26 | def write 27 | IO.write(LB.config.lockfile, dump) 28 | end 29 | 30 | class << self 31 | def write(components) 32 | new(components).write 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/lono/bundler/lonofile.rb: -------------------------------------------------------------------------------- 1 | module Lono::Bundler 2 | class Lonofile 3 | include Singleton 4 | extend Memoist 5 | include LB::Util::Logging 6 | 7 | # dsl meta example: 8 | # {:global=>{{:org=>"boltopspro"}, 9 | # :components=>[{:source=>"git@github.com:boltopspro/vpc", :type=>"blueprint", :name=>"vpc"}] 10 | # } 11 | def components 12 | LB.dsl.meta[:components].map do |options| 13 | Component.new(options) 14 | end 15 | end 16 | memoize :components 17 | 18 | # Checks if any of the components defined in Lonofile has an inferred an org 19 | # In this case the org must be set 20 | # When a source is set with an inferred org and org is not set it looks like this: 21 | # 22 | # dsl.meta has {:source=>"terraform-random-pet"} 23 | # component.source = "terraform-random-pet" 24 | # 25 | # Using info to detect that the org is missing and the Lonofile definition 26 | # has at least one component line that has an inferred org. 27 | # 28 | def missing_org? 29 | components.detect { |component| component.source.split('/').size == 1 } && LB.config.org.nil? 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/lono/bundler/runner.rb: -------------------------------------------------------------------------------- 1 | module Lono::Bundler 2 | class Runner < CLI::Base 3 | def run 4 | Syncer.new(@options).run 5 | Exporter.new(@options).run 6 | # finish_message 7 | end 8 | 9 | def finish_message 10 | no_components_found = true 11 | export_paths.each do |path| 12 | found = Dir.exist?(path) && !Dir.empty?(path) 13 | next unless found 14 | logger.info "components saved to #{path}" 15 | no_components_found = false 16 | end 17 | 18 | logger.info("No components were found.") if no_components_found 19 | end 20 | 21 | def export_paths 22 | export_paths = Lonofile.instance.mods.map(&:export_to).compact.uniq 23 | export_paths << LB.config.export_to 24 | export_paths 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/lono/bundler/util/git.rb: -------------------------------------------------------------------------------- 1 | module Lono::Bundler::Util 2 | module Git 3 | include Logging 4 | include Lono::Utils::Sh 5 | 6 | def git(command) 7 | sh("git #{command}") 8 | rescue LB::GitError => e 9 | action, version = command.split(' ') 10 | if action == "checkout" && version !~ /^v/ 11 | command = "checkout v#{version}" 12 | retry 13 | else 14 | logger.error "ERROR: There was a git error".color(:red) 15 | logger.error "Current dir: #{Dir.pwd}" 16 | logger.error "The error occur when running:" 17 | logger.error e.message 18 | end 19 | exit 1 20 | end 21 | 22 | # Different from Lono::Utils::Sh 23 | def sh(command) 24 | command = "#{command} 2>&1" # always need output for the sha 25 | logger.debug "=> #{command}" 26 | out = `#{command}` 27 | unless $?.success? 28 | if command.include?("git") 29 | raise LB::GitError.new("#{command}\n#{out}") 30 | else 31 | logger.error "ERROR: running #{command}".color(:red) 32 | logger.error out 33 | exit $?.exitstatus 34 | end 35 | end 36 | out 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/lono/bundler/util/logging.rb: -------------------------------------------------------------------------------- 1 | module Lono::Bundler::Util 2 | module Logging 3 | def logger 4 | Lono::Bundler.logger 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/lono/bundler/util/sure.rb: -------------------------------------------------------------------------------- 1 | module Lono::Bundler::Util 2 | module Sure 3 | include Lono::Utils::Sure 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/lono/cfn/base.rb: -------------------------------------------------------------------------------- 1 | module Lono::Cfn 2 | class Base < Lono::CLI::Base 3 | extend Memoist 4 | include Lono::AwsServices 5 | include Lono::Hooks::Concern 6 | include Lono::Utils::Logging 7 | include Lono::Utils::Pretty 8 | include Lono::Utils::Quit 9 | include Lono::Utils::Sure 10 | 11 | include Concerns 12 | 13 | delegate :template_path, to: :build 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/lono/cfn/cancel.rb: -------------------------------------------------------------------------------- 1 | module Lono::Cfn 2 | class Cancel < Base 3 | def run 4 | stack = find_stack(@stack_name) 5 | unless stack 6 | logger.info "The '#{@stack_name}' stack does not exist. Unable to cancel" 7 | quit 1 8 | end 9 | 10 | logger.info "Canceling updates to #{@stack_name}." 11 | logger.info "Current stack status: #{stack.stack_status}" 12 | if stack.stack_status == "CREATE_IN_PROGRESS" 13 | cfn.delete_stack(stack_name: @stack_name) 14 | logger.info "Canceling stack creation." 15 | elsif stack.stack_status == "UPDATE_IN_PROGRESS" 16 | cfn.cancel_update_stack(stack_name: @stack_name) 17 | logger.info "Canceling stack update." 18 | status.wait if @options[:wait] 19 | else 20 | logger.info "The stack is not in a state to that is cancelable: #{stack.stack_status}" 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/lono/cfn/concerns.rb: -------------------------------------------------------------------------------- 1 | module Lono::Cfn 2 | module Concerns 3 | extend Memoist 4 | 5 | include Concerns::Build 6 | include Concerns::TemplateOutput 7 | 8 | def iam 9 | Deploy::Iam.new(@options) 10 | end 11 | memoize :iam 12 | 13 | def notification 14 | Deploy::Notification.new(@options) 15 | end 16 | memoize :notification 17 | 18 | def rollback 19 | Deploy::Rollback.new(@options) 20 | end 21 | memoize :rollback 22 | 23 | def operable 24 | Deploy::Operable.new(@options) 25 | end 26 | memoize :operable 27 | 28 | def plan 29 | Plan.new(@options.merge(build: build, iam: iam)) 30 | end 31 | memoize :plan 32 | 33 | def status 34 | Status.new(@stack) 35 | end 36 | memoize :status 37 | 38 | def tags 39 | Deploy::Tags.new(@options) 40 | end 41 | memoize :tags 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/lono/cfn/concerns/build.rb: -------------------------------------------------------------------------------- 1 | module Lono::Cfn::Concerns 2 | module Build 3 | extend Memoist 4 | 5 | def build 6 | Lono::Builder.new(@options) 7 | end 8 | memoize :build 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/lono/cfn/concerns/template_output.rb: -------------------------------------------------------------------------------- 1 | module Lono::Cfn::Concerns 2 | module TemplateOutput 3 | extend Memoist 4 | 5 | def template_output 6 | Lono::Builder::Template::Output.new(@blueprint) 7 | end 8 | memoize :template_output 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/lono/cfn/delete.rb: -------------------------------------------------------------------------------- 1 | module Lono::Cfn 2 | class Delete < Base 3 | def run 4 | logger.info "Will delete stack #{@stack.color(:green)}" 5 | plan.for_delete 6 | sure?("Are you sure you want to delete the stack?") 7 | run_hooks("down") do 8 | cfn.delete_stack(stack_name: @stack) 9 | end 10 | if @options[:wait] 11 | status.wait 12 | else 13 | logger.info "Deleting stack #{@stack}" 14 | end 15 | end 16 | end 17 | end -------------------------------------------------------------------------------- /lib/lono/cfn/deploy/base.rb: -------------------------------------------------------------------------------- 1 | class Lono::Cfn::Deploy 2 | class Base < Lono::Cfn::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /lib/lono/cfn/deploy/iam.rb: -------------------------------------------------------------------------------- 1 | class Lono::Cfn::Deploy 2 | class Iam < Base 3 | attr_reader :options 4 | 5 | def rerun?(e) 6 | # e.message is "Requires capabilities : [CAPABILITY_IAM]" 7 | # grab CAPABILITY_IAM with regexp 8 | capabilities = e.message.match(/\[(.*)\]/)[1] 9 | confirm = prompt(capabilities) 10 | if confirm =~ /^y/ 11 | logger.info "Re-running: #{command_with_iam(capabilities).color(:green)}" 12 | @retry_capabilities = [capabilities] 13 | true 14 | else 15 | logger.info "Exited" 16 | quit 1 17 | end 18 | end 19 | 20 | def prompt(capabilities) 21 | logger.info <<~EOL 22 | This stack will create IAM resources. Please approve to run the command again with #{capabilities} capabilities. 23 | 24 | #{command_with_iam(capabilities)} 25 | 26 | You can also avoid this prompt by setting config.up.capabilities in config/app.rb 27 | See: https://lono.cloud/docs/config/reference/ 28 | 29 | EOL 30 | logger.print "Please confirm (y/N) " 31 | $stdin.gets 32 | end 33 | 34 | def command_with_iam(capabilities) 35 | "lono #{Lono.argv.join(' ')} --capabilities #{capabilities}" 36 | end 37 | 38 | def capabilities 39 | return @retry_capabilities if @retry_capabilities 40 | return @options[:capabilities] if @options[:capabilities] 41 | if @options[:iam] 42 | ["CAPABILITY_IAM", "CAPABILITY_NAMED_IAM", "CAPABILITY_AUTO_EXPAND"] 43 | else 44 | Lono.config.up.capabilities 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/lono/cfn/deploy/notification.rb: -------------------------------------------------------------------------------- 1 | class Lono::Cfn::Deploy 2 | class Notification < Base 3 | def arns 4 | arns = @options["notification_arns"] || Lono.config.up.notification_arns 5 | arns == [''] ? [] : arns # allow removing the notification_arns setting 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/lono/cfn/deploy/operable.rb: -------------------------------------------------------------------------------- 1 | class Lono::Cfn::Deploy 2 | class Operable < Base 3 | def check! 4 | status = stack_status 5 | unless status =~ /_COMPLETE$/ || status == "UPDATE_ROLLBACK_FAILED" 6 | logger.info "Cannot run operation on stack #{@stack} is not in an updatable state. Stack status: #{status}".color(:red) 7 | quit 1 8 | end 9 | end 10 | 11 | # All CloudFormation states listed here: 12 | # http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-describing-stacks.html 13 | def stack_status 14 | resp = cfn.describe_stacks(stack_name: @stack) 15 | resp.stacks[0].stack_status 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/lono/cfn/deploy/rollback.rb: -------------------------------------------------------------------------------- 1 | class Lono::Cfn::Deploy 2 | class Rollback < Base 3 | def delete_stack 4 | return unless complete? 5 | logger.info "Existing stack in ROLLBACK_COMPLETE state. Deleting stack before continuing." 6 | cfn.delete_stack(stack_name: @stack) 7 | status.wait 8 | status.reset 9 | true 10 | end 11 | 12 | def continue_update 13 | continue_update? 14 | begin 15 | cfn.continue_update_rollback(stack_name: @stack) 16 | rescue Aws::CloudFormation::Errors::ValidationError => e 17 | logger.info "ERROR: Continue update: #{e.message}".color(:red) 18 | quit 1 19 | end 20 | end 21 | 22 | def continue_update? 23 | logger.info <<~EOL 24 | The stack is in the UPDATE_ROLLBACK_FAILED state. More info here: https://amzn.to/2IiEjc5 25 | Would you like to try to continue the update rollback? (y/N) 26 | EOL 27 | 28 | yes = @options[:yes] ? "y" : $stdin.gets 29 | unless yes =~ /^y/ 30 | logger.info "Exiting without continuing the update rollback." 31 | quit 0 32 | end 33 | end 34 | 35 | def complete? 36 | stack&.stack_status == 'ROLLBACK_COMPLETE' 37 | end 38 | 39 | def stack 40 | find_stack(@stack) 41 | end 42 | memoize :stack 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/lono/cfn/deploy/tags.rb: -------------------------------------------------------------------------------- 1 | class Lono::Cfn::Deploy 2 | class Tags < Base 3 | # Maps to CloudFormation format. Example: 4 | # 5 | # {"a"=>"1", "b"=>"2"} 6 | # To 7 | # [{key: "a", value: "1"}, {key: "b", value: "2"}] 8 | # 9 | def values 10 | tags = Lono.config.up.tags 11 | return if tags.nil? # nil = keep current tags. [] = remove all tags 12 | 13 | tags.map do |k,v| 14 | { key: k.to_s, value: v } 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/lono/cfn/download.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'open-uri' 3 | 4 | module Lono::Cfn 5 | class Download < Base 6 | def run 7 | pretty_path = download_path.sub("#{Lono.root}/", '') 8 | logger.info "Downloading template to: #{pretty_path}" 9 | download_template 10 | end 11 | 12 | def download_template 13 | body = download_stack 14 | body = convert_to_yaml(body) 15 | FileUtils.mkdir_p(File.dirname(download_path)) 16 | IO.write(download_path, body) 17 | end 18 | 19 | def download_stack 20 | source = @options[:source] 21 | if source 22 | open(source).read # url or file 23 | else 24 | resp = cfn.get_template( 25 | stack_name: @stack, 26 | template_stage: "Original" 27 | ) 28 | resp.template_body 29 | end 30 | end 31 | 32 | def convert_to_yaml(body) 33 | json?(body) ? YAML.dump(JSON.parse(body)) : body 34 | end 35 | 36 | def json?(body) 37 | !!JSON.parse(body) rescue false 38 | end 39 | 40 | def download_path 41 | "#{Lono.root}/output/#{@blueprint.name}/templates/#{@blueprint.name}.yml" 42 | end 43 | 44 | def name 45 | @options[:name] || @stack 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/lono/cfn/output.rb: -------------------------------------------------------------------------------- 1 | module Lono::Cfn 2 | class Output < Base 3 | def run 4 | stack = find_stack(@stack) 5 | unless stack 6 | logger.info "ERROR: stack #{@stack} not found".color(:red) 7 | quit 1 8 | end 9 | 10 | outputs = stack.outputs.map(&:to_h) 11 | logger.info <<~EOL 12 | #{"Outputs:".color(:green)} 13 | 14 | EOL 15 | if outputs.empty? 16 | logger.stdout outputs.inspect 17 | else 18 | logger.stdout pretty(outputs) 19 | end 20 | end 21 | 22 | private 23 | # Input: 24 | # 25 | # [{ 26 | # output_key: "SecurityGroup", 27 | # output_value: "demo-dev-SecurityGroup-142DZFIEG3G9L" 28 | # }] 29 | # 30 | # Output: 31 | # 32 | # { "SecurityGroup" => "demo-dev-SecurityGroup-142DZFIEG3G9L" } 33 | # 34 | def pretty(outputs) 35 | outs = outputs.inject({}) do |result,h| 36 | result.merge!(h[:output_key] => h[:output_value]) 37 | end 38 | default = {format: "equal"} 39 | options = default.merge(@options.symbolize_keys) 40 | presenter = CliFormat::Presenter.new(options) 41 | presenter.header = ["Key", "Value"] unless presenter.format == "equal" 42 | outs.keys.sort.each do |k| 43 | row = [k,outs[k]] 44 | presenter.rows << row 45 | end 46 | presenter.show 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/lono/cfn/plan/base.rb: -------------------------------------------------------------------------------- 1 | class Lono::Cfn::Plan 2 | class Base < Lono::Cfn::Base 3 | def initialize(options={}) 4 | super 5 | # Allow build and iam to be passed from Cfn::Deploy. Reasons: 6 | # 7 | # 1. build.all only needs to be called once. 8 | # 2. iam.capabilities can be adjusted with retry 9 | # 3. objects are passed in a clear single direction. 10 | # 11 | # However, for CLI commands like `lono plan`, no objects are passed. 12 | # In this case, new instances are created. In both cases the same options 13 | # to the build and iam instances are used. 14 | # 15 | @build = options[:build] || build 16 | @iam = options[:iam] || iam 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/lono/cfn/plan/changeset/base.rb: -------------------------------------------------------------------------------- 1 | class Lono::Cfn::Plan::Changeset 2 | class Base 3 | extend Memoist 4 | include Lono::AwsServices 5 | include Lono::Utils::Logging 6 | include Lono::Cfn::Concerns::TemplateOutput 7 | 8 | def initialize(options={}) 9 | @blueprint = options[:blueprint] 10 | @change_set = options[:change_set] 11 | @stack = options[:stack] 12 | end 13 | 14 | def stack 15 | find_stack(@stack) 16 | end 17 | memoize :stack 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/lono/cfn/plan/changeset/notifications.rb: -------------------------------------------------------------------------------- 1 | class Lono::Cfn::Plan::Changeset 2 | class Notifications < Base 3 | def changes 4 | old = stack.notification_arns 5 | new = @change_set.notification_arns 6 | added = new - old 7 | removed = old - new 8 | return if added.empty? && removed.empty? 9 | 10 | logger.info "Changes to notifications" 11 | log = Proc.new do |k| 12 | logger.info " #{k}" 13 | end 14 | unless added.empty? 15 | logger.info "Added:" 16 | added.each(&log) 17 | end 18 | unless removed.empty? 19 | logger.info "Removed:" 20 | removed.each(&log) 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/lono/cfn/plan/changeset/outputs.rb: -------------------------------------------------------------------------------- 1 | class Lono::Cfn::Plan::Changeset 2 | class Outputs < Base 3 | # Not enough info to show value changes. Show whats possible: Added and Removed keys 4 | def changes 5 | old_keys = stack.outputs.map { |output| output[:output_key] } 6 | new_keys = template_output.outputs.map { |k,_| k } 7 | added_keys = new_keys - old_keys 8 | removed_keys = old_keys - new_keys 9 | return if added_keys.empty? && removed_keys.empty? 10 | 11 | logger.info "Changes to outputs" 12 | log = Proc.new do |k| 13 | logger.info " #{k}" 14 | end 15 | unless added_keys.empty? 16 | logger.info "Added:" 17 | added_keys.each(&log) 18 | end 19 | unless removed_keys.empty? 20 | logger.info "Removed:" 21 | removed_keys.each(&log) 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/lono/cfn/plan/changeset/resources.rb: -------------------------------------------------------------------------------- 1 | class Lono::Cfn::Plan::Changeset 2 | class Resources < Base 3 | def changes 4 | return if @change_set.changes.empty? 5 | logger.info "Resource Changes" 6 | changes = @change_set.changes.sort_by do |change| 7 | change["resource_change"]["action"] 8 | end 9 | changes.each do |change| 10 | display_change(change) 11 | end 12 | end 13 | 14 | # Private: formats a Aws::CloudFormation::Types::Change in pretty human readable form 15 | # 16 | # change - Aws::CloudFormation::Types::Change 17 | # 18 | # Examples 19 | # 20 | # display_change(change) 21 | # => Remove AWS::Route53::RecordSet: DnsRecord testsubdomain.sub.tongueroo.com 22 | # 23 | # Returns nil 24 | # 25 | # change.to_h 26 | # {:type=>"Resource", 27 | # :resource_change=> 28 | # {:action=>"Remove", 29 | # :logical_resource_id=>"DnsRecord", 30 | # :physical_resource_id=>"testsubdomain.sub.tongueroo.com", 31 | # :resource_type=>"AWS::Route53::RecordSet", 32 | # :scope=>[], 33 | # :details=>[]}} 34 | def display_change(change) 35 | message = if change.type == "Resource" 36 | c = change.resource_change 37 | " #{c.action} #{c.resource_type}: #{c.logical_resource_id} #{c.physical_resource_id}" 38 | else 39 | change.to_h 40 | end 41 | 42 | colors = { Remove: :red, Add: :green, Modify: :yellow } 43 | action = change.resource_change.action.to_sym 44 | message = message.color(colors[action]) if colors.has_key?(action) 45 | logger.info message 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/lono/cfn/plan/changeset/tags.rb: -------------------------------------------------------------------------------- 1 | class Lono::Cfn::Plan::Changeset 2 | class Tags < Base 3 | def changes 4 | old = simplify(stack.tags) 5 | new = simplify(@change_set.tags) 6 | diff = Lono::Cfn::Plan::Diff::Data.new(old, new) 7 | diff.show("Tag Changes:") 8 | end 9 | 10 | def simplify(tags) 11 | tags.inject({}) do |result, i| 12 | result.merge(i.key => i.value) 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/lono/cfn/plan/concerns.rb: -------------------------------------------------------------------------------- 1 | class Lono::Cfn::Plan 2 | module Concerns 3 | extend Memoist 4 | 5 | def summary 6 | Summary.new 7 | end 8 | memoize :summary 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/lono/cfn/plan/delete.rb: -------------------------------------------------------------------------------- 1 | class Lono::Cfn::Plan 2 | class Delete < Lono::Cfn::Base 3 | include Concerns 4 | 5 | def run 6 | stack = find_stack(@stack) 7 | unless stack 8 | logger.info "ERROR: stack #{@stack} not found".color(:red) 9 | quit 1 10 | end 11 | 12 | logger.info "Will delete".color(:green) 13 | Lono::Cfn::Show.new(@options).print 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/lono/cfn/plan/diff/base.rb: -------------------------------------------------------------------------------- 1 | module Lono::Cfn::Plan::Diff 2 | class Base 3 | include Lono::Utils::Logging 4 | include Lono::Utils::Pretty 5 | 6 | def initialize(options={}) 7 | @options = options 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/lono/cfn/plan/diff/data.rb: -------------------------------------------------------------------------------- 1 | module Lono::Cfn::Plan::Diff 2 | class Data < Base 3 | extend Memoist 4 | 5 | def initialize(old, new) 6 | @old = old 7 | @new = new 8 | end 9 | 10 | def show(header_message=nil) 11 | changes = calculate 12 | if !changes && header_message 13 | return 14 | end 15 | logger.info header_message if header_message 16 | 17 | unless @added_keys.empty? 18 | logger.info "Added:" 19 | @added_keys.each do |k| 20 | logger.info " #{k}: #{@new[k]}" 21 | end 22 | end 23 | unless @removed_keys.empty? 24 | logger.info "Removed:" 25 | @removed_keys.each do |k| 26 | logger.info " #{k}: #{@old[k]}" 27 | end 28 | end 29 | unless @modified_keys.empty? 30 | logger.info "Modified:" 31 | @modified_keys.each do |k| 32 | logger.info " #{k}: #{show_old(@old[k])} -> #{@new[k]}" 33 | end 34 | end 35 | 36 | unless changes 37 | logger.info "No changes" 38 | end 39 | end 40 | 41 | def show_old(v) 42 | if v.nil? 43 | '(unset)' 44 | elsif v == '' 45 | "''" 46 | else 47 | v 48 | end 49 | end 50 | 51 | def calculate 52 | @added_keys = @new.keys - @old.keys 53 | @removed_keys = @old.keys - @new.keys 54 | all_keys = (@old.keys + @new.keys).uniq 55 | kept_keys = all_keys - @added_keys - @removed_keys 56 | @modified_keys = kept_keys.select { |k| @old[k] != @new[k] } 57 | no_changes = @added_keys.empty? && @removed_keys.empty? && @modified_keys.empty? 58 | !no_changes 59 | end 60 | memoize :calculate 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/lono/cfn/plan/new.rb: -------------------------------------------------------------------------------- 1 | class Lono::Cfn::Plan 2 | class New < Lono::Cfn::Base 3 | include Concerns 4 | 5 | def run 6 | if resource_types.empty? 7 | logger.info "ERROR: At least one Resource member must be defined in the template".color(:red) 8 | quit 1 9 | end 10 | 11 | logger.info "Will create".color(:green) 12 | summary.print(resource_types) 13 | end 14 | 15 | def resource_types 16 | resources = template_output.resources 17 | return {} unless resources 18 | 19 | types = Hash.new(0) 20 | resources.each do |logical_id, resource| 21 | types[resource["Type"]] += 1 22 | end 23 | types 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/lono/cfn/plan/summary.rb: -------------------------------------------------------------------------------- 1 | class Lono::Cfn::Plan 2 | class Summary 3 | include Lono::Utils::Logging 4 | 5 | def print(resource_types) 6 | types = resource_types.sort_by {|r| r[0]} # Hash -> 2D Array 7 | types.each do |a| 8 | type, count = a 9 | logger.info "%3s %s" % [count, type] 10 | end 11 | total = types.inject(0) { |sum,(type,count)| sum += count } 12 | logger.info "%3s %s" % [total, "Total"] 13 | logger.info "" # newline 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/lono/cfn/plan/template.rb: -------------------------------------------------------------------------------- 1 | class Lono::Cfn::Plan 2 | class Template < Base 3 | def run 4 | return unless Lono.config.plan.template 5 | @build.all 6 | logger.info "Template Changes:".color(:green) 7 | download_existing_cfn_template 8 | diff = Diff::File.new(mode: Lono.config.plan.template) 9 | diff.show(existing_template_path, new_cfn_template) 10 | logger.info "" # newline 11 | end 12 | 13 | def download_existing_cfn_template 14 | resp = cfn.get_template(stack_name: @stack, template_stage: "Original") 15 | FileUtils.mkdir_p(File.dirname(existing_template_path)) 16 | IO.write(existing_template_path, resp.template_body) 17 | end 18 | 19 | # for clarity 20 | def new_cfn_template 21 | @blueprint.output_path 22 | end 23 | 24 | def existing_template_path 25 | "/tmp/lono/diff/template/existing.yml" 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/lono/cfn/show.rb: -------------------------------------------------------------------------------- 1 | module Lono::Cfn 2 | class Show < Base 3 | include Plan::Concerns 4 | 5 | def run 6 | logger.info "Info for stack: #{@stack.color(:green)}" 7 | logger.info "Resources:".color(:green) 8 | print 9 | Output.new(@options).run 10 | end 11 | 12 | def print 13 | summary.print(resource_types) 14 | end 15 | 16 | def resource_types 17 | resources = find_stack_resources(@stack) 18 | unless resources 19 | logger.info "ERROR: Stack #{@stack} not found".color(:red) 20 | exit 1 21 | end 22 | 23 | types = Hash.new(0) 24 | resources.each do |resource| 25 | types[resource.resource_type] += 1 26 | end 27 | types 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/lono/cfn/status.rb: -------------------------------------------------------------------------------- 1 | module Lono::Cfn 2 | class Status < CfnStatus 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /lib/lono/cli/abstract.rb: -------------------------------------------------------------------------------- 1 | class Lono::CLI 2 | class Abstract 3 | extend Memoist 4 | include Lono::Utils::Logging 5 | include Lono::Utils::Pretty 6 | 7 | def initialize(options={}) 8 | @options = options 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/lono/cli/base.rb: -------------------------------------------------------------------------------- 1 | class Lono::CLI 2 | class Base < Abstract 3 | include Lono::Concerns::Names 4 | include Lono::Utils::Sh 5 | 6 | def initialize(options={}) 7 | super 8 | @blueprint = Lono::Blueprint.new(options.merge(name: options[:blueprint])) 9 | @stack = names.stack 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/lono/cli/bundle.rb: -------------------------------------------------------------------------------- 1 | class Lono::CLI 2 | class Bundle < Abstract 3 | def run 4 | Lono::Bundler::CLI.start(args) 5 | end 6 | 7 | # Allows bundle to be called without install. So both work: 8 | # 9 | # lono bundle 10 | # lono bundle install 11 | # 12 | def args 13 | args = @options[:args] 14 | if args.empty? or args.first.include?('--') 15 | args.unshift("install") 16 | end 17 | args = ["bundle"] + args 18 | args 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/lono/cli/cfn.rb: -------------------------------------------------------------------------------- 1 | class Lono::CLI 2 | class Cfn < Lono::Command 3 | opts = Lono::CLI::Cfn::Opts.new(self) 4 | 5 | desc "cancel BLUEPRINT", "Cancel a CloudFormation blueprint." 6 | long_desc Help.text("cfn/cancel") 7 | opts.cancel 8 | def cancel(blueprint) 9 | Lono::Cfn::Cancel.new(options.merge(blueprint: blueprint)).run 10 | end 11 | 12 | desc "download BLUEPRINT", "Download CloudFormation template from existing blueprint." 13 | long_desc Help.text("cfn/download") 14 | opts.download 15 | def download(blueprint) 16 | Lono::Cfn::Download.new(options.merge(blueprint: blueprint)).run 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/lono/cli/cfn/opts.rb: -------------------------------------------------------------------------------- 1 | class Lono::CLI::Cfn 2 | class Opts < Lono::CLI::Opts 3 | def create 4 | base_options 5 | wait_options 6 | end 7 | 8 | def update 9 | base_options 10 | wait_options 11 | end 12 | 13 | def deploy 14 | update 15 | end 16 | 17 | def delete 18 | wait_options 19 | end 20 | 21 | def cancel 22 | wait_options 23 | end 24 | 25 | def preview 26 | base_options 27 | with_cli_scope do 28 | option :keep, type: :boolean, desc: "keep the changeset instead of deleting it afterwards" 29 | end 30 | end 31 | 32 | def download 33 | base_options 34 | with_cli_scope do 35 | option :name, desc: "Name you want to save the template as. Default: existing stack name." 36 | option :source, desc: "url or path to file with template" 37 | end 38 | end 39 | 40 | # Lono::Cfn and Lono::Sets 41 | def base_options(rollback: true) 42 | with_cli_scope do 43 | if rollback 44 | option :rollback, type: :boolean, desc: "rollback" # default: nil means CloudFormation default which is true 45 | end 46 | # common to Lono::Cfn and Lono::Sets 47 | option :capabilities, type: :array, desc: "iam capabilities. Ex: CAPABILITY_IAM, CAPABILITY_NAMED_IAM" 48 | option :iam, type: :boolean, desc: "Shortcut for common IAM capabilities: CAPABILITY_IAM, CAPABILITY_NAMED_IAM" 49 | end 50 | end 51 | 52 | def wait_options 53 | with_cli_scope do 54 | option :wait, type: :boolean, desc: "Wait for stack operation to complete.", default: true 55 | option :yes, aliases: :y, type: :boolean, desc: "Skip are you sure prompt" # moved to base but used by commands like `lono down` also. Just keep here. 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/lono/cli/clean.rb: -------------------------------------------------------------------------------- 1 | require "fileutils" 2 | 3 | class Lono::CLI 4 | class Clean 5 | include Lono::Utils::Logging 6 | include Lono::Utils::Sure 7 | 8 | attr_reader :options 9 | def initialize(options={}) 10 | @options = options 11 | @mute = options[:mute] # used by CLI::Build at beginning to clear out the output folder 12 | end 13 | 14 | def run 15 | folders = %w[output tmp] 16 | @mute || sure?("Will remove folders: #{folders.join(' ')}") 17 | folders.each do |folder| 18 | FileUtils.rm_rf("#{Lono.root}/#{folder}") 19 | end 20 | logger.info "Removed folders: #{folders.join(' ')}" unless @mute 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/lono/cli/completion.rb: -------------------------------------------------------------------------------- 1 | class Lono::CLI 2 | class Completion < Base 3 | desc "script", "generates script that can be eval to setup auto-completion" 4 | long_desc Help.text("completion:script") 5 | def script 6 | Completer::Script.generate 7 | end 8 | 9 | desc "completions *PARAMS", "prints words for auto-completion" 10 | long_desc Help.text("completion:list") 11 | def list(*params) 12 | Completer.new(*params).run 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/lono/cli/help.rb: -------------------------------------------------------------------------------- 1 | class Lono::CLI 2 | module Help 3 | extend ActiveSupport::Concern 4 | 5 | class_methods do 6 | def main_commands 7 | %w[ 8 | build 9 | down 10 | list 11 | new 12 | plan 13 | seed 14 | show 15 | up 16 | ] 17 | end 18 | 19 | def help(shell, subcommand) 20 | list = printable_commands(true, subcommand) 21 | list.sort! { |a, b| a[0] <=> b[0] } 22 | filter = Proc.new do |command, desc| 23 | main_commands.detect { |name| command =~ Regexp.new("^lono #{name}") } 24 | end 25 | main = list.select(&filter) 26 | other = list.reject(&filter) 27 | 28 | shell.say <<~EOL 29 | Usage: lono COMMAND [args] 30 | The available commands are listed below. 31 | The primary workflow commands are given first, followed by 32 | less common or more advanced commands. 33 | EOL 34 | shell.say "\nMain Commands:\n\n" 35 | shell.print_table(main, :indent => 2, :truncate => true) 36 | shell.say "\nOther Commands:\n\n" 37 | shell.print_table(other, :indent => 2, :truncate => true) 38 | shell.say <<~EOL 39 | 40 | For more help on each command, you can use the -h option. Example: 41 | 42 | lono up -h 43 | 44 | CLI Reference also available at: https://lono.cloud/reference/ 45 | EOL 46 | end 47 | end 48 | 49 | def text(namespaced_command) 50 | path = namespaced_command.to_s.gsub(':','/') 51 | path = File.expand_path("../help/#{path}.md", __FILE__) 52 | IO.read(path) if File.exist?(path) 53 | end 54 | extend self 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/lono/cli/help/build.md: -------------------------------------------------------------------------------- 1 | Generates CloudFormation template, parameter files, and scripts in lono project and writes them to the `output` folder. 2 | 3 | ## Examples 4 | 5 | lono build BLUEPRINT 6 | lono build BLUEPRINT --clean 7 | lono g BLUEPRINT --clean # shortcut 8 | 9 | ## Example Output 10 | 11 | $ lono build ec2 12 | Building template, parameters, and scripts 13 | Building template: 14 | output/templates/ec2.yml 15 | Building parameters: 16 | output/params/ec2.json 17 | $ 18 | -------------------------------------------------------------------------------- /lib/lono/cli/help/cfn.md: -------------------------------------------------------------------------------- 1 | ## Examples 2 | 3 | lono up demo 4 | lono down demo 5 | -------------------------------------------------------------------------------- /lib/lono/cli/help/cfn/cancel.md: -------------------------------------------------------------------------------- 1 | ## Examples 2 | 3 | $ lono cfn cancel demo 4 | Canceling updates to demo. 5 | Current stack status: UPDATE_IN_PROGRESS 6 | Canceling stack update. 7 | Waiting for stack to complete 8 | 01:20:32PM UPDATE_ROLLBACK_IN_PROGRESS AWS::CloudFormation::Stack demo User Initiated 9 | 01:20:44PM UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS AWS::CloudFormation::Stack demo 10 | 01:20:44PM UPDATE_ROLLBACK_COMPLETE AWS::CloudFormation::Stack demo 11 | Stack rolled back: UPDATE_ROLLBACK_COMPLETE 12 | Time took for stack deployment: 15s. 13 | $ -------------------------------------------------------------------------------- /lib/lono/cli/help/cfn/download.md: -------------------------------------------------------------------------------- 1 | ## Examples 2 | 3 | lono cfn download demo 4 | -------------------------------------------------------------------------------- /lib/lono/cli/help/cfn/preview.md: -------------------------------------------------------------------------------- 1 | ## Examples 2 | 3 | We'll make a change to the default InstanceType of a CloudFromation stack. 4 | 5 | lono cfn plan demo 6 | 7 | The output should look similiar to this: 8 | 9 | Building CloudFormation source code diff... 10 | => colordiff /tmp/existing_cfn_template.yml output/templates/ec2.yml 11 | 14c14 12 | < Default: t2.small 13 | --- 14 | > Default: t2.micro 15 | Building CloudFormation Change Set for plan..... 16 | CloudFormation plan for 'ec2' stack update. Changes: 17 | Modify AWS::EC2::Instance: EC2Instance i-07e939db65120fb75 18 | 19 | The plan includes colors: 20 | 21 | Stack Update 22 | 23 | There are several types of "diffs" in the plan. 24 | 25 | 1. Source code diff of the templates. This is generated by downloading the current CloudFormation template and comparing it with the locally generated one using `colordiff`. 26 | 2. Param diff. 27 | 3. CloudFormation Change Set list of changes. This is generated using [AWS CloudFormation Change Set](https://medium.com/boltops/a-simple-introduction-to-cloudformation-part-4-change-sets-dry-run-mode-c14e41dfeab7) feature. 28 | -------------------------------------------------------------------------------- /lib/lono/cli/help/cfn/status.md: -------------------------------------------------------------------------------- 1 | Shows the status of the stack. If the stack is in progress then tail the status and provide live updates. 2 | 3 | ## Examples 4 | 5 | $ lono cfn status ecs-asg 6 | The current status for the stack ecs-asg is UPDATE_COMPLETE 7 | Stack events: 8 | 11:52:28PM UPDATE_IN_PROGRESS AWS::CloudFormation::Stack ecs-asg User Initiated 9 | 11:52:32PM UPDATE_IN_PROGRESS AWS::CloudWatch::Alarm HighMemory 10 | 11:52:32PM UPDATE_IN_PROGRESS AWS::CloudWatch::Alarm HighCpu 11 | 11:52:32PM UPDATE_COMPLETE AWS::CloudWatch::Alarm HighMemory 12 | 11:52:33PM UPDATE_COMPLETE AWS::CloudWatch::Alarm HighCpu 13 | 11:52:35PM UPDATE_COMPLETE_CLEANUP_IN_PROGRESS AWS::CloudFormation::Stack ecs-asg 14 | 11:52:36PM UPDATE_COMPLETE AWS::CloudFormation::Stack ecs-asg 15 | -------------------------------------------------------------------------------- /lib/lono/cli/help/code/import.md: -------------------------------------------------------------------------------- 1 | ## Examples 2 | 3 | lono code import path/to/file 4 | lono code import http://example.com/url/to/template.yml 5 | lono code import http://example.com/url/to/template.json 6 | lono code import http://example.com/url/to/template.json --blueprint myblueprint 7 | 8 | ## Example with Output 9 | 10 | $ URL=https://s3.amazonaws.com/cloudformation-templates-us-east-1/EC2InstanceWithSecurityGroupSample.template 11 | $ lono code import $URL --blueprint ec2 12 | => Creating new blueprint called ec2. 13 | create blueprints/ec2 14 | create blueprints/ec2/ec2.gemspec 15 | create blueprints/ec2/.gitignore 16 | create blueprints/ec2/.meta/config.yml 17 | create blueprints/ec2/CHANGELOG.md 18 | create blueprints/ec2/Gemfile 19 | create blueprints/ec2/README.md 20 | create blueprints/ec2/Rakefile 21 | create blueprints/ec2/seed/configs.rb 22 | create blueprints/ec2/app/templates 23 | create blueprints/ec2/app/templates/ec2.rb 24 | create config/ec2/params/development.txt 25 | create config/ec2/params/production.txt 26 | ================================================================ 27 | Congrats You have successfully imported a lono blueprint. 28 | 29 | More info: https://lono.cloud/docs/core/blueprints 30 | $ 31 | -------------------------------------------------------------------------------- /lib/lono/cli/help/completion.md: -------------------------------------------------------------------------------- 1 | ## Example 2 | 3 | lono completion 4 | 5 | Prints words for TAB auto-completion. 6 | 7 | ## Examples 8 | 9 | lono completion 10 | lono completion cfn 11 | lono completion up 12 | 13 | To enable, TAB auto-completion add the following to your profile: 14 | 15 | eval $(lono completion_script) 16 | 17 | Auto-completion example usage: 18 | 19 | lono [TAB] 20 | lono cfn [TAB] 21 | lono up [TAB] 22 | lono up --[TAB] 23 | -------------------------------------------------------------------------------- /lib/lono/cli/help/completion_script.md: -------------------------------------------------------------------------------- 1 | To use, add the following to your `~/.bashrc` or `~/.profile`: 2 | 3 | eval $(lono completion_script) 4 | -------------------------------------------------------------------------------- /lib/lono/cli/help/configsets.md: -------------------------------------------------------------------------------- 1 | ## Examples 2 | 3 | When the `lono configsets` command is passed the blueprint will list the configsets for the blueprint. 4 | 5 | $ lono configsets ec2 6 | Configsets used by ec2 blueprint: 7 | +-------+----------------------+---------+---------+ 8 | | Name | Path | Type | From | 9 | +-------+----------------------+---------+---------+ 10 | | httpd | app/configsets/httpd | project | project | 11 | +-------+----------------------+---------+---------+ 12 | $ 13 | 14 | When there are no arguments passed to the `lono configsets` command it will list the project configsets. 15 | 16 | $ lono configsets 17 | Project configsets: 18 | +-------+------------------------+---------+ 19 | | Name | Path | Type | 20 | +-------+------------------------+---------+ 21 | | httpd | app/configsets/httpd | project | 22 | | ruby | vendor/configsets/ruby | vendor | 23 | +-------+------------------------+---------+ 24 | $ -------------------------------------------------------------------------------- /lib/lono/cli/help/down.md: -------------------------------------------------------------------------------- 1 | ## Examples 2 | 3 | $ lono down demo 4 | Are you sure you want to delete the demo-dev stack? (y/N) 5 | y 6 | Deleted demo-dev stack. 7 | $ 8 | 9 | Lono prompts you with an "Are you sure?" message before the stack gets deleted. If you would like to bypass the prompt, you can use the `-y` flag. 10 | 11 | $ lono down demo -y 12 | Deleted demo-dev stack. 13 | $ 14 | -------------------------------------------------------------------------------- /lib/lono/cli/help/new/helper.md: -------------------------------------------------------------------------------- 1 | ## Blueprint Helpers 2 | 3 | When the `--blueprint` option is used a blueprint helper is generated. 4 | 5 | The default name is custom 6 | 7 | $ lono new helper --blueprint demo 8 | create app/blueprints/demo/helpers/custom_helper.rb 9 | 10 | Here's an example with a helper named vars. 11 | 12 | $ lono new helper vars --blueprint demo 13 | create app/blueprints/demo/helpers/vars_helper.rb 14 | 15 | ## Project Helpers 16 | 17 | When no `--blueprint` option is used a project helper is generated. 18 | 19 | The default name is custom. 20 | 21 | $ lono new helper 22 | create app/helpers/custom/custom_helper.rb 23 | 24 | Here's an example with a helper named common. 25 | 26 | $ lono new helper common 27 | create app/helpers/common/common_helper.rb 28 | -------------------------------------------------------------------------------- /lib/lono/cli/help/new/hook.md: -------------------------------------------------------------------------------- 1 | ## Blueprint Hooks 2 | 3 | When the `--blueprint` option is used a blueprint hook is generated. 4 | 5 | $ lono new helper --blueprint demo 6 | create app/blueprints/demo/config/hooks.rb 7 | 8 | ## Project Helpers 9 | 10 | When no `--blueprint` option is used a project hook is generated. 11 | 12 | $ lono new helper 13 | create app/config/hooks.rb 14 | 15 | -------------------------------------------------------------------------------- /lib/lono/cli/help/new/project.md: -------------------------------------------------------------------------------- 1 | ## Example 2 | 3 | $ lono new project infra 4 | Other infra 5 | => Creating new project called infra. 6 | create infra 7 | create infra/.gitignore 8 | create infra/Gemfile 9 | create infra/README.md 10 | create infra/config/app.rb 11 | => Installing dependencies with: bundle install 12 | Bundle complete! 2 Gemfile dependencies, 47 gems now installed. 13 | ================================================================ 14 | Congrats 🎉 You have successfully created a lono project. 15 | 16 | cd infra 17 | 18 | To generate a new blueprint: 19 | 20 | lono new blueprint demo --examples 21 | 22 | To deploy: 23 | 24 | lono up demo 25 | 26 | More info: https://lono.cloud/ 27 | $ -------------------------------------------------------------------------------- /lib/lono/cli/help/param.md: -------------------------------------------------------------------------------- 1 | ## Examples 2 | 3 | $ lono param generate ecs-asg 4 | Building parameters 5 | output/ecs-asg/params/development.json 6 | $ -------------------------------------------------------------------------------- /lib/lono/cli/help/param/generate.md: -------------------------------------------------------------------------------- 1 | ## Example 2 | 3 | To generate CloudFormation json parameter files in the `output` folder: 4 | 5 | lono param generate 6 | -------------------------------------------------------------------------------- /lib/lono/cli/help/pro/blueprints.md: -------------------------------------------------------------------------------- 1 | ## Example 2 | 3 | $ lono pro blueprints 4 | +-------------+------------------------------------------------+-----------------------------------------+ 5 | | Name | Docs | Description | 6 | +-------------+------------------------------------------------+-----------------------------------------+ 7 | | elasticache | https://github.com/boltopspro-docs/elasticache | Amazon Elasticache Memcached and Red... | 8 | | elb | https://github.com/boltopspro-docs/elb | ELB Blueprint: Application or Networ... | 9 | | rds | https://github.com/boltopspro-docs/rds | RDS Database Blueprint | 10 | +-------------+------------------------------------------------+-----------------------------------------+ 11 | $ -------------------------------------------------------------------------------- /lib/lono/cli/help/pro/configsets.md: -------------------------------------------------------------------------------- 1 | ## Example 2 | 3 | $ lono pro configsets 4 | +---------+--------------------------------------------+-----------------------------------------+ 5 | | Name | Docs | Description | 6 | +---------+--------------------------------------------+-----------------------------------------+ 7 | | jenkins | https://github.com/boltopspro-docs/jenkins | Installs and Runs Jenkins | 8 | | ruby | https://github.com/boltopspro-docs/ruby | ruby configset: install ruby on amaz... | 9 | | ssm | https://github.com/boltopspro-docs/ssm | SSM Configset: Install, Configure an... | 10 | +---------+--------------------------------------------+-----------------------------------------+ 11 | $ -------------------------------------------------------------------------------- /lib/lono/cli/help/script/build.md: -------------------------------------------------------------------------------- 1 | ## Examples 2 | 3 | lono script build 4 | -------------------------------------------------------------------------------- /lib/lono/cli/help/script/upload.md: -------------------------------------------------------------------------------- 1 | This command must be ran after `lono script build` since it relies the artifacts of that command. Namely: 2 | 3 | * output/scripts/scripts-md5sum.tgz 4 | * output/data/scripts_info.txt 5 | 6 | ## Examples 7 | 8 | lono script upload 9 | -------------------------------------------------------------------------------- /lib/lono/cli/help/seed.md: -------------------------------------------------------------------------------- 1 | ## Example 2 | 3 | $ lono seed demo 4 | Creating starter config files for ec2 5 | create config/blueprints/ec2/params/dev.txt 6 | create config/blueprints/ec2/vars/dev.rb 7 | 8 | To create the files in the top-level app folder 9 | 10 | $ lono seed ec2 --where app 11 | Creating starter config files for ec2 12 | create app/blueprints/ec2/config/params/dev.txt 13 | create app/blueprints/ec2/config/vars/dev.rb 14 | $ 15 | 16 | You can also set the default where option with 17 | 18 | config/app.rb 19 | 20 | ```ruby 21 | Lono.configure do |config| 22 | config.seed.where = "app" 23 | end 24 | ``` 25 | -------------------------------------------------------------------------------- /lib/lono/cli/help/summary.md: -------------------------------------------------------------------------------- 1 | The `lono summary` command helps you quickly understand a CloudFormation template. 2 | 3 | ## Examples 4 | 5 | $ bundle exec lono summary ec2 6 | Building template 7 | output/ec2/templates/ec2-old.yml 8 | output/ec2/templates/ec2-new.yml 9 | => CloudFormation Template Summary for template ec2-new: 10 | Required Parameters (0): 11 | There are no required parameters. 12 | Optional Parameters (3): 13 | InstanceType (String) Default: t3.micro 14 | Subnet (String) Default: 15 | Vpc (String) Default: 16 | Resources: 17 | 1 AWS::EC2::Instance 18 | 1 AWS::EC2::SecurityGroup 19 | 2 Total 20 | => CloudFormation Template Summary for template ec2-old: 21 | Required Parameters (0): 22 | There are no required parameters. 23 | Optional Parameters (3): 24 | InstanceType (String) Default: t3.micro 25 | Subnet (String) Default: 26 | Vpc (String) Default: 27 | Resources: 28 | 1 AWS::EC2::Instance 29 | 1 AWS::EC2::SecurityGroup 30 | 2 Total 31 | $ 32 | 33 | Blog Post also covers this: [lono summary Tutorial Introduction](https://blog.boltops.com/2017/09/18/lono-inspect-summary-tutorial-introduction) 34 | -------------------------------------------------------------------------------- /lib/lono/cli/help/template.md: -------------------------------------------------------------------------------- 1 | ## Examples 2 | 3 | lono template generate --help 4 | lono template bashify --help 5 | -------------------------------------------------------------------------------- /lib/lono/cli/help/template/bashify.md: -------------------------------------------------------------------------------- 1 | ## Examples 2 | 3 | lono template bashify /path/to/cloudformation-template.json 4 | lono template bashify https://s3.amazonaws.com/cloudformation-templates-us-east-1/EC2WebSiteSample.template 5 | -------------------------------------------------------------------------------- /lib/lono/cli/help/template/generate.md: -------------------------------------------------------------------------------- 1 | ## Examples 2 | 3 | lono template generate 4 | lono template generate --clean 5 | lono template g --clean 6 | 7 | Builds the CloudFormation template files based on lono project and writes them to the output folder on the filesystem. 8 | -------------------------------------------------------------------------------- /lib/lono/cli/help/up.md: -------------------------------------------------------------------------------- 1 | ## Examples 2 | 3 | $ lono up demo 4 | Deploying demo-dev stack 5 | Building template 6 | output/demo/template.yml 7 | Uploading template 8 | Not modified: output/demo/template.yml to s3://lono-bucket-12di8xz5sy72z/dev/output/demo/template.yml 9 | Uploaded to s3. 10 | Building parameters 11 | output/demo/params.json 12 | Going to create stack demo-dev with blueprint demo. (y/N) y 13 | Parameters passed to create_stack: 14 | --- 15 | disable_rollback: false 16 | parameters: 17 | - parameter_key: AccessControl 18 | parameter_value: PublicRead 19 | stack_name: demo-dev 20 | template_url: https://lono-bucket-12di8xz5sy72z.s3.us-west-2.amazonaws.com/dev/output/demo/template.yml 21 | Waiting for stack to complete 22 | 05:07:26AM CREATE_IN_PROGRESS AWS::CloudFormation::Stack demo-dev User Initiated 23 | 05:07:30AM CREATE_IN_PROGRESS AWS::S3::Bucket Bucket 24 | 05:07:31AM CREATE_IN_PROGRESS AWS::S3::Bucket Bucket Resource creation Initiated 25 | 05:07:52AM CREATE_COMPLETE AWS::S3::Bucket Bucket 26 | 05:07:53AM CREATE_COMPLETE AWS::CloudFormation::Stack demo-dev 27 | Stack success status: CREATE_COMPLETE 28 | Time took: 30s. 29 | Create demo-dev stack. 30 | $ 31 | -------------------------------------------------------------------------------- /lib/lono/cli/help/user_data.md: -------------------------------------------------------------------------------- 1 | Generates user_data scripts in `app/user_data` so you can see it for debugging. Let's say you have a script in `app/user_data/bootstrap.sh`. To generate it: 2 | 3 | lono user_data bootstrap 4 | 5 | ## Example Output 6 | 7 | Script: 8 | 9 | #!/bin/bash -exu 10 | 11 | <%= extract_scripts(to: "/opt") %> 12 | 13 | SCRIPTS=/opt/scripts 14 | $SCRIPTS/install_stuff.sh 15 | 16 | Running `lono user_data bootstrap` produces: 17 | 18 | $ lono user_data bootstrap 19 | Detected scripts 20 | Tarballing scripts folder to scripts.tgz 21 | => cd app && dot_clean . 22 | => cd app && tar -c scripts | gzip -n > scripts.tgz 23 | Tarball created at output/scripts/scripts-93b8b29b.tgz 24 | Building user_data for 'bootstrap' at ./app/user_data/bootstrap.sh 25 | #!/bin/bash -exu 26 | 27 | # Generated from the lono extract_scripts helper. 28 | # Downloads scripts from s3, extract them, and setup. 29 | mkdir -p /opt 30 | aws s3 cp s3://mybucket/path/to/folder/development/scripts/scripts-93b8b29b.tgz /opt/ 31 | cd /opt 32 | tar zxf /opt/scripts-93b8b29b.tgz 33 | chown -R ec2-user:ec2-user /opt/scripts 34 | 35 | SCRIPTS=/opt/scripts 36 | $SCRIPTS/install_stuff.sh 37 | $ 38 | -------------------------------------------------------------------------------- /lib/lono/cli/iam.rb: -------------------------------------------------------------------------------- 1 | class Lono::CLI 2 | class Iam < Base 3 | include Lono::Cfn::Concerns::Build 4 | include Lono::Cfn::Concerns::TemplateOutput 5 | 6 | def run 7 | build.template_builder.run 8 | resources = template_output.data['Resources'] 9 | types = resources.map { |logical_id, attrs| attrs['Type'] } 10 | actions = types.map do |t| 11 | service = t.split('::')[1] 12 | "#{service.downcase}:*" 13 | end.uniq 14 | text =<<~EOL 15 | Version: 2012-10-17 16 | Statement: 17 | - Sid: #{@blueprint.name} 18 | Effect: Allow 19 | Resource: "*" 20 | EOL 21 | policy = YAML.load(text) 22 | policy['Statement'][0]['Action'] = actions 23 | puts "IAM Policy Example for blueprint #{@blueprint.name}".color(:green) 24 | puts JSON.pretty_generate(policy) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/lono/cli/list.rb: -------------------------------------------------------------------------------- 1 | class Lono::CLI 2 | class List < Base 3 | attr_reader :options 4 | def initialize(options={}) 5 | @options = options 6 | end 7 | 8 | def run 9 | list_type("blueprints") if show?("blueprint") 10 | list_type("configsets") if show?("configset") 11 | list_type("extensions") if show?("extension") 12 | end 13 | 14 | private 15 | def list_type(type) 16 | Dir.glob("#{Lono.root}/{app,vendor}/#{type}/*").each do |path| 17 | logger.info pretty_path(path) 18 | end 19 | end 20 | 21 | def show?(type) 22 | @options[:type] == type || @options[:type].nil? || @options[:type] == "all" 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/lono/cli/new.rb: -------------------------------------------------------------------------------- 1 | class Lono::CLI 2 | class New < Lono::Command 3 | long_desc Help.text("new/blueprint") 4 | Blueprint.cli_options.each { |args| option(*args) } 5 | register(Blueprint, "blueprint", "blueprint NAME", "Generate new blueprint") 6 | 7 | long_desc Help.text("new/configset") 8 | Configset.cli_options.each { |args| option(*args) } 9 | register(Configset, "configset", "configset NAME", "Generate new configset") 10 | 11 | long_desc Help.text("new/helper") 12 | Helper.cli_options.each { |args| option(*args) } 13 | register(Helper, "helper", "helper NAME", "Generate new helper") 14 | 15 | long_desc Help.text("new/hook") 16 | Hook.cli_options.each { |args| option(*args) } 17 | register(Hook, "hook", "hook NAME", "Generate new hook") 18 | 19 | long_desc Help.text("new/project") 20 | Project.cli_options.each { |args| option(*args) } 21 | register(Project, "project", "project NAME", "Generate new project") 22 | 23 | long_desc Help.text("new/shim") 24 | Shim.cli_options.each { |args| option(*args) } 25 | register(Shim, "shim", "shim NAME", "Generate new shim") 26 | 27 | desc "test SUBCOMMAND", "test subcommands" 28 | long_desc Help.text(:test) 29 | subcommand "test", Test 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/lono/cli/new/blueprint.rb: -------------------------------------------------------------------------------- 1 | class Lono::CLI::New 2 | class Blueprint < Sequence 3 | argument :name 4 | def self.cli_options 5 | [ 6 | [:examples, type: :boolean, desc: "Whether not to generate examples"], 7 | [:force, type: :boolean, desc: "Bypass overwrite are you sure prompt for existing files"], 8 | ] 9 | end 10 | cli_options.each do |args| 11 | class_option(*args) 12 | end 13 | 14 | def set_source 15 | if @options[:examples] 16 | set_template_source "examples/blueprint" 17 | else 18 | set_template_source "blueprint" 19 | end 20 | end 21 | 22 | def create_blueprint 23 | logger.info "=> Creating new blueprint: #{name}" 24 | directory ".", "app/blueprints/#{name}" 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/lono/cli/new/concerns.rb: -------------------------------------------------------------------------------- 1 | class Lono::CLI::New 2 | module Concerns 3 | extend ActiveSupport::Concern 4 | 5 | private 6 | def class_name 7 | name.underscore.camelize 8 | end 9 | 10 | # Files should be named with underscores instead of dashes even though project name can contain a dash. 11 | # This is because autoloading works with underscores in the filenames only. 12 | def underscore_name 13 | name.underscore 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/lono/cli/new/configset.rb: -------------------------------------------------------------------------------- 1 | class Lono::CLI::New 2 | class Configset < Sequence 3 | argument :name 4 | def self.cli_options 5 | [ 6 | [:examples, type: :boolean, desc: "Whether not to generate examples"], 7 | [:force, type: :boolean, desc: "Bypass overwrite are you sure prompt for existing files"], 8 | ] 9 | end 10 | cli_options.each do |args| 11 | class_option(*args) 12 | end 13 | 14 | def set_source 15 | if @options[:examples] 16 | set_template_source "examples/configset" 17 | else 18 | set_template_source "configset" 19 | end 20 | end 21 | 22 | def create_configset 23 | dest = "#{Lono.root}/app/configsets" 24 | directory ".", "#{dest}/#{name}" 25 | end 26 | 27 | def welcome_message 28 | puts <<~EOL 29 | #{"="*64} 30 | Congrats 🎉 You have successfully created a lono configset. 31 | 32 | More info: https://lono.cloud/docs/configsets 33 | 34 | EOL 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/lono/cli/new/helper.rb: -------------------------------------------------------------------------------- 1 | class Lono::CLI::New 2 | class Helper < Lono::CLI::New::Sequence 3 | def self.cli_options 4 | # required for name => underscore_name => app/blueprints/demo/helpers/%underscore_name%_helper.rb.tt 5 | argument :name, default: "custom", desc: "Helper name" 6 | 7 | [ 8 | [:force, type: :boolean, desc: "Bypass overwrite are you sure prompt for existing files"], 9 | [:blueprint, aliases: :b, desc: "Blueprint name. Only use you want a blueprint helper. Otherwise a project helper is generated"], 10 | ] 11 | end 12 | cli_options.each do |args| 13 | class_option(*args) 14 | end 15 | 16 | def set_source 17 | set_template_source "helper" 18 | end 19 | 20 | def create_helper 21 | if @options[:blueprint] 22 | create_blueprint_helper 23 | else 24 | create_project_helper 25 | end 26 | end 27 | 28 | private 29 | def create_blueprint_helper 30 | @blueprint = @options[:blueprint] # allows %underscore_name%_helper.rb.tt to access @blueprint 31 | logger.info "=> Generating #{underscore_name}_helper.rb" 32 | directory ".", "app/blueprints/#{@blueprint}/helpers" 33 | end 34 | 35 | def create_project_helper 36 | logger.info "=> Generating #{underscore_name}_helper.rb" 37 | directory ".", "app/helpers/#{underscore_name}" 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/lono/cli/new/hook.rb: -------------------------------------------------------------------------------- 1 | class Lono::CLI::New 2 | class Hook < Lono::CLI::New::Sequence 3 | def self.cli_options 4 | [ 5 | [:force, type: :boolean, desc: "Bypass overwrite are you sure prompt for existing files"], 6 | [:blueprint, aliases: :b, desc: "Blueprint name. Only use you want a blueprint helper. Otherwise a project helper is generated"], 7 | ] 8 | end 9 | cli_options.each do |args| 10 | class_option(*args) 11 | end 12 | 13 | def set_source 14 | set_template_source "hook" 15 | end 16 | 17 | def create_hook 18 | logger.info "=> Generating hook" 19 | if blueprint 20 | directory ".", "app/blueprints/#{blueprint}" 21 | else 22 | directory ".", "." 23 | end 24 | end 25 | 26 | private 27 | # So templates/hooks/config/hooks.rb.tt has access to blueprint 28 | def blueprint 29 | @options[:blueprint] 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/lono/cli/new/sequence.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | require 'active_support' 3 | require 'active_support/core_ext/string' 4 | require 'thor' 5 | require 'bundler' 6 | 7 | class Lono::CLI::New 8 | class Sequence < Thor::Group 9 | include Concerns 10 | include Lono::Utils::Logging 11 | include Thor::Actions 12 | 13 | private 14 | def self.set_template_source(folder) 15 | path = File.expand_path("../../../templates/#{folder}", __dir__) 16 | source_root path 17 | end 18 | 19 | def set_template_source(*paths) 20 | paths = paths.flatten.map do |path| 21 | File.expand_path("../../../templates/#{path}", __dir__) 22 | end 23 | set_template_paths(paths) 24 | end 25 | 26 | def set_template_paths(*paths) 27 | paths.flatten! 28 | # https://github.com/erikhuda/thor/blob/34df888d721ecaa8cf0cea97d51dc6c388002742/lib/thor/actions.rb#L128 29 | instance_variable_set(:@source_paths, nil) # unset instance variable cache 30 | # Using string with instance_eval because block doesnt have access to path at runtime. 31 | instance_eval %{ 32 | def self.source_paths 33 | #{paths.flatten.inspect} 34 | end 35 | } 36 | end 37 | 38 | def git_installed? 39 | system("type git > /dev/null 2>&1") 40 | end 41 | 42 | def run_git? 43 | options[:git] && git_installed? 44 | end 45 | 46 | def run_git_init 47 | return unless run_git? 48 | puts "=> Initialize git repo" 49 | run("git init") 50 | end 51 | 52 | def env 53 | Lono.env # allows for seed/vars/%env%.rb.tt 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/lono/cli/new/shim.rb: -------------------------------------------------------------------------------- 1 | class Lono::CLI::New 2 | class Shim < Thor::Group 3 | include Thor::Actions 4 | 5 | def self.cli_options 6 | [ 7 | [:path, aliases: %w[p], default: "/usr/local/bin/lono", desc: "path to save the shim script"], 8 | ] 9 | end 10 | cli_options.each { |args| class_option(*args) } 11 | 12 | def self.source_root 13 | File.expand_path("../../../templates/shim", __dir__) 14 | end 15 | 16 | def set_vars 17 | @path = @options[:path] 18 | end 19 | 20 | def create 21 | dest = @path 22 | template "lono", dest 23 | chmod dest, 0755 24 | end 25 | 26 | def message 27 | dir = File.dirname(@path) 28 | puts <<~EOL 29 | A lono shim as been generated at #{@path} 30 | Please make sure that it is found in the $PATH. 31 | 32 | You can double check with: 33 | 34 | which lono 35 | 36 | You should see 37 | 38 | $ which lono 39 | #{@path} 40 | 41 | If you do not, please add #{dir} to your PATH. 42 | You can usually do so by adding this line to ~/.bash_profile and opening a new terminal to check. 43 | 44 | export PATH=#{dir}:/$PATH 45 | 46 | Also note, the shim wrapper contains starter code. Though it should generally work for most systems, 47 | it might require adjustments depending on your system. 48 | EOL 49 | end 50 | 51 | private 52 | def switch_ruby_version_line 53 | rbenv_installed = system("type rbenv > /dev/null 2>&1") 54 | if rbenv_installed 55 | 'eval "$(rbenv init -)"' 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/lono/cli/new/test.rb: -------------------------------------------------------------------------------- 1 | class Lono::CLI::New 2 | class Test < Lono::Command 3 | Help = Lono::CLI::Help 4 | long_desc Help.text("new/test/blueprint") 5 | Blueprint.cli_options.each { |args| option(*args) } 6 | register(Blueprint, "blueprint", "blueprint NAME", "Generate new blueprint test.") 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/lono/cli/new/test/blueprint.rb: -------------------------------------------------------------------------------- 1 | class Lono::CLI::New::Test 2 | class Blueprint < Sequence 3 | argument :name 4 | 5 | def self.cli_options 6 | [ 7 | [:force, type: :boolean, desc: "Bypass overwrite are you sure prompt for existing files"], 8 | ] 9 | end 10 | cli_options.each { |args| class_option(*args) } 11 | 12 | def set_source 13 | name = Lono.config.test.framework 14 | framework = Lono::Plugin.find(name: name, type: "test_framework") 15 | set_template_paths("#{framework.root}/lib/templates/blueprint") 16 | end 17 | 18 | def generate 19 | dest = "app/blueprints/#{name}" 20 | directory ".", dest 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/lono/cli/new/test/sequence.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | require 'active_support' 3 | require 'active_support/core_ext/string' 4 | require 'thor' 5 | require 'bundler' 6 | 7 | class Lono::CLI::New::Test 8 | class Sequence < Lono::CLI::New::Sequence 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/lono/cli/opts.rb: -------------------------------------------------------------------------------- 1 | # Note moving this under Lono::CLI::Opts messes with the zeitwerk autoloader. 2 | # A weird workaround is calling Lono::CLI::Opts right after the autoloader and then it seems to fix itself. 3 | # It may be because there's a custom infleciton cli => CLI. Unsure. 4 | # Unsure if there are other side-effects with the workaround so named this: 5 | # Lono::Opts instead of Lono::CLI::Opts 6 | # 7 | # Also, there's Thor Options class, so this is named Opts to avoid having to fully qualify it. 8 | class Lono::CLI 9 | class Opts 10 | def initialize(cli) 11 | @cli = cli 12 | end 13 | 14 | def clean 15 | with_cli_scope do 16 | option :clean, type: :boolean, default: true, desc: "remove all output files before generating" 17 | end 18 | end 19 | 20 | def yes 21 | with_cli_scope do 22 | option :yes, aliases: :y, type: :boolean, desc: "Bypass are you sure prompt" 23 | end 24 | end 25 | 26 | # Based on https://github.com/rails/thor/blob/ab3b5be455791f4efb79f0efb4f88cc6b59c8ccf/lib/thor/actions.rb#L48 27 | def runtime_options 28 | with_cli_scope do 29 | option :force, :type => :boolean, :aliases => "-f", :group => :runtime, 30 | :desc => "Overwrite files that already exist" 31 | 32 | option :skip, :type => :boolean, :aliases => "-s", :group => :runtime, 33 | :desc => "Skip files that already exist" 34 | end 35 | end 36 | 37 | private 38 | def with_cli_scope(&block) 39 | @cli.instance_eval(&block) 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/lono/cli/s3.rb: -------------------------------------------------------------------------------- 1 | class Lono::CLI 2 | class S3 < Lono::Command 3 | opts = Opts.new(self) 4 | 5 | desc "deploy", "deploys lono managed s3 bucket" 6 | long_desc Help.text("s3/deploy") 7 | def deploy 8 | Lono::S3::Bucket.new(options).deploy 9 | end 10 | 11 | desc "show", "shows lono managed s3 bucket" 12 | long_desc Help.text("s3/show") 13 | opts.yes 14 | def show 15 | Lono::S3::Bucket.new(options).show 16 | end 17 | 18 | desc "delete", "deletes lono managed s3 bucket" 19 | long_desc Help.text("s3/delete") 20 | opts.yes 21 | def delete 22 | Lono::S3::Bucket.new(options).delete 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/lono/cli/script.rb: -------------------------------------------------------------------------------- 1 | class Lono::CLI 2 | class Script < Lono::Command 3 | desc "build", "Builds `output/scripts/scripts-md5sum.tgz` from `app/script` folder" 4 | long_desc Help.text("script/build") 5 | def build(blueprint) 6 | Build.new(blueprint, options).run 7 | end 8 | 9 | desc "upload", "Uploads `output/scripts/scripts-md5sum.tgz` to s3" 10 | long_desc Help.text("script/upload") 11 | def upload(blueprint) 12 | Upload.new(blueprint, options).run 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/lono/cli/script/base.rb: -------------------------------------------------------------------------------- 1 | class Lono::CLI::Script 2 | class Base < Lono::CLI::Base 3 | SCRIPTS_INFO_PATH = "#{Lono.root}/output/data/scripts_info.txt" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/lono/cli/script/upload.rb: -------------------------------------------------------------------------------- 1 | require "filesize" 2 | 3 | class Lono::CLI::Script 4 | class Upload < Base 5 | include Lono::AwsServices 6 | 7 | def run 8 | return unless scripts_built? 9 | 10 | upload(tarball_path) 11 | puts "Uploaded #{File.basename(s3_dest)} to s3." 12 | end 13 | 14 | def upload(tarball_path) 15 | puts "Uploading scripts.tgz (#{filesize}) to #{s3_dest}" 16 | obj = s3_resource.bucket(bucket_name).object(key) 17 | start_time = Time.now 18 | obj.upload_file(tarball_path) 19 | time_took = pretty_time(Time.now-start_time).color(:green) 20 | puts "Time took to upload code to s3: #{time_took}" 21 | end 22 | 23 | def filesize 24 | Filesize.from(File.size(tarball_path).to_s + " B").pretty 25 | end 26 | 27 | def s3_dest 28 | "s3://#{bucket_name}/#{key}" 29 | end 30 | 31 | def key 32 | # Example key: cloudformation/development/scripts/scripts-md5 33 | "#{Lono.env}/output/#{@blueprint.name}/scripts/#{File.basename(tarball_path)}" 34 | end 35 | 36 | def bucket_name 37 | Lono::S3::Bucket.name 38 | end 39 | 40 | # Scripts are only built if the app/scripts folder is non empty 41 | def scripts_built? 42 | File.exist?(SCRIPTS_INFO_PATH) && !tarball_path.empty? 43 | end 44 | 45 | def tarball_path 46 | IO.read(SCRIPTS_INFO_PATH).strip 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/lono/cli/seed.rb: -------------------------------------------------------------------------------- 1 | class Lono::CLI 2 | class Seed < Lono::CLI::Base 3 | def create 4 | logger.info "Creating starter config files for #{@blueprint.name}" 5 | seeder = Lono::Seeder.new(@options) 6 | seeder.run 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/lono/cli/status.rb: -------------------------------------------------------------------------------- 1 | class Lono::CLI 2 | class Status < Lono::CLI::Base 3 | include Lono::AwsServices 4 | 5 | def run 6 | names = Lono::Names.new(@options) 7 | stack = find_stack(@stack) 8 | if stack 9 | status = Lono::Cfn::Status.new(@stack, @options) 10 | success = status.run 11 | exit 1 unless success 12 | else 13 | logger.error "ERROR: stack #{@stack} not found".color(:red) 14 | exit 1 15 | end 16 | end 17 | end 18 | end -------------------------------------------------------------------------------- /lib/lono/cli/test.rb: -------------------------------------------------------------------------------- 1 | class Lono::CLI 2 | class Test 3 | def initialize(options={}) 4 | @options = options 5 | end 6 | 7 | def run 8 | config = Lono.config 9 | test_command = config.test.framework 10 | execute(test_command) 11 | end 12 | 13 | def execute(command) 14 | command = adjust_command(command) 15 | puts "=> #{command}" 16 | Kernel.exec(command) 17 | end 18 | 19 | def adjust_command(command) 20 | if cd_into_test? 21 | command = "bundle exec #{command}" unless command.include?("bundle exec") 22 | command = "cd test && #{command}" 23 | else 24 | command 25 | end 26 | end 27 | 28 | # Automatically cd into the test folder in case running within the root of a module. 29 | # Detect/guess that we're in a module folder vs the lono project 30 | def cd_into_test? 31 | !File.exist?("app") && File.exist?("test") && 32 | (File.exist?("template.rb") || File.exist?("template")) 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/lono/completer/script.rb: -------------------------------------------------------------------------------- 1 | class Lono::Completer::Script 2 | def self.generate 3 | bash_script = File.expand_path("script.sh", File.dirname(__FILE__)) 4 | puts "source #{bash_script}" 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/lono/completer/script.sh: -------------------------------------------------------------------------------- 1 | _lono() { 2 | COMPREPLY=() 3 | local word="${COMP_WORDS[COMP_CWORD]}" 4 | local words=("${COMP_WORDS[@]}") 5 | unset words[0] 6 | local completion=$(lono completion ${words[@]}) 7 | COMPREPLY=( $(compgen -W "$completion" -- "$word") ) 8 | } 9 | 10 | complete -F _lono lono 11 | -------------------------------------------------------------------------------- /lib/lono/component.rb: -------------------------------------------------------------------------------- 1 | module Lono 2 | class Component 3 | attr_reader :name 4 | def initialize(options={}) 5 | @options = options 6 | @name = options[:name] 7 | end 8 | 9 | def type 10 | self.class.name.to_s.split('::').last.underscore # IE: blueprint 11 | end 12 | 13 | def exist? 14 | !root.nil? 15 | end 16 | 17 | def type_dir 18 | type.pluralize # IE: blueprints 19 | end 20 | 21 | def root 22 | paths = Dir.glob("#{Lono.root}/{app,vendor}/#{type_dir}/*") 23 | paths.find do |path| 24 | found = path.sub(%r{.*/(app|vendor)/}, '') 25 | found == "#{type_dir}/#{@name}" # exact match 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/lono/concerns/aws_info.rb: -------------------------------------------------------------------------------- 1 | # Dont name AwsData. Think prefer AwsInfo vs AwsConcern 2 | module Lono::Concerns 3 | module AwsInfo 4 | extend Memoist 5 | delegate :region, to: :aws_data 6 | alias_method :aws_region, :region 7 | alias_method :current_region, :region 8 | 9 | def aws_data 10 | AwsData.new 11 | end 12 | memoize :aws_data 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/lono/concerns/names.rb: -------------------------------------------------------------------------------- 1 | module Lono::Concerns 2 | module Names 3 | extend Memoist 4 | def names 5 | Lono::Names.new(@options) 6 | end 7 | memoize :names 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/lono/configset.rb: -------------------------------------------------------------------------------- 1 | module Lono 2 | class Configset < Component 3 | attr_reader :resource 4 | attr_accessor :metadata 5 | def initialize(options={}) 6 | super 7 | @resource = options[:resource] 8 | end 9 | 10 | def path 11 | return unless root 12 | exts = %w[rb yml json] # rb highest precedence 13 | paths = exts.map { |ext| "#{root}/configset.#{ext}" } 14 | paths.find { |p| File.exist?(p) } 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/lono/core.rb: -------------------------------------------------------------------------------- 1 | require 'pathname' 2 | 3 | module Lono 4 | module Core 5 | extend Memoist 6 | 7 | cattr_accessor :argv 8 | cattr_accessor :check_project, default: true 9 | 10 | # allow testing frameworks to switch root 11 | cattr_writer :root 12 | def root 13 | path = @@root || ENV['LONO_ROOT'] || Dir.pwd 14 | Pathname.new(path) 15 | end 16 | 17 | def app 18 | ENV['LONO_APP'] 19 | end 20 | memoize :app 21 | 22 | def role 23 | ENV['LONO_ROLE'] 24 | end 25 | memoize :role 26 | 27 | def env 28 | ENV['LONO_ENV'] || 'dev' 29 | end 30 | memoize :env 31 | 32 | def extra 33 | ENV['LONO_EXTRA'] 34 | end 35 | memoize :extra 36 | 37 | def tmp_root 38 | ENV['LONO_TMP_ROOT'] || "/tmp/lono" 39 | end 40 | memoize :tmp_root 41 | 42 | def log_root 43 | "#{root}/log" 44 | end 45 | 46 | def configure(&block) 47 | App.instance.configure(&block) 48 | end 49 | 50 | # Generally, use the Lono.config instead of App.instance.config since it guarantees the load_project_config call 51 | def config 52 | App.instance.load_project_config 53 | App.instance.config 54 | end 55 | memoize :config 56 | 57 | # allow different logger when running up all or rspec-lono 58 | cattr_writer :logger 59 | def logger 60 | @@logger ||= config.logger 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/lono/ext.rb: -------------------------------------------------------------------------------- 1 | require_relative "ext/bundler" 2 | require_relative "ext/core/module" 3 | require_relative "ext/core/object" 4 | require_relative "ext/core/string" 5 | -------------------------------------------------------------------------------- /lib/lono/ext/bundler.rb: -------------------------------------------------------------------------------- 1 | # Bundler 2.0 does yet not have with_unbundled_env 2 | # Bundler 2.1 deprecates with_clean_env for with_unbundled_env 3 | 4 | require "bundler" 5 | def Bundler.with_unbundled_env(&block) 6 | with_clean_env(&block) 7 | end unless Bundler.respond_to?(:with_unbundled_env) 8 | -------------------------------------------------------------------------------- /lib/lono/ext/core/module.rb: -------------------------------------------------------------------------------- 1 | class Module 2 | # Include all modules within the relative folder. IE: for dsl/syntax/mod/* 3 | # 4 | # include Common 5 | # include Provider 6 | # # etc 7 | # 8 | # Caller lines are different for OSes: 9 | # 10 | # windows: "C:/Ruby31-x64/lib/ruby/gems/3.1.0/gems/lono-1.1.1/lib/lono/builder.rb:34:in `build'" 11 | # linux: "/home/ec2-user/.rvm/gems/ruby-3.0.3/gems/lono-1.1.1/lib/lono/compiler/dsl/syntax/mod.rb:4:in `'" 12 | # 13 | def include_modules(dir) 14 | caller_line = caller[0] 15 | parts = caller_line.split(':') 16 | calling_file = caller_line.match(/^[a-zA-Z]:/) ? parts[1] : parts[0] 17 | parent_dir = File.dirname(calling_file) 18 | 19 | full_dir = "#{parent_dir}/#{dir}" 20 | paths = Dir.glob("#{full_dir}/**/*.rb") 21 | if paths.empty? 22 | raise "Empty include_modules full_dir: #{full_dir}" 23 | end 24 | paths.each do |path| 25 | regexp = Regexp.new(".*/lib/") 26 | mod = path.sub(regexp, '').sub('.rb','').camelize 27 | c = mod.constantize 28 | include c if c.class == Module 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/lono/ext/core/object.rb: -------------------------------------------------------------------------------- 1 | class Object 2 | # Load custom helper methods from project 3 | def load_helper_files(dir, type: :blueprint) 4 | paths = Dir.glob("#{dir}/**/*.rb") 5 | paths.sort_by! { |p| p.size } # so namespaces are loaded first 6 | paths.each do |path| 7 | next unless File.file?(path) 8 | 9 | filename = if type == :project 10 | path.sub(%r{.*/helpers/[a-zA-Z\-_]+/},'').sub('.rb','') 11 | else # blueprint, configset 12 | path.sub(%r{.*/helpers/},'').sub('.rb','') 13 | end 14 | module_name = filename.camelize 15 | 16 | # Prepend a period so require works when LONO_ROOT is set to a relative path without a period. 17 | # Example: LONO_ROOT=tmp/lono_project 18 | first_char = path[0..0] 19 | path = "./#{path}" unless %w[. /].include?(first_char) 20 | 21 | # Examples: 22 | # project: 23 | # path: app/helpers/custom/custom_helper.rb 24 | # module_name: CustomHelper 25 | # blueprint: 26 | # path: app/blueprints/demo/helpers/outputs.rb 27 | # module_name: Outputs 28 | require path 29 | if path.include?("_helper.rb") 30 | self.class.send :include, module_name.constantize 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/lono/ext/core/string.rb: -------------------------------------------------------------------------------- 1 | module Lono 2 | refine String do 3 | def camelcase 4 | self.underscore.camelize 5 | end 6 | end 7 | end 8 | 9 | using Lono 10 | -------------------------------------------------------------------------------- /lib/lono/files.rb: -------------------------------------------------------------------------------- 1 | # Class name is Lono::Files because can represent a single file or a directory. 2 | # Also, it avoids conflicting with the stdlib File class name. 3 | # Files is like a "FileList". Using shorter Files name. 4 | module Lono 5 | class Files < Lono::CLI::Base 6 | include Concerns::Registration 7 | include Concerns::PostProcessing 8 | 9 | attr_reader :path, :layer, :type 10 | def initialize(options={}) 11 | super 12 | @path = options[:path] 13 | @layer = options[:layer] 14 | @type = @layer ? "layer" : "normal" 15 | @caller = options[:caller] # original caller stack at registration time 16 | end 17 | 18 | def full_path 19 | "#{@blueprint.root}/#{@path}" 20 | end 21 | 22 | class << self 23 | def register(options={}) 24 | path = options[:path] 25 | layer = options[:layer] 26 | # Registration uniquess based on both path and layer option 27 | file = files.find { |f| f.path == path && f.layer == layer } 28 | unless file 29 | file = Files.new(options.merge(caller: caller)) 30 | files << file 31 | end 32 | file.marker 33 | end 34 | 35 | delegate :empty?, :files, to: :files 36 | def files 37 | Registry.files 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/lono/files/base.rb: -------------------------------------------------------------------------------- 1 | class Lono::Files 2 | class Base < Lono::CLI::Base 3 | delegate :full_path, :output_path, :zip_name, :zip_path, to: :files 4 | attr_reader :files 5 | def initialize(options={}) 6 | super 7 | # @files is reference to Lono::Files self instance 8 | # IE: Lono::Files instance is the caller 9 | @files = options[:files] 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/lono/files/builder.rb: -------------------------------------------------------------------------------- 1 | class Lono::Files 2 | class Builder < Base 3 | extend Memoist 4 | include Lono::Builder::Context 5 | include Lono::Builder::Dsl::Syntax 6 | # Tricky must define files again to avoid conflict with files from loading the Dsl::Syntax 7 | attr_reader :files 8 | 9 | def run 10 | load_context 11 | if File.directory?(full_path) 12 | directory(@files) 13 | else 14 | template(@files) 15 | end 16 | end 17 | 18 | def template(files) 19 | sequence.send(:set_template_paths, @blueprint.root) 20 | src = files.path 21 | dest = files.output_path 22 | sequence.template(src, dest) 23 | end 24 | 25 | def directory(files) 26 | src = files.full_path 27 | sequence.send(:set_template_paths, src) 28 | sequence.destination_root = @files.output_path 29 | sequence.directory(".", verbose: false, force: true, context: binding) # Thor::Action 30 | end 31 | 32 | def sequence 33 | Lono::CLI::New::Sequence.new(@options) 34 | end 35 | memoize :sequence 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/lono/files/builder/lambda_layer.rb: -------------------------------------------------------------------------------- 1 | class Lono::Files::Builder 2 | class LambdaLayer < Lono::Files::Base 3 | def run 4 | layer = @options[:layer] 5 | return unless layer # triggers Lambda Layer building 6 | unless layer =~ /ruby/ 7 | logger.warn "WARN: Currently only support Ruby lambda layers automatically".color(:yellow) 8 | return 9 | end 10 | 11 | klass_name = "Lono::Files::Builder::LambdaLayer::#{layer.camelize}Packager" 12 | klass = klass_name.constantize 13 | packager = klass.new(@options) 14 | packager.build 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/lono/files/builder/lambda_layer/rsync.rb: -------------------------------------------------------------------------------- 1 | require 'shellwords' 2 | 3 | class Lono::Files::Builder::LambdaLayer 4 | module Rsync 5 | def sh(command) 6 | logger.debug "=> #{command}" 7 | out = `#{command}` 8 | logger.debug out 9 | success = $?.success? 10 | raise("ERROR: running command #{command}").color(:red) unless success 11 | success 12 | end 13 | 14 | def rsync(src, dest) 15 | # Using FileUtils.cp_r doesnt work if there are special files like socket files in the src dir. 16 | # Instead of using this hack https://bugs.ruby-lang.org/issues/10104 17 | # Using rsync to perform the copy. 18 | src.chop! if src.ends_with?('/') 19 | dest.chop! if dest.ends_with?('/') 20 | check_rsync_installed! 21 | # Ensures required trailing slashes 22 | FileUtils.mkdir_p(File.dirname(dest)) 23 | sh "rsync -a --links --no-specials --no-devices #{Shellwords.escape(src)}/ #{Shellwords.escape(dest)}/" 24 | end 25 | 26 | @@rsync_installed = false 27 | def check_rsync_installed! 28 | return if @@rsync_installed # only check once 29 | if system "type rsync > /dev/null 2>&1" 30 | @@rsync_installed = true 31 | else 32 | raise "ERROR: Rsync is required. Rsync does not seem to be installed.".color(:red) 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/lono/files/builder/lambda_layer/ruby_version.rb: -------------------------------------------------------------------------------- 1 | class Lono::Files::Builder::LambdaLayer 2 | class RubyVersion < Lono::Files::Base 3 | def initialize(options={}) 4 | # cfn passed from Finalizer::Files::Build#build_files to 5 | # Lono::Files#build_lambda_layer(@cfn) 6 | # This because Lono::Files at initialization only has registration info 7 | # The build info is passed to the build_lambda_layer method 8 | @cfn = options[:cfn] 9 | super 10 | end 11 | 12 | def check! 13 | # IE: logical_id = LayerVersion 14 | # attrs = attrs with attrs['Properties'] 15 | logical_id, attrs = @cfn['Resources'].find do |logical_id, attrs| 16 | attrs.dig('Properties', 'Content', 'S3Key') == @files.marker 17 | end 18 | props = attrs['Properties'] 19 | major, minor, _ = RUBY_VERSION.split('.') 20 | current_ruby = "ruby#{major}.#{minor}" 21 | ok = props['CompatibleRuntimes'].include?(current_ruby) 22 | return if ok 23 | 24 | runtimes = props['CompatibleRuntimes'].map { |s| s.sub('ruby','') }.join(', ') 25 | logger.error "ERROR: Current Ruby Version does not match the Lambda Layer Ruby Version".color(:red) 26 | logger.error <<~EOL 27 | Current Ruby: #{RUBY_VERSION} 28 | Lambda Layer Ruby Version: #{runtimes} 29 | 30 | Lono is unable to package up a Lambda Layer 31 | unless you use the same Ruby version on this machine. 32 | EOL 33 | logger.error "Resource: #{logical_id}" 34 | logger.error YAML.dump(attrs) 35 | exit 1 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/lono/files/compressor.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | 3 | class Lono::Files 4 | class Compressor < Base 5 | def run 6 | if File.directory?(output_path) 7 | zip_directory 8 | else 9 | zip_file 10 | end 11 | end 12 | 13 | def zip_file 14 | logger.debug "Zipping file to #{pretty_path(zip_path)}" 15 | # create zipfile at same level of file 16 | command = "cd #{File.dirname(output_path)} && zip -q #{zip_name} #{File.basename(output_path)}" 17 | execute_zip(command) 18 | end 19 | 20 | def zip_directory 21 | logger.debug "Zipping folder to #{zip_path}" 22 | command = "cd #{output_path} && zip --symlinks -rq #{zip_name} ." # create zipfile witih directory 23 | execute_zip(command) 24 | FileUtils.mv("#{output_path}/#{zip_name}", "#{File.dirname(output_path)}/#{zip_name}") # move zip back to the parent directory 25 | end 26 | 27 | private 28 | def execute_zip(command) 29 | check_zip_installed! 30 | logger.debug "=> #{command}".color(:green) 31 | `#{command}` 32 | return if $?.success? 33 | logger.info "ERROR: Fail trying to zip files".color(:red) 34 | logger.info "Command: #{command}".color(:red) 35 | exit 1 36 | end 37 | 38 | @@zip_installed = false 39 | def check_zip_installed! 40 | @@zip_installed = system("type zip > /dev/null 2>&1") 41 | if @@zip_installed 42 | @@zip_installed = true 43 | return 44 | end 45 | logger.error "ERROR: The command 'zip' is not installed.".color(:red) 46 | logger.error <<~EOL 47 | The file helper requires the zip command. 48 | Please install the zip command on this system. 49 | EOL 50 | exit 1 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/lono/files/concerns/post_processing.rb: -------------------------------------------------------------------------------- 1 | module Lono::Files::Concerns 2 | module PostProcessing 3 | extend Memoist 4 | 5 | def build 6 | Lono::Files::Builder.new(@options.merge(files: self)).run 7 | end 8 | 9 | def build_lambda_layer(cfn) 10 | Lono::Files::Builder::LambdaLayer.new(@options.merge(files: self, cfn: cfn)).run 11 | end 12 | 13 | def compress 14 | Lono::Files::Compressor.new(@options.merge(files: self)).run 15 | end 16 | 17 | def upload 18 | uploader.upload # Lono::S3::Uploader#upload 19 | end 20 | 21 | delegate :s3_path, :s3_key, to: :uploader 22 | def uploader 23 | Lono::S3::Uploader.new(zip_path) 24 | end 25 | memoize :uploader 26 | 27 | def zip_name 28 | name = File.basename(full_path) 29 | name << "-layer" if layer 30 | "#{name}-#{Lono::Md5.sum(full_path)}.zip" 31 | end 32 | 33 | def zip_path 34 | "#{File.dirname(output_path)}/#{zip_name}" 35 | end 36 | 37 | def output_path 38 | if layer 39 | # For Lambda Layer the final output path is the opt folder, where final artifact is zipped 40 | "#{Lono.root}/output/#{@blueprint.name}/layer/#{path}/opt" # Note: layer and opt 41 | else 42 | # For normal files the final output path in the compiled path 43 | compiled_path 44 | end 45 | end 46 | 47 | # Also used by LambdaLayer::RubyPackager#compiled_area 48 | def compiled_path 49 | "#{Lono.root}/output/#{@blueprint.name}/normal/#{path}" 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/lono/files/concerns/registration.rb: -------------------------------------------------------------------------------- 1 | module Lono::Files::Concerns 2 | module Registration 3 | def call_line 4 | @caller.find do |line| 5 | line.include?(Lono.root.to_s) 6 | end 7 | end 8 | 9 | # IE: 10 | # LONO:://files/function-normal 11 | # LONO:://files/function-layer 12 | def marker 13 | "LONO://#{@path}-#{@type}" 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/lono/files/registry.rb: -------------------------------------------------------------------------------- 1 | class Lono::Files 2 | class Registry 3 | class_attribute :files 4 | self.files = [] 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/lono/hooks/builder.rb: -------------------------------------------------------------------------------- 1 | module Lono::Hooks 2 | class Builder 3 | extend Memoist 4 | include Dsl 5 | include DslEvaluator 6 | include Lono::Utils 7 | 8 | # IE: dsl_file: config/hooks.rb 9 | attr_accessor :name 10 | def initialize(blueprint, name) 11 | @blueprint, @name = blueprint, name 12 | @hooks = {before: {}, after: {}} 13 | end 14 | 15 | def build 16 | evaluate_file("#{Lono.root}/config/hooks.rb") 17 | evaluate_file("#{@blueprint.root}/config/hooks.rb") 18 | @hooks.deep_stringify_keys! 19 | end 20 | memoize :build 21 | 22 | def run_hooks 23 | build 24 | run_each_hook("before") 25 | out = yield if block_given? 26 | run_each_hook("after") 27 | out 28 | end 29 | 30 | def run_each_hook(type) 31 | hooks = @hooks.dig(type, @name) || [] 32 | hooks.each do |hook| 33 | run_hook(type, hook) 34 | end 35 | end 36 | 37 | def run_hook(type, hook) 38 | return unless run?(hook) 39 | 40 | id = "#{type} #{@name}" 41 | label = " label: #{hook["label"]}" if hook["label"] 42 | logger.info "Hook: Running #{id} hook#{label}".color(:cyan) if Lono.config.hooks.show 43 | Runner.new(@blueprint, hook).run 44 | end 45 | 46 | def run?(hook) 47 | !!hook["execute"] 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/lono/hooks/concern.rb: -------------------------------------------------------------------------------- 1 | module Lono::Hooks 2 | module Concern 3 | def run_hooks(name, &block) 4 | hooks = Builder.new(@blueprint, name) 5 | hooks.build # build hooks 6 | hooks.run_hooks(&block) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/lono/hooks/dsl.rb: -------------------------------------------------------------------------------- 1 | module Lono::Hooks 2 | module Dsl 3 | def before(*commands, **props) 4 | commands.each do |name| 5 | each_hook(:before, name, props) 6 | end 7 | end 8 | 9 | def after(*commands, **props) 10 | commands.each do |name| 11 | each_hook(:after, name, props) 12 | end 13 | end 14 | 15 | def each_hook(type, name, props={}) 16 | @hooks[type][name] ||= [] 17 | @hooks[type][name] << props 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/lono/hooks/runner.rb: -------------------------------------------------------------------------------- 1 | module Lono::Hooks 2 | class Runner 3 | include Lono::Utils 4 | 5 | # exposing blueprint and hook so lono hooks have access to them via runner context. IE: 6 | # 7 | # class EnvExporter 8 | # def call(runner) 9 | # puts "runner.hook #{runner.hook}" 10 | # end 11 | # end 12 | # 13 | # Docs: http://lono.cloud/docs/config/hooks/ruby/#method-argument 14 | # 15 | attr_reader :blueprint, :hook 16 | def initialize(blueprint, hook) 17 | @blueprint, @hook = blueprint, hook 18 | @execute = @hook["execute"] 19 | end 20 | 21 | def run 22 | case @execute 23 | when String 24 | sh(@execute, exit_on_fail: @hook["exit_on_fail"]) 25 | when -> (e) { e.respond_to?(:public_instance_methods) && e.public_instance_methods.include?(:call) } 26 | executor = @execute.new 27 | when -> (e) { e.respond_to?(:call) } 28 | executor = @execute 29 | else 30 | logger.warn "WARN: execute option not set for hook: #{@hook.inspect}" 31 | end 32 | 33 | return unless executor 34 | 35 | meth = executor.method(:call) 36 | case meth.arity 37 | when 0 38 | executor.call # backwards compatibility 39 | when 1 40 | executor.call(self) 41 | else 42 | raise "The #{executor} call method definition has been more than 1 arguments and is not supported" 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/lono/importer.rb: -------------------------------------------------------------------------------- 1 | module Lono 2 | class Importer < Lono::CLI::Base 3 | def run 4 | # Examples: 5 | # Lono::Importer::Erb.new(source, options.clone).run 6 | # Lono::Importer::Dsl.new(source, options.clone).run 7 | type = @options[:type] || 'dsl' 8 | importer_class = "Lono::Importer::#{type.classify}" 9 | importer_class = Object.const_get(importer_class) 10 | importer_class.new(@options).run 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/lono/layering.rb: -------------------------------------------------------------------------------- 1 | require "active_support/lazy_load_hooks" 2 | 3 | module Lono 4 | module Layering 5 | def layers 6 | pre_layers + main_layers + post_layers 7 | end 8 | 9 | def main_layers 10 | super 11 | end 12 | 13 | def pre_layers 14 | [] 15 | end 16 | 17 | def post_layers 18 | [] 19 | end 20 | end 21 | end 22 | 23 | ActiveSupport.run_load_hooks(:lono_layering, Lono::Layering) 24 | -------------------------------------------------------------------------------- /lib/lono/logger.rb: -------------------------------------------------------------------------------- 1 | require 'logger' 2 | 3 | module Lono 4 | class Logger < ::Logger 5 | def initialize(*args) 6 | super 7 | self.formatter = Formatter.new 8 | self.level = ENV['LONO_LOG_LEVEL'] || :info # note: only respected when config.logger not set in config/app.rb 9 | end 10 | 11 | def format_message(severity, datetime, progname, msg) 12 | line = if @logdev.dev == $stdout || @logdev.dev == $stderr || @logdev.dev.is_a?(StringIO) 13 | msg # super simple format if stdout 14 | else 15 | super # use the configured formatter 16 | end 17 | line =~ /\n$/ ? line : "#{line}\n" 18 | end 19 | 20 | # Used to allow output to always go to stdout 21 | def stdout(msg, newline: true) 22 | if newline 23 | puts msg 24 | else 25 | print msg 26 | end 27 | end 28 | 29 | public :print 30 | public :printf 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/lono/logger/formatter.rb: -------------------------------------------------------------------------------- 1 | class Lono::Logger 2 | class Formatter < ::Logger::Formatter 3 | def call(severity, time, progname, msg) 4 | # careful changing the format. All::Summary uses a regexp on this format to remove the timestamp 5 | "[#{format_datetime(time)} ##{Process.pid} #{progname}]: #{msg}" 6 | end 7 | 8 | private 9 | def format_datetime(time) 10 | time.strftime("%Y-%m-%dT%H:%M:%S") 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/lono/names.rb: -------------------------------------------------------------------------------- 1 | module Lono 2 | class Names 3 | extend Memoist 4 | 5 | attr_reader :blueprint 6 | def initialize(options={}) 7 | @options = options 8 | @blueprint = options[:blueprint] 9 | end 10 | 11 | def stack 12 | expansion(Lono.config.names.stack) # IE: :APP-:BLUEPRINT-:ENV 13 | end 14 | memoize :stack 15 | 16 | def expansion(string) 17 | return string unless string.is_a?(String) # in case of nil 18 | 19 | string = string.dup 20 | vars = string.scan(/:\w+/) # => [":APP", ":BLUEPRINT", :ENV"] 21 | vars.each do |var| 22 | string.gsub!(var, var_value(var)) 23 | end 24 | cleanse(string) 25 | end 26 | 27 | def var_value(unexpanded) 28 | name = unexpanded.sub(':','').downcase 29 | if respond_to?(name) 30 | send(name).to_s # value 31 | else 32 | unexpanded 33 | end 34 | end 35 | 36 | def cleanse(string) 37 | string.sub(/^-+/,'').sub(/-+$/,'') # remove leading and trailing - 38 | .gsub(%r{-+},'-') # remove double dashes are more. IE: -- => - 39 | .gsub('_','-') # underscores are not allowed in CloudFormation stack names 40 | end 41 | 42 | delegate :app, :role, :env, :extra, to: :lono 43 | def lono 44 | Lono 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/lono/plugin.rb: -------------------------------------------------------------------------------- 1 | module Lono 2 | class Plugin 3 | include Meta 4 | delegate_to_meta :name, :root 5 | 6 | def initialize(options={}) 7 | @options = options 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/lono/plugin/meta.rb: -------------------------------------------------------------------------------- 1 | class Lono::Plugin 2 | module Meta 3 | extend ActiveSupport::Concern 4 | 5 | included do 6 | cattr_accessor :meta, default: [] 7 | end 8 | 9 | class_methods do 10 | def register(o={}) 11 | found = meta.find { |m| m[:name] == o[:name] && m[:type] == o[:type] } 12 | meta << o unless found 13 | end 14 | 15 | def find(o={}) 16 | found = meta.find { |m| m[:name] == o[:name] && m[:type] == o[:type] } 17 | Lono::Plugin.new(found) if found 18 | end 19 | 20 | def delegate_to_meta(*attrs) 21 | attrs.each do |attr| 22 | attr = attr.to_sym 23 | define_method attr do 24 | @options[attr] 25 | end 26 | end 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/lono/plugin/tester.rb: -------------------------------------------------------------------------------- 1 | class Lono::Plugin 2 | class Tester 3 | class << self 4 | def register(name, options={}) 5 | Lono::Plugin.register( 6 | name: name, 7 | type: "test_framework", 8 | root: options[:root], 9 | ) 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/lono/registration.rb: -------------------------------------------------------------------------------- 1 | module Lono 2 | class Registration < Command 3 | desc "check", "Check .lono/registration.yml file" 4 | long_desc Help.text(:check) 5 | option :debug, type: :boolean, desc: "Enable debug mode" 6 | def check 7 | User.new(options.merge(cli: true)).check 8 | end 9 | 10 | desc "temp_check", "Check .lono/temp.yml file", hide: true 11 | def temp_check 12 | Temp.new.check 13 | end 14 | 15 | def self.check 16 | Check.new.check 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/lono/s3/aws_setup.rb: -------------------------------------------------------------------------------- 1 | module Lono::S3 2 | class AwsSetup 3 | include Lono::AwsServices 4 | include Lono::Utils::Logging 5 | 6 | def check! 7 | s3.config.region 8 | rescue Aws::Errors::MissingRegionError => e 9 | logger.info "ERROR: #{e.class}: #{e.message}".color(:red) 10 | logger.info <<~EOL 11 | Unable to detect the AWS_REGION to make AWS API calls. This is might be because the AWS access 12 | has not been set up yet. Please either your ~/.aws files. 13 | EOL 14 | exit 1 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/lono/s3/rollback.rb: -------------------------------------------------------------------------------- 1 | module Lono::S3 2 | class Rollback < Lono::Cfn::Deploy::Rollback 3 | # override initialize 4 | def initialize(stack) 5 | @stack = stack 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/lono/script/base.rb: -------------------------------------------------------------------------------- 1 | module Lono::Script 2 | class Base < Lono::CLI::Base 3 | SCRIPTS_INFO_PATH = "#{Lono.root}/output/data/scripts_info.txt" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/lono/script/upload.rb: -------------------------------------------------------------------------------- 1 | require "filesize" 2 | 3 | module Lono::Script 4 | class Upload < Base 5 | include Lono::AwsServices 6 | include Lono::Utils::Pretty 7 | include Lono::Utils::Logging 8 | 9 | def run 10 | return unless scripts_built? 11 | 12 | upload(tarball_path) 13 | logger.info "Uploaded #{File.basename(s3_dest)} to s3." 14 | end 15 | 16 | def upload(tarball_path) 17 | logger.info "Uploading scripts.tgz (#{filesize}) to #{s3_dest}" 18 | obj = s3_resource.bucket(bucket_name).object(key) 19 | start_time = Time.now 20 | obj.upload_file(tarball_path) 21 | time_took = pretty_time(Time.now-start_time).color(:green) 22 | logger.info "Time took to upload code to s3: #{time_took}" 23 | end 24 | 25 | def filesize 26 | Filesize.from(File.size(tarball_path).to_s + " B").pretty 27 | end 28 | 29 | def s3_dest 30 | "s3://#{bucket_name}/#{key}" 31 | end 32 | 33 | def key 34 | # Example key: cloudformation/development/scripts/scripts-md5 35 | "#{dest_folder}/#{File.basename(tarball_path)}" 36 | end 37 | 38 | def bucket_name 39 | Lono::S3::Bucket.name 40 | end 41 | 42 | def dest_folder 43 | "#{Lono.env}/scripts" 44 | end 45 | 46 | # Scripts are only built if the scripts folder is non empty 47 | def scripts_built? 48 | File.exist?(SCRIPTS_INFO_PATH) && !tarball_path.empty? 49 | end 50 | 51 | def tarball_path 52 | IO.read(SCRIPTS_INFO_PATH).strip 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/lono/user_data.rb: -------------------------------------------------------------------------------- 1 | module Lono 2 | # This class is not use by lono internally. It is really only meant to be 3 | # exposed to the lono user_data command so users can debug their generated 4 | # user_data scripts. It is useful for debugging. 5 | # 6 | # Normally, the Lono::Erb#run method generates the CloudFormation template 7 | # and embeds user-data script into the template. 8 | class UserData < Lono::CLI::Base 9 | include Lono::Builder::Dsl::Syntax 10 | 11 | def initialize(options={}) 12 | super 13 | @name = options[:name] 14 | @path = "#{Lono.root}/app/user_data/#{@name}.sh" 15 | end 16 | 17 | def generate 18 | pretty_path = pretty_path(@path) 19 | logger.info "Building user_data for '#{@name}' at #{pretty_path}" 20 | if File.exist?(@path) 21 | logger.info RenderMePretty.result(@path, context: self) 22 | else 23 | logger.info "ERROR: #{pretty_path} does not exist".color(:red) 24 | exit 1 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/lono/utils.rb: -------------------------------------------------------------------------------- 1 | module Lono 2 | module Utils 3 | include_modules "utils" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/lono/utils/call_line.rb: -------------------------------------------------------------------------------- 1 | module Lono::Utils 2 | module CallLine 3 | include Pretty 4 | 5 | def lono_call_line 6 | caller.find { |l| l.include?(Lono.root.to_s) } 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/lono/utils/logging.rb: -------------------------------------------------------------------------------- 1 | module Lono::Utils 2 | module Logging 3 | def logger 4 | Lono.logger 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/lono/utils/pretty.rb: -------------------------------------------------------------------------------- 1 | module Lono::Utils 2 | module Pretty 3 | def pretty_path(path) 4 | path.sub("#{Lono.root}/",'') 5 | end 6 | 7 | # http://stackoverflow.com/questions/4175733/convert-duration-to-hoursminutesseconds-or-similar-in-rails-3-or-ruby 8 | def pretty_time(total_seconds) 9 | minutes = (total_seconds / 60) % 60 10 | seconds = total_seconds % 60 11 | if total_seconds < 60 12 | "#{seconds.to_i}s" 13 | else 14 | "#{minutes.to_i}m #{seconds.to_i}s" 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/lono/utils/quit.rb: -------------------------------------------------------------------------------- 1 | module Lono::Utils 2 | module Quit 3 | def quit(code) 4 | ENV['LONO_TEST'] ? raise : exit(code) 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/lono/utils/sh.rb: -------------------------------------------------------------------------------- 1 | module Lono::Utils 2 | module Sh 3 | def sh(command, options={}) 4 | on_fail = options[:on_fail].nil? ? "raise" : options[:on_fail] 5 | strategy = options[:strategy].nil? ? "system" : options[:strategy] # system or backticks 6 | 7 | logger.info "=> #{command}" 8 | 9 | if strategy == "backticks" 10 | out = `#{command}` 11 | logger.debug out 12 | success = $?.success? 13 | else 14 | success = system(command) 15 | end 16 | 17 | result = strategy == "backticks" ? out : success 18 | return result if success 19 | 20 | logger.error "ERROR: Running #{command}" 21 | case on_fail.to_sym 22 | when :raise 23 | raise 24 | when :exit 25 | status = $?.exitstatus 26 | exit status 27 | end 28 | 29 | result 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/lono/utils/sure.rb: -------------------------------------------------------------------------------- 1 | module Lono::Utils 2 | module Sure 3 | def sure?(message, desc=nil) 4 | if @options[:yes] 5 | yes = 'y' 6 | else 7 | out = message 8 | if desc 9 | out += "\n#{desc}\nAre you sure? (y/N) " 10 | else 11 | out += " (y/N) " 12 | end 13 | print out 14 | yes = $stdin.gets 15 | end 16 | 17 | unless yes =~ /^y/ 18 | puts "Whew! Exiting." 19 | exit 0 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/lono/version.rb: -------------------------------------------------------------------------------- 1 | module Lono 2 | VERSION = "8.0.0-rc6" 3 | end 4 | -------------------------------------------------------------------------------- /lib/lono/yamler/loader.rb: -------------------------------------------------------------------------------- 1 | require "yaml" 2 | 3 | module Lono::Yamler 4 | class Loader 5 | def self.load(text) 6 | new(text).load 7 | end 8 | 9 | def initialize(text) 10 | @text = text 11 | end 12 | 13 | def load 14 | add_domain_types! 15 | YAML.load(@text) 16 | end 17 | 18 | private 19 | def add_domain_types! 20 | intrinsic_functions.each do |name| 21 | YAML.add_domain_type('', name) do |type,val| 22 | key = type.split('::').last 23 | key = "Fn::" + key unless name == 'Ref' 24 | { key => val } 25 | end 26 | end 27 | end 28 | 29 | def intrinsic_functions 30 | %w[ 31 | And 32 | Base64 33 | Cidr 34 | Equals 35 | FindInMap 36 | GetAtt 37 | GetAZs 38 | If 39 | If 40 | ImportValue 41 | Join 42 | Not 43 | Or 44 | Ref 45 | Select 46 | Split 47 | Sub 48 | Transform 49 | ] 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/lono/yamler/validator.rb: -------------------------------------------------------------------------------- 1 | module Lono::Yamler 2 | class Validator 3 | include Lono::Utils::Logging 4 | include Lono::Utils::Pretty 5 | 6 | def initialize(path) 7 | @path = path 8 | end 9 | 10 | def validate! 11 | validate_yaml(@path) 12 | end 13 | 14 | def validate_yaml(path) 15 | text = IO.read(path) 16 | begin 17 | YAML.load(text) 18 | rescue Psych::SyntaxError => e 19 | handle_yaml_syntax_error(e, path) 20 | end 21 | end 22 | 23 | def handle_yaml_syntax_error(e, path) 24 | logger.error "ERROR: Invalid YAML. #{e.message}".color(:red) 25 | logger.error "Template for debugging: #{pretty_path(path)}" 26 | 27 | # Grab line info. Example error: 28 | # ERROR: (): could not find expected ':' while scanning a simple key at line 2 column 1 29 | md = e.message.match(/at line (\d+) column (\d+)/) 30 | line = md[1].to_i 31 | 32 | DslEvaluator.print_code(path, line) 33 | exit 1 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/templates/blueprint/template.rb: -------------------------------------------------------------------------------- 1 | # This is where you put your infrastructure code 2 | # Docs: https://lono.cloud/docs/dsl/ 3 | -------------------------------------------------------------------------------- /lib/templates/configset/configset.rb: -------------------------------------------------------------------------------- 1 | # This is where you write your configset code 2 | # Docs: https://lono.cloud/docs/configsets/ 3 | -------------------------------------------------------------------------------- /lib/templates/examples/blueprint/template.rb: -------------------------------------------------------------------------------- 1 | parameter("BucketName", Conditional: true) 2 | parameter("AccessControl", Default: "Private") 3 | 4 | resource("Bucket", "AWS::S3::Bucket", 5 | BucketName: ref("BucketName", Conditional: true), 6 | AccessControl: ref("AccessControl"), 7 | ) 8 | 9 | output("BucketName", ref("Bucket")) 10 | -------------------------------------------------------------------------------- /lib/templates/examples/configset/configset.rb: -------------------------------------------------------------------------------- 1 | configset("config1") do 2 | command("test", 3 | command: 'echo "$CFNTEST" > test1.txt', 4 | env: { 5 | CFNTEST: "I come from config1" 6 | } 7 | ) 8 | end 9 | configset("config2") do 10 | command("test", 11 | command: 'echo "$CFNTEST" > test2.txt', 12 | env: { 13 | CFNTEST: "I come from config2" 14 | } 15 | ) 16 | end 17 | -------------------------------------------------------------------------------- /lib/templates/helper/%underscore_name%_helper.rb.tt: -------------------------------------------------------------------------------- 1 | module <%= class_name %>Helper 2 | # Your code goes here 3 | # def custom_method 4 | # ... 5 | # end 6 | end 7 | -------------------------------------------------------------------------------- /lib/templates/hook/config/hooks.rb.tt: -------------------------------------------------------------------------------- 1 | <% 2 | message = if blueprint 3 | "app/blueprints/#{blueprint}/config/hooks.rb: test blueprint hook for build" 4 | else 5 | "config/hooks.rb: test project hook for build" 6 | end 7 | -%> 8 | before("build", 9 | execute: "echo '<%= message %> before hook'", 10 | ) 11 | 12 | after("build", 13 | execute: "echo '<%= message %> after hook'" 14 | ) 15 | -------------------------------------------------------------------------------- /lib/templates/project/.gitignore: -------------------------------------------------------------------------------- 1 | .lono 2 | output 3 | tmp 4 | -------------------------------------------------------------------------------- /lib/templates/project/Gemfile.tt: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # gem "lono", "~> <%= Lono::VERSION %>" 4 | 5 | gem "lono", git: "https://github.com/boltops-tools/lono", branch: "master" 6 | gem "rspec-lono", git: "https://github.com/boltops-tools/rspec-lono", branch: "master" 7 | -------------------------------------------------------------------------------- /lib/templates/project/README.md: -------------------------------------------------------------------------------- 1 | # Lono Project 2 | 3 | This is a Lono project. It contains code to provision Cloud infrastructure built with [CloudFormation](https://aws.amazon.com/cloudformation/) and the [Lono Framework](https://lono.cloud/). 4 | 5 | ## Deploy 6 | 7 | To deploy 8 | 9 | lono up demo # where demo app/blueprints/demo 10 | -------------------------------------------------------------------------------- /lib/templates/project/config/app.rb: -------------------------------------------------------------------------------- 1 | Lono.configure do |config| 2 | config.logger.level = "info" 3 | end 4 | -------------------------------------------------------------------------------- /lib/templates/shim/lono: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | <%= switch_ruby_version_line %> 3 | if [ -f config/app.rb ]; then 4 | exec bundle exec lono "$@" 5 | else 6 | exec lono "$@" 7 | fi 8 | -------------------------------------------------------------------------------- /spec/fixtures/configsets/dsl/httpd/configset.rb: -------------------------------------------------------------------------------- 1 | package("yum", 2 | httpd: [] 3 | ) 4 | file("/var/www/html/index.html", 5 | content: "

headline

" 6 | ) 7 | service("sysvinit", 8 | httpd: { 9 | enabled: true, 10 | ensureRunning: true, 11 | } 12 | ) 13 | -------------------------------------------------------------------------------- /spec/fixtures/configsets/erb/httpd/configset.yml: -------------------------------------------------------------------------------- 1 | AWS::CloudFormation::Init: 2 | configSets: 3 | default: 4 | - httpd 5 | httpd: 6 | packages: 7 | yum: 8 | httpd: [] 9 | files: 10 | "/var/www/html/index.html": 11 | content: | 12 | <%= indent(@html, 10) %> 13 | services: 14 | sysvinit: 15 | httpd: 16 | enabled: true 17 | ensureRunning: true -------------------------------------------------------------------------------- /spec/fixtures/configsets/erb/httpd/vars.rb: -------------------------------------------------------------------------------- 1 | @html = "test html" 2 | -------------------------------------------------------------------------------- /spec/fixtures/configsets/snippets/config1.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWS::CloudFormation::Init": { 3 | "configSets": { 4 | "default": ["aaa1", "aaa2"] 5 | }, 6 | "aaa1": { 7 | "commands": { 8 | "test": { 9 | "command": "echo from-aaa1 > test1.txt" 10 | } 11 | } 12 | }, 13 | "aaa2": { 14 | "commands": { 15 | "test": { 16 | "command": "echo from-aaa2 > test1.txt" 17 | } 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /spec/fixtures/configsets/snippets/config2.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWS::CloudFormation::Init": { 3 | "configSets": { 4 | "default": ["bbb1", "bbb2"] 5 | }, 6 | "bbb1": { 7 | "commands": { 8 | "test": { 9 | "command": "echo from-bbb1 > test2.txt" 10 | } 11 | } 12 | }, 13 | "bbb2": { 14 | "commands": { 15 | "test": { 16 | "command": "echo from-bbb2 > test2.txt" 17 | } 18 | } 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /spec/fixtures/configsets/snippets/single.yml: -------------------------------------------------------------------------------- 1 | --- 2 | AWS::CloudFormation::Init: 3 | config: 4 | commands: 5 | c1: 6 | command: echo c1 >> test.txt 7 | c2: 8 | command: echo c2 >> test.txt 9 | -------------------------------------------------------------------------------- /spec/fixtures/configsets/templates/ec2-and-sg-metadata.yml: -------------------------------------------------------------------------------- 1 | Resources: 2 | Instance: 3 | Type: AWS::EC2::Instance 4 | Properties: 5 | ImageId: ami-111 6 | InstanceType: t3.micro 7 | Metadata: 8 | AWS::CloudFormation::Init: 9 | configSets: 10 | default: 11 | - "existing" 12 | existing: 13 | commands: 14 | test: 15 | command: "echo existing1 >> /tmp/test.txt" 16 | SecurityGroup: 17 | Type: AWS::EC2::SecurityGroup 18 | Properties: 19 | GroupName: test 20 | Metadata: 21 | AWS::CloudFormation::Init: 22 | configSets: 23 | default: 24 | - "existing" 25 | existing: 26 | commands: 27 | test: 28 | command: "echo existing2 >> /tmp/test.txt" 29 | -------------------------------------------------------------------------------- /spec/fixtures/configsets/templates/ec2-and-sg.yml: -------------------------------------------------------------------------------- 1 | Resources: 2 | Instance: 3 | Type: AWS::EC2::Instance 4 | Properties: 5 | ImageId: ami-111 6 | InstanceType: t3.micro 7 | SecurityGroup: 8 | Type: AWS::EC2::SecurityGroup 9 | Properties: 10 | GroupName: test 11 | -------------------------------------------------------------------------------- /spec/fixtures/configsets/templates/ec2-multiple.yml: -------------------------------------------------------------------------------- 1 | Resources: 2 | Instance: 3 | Metadata: 4 | AWS::CloudFormation::Init: 5 | configSets: 6 | default: 7 | - "existing" 8 | existing: 9 | commands: 10 | test: 11 | command: "echo existing >> /tmp/test.txt" 12 | Type: AWS::EC2::Instance 13 | Properties: 14 | ImageId: ami-111 15 | InstanceType: t3.micro 16 | -------------------------------------------------------------------------------- /spec/fixtures/configsets/templates/ec2-no-metadata.yml: -------------------------------------------------------------------------------- 1 | Resources: 2 | Instance: 3 | Type: AWS::EC2::Instance 4 | Properties: 5 | ImageId: ami-111 6 | InstanceType: t3.micro 7 | -------------------------------------------------------------------------------- /spec/fixtures/configsets/templates/ec2-single.yml: -------------------------------------------------------------------------------- 1 | Resources: 2 | Instance: 3 | Metadata: 4 | AWS::CloudFormation::Init: 5 | config: 6 | commands: 7 | existing: 8 | command: existing >> test.txt 9 | Type: AWS::EC2::Instance 10 | Properties: 11 | ImageId: ami-111 12 | InstanceType: t3.micro 13 | -------------------------------------------------------------------------------- /spec/fixtures/md5/a.rb: -------------------------------------------------------------------------------- 1 | a 2 | -------------------------------------------------------------------------------- /spec/fixtures/md5/folder/a.rb: -------------------------------------------------------------------------------- 1 | a 2 | -------------------------------------------------------------------------------- /spec/fixtures/md5/folder1/folder2/a.rb: -------------------------------------------------------------------------------- 1 | a 2 | -------------------------------------------------------------------------------- /spec/fixtures/validator/bad.yml: -------------------------------------------------------------------------------- 1 | test: 1 2 | foo 3 | test 4 | -------------------------------------------------------------------------------- /spec/lono/builder/configset/definition/dsl_spec.rb: -------------------------------------------------------------------------------- 1 | describe Lono::Builder::Configset::Definition::Dsl do 2 | let(:dsl) do 3 | path = "spec/fixtures/configsets/dsl/httpd/configset.rb" 4 | meta = {path: path, resource: "LaunchTemplate", name: "main"} 5 | dsl = Lono::Builder::Configset::Definition::Dsl.new(meta: meta) 6 | configset = Lono::Configset.new(meta) 7 | allow(configset).to receive(:path).and_return path 8 | dsl.instance_variable_set(:@configset, configset) 9 | dsl 10 | end 11 | 12 | context("example") do 13 | it "evaluate" do 14 | metadata = dsl.evaluate 15 | expect(metadata).to be_a(Hash) 16 | data = metadata["Metadata"] 17 | init_key = data.key?("AWS::CloudFormation::Init") 18 | expect(init_key).to be true 19 | template =<<~EOL 20 | --- 21 | AWS::CloudFormation::Init: 22 | configSets: 23 | default: 24 | - main 25 | main: 26 | packages: 27 | yum: 28 | httpd: [] 29 | files: 30 | "/var/www/html/index.html": 31 | content: "

headline

" 32 | services: 33 | sysvinit: 34 | httpd: 35 | enabled: true 36 | ensureRunning: true 37 | EOL 38 | expect(YAML.dump(data)).to eq template 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/lono/builder/configset/definition/erb_spec.rb: -------------------------------------------------------------------------------- 1 | describe Lono::Builder::Configset::Definition::Erb do 2 | let(:erb) do 3 | path = "spec/fixtures/configsets/erb/httpd/configset.yml" 4 | meta = {path: path, resource: "LaunchTemplate", name: "main"} 5 | erb = Lono::Builder::Configset::Definition::Erb.new(blueprint: "demo", meta: meta) 6 | configset = Lono::Configset.new(meta) 7 | allow(configset).to receive(:path).and_return path 8 | allow(configset).to receive(:root).and_return "spec/fixtures/configsets/erb/httpd" 9 | erb.instance_variable_set(:@configset, configset) 10 | erb 11 | end 12 | 13 | context("example") do 14 | it "evaluate" do 15 | metadata = erb.evaluate 16 | expect(metadata).to be_a(Hash) 17 | data = metadata["Metadata"] 18 | init_key = data.key?("AWS::CloudFormation::Init") 19 | expect(init_key).to be true 20 | template =<<~EOL 21 | --- 22 | AWS::CloudFormation::Init: 23 | configSets: 24 | default: 25 | - httpd 26 | httpd: 27 | packages: 28 | yum: 29 | httpd: [] 30 | files: 31 | "/var/www/html/index.html": 32 | content: 'test html 33 | 34 | ' 35 | services: 36 | sysvinit: 37 | httpd: 38 | enabled: true 39 | ensureRunning: true 40 | EOL 41 | expect(YAML.dump(data)).to eq template 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/lono/cfn/plan/diff/data_spec.rb: -------------------------------------------------------------------------------- 1 | describe Lono::Cfn::Plan::Diff::Data do 2 | it "diff" do 3 | old = {k1: "v1", k2: "v2", k4: "v4"} 4 | new = {k2: "v2changed", k3: "v3", k4: "v4"} 5 | data = described_class.new(old, new) 6 | data.show 7 | expect(logs).to include("Added") 8 | expect(logs).to include("Removed") 9 | expect(logs).to include("Modified") 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/lono/cli_spec.rb: -------------------------------------------------------------------------------- 1 | describe Lono::CLI do 2 | it "version" do 3 | out = sh "exe/lono version" 4 | expect(out).to match /\d+/ 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/lono/completion_spec.rb: -------------------------------------------------------------------------------- 1 | describe Lono::CLI do 2 | describe "lono completion" do 3 | commands = { 4 | # "new" => "name", # options is the completion because it's a Thor::Group 5 | "up" => "blueprint", 6 | } 7 | commands.each do |command, expected_word| 8 | it "#{command}" do 9 | out = execute("exe/lono completion #{command}") 10 | expect(out).to include(expected_word) # only checking for one word for simplicity 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/lono/md5_spec.rb: -------------------------------------------------------------------------------- 1 | describe Lono::Md5 do 2 | let(:md5) { Lono::Md5 } 3 | context "file" do 4 | it "md5" do 5 | sum = md5.sum("spec/fixtures/md5/a.rb") 6 | expect(sum).to match "a03f0901" 7 | end 8 | 9 | it "name" do 10 | name = md5.name("spec/fixtures/md5/a.rb") 11 | expect(name).to eq "spec/fixtures/md5/a-a03f0901.rb" 12 | end 13 | end 14 | 15 | context "folder" do 16 | it "md5" do 17 | sum = md5.sum("spec/fixtures/md5/folder") 18 | expect(sum).to eq "a03f0901" 19 | end 20 | 21 | it "name" do 22 | name = md5.name("spec/fixtures/md5/folder") 23 | expect(name).to eq "spec/fixtures/md5/folder-a03f0901.zip" 24 | end 25 | end 26 | 27 | context "nested folder" do 28 | it "md5" do 29 | sum = md5.sum("spec/fixtures/md5/folder1/folder2") 30 | expect(sum).to eq "a03f0901" 31 | end 32 | 33 | it "name" do 34 | name = md5.name("spec/fixtures/md5/folder1/folder2") 35 | expect(name).to eq "spec/fixtures/md5/folder1/folder2-a03f0901.zip" 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/lono/yamler/validator_spec.rb: -------------------------------------------------------------------------------- 1 | describe Lono::Yamler::Validator do 2 | subject { Lono::Yamler::Validator.new(path) } 3 | let(:path) { "spec/fixtures/validator/bad.yml" } 4 | it "validate shows exact line of error code where yaml is invalid" do 5 | out = subject.validate! 6 | expect(out).to include("2 foo") 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # Ensures aws api never called. Fixture home folder does not contain ~/.aws/credentials 2 | ENV['HOME'] = "#{Dir.pwd}/spec/fixtures/home" 3 | ENV['AWS_REGION'] ||= "us-west-2" 4 | ENV['LONO_TEST'] = '1' 5 | ENV['LONO_ROOT'] = "#{Dir.pwd}/spec/fixtures/project" 6 | 7 | require "pp" 8 | require "byebug" 9 | 10 | root = File.expand_path("../", File.dirname(__FILE__)) 11 | require "#{root}/lib/lono" 12 | 13 | Dir.glob("./spec/spec_helper/**/*.rb").each do |file| 14 | require file 15 | end 16 | module Helper 17 | include SpecHelper::Logging 18 | include SpecHelper::Execute 19 | end 20 | 21 | RSpec.configure do |c| 22 | c.include Helper 23 | c.before(:each) do 24 | override_logger 25 | end 26 | c.after(:each) do 27 | flush_overriden_logger 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/spec_helper/execute.rb: -------------------------------------------------------------------------------- 1 | module SpecHelper 2 | module Execute 3 | def execute(cmd) 4 | show_command = ENV['LONO_DEBUG_SPECS'] || ENV['SHOW_COMMAND'] 5 | puts "Running: #{cmd}" if show_command 6 | out = `#{cmd}` 7 | puts out if show_command 8 | out 9 | end 10 | alias_method :sh, :execute 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/spec_helper/logging.rb: -------------------------------------------------------------------------------- 1 | module SpecHelper 2 | module Logging 3 | # Should be called by before(:each) 4 | def override_logger 5 | @logs ||= StringIO.new 6 | logger = Lono::Logger.new(@logs) 7 | Lono.logger = logger 8 | end 9 | 10 | # Should be called by after(:each) 11 | def flush_overriden_logger 12 | @logs = StringIO.new 13 | end 14 | 15 | # stored logs 16 | def logs 17 | @logs.string 18 | end 19 | end 20 | end 21 | --------------------------------------------------------------------------------