diff --git a/.github/workflows/_update_terraform.yml b/.github/workflows/_update_terraform.yml new file mode 100644 index 0000000..3547d23 --- /dev/null +++ b/.github/workflows/_update_terraform.yml @@ -0,0 +1,60 @@ +name: Update Terraform +on: + workflow_call: + secrets: + PERSONAL_ACCESS_TOKEN: + required: true + inputs: + image_tag: + description: Tag for the image for docker/ghcr registries + required: true + type: string + deployment_environment: + description: The terraform target environment + required: true + type: string + default: staging +jobs: + update: + runs-on: ubuntu-latest + env: + GIT_SHA: ${{ github.sha }} + GIT_TAG: ${{ inputs.image_tag }} + steps: + - name: Checkout terraform config repo + uses: actions/checkout@v4 + with: + # public repo with terraform configuration + repository: "datacite/mastino" + persist-credentials: false + - name: Setup dokerize and template parameters + run: | + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + wget https://github.com/jwilder/dockerize/releases/download/v0.6.0/dockerize-linux-amd64-v0.6.0.tar.gz + tar -xzvf dockerize-linux-amd64-v0.6.0.tar.gz + rm dockerize-linux-amd64-v0.6.0.tar.gz + + - name: Conditionally update staging environment + if: ${{ (inputs.deployment_environment == 'staging') }} + run: | + ./dockerize -template stage/services/client-api/_events.auto.tfvars.tmpl:stage/services/client-api/_events.auto.tfvars + git add stage/services/client-api/_events.auto.tfvars + git commit -m "Adding events git variables for commit ${{ github.sha }}" + + - name: Conditionally update production/test environments + if: ${{ (inputs.deployment_environment == 'production') }} + run: | + ./dockerize -template prod-eu-west/services/client-api/_events.auto.tfvars.tmpl:prod-eu-west/services/client-api/_events.auto.tfvars + ./dockerize -template test/services/client-api/_events.auto.tfvars.tmpl:test/services/client-api/_events.auto.tfvars + + git add prod-eu-west/services/client-api/_events.auto.tfvars + git add test/services/client-api/_events.auto.tfvars + git commit -m "Adding events git variables for tag ${{ inputs.image_tag }}" + - name: Push changes + uses: ad-m/github-push-action@v0.8.0 + with: + github_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} + repository: "datacite/mastino" + branch: "refs/heads/master" + tags: false diff --git a/.github/workflows/branch_to_staging.yml b/.github/workflows/branch_to_staging.yml new file mode 100644 index 0000000..10d5221 --- /dev/null +++ b/.github/workflows/branch_to_staging.yml @@ -0,0 +1,23 @@ +name: Build/Deploy Branch to Staging +on: + workflow_dispatch: +jobs: + lint: + uses: ./.github/workflows/rubocop.yml + test: + uses: ./.github/workflows/parallel_ci.yml + secrets: inherit + call_build_and_push: + needs: test + uses: ./.github/workflows/build.yml + with: + image_name: ${{ github.repository }} + image_tag: ${{ github.ref_name }} + secrets: inherit + deploy: + needs: [test, call_build_and_push] + uses: ./.github/workflows/_update_terraform.yml + with: + image_tag: ${{ github.ref_name }} + deployment_environment: staging + secrets: inherit diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..17a95dd --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,54 @@ +name: Build and Tag +on: + workflow_call: + secrets: + DOCKERHUB_USERNAME: + required: true + DOCKERHUB_TOKEN: + required: true + inputs: + image_name: + description: The name of the image for docker/ghcr registries + required: true + type: string + image_tag: + description: Tag for the image for docker/ghcr registries + required: true + type: string +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Cache Docker layers + uses: actions/cache@v4 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and Push + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + push: true + tags: | + ${{ inputs.image_name }}:${{ inputs.image_tag }} + ghcr.io/${{ inputs.image_name }}:${{ inputs.image_tag }} + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..7ce4b78 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,25 @@ +name: Deploy Main to Staging +on: + push: + branches: + - main +jobs: + lint: + uses: ./.github/workflows/rubocop.yml + test: + uses: ./.github/workflows/parallel_ci.yml + secrets: inherit + call_build_and_push: + needs: test + uses: ./.github/workflows/build.yml + with: + image_name: ${{ github.repository }} + image_tag: main + secrets: inherit + deploy: + needs: [test, call_build_and_push] + uses: ./.github/workflows/_update_terraform.yml + with: + image_tag: main + deployment_environment: staging + secrets: inherit diff --git a/.github/workflows/parallel_ci.yml b/.github/workflows/parallel_ci.yml new file mode 100644 index 0000000..433f9db --- /dev/null +++ b/.github/workflows/parallel_ci.yml @@ -0,0 +1,32 @@ +name: Parallel CI +on: + workflow_call: +jobs: + parallel-test: + runs-on: ubuntu-latest + strategy: + fail-fast: true + services: + memcached: + image: memcached:1.4.31 + ports: + - 11211/udp + env: + LOG_LEVEL: "error" + MYSQL_HOST: "127.0.0.1" + MYSQL_DATABASE: datacite + MYSQL_USER: root + ES_HOST: "localhost:9200" + ELASTIC_PASSWORD: "AnUnsecurePassword123" + steps: + - name: Checkout Code + uses: actions/checkout@v3 + + - name: Set up Ruby 3.1.6 + uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.1.6" + bundler-cache: true + + - name: Run Specs + run: bundle exec rspec diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml new file mode 100644 index 0000000..5a0fc42 --- /dev/null +++ b/.github/workflows/pull_request.yml @@ -0,0 +1,13 @@ +name: Lint and Test Pull Request +on: + pull_request: + branches: + - main + workflow_dispatch: +jobs: + lint: + uses: ./.github/workflows/rubocop.yml + parallel-test: + needs: lint + uses: ./.github/workflows/parallel_ci.yml + secrets: inherit diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..7c274cb --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,24 @@ +name: Release to Production +on: + release: + types: [published] +jobs: + lint: + uses: ./.github/workflows/rubocop.yml + test: + uses: ./.github/workflows/parallel_ci.yml + secrets: inherit + call_build_and_push: + needs: test + uses: ./.github/workflows/build.yml + with: + image_name: ${{ github.repository }} + image_tag: ${{ github.ref_name }} + secrets: inherit + deploy: + needs: [test, call_build_and_push] + uses: ./.github/workflows/_update_terraform.yml + with: + image_tag: ${{ github.ref_name }} + deployment_environment: production + secrets: inherit diff --git a/.github/workflows/rubocop.yml b/.github/workflows/rubocop.yml new file mode 100644 index 0000000..81b1fed --- /dev/null +++ b/.github/workflows/rubocop.yml @@ -0,0 +1,29 @@ +name: RuboCop + +on: + workflow_call: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up Ruby 3.0 + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.1.6 + - name: Cache gems + uses: actions/cache@v4 + with: + path: vendor/bundle + key: ${{ runner.os }}-rubocop-${{ hashFiles('**/Gemfile.lock') }} + restore-keys: | + ${{ runner.os }}-rubocop- + - name: Install gems + run: | + bundle config path vendor/bundle + bundle config set without 'default doc job cable storage ujs test db' + bundle install --jobs 4 --retry 3 + - name: Run RuboCop + run: bundle exec rubocop --parallel diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cec82e6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,58 @@ +# See https://help.github.com/articles/ignoring-files for more about ignoring files. +# +# If you find yourself ignoring temporary files generated by your text editor +# or operating system, you probably want to add a global ignore instead: +# git config --global core.excludesfile '~/.gitignore_global' + +# Ignore bundler config. +/.bundle + +# Ignore the default SQLite database. +/db/*.sqlite3 +/db/*.sqlite3-journal + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +.byebug_history +.DS_Store +.svn +*.rbc +*.sassc +.sass-cache +capybara-*.html +.rspec +/.bundle +/.capistrano +/vendor/node_modules/* +/vendor/bower_components/* +/vendor/npm-debug.log +/vendor/bundle/* +/log/* +/tmp/* +/data/* +/db/*.sqlite3 +/public/* +!/public/.keep +/coverage/ +/spec/tmp/ +/config/data_bags/* +**.orig +rerun.txt +pickle-email-*.html +.idea/* +erl_crash.dump +optimise-db.txt +doc/dependencies* +.env +.env.* +!.env.example +!.env.travis +!.env.build +docker-compose.override.yml +.vscode +.solargraph.yml +.devspace \ No newline at end of file diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..f5fd5a9 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,31 @@ +inherit_gem: + rubocop-shopify: rubocop.yml + +plugins: + - rubocop-performance + - rubocop-rails + - rubocop-rspec + +require: + - rubocop-factory_bot + +AllCops: + TargetRubyVersion: 3.0 + Exclude: + - "bin/**/*" + - "log/**/*" + - "tmp/**/*" + - "vendor/**/*" + - "db/schema.rb" + +Style/MethodCallWithArgsParentheses: + EnforcedStyle: require_parentheses + +Style/StringLiterals: + EnforcedStyle: double_quotes + +Style/StringLiteralsInInterpolation: + EnforcedStyle: double_quotes + +Layout/SpaceInsideHashLiteralBraces: + Enabled: true diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0a0da55 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,68 @@ +FROM phusion/passenger-ruby31:3.0.7 + +# Set correct environment variables. +ENV HOME /home/app +ENV LC_ALL en_US.UTF-8 +ENV LANG en_US.UTF-8 + +# Allow app user to read /etc/container_environment +RUN usermod -a -G docker_env app + +# Use baseimage-docker's init process. +CMD ["/sbin/my_init"] + +# Use Ruby 3.1.6 +RUN bash -lc 'rvm --default use ruby-3.1.6' + +# Update installed APT packages +RUN apt-get update && apt-get upgrade -y -o Dpkg::Options::="--force-confold" && \ + apt-get install ntp wget nano tmux tzdata shared-mime-info -y && \ + apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +# Enable Passenger and Nginx and remove the default site +# Preserve env variables for nginx +RUN rm -f /etc/service/nginx/down && \ + rm /etc/nginx/sites-enabled/default +COPY vendor/docker/webapp.conf /etc/nginx/sites-enabled/webapp.conf +COPY vendor/docker/00_app_env.conf /etc/nginx/conf.d/00_app_env.conf + +# Use Amazon NTP servers +COPY vendor/docker/ntp.conf /etc/ntp.conf + +# Add Runit script for shoryuken workers +WORKDIR /home/app/webapp +RUN mkdir /etc/service/shoryuken +COPY vendor/docker/shoryuken.sh /etc/service/shoryuken/run + +# Copy webapp folder +COPY . /home/app/webapp/ +RUN mkdir -p tmp/pids && \ + mkdir -p tmp/storage && \ + chown -R app:app /home/app/webapp && \ + chmod -R 755 /home/app/webapp + +# Install Ruby gems +WORKDIR /home/app/webapp +RUN mkdir -p vendor/bundle && \ + chown -R app:app . && \ + chmod -R 755 . && \ + gem install rubygems-update -v 3.5.6 && \ + gem install bundler:2.5.6 && \ + /sbin/setuser app bundle config set --local path 'vendor/bundle' && \ + /sbin/setuser app bundle install + +# enable SSH +RUN rm -f /etc/service/sshd/down && \ + /etc/my_init.d/00_regen_ssh_host_keys.sh + +# Run additional scripts during container startup (i.e. not at build time) +RUN mkdir -p /etc/my_init.d + +# install custom ssh key during startup +COPY vendor/docker/10_ssh.sh /etc/my_init.d/10_ssh.sh + +# COPY vendor/docker/80_flush_cache.sh /etc/my_init.d/80_flush_cache.sh +COPY vendor/docker/90_migrate.sh /etc/my_init.d/90_migrate.sh + +# Expose web +EXPOSE 80 diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..dc51812 --- /dev/null +++ b/Gemfile @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +ruby "3.1.6" + +gem "rails", "~> 7.1.3", ">= 7.1.3.2" +gem "bootsnap", require: false +gem "rack-cors" +gem "datadog", require: "datadog/auto_instrument" +gem "shoryuken", "~> 4.0" +gem "aws-sdk-sqs", "~> 1.3" +gem "lograge", "~> 0.11.2" +gem "logstash-event", "~> 1.2", ">= 1.2.02" +gem "logstash-logger", "~> 0.26.1" +gem "mysql2", "~> 0.5.3" +gem "dotenv" +gem "sentry-raven", "~> 3.1", ">= 3.1.2" +gem "elasticsearch", "~> 7.17", ">= 7.17.10" +gem "elasticsearch-model", "~> 7.2.1", ">= 7.2.1", require: "elasticsearch/model" +gem "elasticsearch-rails", "~> 7.2.1", ">= 7.2.1" +gem "elasticsearch-transport", "~> 7.17", ">= 7.17.10" + +# This gem will allow us to write tests without the need for a database +gem "activerecord-nulldb-adapter", "~> 1.1", ">= 1.1.1" + +group :development, :test do + gem "debug", platforms: [:mri, :windows] + gem "rubocop", require: false + gem "rubocop-shopify", require: false + gem "rubocop-rspec", require: false + gem "rubocop-performance", require: false + gem "rubocop-factory_bot", require: false + gem "rubocop-rails", require: false + gem "factory_bot_rails", require: false + gem "bundler-audit", require: false + gem "brakeman", require: false + gem "rspec-rails", "~> 7.0.0" +end + +group :development do +end diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..0cbc789 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,387 @@ +GEM + remote: https://rubygems.org/ + specs: + actioncable (7.1.5.1) + actionpack (= 7.1.5.1) + activesupport (= 7.1.5.1) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + zeitwerk (~> 2.6) + actionmailbox (7.1.5.1) + actionpack (= 7.1.5.1) + activejob (= 7.1.5.1) + activerecord (= 7.1.5.1) + activestorage (= 7.1.5.1) + activesupport (= 7.1.5.1) + mail (>= 2.7.1) + net-imap + net-pop + net-smtp + actionmailer (7.1.5.1) + actionpack (= 7.1.5.1) + actionview (= 7.1.5.1) + activejob (= 7.1.5.1) + activesupport (= 7.1.5.1) + mail (~> 2.5, >= 2.5.4) + net-imap + net-pop + net-smtp + rails-dom-testing (~> 2.2) + actionpack (7.1.5.1) + actionview (= 7.1.5.1) + activesupport (= 7.1.5.1) + nokogiri (>= 1.8.5) + racc + rack (>= 2.2.4) + rack-session (>= 1.0.1) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + actiontext (7.1.5.1) + actionpack (= 7.1.5.1) + activerecord (= 7.1.5.1) + activestorage (= 7.1.5.1) + activesupport (= 7.1.5.1) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (7.1.5.1) + activesupport (= 7.1.5.1) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (7.1.5.1) + activesupport (= 7.1.5.1) + globalid (>= 0.3.6) + activemodel (7.1.5.1) + activesupport (= 7.1.5.1) + activerecord (7.1.5.1) + activemodel (= 7.1.5.1) + activesupport (= 7.1.5.1) + timeout (>= 0.4.0) + activerecord-nulldb-adapter (1.1.1) + activerecord (>= 6.0, < 8.1) + activestorage (7.1.5.1) + actionpack (= 7.1.5.1) + activejob (= 7.1.5.1) + activerecord (= 7.1.5.1) + activesupport (= 7.1.5.1) + marcel (~> 1.0) + activesupport (7.1.5.1) + base64 + benchmark (>= 0.3) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + logger (>= 1.4.2) + minitest (>= 5.1) + mutex_m + securerandom (>= 0.3) + tzinfo (~> 2.0) + ast (2.4.2) + aws-eventstream (1.3.1) + aws-partitions (1.1060.0) + aws-sdk-core (3.220.0) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + base64 + jmespath (~> 1, >= 1.6.1) + aws-sdk-sqs (1.93.0) + aws-sdk-core (~> 3, >= 3.216.0) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.11.0) + aws-eventstream (~> 1, >= 1.0.2) + base64 (0.2.0) + benchmark (0.4.0) + bigdecimal (3.1.9) + bootsnap (1.18.4) + msgpack (~> 1.2) + brakeman (7.0.0) + racc + builder (3.3.0) + bundler-audit (0.9.2) + bundler (>= 1.2.0, < 3) + thor (~> 1.0) + concurrent-ruby (1.3.5) + connection_pool (2.5.0) + crass (1.0.6) + datadog (2.12.0) + datadog-ruby_core_source (~> 3.4) + libdatadog (~> 16.0.1.1.0) + libddwaf (~> 1.18.0.0.1) + logger + msgpack + datadog-ruby_core_source (3.4.0) + date (3.4.1) + debug (1.10.0) + irb (~> 1.10) + reline (>= 0.3.8) + diff-lcs (1.6.0) + dotenv (3.1.7) + drb (2.2.1) + elasticsearch (7.17.11) + elasticsearch-api (= 7.17.11) + elasticsearch-transport (= 7.17.11) + elasticsearch-api (7.17.11) + multi_json + elasticsearch-model (7.2.1) + activesupport (> 3) + elasticsearch (~> 7) + hashie + elasticsearch-rails (7.2.1) + elasticsearch-transport (7.17.11) + base64 + faraday (>= 1, < 3) + multi_json + erubi (1.13.1) + factory_bot (6.5.1) + activesupport (>= 6.1.0) + factory_bot_rails (6.4.4) + factory_bot (~> 6.5) + railties (>= 5.0.0) + faraday (2.12.2) + faraday-net_http (>= 2.0, < 3.5) + json + logger + faraday-net_http (3.4.0) + net-http (>= 0.5.0) + ffi (1.17.1-aarch64-linux-gnu) + ffi (1.17.1-arm64-darwin) + ffi (1.17.1-x86_64-linux-gnu) + globalid (1.2.1) + activesupport (>= 6.1) + hashie (5.0.0) + i18n (1.14.7) + concurrent-ruby (~> 1.0) + io-console (0.8.0) + irb (1.15.1) + pp (>= 0.6.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + jmespath (1.6.2) + json (2.10.1) + language_server-protocol (3.17.0.4) + libdatadog (16.0.1.1.0) + libdatadog (16.0.1.1.0-aarch64-linux) + libdatadog (16.0.1.1.0-x86_64-linux) + libddwaf (1.18.0.0.1-aarch64-linux) + ffi (~> 1.0) + libddwaf (1.18.0.0.1-arm64-darwin) + ffi (~> 1.0) + libddwaf (1.18.0.0.1-x86_64-linux) + ffi (~> 1.0) + lint_roller (1.1.0) + logger (1.6.6) + lograge (0.11.2) + actionpack (>= 4) + activesupport (>= 4) + railties (>= 4) + request_store (~> 1.0) + logstash-event (1.2.02) + logstash-logger (0.26.1) + logstash-event (~> 1.2) + loofah (2.24.0) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + mail (2.8.1) + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + marcel (1.0.4) + mini_mime (1.1.5) + minitest (5.25.4) + msgpack (1.8.0) + multi_json (1.15.0) + mutex_m (0.3.0) + mysql2 (0.5.6) + net-http (0.6.0) + uri + net-imap (0.5.6) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-smtp (0.5.1) + net-protocol + nio4r (2.7.4) + nokogiri (1.18.3-aarch64-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.3-arm64-darwin) + racc (~> 1.4) + nokogiri (1.18.3-x86_64-linux-gnu) + racc (~> 1.4) + parallel (1.26.3) + parser (3.3.7.1) + ast (~> 2.4.1) + racc + pp (0.6.2) + prettyprint + prettyprint (0.2.0) + psych (5.2.3) + date + stringio + racc (1.8.1) + rack (3.1.11) + rack-cors (2.0.2) + rack (>= 2.0.0) + rack-session (2.1.0) + base64 (>= 0.1.0) + rack (>= 3.0.0) + rack-test (2.2.0) + rack (>= 1.3) + rackup (2.2.1) + rack (>= 3) + rails (7.1.5.1) + actioncable (= 7.1.5.1) + actionmailbox (= 7.1.5.1) + actionmailer (= 7.1.5.1) + actionpack (= 7.1.5.1) + actiontext (= 7.1.5.1) + actionview (= 7.1.5.1) + activejob (= 7.1.5.1) + activemodel (= 7.1.5.1) + activerecord (= 7.1.5.1) + activestorage (= 7.1.5.1) + activesupport (= 7.1.5.1) + bundler (>= 1.15.0) + railties (= 7.1.5.1) + rails-dom-testing (2.2.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.6.2) + loofah (~> 2.21) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) + railties (7.1.5.1) + actionpack (= 7.1.5.1) + activesupport (= 7.1.5.1) + irb + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + zeitwerk (~> 2.6) + rainbow (3.1.1) + rake (13.2.1) + rdoc (6.12.0) + psych (>= 4.0.0) + regexp_parser (2.10.0) + reline (0.6.0) + io-console (~> 0.5) + request_store (1.7.0) + rack (>= 1.4) + rspec-core (3.13.3) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.3) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.2) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-rails (7.0.2) + actionpack (>= 7.0) + activesupport (>= 7.0) + railties (>= 7.0) + rspec-core (~> 3.13) + rspec-expectations (~> 3.13) + rspec-mocks (~> 3.13) + rspec-support (~> 3.13) + rspec-support (3.13.2) + rubocop (1.73.2) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.38.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.38.1) + parser (>= 3.3.1.0) + rubocop-factory_bot (2.26.1) + rubocop (~> 1.61) + rubocop-performance (1.24.0) + lint_roller (~> 1.1) + rubocop (>= 1.72.1, < 2.0) + rubocop-ast (>= 1.38.0, < 2.0) + rubocop-rails (2.30.3) + activesupport (>= 4.2.0) + lint_roller (~> 1.1) + rack (>= 1.1) + rubocop (>= 1.72.1, < 2.0) + rubocop-ast (>= 1.38.0, < 2.0) + rubocop-rspec (3.5.0) + lint_roller (~> 1.1) + rubocop (~> 1.72, >= 1.72.1) + rubocop-shopify (2.16.0) + rubocop (~> 1.62) + ruby-progressbar (1.13.0) + securerandom (0.4.1) + sentry-raven (3.1.2) + faraday (>= 1.0) + shoryuken (4.0.3) + aws-sdk-core (>= 2) + concurrent-ruby + thor + stringio (3.1.5) + thor (1.3.2) + timeout (0.4.3) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unicode-display_width (3.1.4) + unicode-emoji (~> 4.0, >= 4.0.4) + unicode-emoji (4.0.4) + uri (1.0.3) + websocket-driver (0.7.7) + base64 + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + zeitwerk (2.6.18) + +PLATFORMS + aarch64-linux + arm64-darwin-24 + x86_64-linux + +DEPENDENCIES + activerecord-nulldb-adapter (~> 1.1, >= 1.1.1) + aws-sdk-sqs (~> 1.3) + bootsnap + brakeman + bundler-audit + datadog + debug + dotenv + elasticsearch (~> 7.17, >= 7.17.10) + elasticsearch-model (~> 7.2.1, >= 7.2.1) + elasticsearch-rails (~> 7.2.1, >= 7.2.1) + elasticsearch-transport (~> 7.17, >= 7.17.10) + factory_bot_rails + lograge (~> 0.11.2) + logstash-event (~> 1.2, >= 1.2.02) + logstash-logger (~> 0.26.1) + mysql2 (~> 0.5.3) + rack-cors + rails (~> 7.1.3, >= 7.1.3.2) + rspec-rails (~> 7.0.0) + rubocop + rubocop-factory_bot + rubocop-performance + rubocop-rails + rubocop-rspec + rubocop-shopify + sentry-raven (~> 3.1, >= 3.1.2) + shoryuken (~> 4.0) + +RUBY VERSION + ruby 3.1.6p260 + +BUNDLED WITH + 2.5.6 \ No newline at end of file diff --git a/README.md b/README.md index f8303de..23471d8 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ -# events +# DataCite Events Service + DataCite Events Service diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..d2a78aa --- /dev/null +++ b/Rakefile @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require_relative "config/application" + +Rails.application.load_tasks diff --git a/app/channels/application_cable/channel.rb b/app/channels/application_cable/channel.rb new file mode 100644 index 0000000..9aec230 --- /dev/null +++ b/app/channels/application_cable/channel.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module ApplicationCable + class Channel < ActionCable::Channel::Base + end +end diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb new file mode 100644 index 0000000..8d6c2a1 --- /dev/null +++ b/app/channels/application_cable/connection.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module ApplicationCable + class Connection < ActionCable::Connection::Base + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb new file mode 100644 index 0000000..13c271f --- /dev/null +++ b/app/controllers/application_controller.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class ApplicationController < ActionController::API +end diff --git a/app/controllers/concerns/.keep b/app/controllers/concerns/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb new file mode 100644 index 0000000..b4af3d9 --- /dev/null +++ b/app/controllers/events_controller.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class EventsController < ApplicationController + def index + render(json: { message: "index" }) + end + + def create + render(json: { message: "create" }) + end + + def update + render(json: { message: "update" }) + end +end diff --git a/app/controllers/heartbeat_controller.rb b/app/controllers/heartbeat_controller.rb new file mode 100644 index 0000000..f3d9979 --- /dev/null +++ b/app/controllers/heartbeat_controller.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class HeartbeatController < ApplicationController + def index + message = { healthy: true } + render(json: message) + end +end diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb new file mode 100644 index 0000000..bef3959 --- /dev/null +++ b/app/jobs/application_job.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class ApplicationJob < ActiveJob::Base + # Automatically retry jobs that encountered a deadlock + # retry_on ActiveRecord::Deadlocked + + # Most jobs are safe to ignore if the underlying records are no longer available + # discard_on ActiveJob::DeserializationError +end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb new file mode 100644 index 0000000..5cc63a0 --- /dev/null +++ b/app/mailers/application_mailer.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class ApplicationMailer < ActionMailer::Base + default from: "from@example.com" + layout "mailer" +end diff --git a/app/models/application_record.rb b/app/models/application_record.rb new file mode 100644 index 0000000..08dc537 --- /dev/null +++ b/app/models/application_record.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class ApplicationRecord < ActiveRecord::Base + primary_abstract_class +end diff --git a/app/models/concerns/.keep b/app/models/concerns/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/models/event.rb b/app/models/event.rb new file mode 100644 index 0000000..9844d73 --- /dev/null +++ b/app/models/event.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class Event < ApplicationRecord + # include Modelable + # include Identifiable + # include Elasticsearch::Model +end diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb new file mode 100644 index 0000000..3aac900 --- /dev/null +++ b/app/views/layouts/mailer.html.erb @@ -0,0 +1,13 @@ + + + + + + + + + <%= yield %> + + diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb new file mode 100644 index 0000000..37f0bdd --- /dev/null +++ b/app/views/layouts/mailer.text.erb @@ -0,0 +1 @@ +<%= yield %> diff --git a/bin/bundle b/bin/bundle new file mode 100755 index 0000000..981e650 --- /dev/null +++ b/bin/bundle @@ -0,0 +1,114 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'bundle' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "rubygems" + +m = Module.new do + module_function + + def invoked_as_script? + File.expand_path($0) == File.expand_path(__FILE__) + end + + def env_var_version + ENV["BUNDLER_VERSION"] + end + + def cli_arg_version + return unless invoked_as_script? # don't want to hijack other binstubs + return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` + bundler_version = nil + update_index = nil + ARGV.each_with_index do |a, i| + if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN + bundler_version = a + end + next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ + bundler_version = $1 + update_index = i + end + bundler_version + end + + def gemfile + gemfile = ENV["BUNDLE_GEMFILE"] + return gemfile if gemfile && !gemfile.empty? + + File.expand_path("../Gemfile", __dir__) + end + + def lockfile + lockfile = + case File.basename(gemfile) + when "gems.rb" then gemfile.sub(/\.rb$/, gemfile) + else "#{gemfile}.lock" + end + File.expand_path(lockfile) + end + + def lockfile_version + return unless File.file?(lockfile) + lockfile_contents = File.read(lockfile) + return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ + Regexp.last_match(1) + end + + def bundler_requirement + @bundler_requirement ||= + env_var_version || cli_arg_version || + bundler_requirement_for(lockfile_version) + end + + def bundler_requirement_for(version) + return "#{Gem::Requirement.default}.a" unless version + + bundler_gem_version = Gem::Version.new(version) + + requirement = bundler_gem_version.approximate_recommendation + + return requirement unless Gem.rubygems_version < Gem::Version.new("2.7.0") + + requirement += ".a" if bundler_gem_version.prerelease? + + requirement + end + + def load_bundler! + ENV["BUNDLE_GEMFILE"] ||= gemfile + + activate_bundler + end + + def activate_bundler + gem_error = activation_error_handling do + gem "bundler", bundler_requirement + end + return if gem_error.nil? + require_error = activation_error_handling do + require "bundler/version" + end + return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) + warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" + exit 42 + end + + def activation_error_handling + yield + nil + rescue StandardError, LoadError => e + e + end +end + +m.load_bundler! + +if m.invoked_as_script? + load Gem.bin_path("bundler", "bundle") +end diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint new file mode 100755 index 0000000..67ef493 --- /dev/null +++ b/bin/docker-entrypoint @@ -0,0 +1,8 @@ +#!/bin/bash -e + +# If running the rails server then create or migrate existing database +if [ "${1}" == "./bin/rails" ] && [ "${2}" == "server" ]; then + ./bin/rails db:prepare +fi + +exec "${@}" diff --git a/bin/rails b/bin/rails new file mode 100755 index 0000000..efc0377 --- /dev/null +++ b/bin/rails @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +APP_PATH = File.expand_path("../config/application", __dir__) +require_relative "../config/boot" +require "rails/commands" diff --git a/bin/rake b/bin/rake new file mode 100755 index 0000000..4fbf10b --- /dev/null +++ b/bin/rake @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +require_relative "../config/boot" +require "rake" +Rake.application.run diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000..3cd5a9d --- /dev/null +++ b/bin/setup @@ -0,0 +1,33 @@ +#!/usr/bin/env ruby +require "fileutils" + +# path to your application root. +APP_ROOT = File.expand_path("..", __dir__) + +def system!(*args) + system(*args, exception: true) +end + +FileUtils.chdir APP_ROOT do + # This script is a way to set up or update your development environment automatically. + # This script is idempotent, so that you can run it at any time and get an expectable outcome. + # Add necessary setup steps to this file. + + puts "== Installing dependencies ==" + system! "gem install bundler --conservative" + system("bundle check") || system!("bundle install") + + # puts "\n== Copying sample files ==" + # unless File.exist?("config/database.yml") + # FileUtils.cp "config/database.yml.sample", "config/database.yml" + # end + + puts "\n== Preparing database ==" + system! "bin/rails db:prepare" + + puts "\n== Removing old logs and tempfiles ==" + system! "bin/rails log:clear tmp:clear" + + puts "\n== Restarting application server ==" + system! "bin/rails restart" +end diff --git a/config.ru b/config.ru new file mode 100644 index 0000000..2e03084 --- /dev/null +++ b/config.ru @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# This file is used by Rack-based servers to start the application. + +require_relative "config/environment" + +run Rails.application +Rails.application.load_server diff --git a/config/application.rb b/config/application.rb new file mode 100644 index 0000000..21cda13 --- /dev/null +++ b/config/application.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +require_relative "boot" + +require "rails/all" + +Bundler.require(*Rails.groups) + +env_file = File.expand_path("../.env", __dir__) + +if File.exist?(env_file) + require "dotenv" + Dotenv.load!(env_file) +end + +env_json_file = "/etc/container_environment.json" +if File.exist?(env_json_file) + env_vars = JSON.parse(File.read(env_json_file)) + env_vars.each { |k, v| ENV[k] = v } +end + +module Events + class Application < Rails::Application + config.load_defaults(7.1) + + config.autoload_lib(ignore: nil) + + config.api_only = true + + config.middleware.use(Rack::Deflater) + + config.active_job.logger = Logger.new(nil) + + # Start: Configure Logging + config.lograge.enabled = true + config.lograge.formatter = Lograge::Formatters::Logstash.new + config.lograge.logger = LogStashLogger.new(type: :stdout) + config.logger = config.lograge.logger ## LogStashLogger needs to be pass to rails logger, see roidrage/lograge#26 + config.log_level = ENV["LOG_LEVEL"].to_sym + + config.lograge.ignore_actions = [ + "HeartbeatController#index", + "IndexController#index", + ] + config.lograge.ignore_custom = lambda do |event| + event.payload.inspect.length > 100_000 + end + config.lograge.base_controller_class = "ActionController::API" + + config.lograge.custom_options = lambda do |event| + correlation = Datadog::Tracing.correlation + exceptions = ["controller", "action", "format", "id"] + { + dd: { + env: correlation.env, + service: correlation.service, + version: correlation.version, + trace_id: correlation.trace_id, + span_id: correlation.span_id, + }, + ddsource: ["ruby"], + params: event.payload[:params].except(*exceptions), + uid: event.payload[:uid], + } + end + # End: Configure Logging + end +end diff --git a/config/boot.rb b/config/boot.rb new file mode 100644 index 0000000..aef6d03 --- /dev/null +++ b/config/boot.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +require "bundler/setup" # Set up gems listed in the Gemfile. +require "bootsnap/setup" # Speed up boot time by caching expensive operations. diff --git a/config/cable.yml b/config/cable.yml new file mode 100644 index 0000000..8153dd5 --- /dev/null +++ b/config/cable.yml @@ -0,0 +1,10 @@ +development: + adapter: async + +test: + adapter: test + +production: + adapter: redis + url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> + channel_prefix: events_production diff --git a/config/database.yml b/config/database.yml new file mode 100644 index 0000000..822ed78 --- /dev/null +++ b/config/database.yml @@ -0,0 +1,31 @@ +mysql: &mysql + adapter: mysql2 + +defaults: &defaults + pool: <%= ENV['CONCURRENCY'].to_i + 5 %> + timeout: 5000 + wait_timeout: 1800 + encoding: utf8mb4 + username: <%= ENV['MYSQL_USER'] %> + password: <%= ENV['MYSQL_PASSWORD'] %> + database: <%= ENV['MYSQL_DATABASE'] %> + host: <%= ENV['MYSQL_HOST'] %> + port: <%= ENV['MYSQL_PORT'] %> + + <<: *mysql + +development: + <<: *defaults + +test: + <<: *defaults + database: <%= ENV['MYSQL_DATABASE'] %>_test<%= ENV['TEST_ENV_NUMBER'] %> + +production: + <<: *defaults + +stage: + <<: *defaults + +uat: + <<: *defaults diff --git a/config/environment.rb b/config/environment.rb new file mode 100644 index 0000000..7df99e8 --- /dev/null +++ b/config/environment.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +# Load the Rails application. +require_relative "application" + +# Initialize the Rails application. +Rails.application.initialize! diff --git a/config/environments/development.rb b/config/environments/development.rb new file mode 100644 index 0000000..3f7d431 --- /dev/null +++ b/config/environments/development.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # In the development environment your application's code is reloaded any time + # it changes. This slows down response time but is perfect for development + # since you don't have to restart the web server when you make code changes. + config.enable_reloading = true + + # Do not eager load code on boot. + config.eager_load = false + + # Show full error reports. + config.consider_all_requests_local = true + + # Enable server timing + config.server_timing = true + + # Enable/disable caching. By default caching is disabled. + # Run rails dev:cache to toggle caching. + if Rails.root.join("tmp/caching-dev.txt").exist? + config.cache_store = :memory_store + config.public_file_server.headers = { + "Cache-Control" => "public, max-age=#{2.days.to_i}", + } + else + config.action_controller.perform_caching = false + + config.cache_store = :null_store + end + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local + + # Don't care if the mailer can't send. + config.action_mailer.raise_delivery_errors = false + + config.action_mailer.perform_caching = false + + # Print deprecation notices to the Rails logger. + config.active_support.deprecation = :log + + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + + # Raise an error on page load if there are pending migrations. + config.active_record.migration_error = :page_load + + # Highlight code that triggered database queries in logs. + config.active_record.verbose_query_logs = true + + # Highlight code that enqueued background job in logs. + config.active_job.verbose_enqueue_logs = true + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true + + # Uncomment if you wish to allow Action Cable access from any origin. + # config.action_cable.disable_request_forgery_protection = true + + # Raise error when a before_action's only/except options reference missing actions + config.action_controller.raise_on_missing_callback_actions = true +end diff --git a/config/environments/production.rb b/config/environments/production.rb new file mode 100644 index 0000000..19dbee2 --- /dev/null +++ b/config/environments/production.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Code is not reloaded between requests. + config.enable_reloading = false + + # Eager load code on boot. This eager loads most of Rails and + # your application in memory, allowing both threaded web servers + # and those relying on copy on write to perform better. + # Rake tasks automatically ignore this option for performance. + config.eager_load = true + + # Full error reports are disabled and caching is turned on. + config.consider_all_requests_local = false + + # Ensures that a master key has been made available in ENV["RAILS_MASTER_KEY"], config/master.key, or an environment + # key such as config/credentials/production.key. This key is used to decrypt credentials (and other encrypted files). + # config.require_master_key = true + + # Disable serving static files from `public/`, relying on NGINX/Apache to do so instead. + # config.public_file_server.enabled = false + + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.asset_host = "http://assets.example.com" + + # Specifies the header that your server uses for sending files. + # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache + # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local + + # Mount Action Cable outside main process or domain. + # config.action_cable.mount_path = nil + # config.action_cable.url = "wss://example.com/cable" + # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ] + + # Assume all access to the app is happening through a SSL-terminating reverse proxy. + # Can be used together with config.force_ssl for Strict-Transport-Security and secure cookies. + # config.assume_ssl = true + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + config.force_ssl = true + + # Log to STDOUT by default + # config.logger = ActiveSupport::Logger.new($stdout) + # .tap { |logger| logger.formatter = Logger::Formatter.new } + # .then { |logger| ActiveSupport::TaggedLogging.new(logger) } + + # Prepend all log lines with the following tags. + config.log_tags = [:request_id] + + # "info" includes generic and useful information about system operation, but avoids logging too much + # information to avoid inadvertent exposure of personally identifiable information (PII). If you + # want to log everything, set the level to "debug". + config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") + + # Use a different cache store in production. + # config.cache_store = :mem_cache_store + + # Use a real queuing backend for Active Job (and separate queues per environment). + # config.active_job.queue_adapter = :resque + # config.active_job.queue_name_prefix = "events_production" + + config.action_mailer.perform_caching = false + + # Ignore bad email addresses and do not raise email delivery errors. + # Set this to true and configure the email server for immediate delivery to raise delivery errors. + # config.action_mailer.raise_delivery_errors = false + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation cannot be found). + config.i18n.fallbacks = true + + # Don't log any deprecations. + config.active_support.report_deprecations = false + + # Do not dump schema after migrations. + config.active_record.dump_schema_after_migration = false + + # Enable DNS rebinding protection and other `Host` header attacks. + # config.hosts = [ + # "example.com", # Allow requests from example.com + # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` + # ] + # Skip DNS rebinding protection for the default health check endpoint. + # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } +end diff --git a/config/environments/test.rb b/config/environments/test.rb new file mode 100644 index 0000000..670c12d --- /dev/null +++ b/config/environments/test.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require "active_support/core_ext/integer/time" + +# The test environment is used exclusively to run your application's +# test suite. You never need to work with it otherwise. Remember that +# your test database is "scratch space" for the test suite and is wiped +# and recreated between test runs. Don't rely on the data there! + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # While tests run files are not watched, reloading is not necessary. + config.enable_reloading = false + + # Eager loading loads your entire application. When running a single test locally, + # this is usually not necessary, and can slow down your test suite. However, it's + # recommended that you enable it in continuous integration systems to ensure eager + # loading is working properly before deploying your code. + config.eager_load = ENV["CI"].present? + + # Configure public file server for tests with Cache-Control for performance. + config.public_file_server.enabled = true + config.public_file_server.headers = { + "Cache-Control" => "public, max-age=#{1.hour.to_i}", + } + + # Show full error reports and disable caching. + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + config.cache_store = :null_store + + # Render exception templates for rescuable exceptions and raise for other exceptions. + config.action_dispatch.show_exceptions = :rescuable + + # Disable request forgery protection in test environment. + config.action_controller.allow_forgery_protection = false + + # Store uploaded files on the local file system in a temporary directory. + config.active_storage.service = :test + + config.action_mailer.perform_caching = false + + # Tell Action Mailer not to deliver emails to the real world. + # The :test delivery method accumulates sent emails in the + # ActionMailer::Base.deliveries array. + config.action_mailer.delivery_method = :test + + # Print deprecation notices to the stderr. + config.active_support.deprecation = :stderr + + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true + + # Raise error when a before_action's only/except options reference missing actions + config.action_controller.raise_on_missing_callback_actions = true +end diff --git a/config/initializers/_shoryuken.rb b/config/initializers/_shoryuken.rb new file mode 100644 index 0000000..660f367 --- /dev/null +++ b/config/initializers/_shoryuken.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +if Rails.env.development? + Aws.config.update({ + endpoint: ENV["AWS_ENDPOINT"], + region: "us-east-1", + credentials: Aws::Credentials.new("test", "test"), + }) +end + +# Shoryuken middleware to capture worker errors and send them on to Sentry.io +module Shoryuken + module Middleware + module Server + class RavenReporter + def call(_worker_instance, queue, _sqs_msg, body, &block) + tags = { job: body["job_class"], queue: queue } + context = { message: body } + Raven.capture(tags: tags, extra: context, &block) + end + end + end + end +end + +Shoryuken.configure_server do |config| + config.server_middleware do |chain| + # remove logging of timing events + chain.remove(Shoryuken::Middleware::Server::Timing) + chain.add(Shoryuken::Middleware::Server::RavenReporter) + end +end + +Shoryuken.active_job_queue_name_prefixing = true diff --git a/config/initializers/cors.rb b/config/initializers/cors.rb new file mode 100644 index 0000000..7af62e5 --- /dev/null +++ b/config/initializers/cors.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# Be sure to restart your server when you modify this file. + +# Avoid CORS issues when API is called from the frontend app. +# Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin Ajax requests. + +# Read more: https://github.com/cyu/rack-cors + +# Rails.application.config.middleware.insert_before 0, Rack::Cors do +# allow do +# origins "example.com" +# +# resource "*", +# headers: :any, +# methods: [:get, :post, :put, :patch, :delete, :options, :head] +# end +# end diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb new file mode 100644 index 0000000..8876a0c --- /dev/null +++ b/config/initializers/filter_parameter_logging.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# Be sure to restart your server when you modify this file. + +# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. +# Use this to limit dissemination of sensitive information. +# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. +Rails.application.config.filter_parameters += [ + :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, +] diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb new file mode 100644 index 0000000..9e049dc --- /dev/null +++ b/config/initializers/inflections.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# Be sure to restart your server when you modify this file. + +# Add new inflection rules using the following format. Inflections +# are locale specific, and you may define rules for as many different +# locales as you wish. All of these examples are active by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.plural /^(ox)$/i, "\\1en" +# inflect.singular /^(ox)en/i, "\\1" +# inflect.irregular "person", "people" +# inflect.uncountable %w( fish sheep ) +# end + +# These inflection rules are supported but not enabled by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.acronym "RESTful" +# end diff --git a/config/locales/en.yml b/config/locales/en.yml new file mode 100644 index 0000000..6c349ae --- /dev/null +++ b/config/locales/en.yml @@ -0,0 +1,31 @@ +# Files in the config/locales directory are used for internationalization and +# are automatically loaded by Rails. If you want to use locales other than +# English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t "hello" +# +# In views, this is aliased to just `t`: +# +# <%= t("hello") %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# To learn more about the API, please read the Rails Internationalization guide +# at https://guides.rubyonrails.org/i18n.html. +# +# Be aware that YAML interprets the following case-insensitive strings as +# booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings +# must be quoted to be interpreted as strings. For example: +# +# en: +# "yes": yup +# enabled: "ON" + +en: + hello: "Hello world" diff --git a/config/puma.rb b/config/puma.rb new file mode 100644 index 0000000..40efb18 --- /dev/null +++ b/config/puma.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +# This configuration file will be evaluated by Puma. The top-level methods that +# are invoked here are part of Puma's configuration DSL. For more information +# about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. + +# Puma can serve each request in a thread from an internal thread pool. +# The `threads` method setting takes two numbers: a minimum and maximum. +# Any libraries that use thread pools should be configured to match +# the maximum value specified for Puma. Default is set to 5 threads for minimum +# and maximum; this matches the default thread size of Active Record. +max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } +min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } +threads min_threads_count, max_threads_count + +# Specifies that the worker count should equal the number of processors in production. +if ENV["RAILS_ENV"] == "production" + require "concurrent-ruby" + worker_count = Integer(ENV.fetch("WEB_CONCURRENCY") { Concurrent.physical_processor_count }) + workers worker_count if worker_count > 1 +end + +# Specifies the `worker_timeout` threshold that Puma will use to wait before +# terminating a worker in development environments. +worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development" + +# Specifies the `port` that Puma will listen on to receive requests; default is 3000. +port ENV.fetch("PORT") { 3000 } + +# Specifies the `environment` that Puma will run in. +environment ENV.fetch("RAILS_ENV") { "development" } + +# Specifies the `pidfile` that Puma will use. +pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } + +# Allow puma to be restarted by `bin/rails restart` command. +plugin :tmp_restart diff --git a/config/routes.rb b/config/routes.rb new file mode 100644 index 0000000..9a6d59a --- /dev/null +++ b/config/routes.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +Rails.application.routes.draw do + resources :heartbeat, only: [:index] + resources :events, only: [:index, :create, :update] +end diff --git a/config/shoryuken.yml b/config/shoryuken.yml new file mode 100644 index 0000000..b3f2ff6 --- /dev/null +++ b/config/shoryuken.yml @@ -0,0 +1,11 @@ +concurrency: 10 +delay: 0 +pidfile: tmp/pids/shoryuken.pid +queues: + - events + +groups: + events: + concurrency: 10 + queues: + - events diff --git a/config/storage.yml b/config/storage.yml new file mode 100644 index 0000000..4942ab6 --- /dev/null +++ b/config/storage.yml @@ -0,0 +1,34 @@ +test: + service: Disk + root: <%= Rails.root.join("tmp/storage") %> + +local: + service: Disk + root: <%= Rails.root.join("storage") %> + +# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) +# amazon: +# service: S3 +# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> +# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> +# region: us-east-1 +# bucket: your_own_bucket-<%= Rails.env %> + +# Remember not to checkin your GCS keyfile to a repository +# google: +# service: GCS +# project: your_project +# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> +# bucket: your_own_bucket-<%= Rails.env %> + +# Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) +# microsoft: +# service: AzureStorage +# storage_account_name: your_account_name +# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> +# container: your_container_name-<%= Rails.env %> + +# mirror: +# service: Mirror +# primary: local +# mirrors: [ amazon, google, microsoft ] diff --git a/db/seeds.rb b/db/seeds.rb new file mode 100644 index 0000000..0f16211 --- /dev/null +++ b/db/seeds.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +# This file should ensure the existence of records required to run the application in every environment (production, +# development, test). The code here should be idempotent so that it can be executed at any point in every environment. +# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). +# +# Example: +# +# ["Action", "Comedy", "Drama", "Horror"].each do |genre_name| +# MovieGenre.find_or_create_by!(name: genre_name) +# end diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7c93252 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,32 @@ +services: + api: + container_name: events_api + build: + context: ./ + dockerfile: Dockerfile + ports: + - "8700:80" + volumes: + - ./app:/home/app/webapp/app + - ./config:/home/app/webapp/config + - ./db:/home/app/webapp/db + - ./lib:/home/app/webapp/lib + - ./spec:/home/app/webapp/spec + - ./storage:/home/app/webapp/storage + - ./tmp:/home/app/webapp/tmp + - bundle_cache:/home/app/webapp/vendor/bundle + - ./Gemfile:/home/app/webapp/Gemfile + dns: + - 10.0.2.20 + networks: + - localstack_network + +volumes: + bundle_cache: + gemfile: + data: + driver: local + +networks: + localstack_network: + external: true diff --git a/lib/tasks/.keep b/lib/tasks/.keep new file mode 100644 index 0000000..e69de29 diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb new file mode 100644 index 0000000..f22ca0e --- /dev/null +++ b/spec/rails_helper.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require "spec_helper" +ENV["RAILS_ENV"] ||= "test" +require_relative "../config/environment" + +abort("The Rails environment is running in production mode!") if Rails.env.production? + +require "rspec/rails" + +# TODO: add this back when you create the new events database +# begin +# ActiveRecord::Migration.maintain_test_schema! +# rescue ActiveRecord::PendingMigrationError => e +# abort(e.to_s.strip) +# end + +RSpec.configure do |config| + config.fixture_paths = [ + Rails.root.join("spec/fixtures"), + ] + + config.use_transactional_fixtures = true + + config.infer_spec_type_from_file_location! + + config.filter_rails_from_backtrace! + + # Use the activerecord-nulldb-adapter to skip db dependency for specs + ActiveRecord::Base.establish_connection(adapter: :nulldb) +end diff --git a/spec/requests/heartbeat_controller_spec.rb b/spec/requests/heartbeat_controller_spec.rb new file mode 100644 index 0000000..7f5c70f --- /dev/null +++ b/spec/requests/heartbeat_controller_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe("HeartbeatControllers", type: :request) do + describe "GET /index" do + it "returns a 200 status code" do + get "/heartbeat" + expect(response).to(have_http_status(200)) + end + + it "returns json" do + get "/heartbeat" + expect(response.content_type).to(eq("application/json; charset=utf-8")) + end + + it "retrurns the expected data" do + get "/heartbeat" + expect(JSON.parse(response.body, symbolize_names: true)).to(eq({ healthy: true })) + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..26cea8d --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +RSpec.configure do |config| + ENV["SKIP_TEST_DATABASE_TRUNCATE"] = "true" + + config.expect_with(:rspec) do |expectations| + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + config.mock_with(:rspec) do |mocks| + mocks.verify_partial_doubles = true + end + + config.shared_context_metadata_behavior = :apply_to_host_groups +end diff --git a/storage/.keep b/storage/.keep new file mode 100644 index 0000000..e69de29 diff --git a/vendor/.keep b/vendor/.keep new file mode 100644 index 0000000..e69de29 diff --git a/vendor/docker/00_app_env.conf b/vendor/docker/00_app_env.conf new file mode 100644 index 0000000..c4a9494 --- /dev/null +++ b/vendor/docker/00_app_env.conf @@ -0,0 +1,2 @@ +# File will be overwritten if user runs the container with `-e PASSENGER_APP_ENV=...`! +passenger_app_env development; diff --git a/vendor/docker/10_ssh.sh b/vendor/docker/10_ssh.sh new file mode 100755 index 0000000..b6b27ea --- /dev/null +++ b/vendor/docker/10_ssh.sh @@ -0,0 +1,4 @@ +#!/bin/sh +if [ "${PUBLIC_KEY}" ]; then + echo "${PUBLIC_KEY}" > /root/.ssh/authorized_keys +fi diff --git a/vendor/docker/80_flush_cache.sh b/vendor/docker/80_flush_cache.sh new file mode 100755 index 0000000..0b778e4 --- /dev/null +++ b/vendor/docker/80_flush_cache.sh @@ -0,0 +1,2 @@ +#!/bin/sh +/sbin/setuser app bundle exec rake memcached:flush diff --git a/vendor/docker/90_migrate.sh b/vendor/docker/90_migrate.sh new file mode 100644 index 0000000..b3181f4 --- /dev/null +++ b/vendor/docker/90_migrate.sh @@ -0,0 +1,5 @@ +#!/bin/sh +# if [ "${SERVER_ROLE}" != "secondary" ]; then +# /sbin/setuser app bundle exec rake db:migrate +# /sbin/setuser app bundle exec rake db:seed +# fi diff --git a/vendor/docker/_events.auto.tfvars.tmpl b/vendor/docker/_events.auto.tfvars.tmpl new file mode 100644 index 0000000..0ee54df --- /dev/null +++ b/vendor/docker/_events.auto.tfvars.tmpl @@ -0,0 +1,4 @@ +events_tags = { + sha = "{{ .Env.GIT_SHA }}" + version = "{{ .Env.GIT_TAG }}" +} diff --git a/vendor/docker/ntp.conf b/vendor/docker/ntp.conf new file mode 100644 index 0000000..b075221 --- /dev/null +++ b/vendor/docker/ntp.conf @@ -0,0 +1,4 @@ +server 0.amazon.pool.ntp.org iburst +server 1.amazon.pool.ntp.org iburst +server 2.amazon.pool.ntp.org iburst +server 3.amazon.pool.ntp.org iburst diff --git a/vendor/docker/passenger-datadog.sh b/vendor/docker/passenger-datadog.sh new file mode 100755 index 0000000..de9281e --- /dev/null +++ b/vendor/docker/passenger-datadog.sh @@ -0,0 +1,3 @@ +#!/bin/sh +exec 2>&1 +exec /usr/local/bin/passenger-datadog-monitor -port=8125 diff --git a/vendor/docker/shoryuken.sh b/vendor/docker/shoryuken.sh new file mode 100755 index 0000000..a32ac45 --- /dev/null +++ b/vendor/docker/shoryuken.sh @@ -0,0 +1,7 @@ +#!/bin/sh +cd /home/app/webapp +exec 2>&1 +# If AWS_REGION is set and not explicitly disabled with DISABLE_QUEUE_WORKER, start shoryuken +if [ -n "$AWS_REGION" ]; then + exec /sbin/setuser app bundle exec shoryuken -R -C config/shoryuken.yml +fi diff --git a/vendor/docker/webapp.conf b/vendor/docker/webapp.conf new file mode 100644 index 0000000..81bc5a4 --- /dev/null +++ b/vendor/docker/webapp.conf @@ -0,0 +1,12 @@ +server { + listen 80 default_server; + server_name _; + root /home/app/webapp/public; + + passenger_enabled on; + passenger_user app; + passenger_ruby /usr/bin/ruby; + passenger_preload_bundler on; + + merge_slashes off; +}