├── .browserslistrc ├── .dockerignore ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── docker.yml │ └── main.yml ├── .gitignore ├── .ruby-version ├── Dockerfile ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Procfile.dev ├── README.md ├── Rakefile ├── app ├── assets │ ├── builds │ │ └── .keep │ ├── config │ │ └── manifest.js │ ├── images │ │ └── .keep │ └── stylesheets │ │ ├── application.scss │ │ └── checks.scss ├── channels │ └── application_cable │ │ ├── channel.rb │ │ └── connection.rb ├── controllers │ ├── application_controller.rb │ ├── checks_controller.rb │ └── concerns │ │ └── .keep ├── helpers │ ├── application_helper.rb │ └── checks_helper.rb ├── javascript │ ├── application.js │ ├── channels │ │ ├── consumer.js │ │ └── index.js │ └── packs │ │ └── application.js ├── jobs │ └── application_job.rb ├── lib │ └── ciao │ │ ├── notifications │ │ ├── base.rb │ │ ├── mail_notification.rb │ │ └── webhook_notification.rb │ │ ├── parsers │ │ └── webhook_parser.rb │ │ └── renderers │ │ ├── base.rb │ │ └── replace_renderer.rb ├── mailers │ ├── application_mailer.rb │ └── check_mailer.rb ├── models │ ├── application_record.rb │ ├── check.rb │ ├── concerns │ │ └── .keep │ └── status_change.rb ├── validators │ ├── cron_validator.rb │ └── http_url_validator.rb └── views │ ├── check_mailer │ ├── change_status_mail.text.erb │ └── tls_expires_mail.text.erb │ ├── checks │ ├── _check.json.jbuilder │ ├── _form.html.erb │ ├── admin.html.erb │ ├── dashboard.html.erb │ ├── edit.html.erb │ ├── index.html.erb │ ├── index.json.jbuilder │ ├── job.html.erb │ ├── job.json.jbuilder │ ├── new.html.erb │ ├── show.html.erb │ └── show.json.jbuilder │ └── layouts │ ├── application.html.erb │ ├── mailer.html.erb │ └── mailer.text.erb ├── babel.config.js ├── bin ├── bundle ├── dev ├── rails ├── rake ├── setup ├── spring └── yarn ├── config.ru ├── config ├── application.rb ├── boot.rb ├── cable.yml ├── database.yml ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── initializers │ ├── app.rb │ ├── application_controller_renderer.rb │ ├── assets.rb │ ├── backtrace_silencers.rb │ ├── content_security_policy.rb │ ├── cookies_serializer.rb │ ├── create_background_jobs.rb │ ├── filter_parameter_logging.rb │ ├── inflections.rb │ ├── mime_types.rb │ ├── notifications.rb │ ├── rufus_scheduler.rb │ ├── simple_form.rb │ ├── simple_form_bootstrap.rb │ ├── wrap_parameters.rb │ └── yabeda.rb ├── locales │ ├── en.yml │ └── simple_form.en.yml ├── puma.rb ├── routes.rb ├── secrets.yaml ├── spring.rb ├── storage.yml └── webpack │ └── webpack.config.js ├── db ├── migrate │ ├── 20190511005035_create_checks.rb │ ├── 20190511124347_add_url_to_checks.rb │ ├── 20190511124951_add_status_to_checks.rb │ ├── 20190511125018_add_active_to_checks.rb │ ├── 20190511134031_add_job_to_checks.rb │ ├── 20190511231027_remove_description_from_checks.rb │ ├── 20190513113609_add_last_contact_at_to_checks.rb │ ├── 20190513131721_add_next_contact_at_to_checks.rb │ ├── 20221114215533_create_status_changes.rb │ ├── 20230613104350_add_tls_expires_at_to_checks.rb │ ├── 20230613114013_add_tls_expires_in_days_to_checks.rb │ └── 20230615063542_add_tls_job_to_checks.rb ├── schema.rb ├── seeds.rb └── sqlite │ └── .keep ├── guestbook.md ├── helm-chart ├── ciao-0.5.0.tgz └── ciao │ ├── .gitignore │ ├── .helmignore │ ├── Chart.yaml │ ├── README.md │ ├── templates │ ├── NOTES.txt │ ├── _helpers.tpl │ ├── deployment.yaml │ ├── ingress.yaml │ ├── pvc.yaml │ ├── service.yaml │ └── tests │ │ └── test-connection.yaml │ └── values.yaml ├── lib ├── assets │ └── .keep ├── tasks │ └── .keep └── templates │ └── erb │ └── scaffold │ └── _form.html.erb ├── log └── .keep ├── package.json ├── postcss.config.js ├── public ├── 404.html ├── 422.html ├── 500.html ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── robots.txt └── version ├── scripts └── chart │ ├── cd.sh │ └── ci.sh ├── smtp_configuration.md ├── start.sh ├── storage └── .keep ├── test ├── application_system_test_case.rb ├── channels │ └── application_cable │ │ └── connection_test.rb ├── controllers │ ├── .keep │ └── checks_controller_test.rb ├── fixtures │ ├── .keep │ ├── checks.yml │ ├── files │ │ └── .keep │ └── status_changes.yml ├── helpers │ └── .keep ├── integration │ └── .keep ├── mailers │ ├── .keep │ ├── check_mailer_test.rb │ └── previews │ │ └── check_mailer_preview.rb ├── models │ ├── .keep │ ├── check_test.rb │ └── status_change_test.rb ├── system │ ├── .keep │ └── checks_test.rb ├── test_helper.rb └── unit │ └── ciao │ ├── notifications │ └── webhook_notification_test.rb │ ├── parsers │ └── webhook_parser_test.rb │ └── renderers │ └── replace_renderer_test.rb ├── tmp └── .keep ├── vendor └── .keep ├── webhook_configuration.md └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | defaults 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Ignore all logfiles and tempfiles. 2 | /log/* 3 | /tmp/* 4 | /public/packs/* 5 | !/log/.keep 6 | !/tmp/.keep 7 | .DS_Store 8 | .idea 9 | .env.development 10 | .env.staging 11 | 12 | # Ignore node_modules 13 | /node_modules 14 | 15 | # Ignore bundler config. 16 | /.bundle 17 | 18 | # Ignore the tags file used by VIM 19 | tags 20 | 21 | # Ignore Byebug command history file. 22 | .byebug_history 23 | 24 | # Ignore .git as it's not needed with the docker built. 25 | .git 26 | .cache 27 | 28 | # Ignore master key for decrypting credentials and more. 29 | /config/master.key 30 | 31 | # Ignore Dockerfile 32 | /Dockerfile 33 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_size = 2 6 | indent_style = space 7 | end_of_line = lf 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [Dockerfile] 12 | indent_size = 4 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | Use Markdown code blocks or attachments for logs and code. 27 | 28 | **Desktop (please complete the following information):** 29 | - OS: [e.g. iOS] 30 | - Browser [e.g. chrome, safari] 31 | - Version [e.g. 22] 32 | 33 | **Smartphone (please complete the following information):** 34 | - Device: [e.g. iPhone6] 35 | - OS: [e.g. iOS8.1] 36 | - Browser [e.g. stock browser, safari] 37 | - Version [e.g. 22] 38 | 39 | **Additional context** 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: build and push docker image 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: checkout code 13 | uses: actions/checkout@v2 14 | 15 | - name: install buildx 16 | id: buildx 17 | uses: crazy-max/ghaction-docker-buildx@v1 18 | with: 19 | version: latest 20 | 21 | - name: login to docker hub 22 | run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin 23 | 24 | - name: build the image 25 | run: | 26 | docker buildx build \ 27 | --tag brotandgames/ciao:$(cat ./public/version) \ 28 | --platform linux/amd64,linux/arm/v7,linux/arm64 . 29 | 30 | - name: push the image 31 | if: "!contains(github.ref, '-rc')" 32 | run: | 33 | docker buildx build --push \ 34 | --tag brotandgames/ciao:$(cat ./public/version) \ 35 | --tag brotandgames/ciao:latest \ 36 | --platform linux/amd64,linux/arm/v7,linux/arm64 . 37 | 38 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: 3 | push: 4 | branches: [ master ] 5 | pull_request: 6 | branches: [ master ] 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: ruby/setup-ruby@v1 13 | with: 14 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 15 | - run: | 16 | yarn install --check-files 17 | bundle exec standardrb 18 | # TODO: The asset "application.js" is not present in the asset pipeline. (GitHub Action) 19 | touch ./app/assets/builds/application.js 20 | bundle exec rake 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore the default SQLite database (prior to ciao 1.1.0). 11 | /db/*.sqlite3 12 | /db/*.sqlite3-journal 13 | 14 | # Ignore the SQLite database (ciao >= 1.1.0). 15 | # Moved to subfolder to enable docker to use a volume. 16 | /db/sqlite/*.sqlite3 17 | /db/sqlite/*.sqlite3-journal 18 | /db/sqlite/test.sqlite* 19 | 20 | # Ignore all logfiles and tempfiles. 21 | /log/* 22 | /tmp/* 23 | !/log/.keep 24 | !/tmp/.keep 25 | 26 | # Ignore uploaded files in development. 27 | /storage/* 28 | !/storage/.keep 29 | 30 | /public/assets 31 | .byebug_history 32 | 33 | # Ignore master key for decrypting credentials and more. 34 | /config/master.key 35 | 36 | /public/packs 37 | /public/packs-test 38 | /node_modules 39 | /yarn-error.log 40 | yarn-debug.log* 41 | .yarn-integrity 42 | .DS_Store 43 | 44 | # yard generated 45 | doc 46 | .yardoc 47 | 48 | /app/assets/builds/* 49 | !/app/assets/builds/.keep 50 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.3.7 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:3.3.7-alpine 2 | 3 | # for postgres: postgresql-dev 4 | RUN apk add --no-cache \ 5 | sqlite-dev \ 6 | tzdata \ 7 | yarn 8 | 9 | WORKDIR /app 10 | 11 | ARG RACK_ENV=production 12 | ENV RACK_ENV=$RACK_ENV 13 | 14 | ADD Gemfile* /app/ 15 | RUN set -x \ 16 | && apk add --no-cache --virtual .build-deps \ 17 | build-base \ 18 | libxml2-dev \ 19 | libxslt-dev \ 20 | git \ 21 | libffi-dev \ 22 | musl-dev \ 23 | linux-headers \ 24 | && gem install bundler \ 25 | && bundle config set --local without 'development:test' \ 26 | && bundle install --jobs 20 -j"$(nproc)" --retry 3 \ 27 | # Remove unneeded files (cached *.gem, *.o, *.c) 28 | && rm -rf \ 29 | /usr/local/bundle/cache/*.gem \ 30 | /usr/local/bundle/gems/**/*.c \ 31 | /usr/local/bundle/gems/**/*.o \ 32 | && apk del .build-deps 33 | 34 | COPY package.json yarn.lock /app/ 35 | RUN set -x \ 36 | && yarn install \ 37 | && rm -rf /tmp/* 38 | 39 | COPY . ./ 40 | 41 | # The command '/bin/sh -c rake assets:precompile' needs the RAILS_MASTER_KEY to be set!? 42 | # https://github.com/rails/rails/issues/32947 43 | # 44 | # Added xz-libs because nokogiri needs liblzma.so.5 45 | # during rake tasks (eg. assets-precompile) 46 | # 47 | # Added gcompat 48 | # https://nokogiri.org/tutorials/installing_nokogiri.html#linux-musl-error-loading-shared-library 49 | RUN set -x \ 50 | && apk add --no-cache xz-libs gcompat \ 51 | && SECRET_KEY_BASE=foo bundle exec rake assets:precompile \ 52 | # Remove folders not needed in resulting image 53 | && rm -rf \ 54 | /tmp/* \ 55 | app/assets \ 56 | lib/assets \ 57 | node_modules \ 58 | spec \ 59 | tmp/cache \ 60 | vendor/assets 61 | 62 | ENV RAILS_LOG_TO_STDOUT=true 63 | ENV RAILS_SERVE_STATIC_FILES=true 64 | ENV EXECJS_RUNTIME=Disabled 65 | 66 | EXPOSE 3000 67 | 68 | VOLUME /app/db/sqlite 69 | 70 | CMD ["./start.sh"] 71 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 5 | 6 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' 7 | gem "rails", "~> 6.1" 8 | # Use sqlite3 as the database for Active Record 9 | # That platforms: [:ruby] disables usage of the precompiled native gem 10 | # and forces native compilation, which is essential on Alpine 11 | gem "sqlite3", "~> 1.4", platforms: [:ruby] 12 | # Use Puma as the app server 13 | gem "puma", "~> 6.4" 14 | # Use SCSS for stylesheets 15 | gem "sass-rails", "~> 5" 16 | # https://github.com/rails/jsbundling-rails 17 | gem "jsbundling-rails" 18 | # Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks 19 | gem "turbolinks", "~> 5" 20 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder 21 | gem "jbuilder", "~> 2.9" 22 | # Use Redis adapter to run Action Cable in production 23 | # gem 'redis', '~> 4.0' 24 | # Use Active Model has_secure_password 25 | # gem 'bcrypt', '~> 3.1.7' 26 | 27 | # Use Active Storage variant 28 | # gem 'image_processing', '~> 1.2' 29 | 30 | # Reduces boot times through caching; required in config/boot.rb 31 | gem "bootsnap", ">= 1.4.4", require: false 32 | 33 | group :development, :test do 34 | # Call 'byebug' anywhere in the code to stop execution and get a debugger console 35 | gem "byebug", platforms: %i[mri mingw x64_mingw] 36 | gem "standardrb" 37 | end 38 | 39 | group :development do 40 | gem "letter_opener" 41 | gem "listen", "~> 3.3" 42 | # Access an interactive console on exception pages or by calling 'console' anywhere in the code. 43 | gem "web-console", ">= 4.1.0" 44 | # Display performance information such as SQL time and flame graphs for each request in your browser. 45 | # Can be configured to work on production as well see: https://github.com/MiniProfiler/rack-mini-profiler/blob/master/README.md 46 | gem "rack-mini-profiler", "~> 2.0" 47 | # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring 48 | # Issue: https://github.com/rails/spring/issues/734 49 | gem "spring", "~> 4.2.0" 50 | gem "yard" 51 | end 52 | 53 | group :test do 54 | # Adds support for Capybara system testing and selenium driver 55 | gem "capybara", ">= 3.26" 56 | gem "mocha" 57 | gem "selenium-webdriver", ">= 4.0.0.rc1" 58 | # Easy installation and use of web drivers to run system tests with browsers 59 | gem "webdrivers" 60 | gem "webmock" 61 | end 62 | 63 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 64 | gem "tzinfo-data", platforms: %i[mingw mswin x64_mingw jruby] 65 | 66 | # tabler-rubygem is only compatible with bootstrap 4 67 | # The `bg-variant` mixin has been deprecated as of v4.4.0. It will be removed entirely in v5. 68 | gem "bootstrap", "~> 4" 69 | gem "drb" 70 | gem "mutex_m" 71 | # https://stackoverflow.com/questions/78617432/strange-bundle-update-issue-disappearing-net-pop-0-1-2-dependency 72 | gem "net-pop", github: "ruby/net-pop" 73 | gem "prometheus-client" 74 | gem "rufus-scheduler" 75 | gem "simple_form" 76 | gem "tabler-rubygem" 77 | gem "yabeda-prometheus" 78 | gem "yabeda-rails" 79 | 80 | gem "sassc-rails", "~> 2.1" 81 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: https://github.com/ruby/net-pop.git 3 | revision: e8d0afe2773b9eb6a23c39e9e437f6fc0fc7c733 4 | specs: 5 | net-pop (0.1.2) 6 | net-protocol 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | actioncable (6.1.7.10) 12 | actionpack (= 6.1.7.10) 13 | activesupport (= 6.1.7.10) 14 | nio4r (~> 2.0) 15 | websocket-driver (>= 0.6.1) 16 | actionmailbox (6.1.7.10) 17 | actionpack (= 6.1.7.10) 18 | activejob (= 6.1.7.10) 19 | activerecord (= 6.1.7.10) 20 | activestorage (= 6.1.7.10) 21 | activesupport (= 6.1.7.10) 22 | mail (>= 2.7.1) 23 | actionmailer (6.1.7.10) 24 | actionpack (= 6.1.7.10) 25 | actionview (= 6.1.7.10) 26 | activejob (= 6.1.7.10) 27 | activesupport (= 6.1.7.10) 28 | mail (~> 2.5, >= 2.5.4) 29 | rails-dom-testing (~> 2.0) 30 | actionpack (6.1.7.10) 31 | actionview (= 6.1.7.10) 32 | activesupport (= 6.1.7.10) 33 | rack (~> 2.0, >= 2.0.9) 34 | rack-test (>= 0.6.3) 35 | rails-dom-testing (~> 2.0) 36 | rails-html-sanitizer (~> 1.0, >= 1.2.0) 37 | actiontext (6.1.7.10) 38 | actionpack (= 6.1.7.10) 39 | activerecord (= 6.1.7.10) 40 | activestorage (= 6.1.7.10) 41 | activesupport (= 6.1.7.10) 42 | nokogiri (>= 1.8.5) 43 | actionview (6.1.7.10) 44 | activesupport (= 6.1.7.10) 45 | builder (~> 3.1) 46 | erubi (~> 1.4) 47 | rails-dom-testing (~> 2.0) 48 | rails-html-sanitizer (~> 1.1, >= 1.2.0) 49 | activejob (6.1.7.10) 50 | activesupport (= 6.1.7.10) 51 | globalid (>= 0.3.6) 52 | activemodel (6.1.7.10) 53 | activesupport (= 6.1.7.10) 54 | activerecord (6.1.7.10) 55 | activemodel (= 6.1.7.10) 56 | activesupport (= 6.1.7.10) 57 | activestorage (6.1.7.10) 58 | actionpack (= 6.1.7.10) 59 | activejob (= 6.1.7.10) 60 | activerecord (= 6.1.7.10) 61 | activesupport (= 6.1.7.10) 62 | marcel (~> 1.0) 63 | mini_mime (>= 1.1.0) 64 | activesupport (6.1.7.10) 65 | concurrent-ruby (~> 1.0, >= 1.0.2) 66 | i18n (>= 1.6, < 2) 67 | minitest (>= 5.1) 68 | tzinfo (~> 2.0) 69 | zeitwerk (~> 2.3) 70 | addressable (2.8.7) 71 | public_suffix (>= 2.0.2, < 7.0) 72 | anyway_config (2.7.1) 73 | ruby-next-core (~> 1.0) 74 | ast (2.4.3) 75 | autoprefixer-rails (10.4.19.0) 76 | execjs (~> 2) 77 | base64 (0.2.0) 78 | bigdecimal (3.1.9) 79 | bindex (0.8.1) 80 | bootsnap (1.18.4) 81 | msgpack (~> 1.2) 82 | bootstrap (4.6.2.1) 83 | autoprefixer-rails (>= 9.1.0) 84 | popper_js (>= 1.16.1, < 2) 85 | builder (3.3.0) 86 | byebug (11.1.3) 87 | capybara (3.40.0) 88 | addressable 89 | matrix 90 | mini_mime (>= 0.1.3) 91 | nokogiri (~> 1.11) 92 | rack (>= 1.6.0) 93 | rack-test (>= 0.6.3) 94 | regexp_parser (>= 1.5, < 3.0) 95 | xpath (~> 3.2) 96 | childprocess (5.1.0) 97 | logger (~> 1.5) 98 | concurrent-ruby (1.3.5) 99 | crack (1.0.0) 100 | bigdecimal 101 | rexml 102 | crass (1.0.6) 103 | date (3.4.1) 104 | drb (2.2.1) 105 | dry-initializer (3.2.0) 106 | erubi (1.13.1) 107 | et-orbi (1.2.11) 108 | tzinfo 109 | execjs (2.10.0) 110 | ffi (1.17.1-aarch64-linux-gnu) 111 | ffi (1.17.1-aarch64-linux-musl) 112 | ffi (1.17.1-arm-linux-gnu) 113 | ffi (1.17.1-arm-linux-musl) 114 | ffi (1.17.1-arm64-darwin) 115 | ffi (1.17.1-x86_64-darwin) 116 | ffi (1.17.1-x86_64-linux-gnu) 117 | ffi (1.17.1-x86_64-linux-musl) 118 | fugit (1.11.1) 119 | et-orbi (~> 1, >= 1.2.11) 120 | raabro (~> 1.4) 121 | globalid (1.2.1) 122 | activesupport (>= 6.1) 123 | hashdiff (1.1.2) 124 | i18n (1.14.7) 125 | concurrent-ruby (~> 1.0) 126 | jbuilder (2.13.0) 127 | actionview (>= 5.0.0) 128 | activesupport (>= 5.0.0) 129 | jsbundling-rails (1.3.1) 130 | railties (>= 6.0.0) 131 | json (2.10.2) 132 | language_server-protocol (3.17.0.4) 133 | launchy (3.1.1) 134 | addressable (~> 2.8) 135 | childprocess (~> 5.0) 136 | logger (~> 1.6) 137 | letter_opener (1.10.0) 138 | launchy (>= 2.2, < 4) 139 | lint_roller (1.1.0) 140 | listen (3.9.0) 141 | rb-fsevent (~> 0.10, >= 0.10.3) 142 | rb-inotify (~> 0.9, >= 0.9.10) 143 | logger (1.6.6) 144 | loofah (2.24.0) 145 | crass (~> 1.0.2) 146 | nokogiri (>= 1.12.0) 147 | mail (2.8.1) 148 | mini_mime (>= 0.1.1) 149 | net-imap 150 | net-pop 151 | net-smtp 152 | marcel (1.0.4) 153 | matrix (0.4.2) 154 | method_source (1.1.0) 155 | mini_mime (1.1.5) 156 | minitest (5.25.5) 157 | mocha (2.7.1) 158 | ruby2_keywords (>= 0.0.5) 159 | msgpack (1.8.0) 160 | mutex_m (0.3.0) 161 | net-imap (0.5.6) 162 | date 163 | net-protocol 164 | net-protocol (0.2.2) 165 | timeout 166 | net-smtp (0.5.1) 167 | net-protocol 168 | nio4r (2.7.4) 169 | nokogiri (1.18.5-aarch64-linux-gnu) 170 | racc (~> 1.4) 171 | nokogiri (1.18.5-aarch64-linux-musl) 172 | racc (~> 1.4) 173 | nokogiri (1.18.5-arm-linux-gnu) 174 | racc (~> 1.4) 175 | nokogiri (1.18.5-arm-linux-musl) 176 | racc (~> 1.4) 177 | nokogiri (1.18.5-arm64-darwin) 178 | racc (~> 1.4) 179 | nokogiri (1.18.5-x86_64-darwin) 180 | racc (~> 1.4) 181 | nokogiri (1.18.5-x86_64-linux-gnu) 182 | racc (~> 1.4) 183 | nokogiri (1.18.5-x86_64-linux-musl) 184 | racc (~> 1.4) 185 | parallel (1.26.3) 186 | parser (3.3.7.2) 187 | ast (~> 2.4.1) 188 | racc 189 | popper_js (1.16.1) 190 | prometheus-client (4.2.4) 191 | base64 192 | public_suffix (6.0.1) 193 | puma (6.6.0) 194 | nio4r (~> 2.0) 195 | raabro (1.4.0) 196 | racc (1.8.1) 197 | rack (2.2.13) 198 | rack-mini-profiler (2.3.4) 199 | rack (>= 1.2.0) 200 | rack-test (2.2.0) 201 | rack (>= 1.3) 202 | rails (6.1.7.10) 203 | actioncable (= 6.1.7.10) 204 | actionmailbox (= 6.1.7.10) 205 | actionmailer (= 6.1.7.10) 206 | actionpack (= 6.1.7.10) 207 | actiontext (= 6.1.7.10) 208 | actionview (= 6.1.7.10) 209 | activejob (= 6.1.7.10) 210 | activemodel (= 6.1.7.10) 211 | activerecord (= 6.1.7.10) 212 | activestorage (= 6.1.7.10) 213 | activesupport (= 6.1.7.10) 214 | bundler (>= 1.15.0) 215 | railties (= 6.1.7.10) 216 | sprockets-rails (>= 2.0.0) 217 | rails-dom-testing (2.2.0) 218 | activesupport (>= 5.0.0) 219 | minitest 220 | nokogiri (>= 1.6) 221 | rails-html-sanitizer (1.6.2) 222 | loofah (~> 2.21) 223 | 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) 224 | railties (6.1.7.10) 225 | actionpack (= 6.1.7.10) 226 | activesupport (= 6.1.7.10) 227 | method_source 228 | rake (>= 12.2) 229 | thor (~> 1.0) 230 | rainbow (3.1.1) 231 | rake (13.2.1) 232 | rb-fsevent (0.11.2) 233 | rb-inotify (0.11.1) 234 | ffi (~> 1.0) 235 | regexp_parser (2.10.0) 236 | rexml (3.4.1) 237 | rubocop (1.73.2) 238 | json (~> 2.3) 239 | language_server-protocol (~> 3.17.0.2) 240 | lint_roller (~> 1.1.0) 241 | parallel (~> 1.10) 242 | parser (>= 3.3.0.2) 243 | rainbow (>= 2.2.2, < 4.0) 244 | regexp_parser (>= 2.9.3, < 3.0) 245 | rubocop-ast (>= 1.38.0, < 2.0) 246 | ruby-progressbar (~> 1.7) 247 | unicode-display_width (>= 2.4.0, < 4.0) 248 | rubocop-ast (1.41.0) 249 | parser (>= 3.3.7.2) 250 | rubocop-performance (1.24.0) 251 | lint_roller (~> 1.1) 252 | rubocop (>= 1.72.1, < 2.0) 253 | rubocop-ast (>= 1.38.0, < 2.0) 254 | ruby-next-core (1.1.1) 255 | ruby-progressbar (1.13.0) 256 | ruby2_keywords (0.0.5) 257 | rubyzip (2.4.1) 258 | rufus-scheduler (3.9.2) 259 | fugit (~> 1.1, >= 1.11.1) 260 | sass (3.7.4) 261 | sass-listen (~> 4.0.0) 262 | sass-listen (4.0.0) 263 | rb-fsevent (~> 0.9, >= 0.9.4) 264 | rb-inotify (~> 0.9, >= 0.9.7) 265 | sass-rails (5.1.0) 266 | railties (>= 5.2.0) 267 | sass (~> 3.1) 268 | sprockets (>= 2.8, < 4.0) 269 | sprockets-rails (>= 2.0, < 4.0) 270 | tilt (>= 1.1, < 3) 271 | sassc (2.4.0) 272 | ffi (~> 1.9) 273 | sassc-rails (2.1.2) 274 | railties (>= 4.0.0) 275 | sassc (>= 2.0) 276 | sprockets (> 3.0) 277 | sprockets-rails 278 | tilt 279 | selenium-webdriver (4.10.0) 280 | rexml (~> 3.2, >= 3.2.5) 281 | rubyzip (>= 1.2.2, < 3.0) 282 | websocket (~> 1.0) 283 | simple_form (5.3.1) 284 | actionpack (>= 5.2) 285 | activemodel (>= 5.2) 286 | spring (4.2.1) 287 | sprockets (3.7.5) 288 | base64 289 | concurrent-ruby (~> 1.0) 290 | rack (> 1, < 3) 291 | sprockets-rails (3.5.2) 292 | actionpack (>= 6.1) 293 | activesupport (>= 6.1) 294 | sprockets (>= 3.0.0) 295 | sqlite3 (1.7.3-aarch64-linux) 296 | sqlite3 (1.7.3-arm-linux) 297 | sqlite3 (1.7.3-arm64-darwin) 298 | sqlite3 (1.7.3-x86_64-darwin) 299 | sqlite3 (1.7.3-x86_64-linux) 300 | standard (1.47.0) 301 | language_server-protocol (~> 3.17.0.2) 302 | lint_roller (~> 1.0) 303 | rubocop (~> 1.73.0) 304 | standard-custom (~> 1.0.0) 305 | standard-performance (~> 1.7) 306 | standard-custom (1.0.2) 307 | lint_roller (~> 1.0) 308 | rubocop (~> 1.50) 309 | standard-performance (1.7.0) 310 | lint_roller (~> 1.1) 311 | rubocop-performance (~> 1.24.0) 312 | standardrb (1.0.1) 313 | standard 314 | tabler-rubygem (0.1.4) 315 | autoprefixer-rails (>= 6.0.3) 316 | thor (1.3.2) 317 | tilt (2.6.0) 318 | timeout (0.4.3) 319 | turbolinks (5.2.1) 320 | turbolinks-source (~> 5.2) 321 | turbolinks-source (5.2.0) 322 | tzinfo (2.0.6) 323 | concurrent-ruby (~> 1.0) 324 | unicode-display_width (3.1.4) 325 | unicode-emoji (~> 4.0, >= 4.0.4) 326 | unicode-emoji (4.0.4) 327 | web-console (4.2.1) 328 | actionview (>= 6.0.0) 329 | activemodel (>= 6.0.0) 330 | bindex (>= 0.4.0) 331 | railties (>= 6.0.0) 332 | webdrivers (5.3.1) 333 | nokogiri (~> 1.6) 334 | rubyzip (>= 1.3.0) 335 | selenium-webdriver (~> 4.0, < 4.11) 336 | webmock (3.25.1) 337 | addressable (>= 2.8.0) 338 | crack (>= 0.3.2) 339 | hashdiff (>= 0.4.0, < 2.0.0) 340 | websocket (1.2.11) 341 | websocket-driver (0.7.7) 342 | base64 343 | websocket-extensions (>= 0.1.0) 344 | websocket-extensions (0.1.5) 345 | xpath (3.2.0) 346 | nokogiri (~> 1.8) 347 | yabeda (0.13.1) 348 | anyway_config (>= 1.0, < 3) 349 | concurrent-ruby 350 | dry-initializer 351 | yabeda-prometheus (0.9.1) 352 | prometheus-client (>= 3.0, < 5.0) 353 | rack 354 | yabeda (~> 0.10) 355 | yabeda-rails (0.9.0) 356 | activesupport 357 | anyway_config (>= 1.3, < 3) 358 | railties 359 | yabeda (~> 0.8) 360 | yard (0.9.37) 361 | zeitwerk (2.7.2) 362 | 363 | PLATFORMS 364 | aarch64-linux 365 | aarch64-linux-gnu 366 | aarch64-linux-musl 367 | arm-linux 368 | arm-linux-gnu 369 | arm-linux-musl 370 | arm64-darwin 371 | x86_64-darwin 372 | x86_64-linux 373 | x86_64-linux-gnu 374 | x86_64-linux-musl 375 | 376 | DEPENDENCIES 377 | bootsnap (>= 1.4.4) 378 | bootstrap (~> 4) 379 | byebug 380 | capybara (>= 3.26) 381 | drb 382 | jbuilder (~> 2.9) 383 | jsbundling-rails 384 | letter_opener 385 | listen (~> 3.3) 386 | mocha 387 | mutex_m 388 | net-pop! 389 | prometheus-client 390 | puma (~> 6.4) 391 | rack-mini-profiler (~> 2.0) 392 | rails (~> 6.1) 393 | rufus-scheduler 394 | sass-rails (~> 5) 395 | sassc-rails (~> 2.1) 396 | selenium-webdriver (>= 4.0.0.rc1) 397 | simple_form 398 | spring (~> 4.2.0) 399 | sqlite3 (~> 1.4) 400 | standardrb 401 | tabler-rubygem 402 | turbolinks (~> 5) 403 | tzinfo-data 404 | web-console (>= 4.1.0) 405 | webdrivers 406 | webmock 407 | yabeda-prometheus 408 | yabeda-rails 409 | yard 410 | 411 | BUNDLED WITH 412 | 2.5.22 413 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Brot & Games 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile.dev: -------------------------------------------------------------------------------- 1 | web: bin/rails server -p 3000 2 | js: yarn build --watch 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ciao 2 | 3 | [![Latest release](https://img.shields.io/github/release/brotandgames/ciao.svg)](https://github.com/brotandgames/ciao/releases/latest) 4 | [![Docker pulls](https://img.shields.io/docker/pulls/brotandgames/ciao.svg)](https://store.docker.com/community/images/brotandgames/ciao) 5 | [![Website link](https://brotandgames.com/assets/ciao-link-website.svg)](https://brotandgames.com/ciao/) 6 | [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/brotandgames/ciao/master/LICENSE) 7 | [![Build Status](https://github.com/brotandgames/ciao/actions/workflows/main.yml/badge.svg)](https://github.com/brotandgames/ciao/actions/) 8 | 9 | **[ciao](https://www.brotandgames.com/ciao/)** checks HTTP(S) URL endpoints for a HTTP status code (or errors on the lower TCP stack) and sends a notification on status change via E-Mail or Webhooks. 10 | 11 | It uses Cron syntax to schedule the checks and comes along with a Web UI and a RESTful JSON API. 12 | 13 | ![ciao Checks overview](https://brotandgames.com/assets/ciao-checks.png "ciao Checks overview") 14 | *You can find more screenshots on the [Homepage](https://www.brotandgames.com/ciao/).* 15 | 16 | **ciao** (/tʃaʊ/) - **c**heck **i**n **a**nd **o**ut - borrowed from Italian *ciao* for greeting someone. 17 | 18 | *Motivation:* create an open source web application for checking URL statuses with an UI and a REST API which is easy to install and maintain (no external dependencies like Databases, Caches etc.) in public and private environments. 19 | 20 | Follow [@brotandgames](https://www.twitter.com/brotandgames) on Twitter to get the latest News like Releases. Use [#ciaohttp](https://twitter.com/hashtag/ciaohttp) Hashtag for ciao related stuff. 21 | 22 | ## Quickstart 23 | 24 | ```sh 25 | docker run --name ciao -p 8090:3000 brotandgames/ciao 26 | ``` 27 | 28 | Open http://localhost:8090 in your webbrowser. 29 | 30 | ## Features 31 | 32 | * Check HTTP/S endpoints in an interval 33 | * Use Cron syntax like `* * * * *` (every minute), `*/15 * * * *` (every 15 minutes), `@hourly` or `@daily` etc. 34 | * Keep track of status changes (since version 1.8.0) 35 | * Check TLS certificate expiration once a day and get a notification if it expires in less than 30 days (since version 1.9.0) 36 | * Web UI 37 | * [RESTful JSON API](#rest-api) 38 | * Get a notification on status change via [E-Mail](smtp_configuration.md) eg. Gmail, Sendgrid, MailChimp etc. (optional) 39 | * Get a notification on status change via [Webhooks](webhook_configuration.md) eg. Rocket.Chat, Slack etc. (optional) 40 | * Configuration via ENVIRONMENT variables (suitable for most runtimes) 41 | * Expose Prometheus Metrics endpoint `/metrics` with information to digest by tools like Grafana (optional) 42 | * Protect with HTTP Basic auth on application basis (optional, only recommended in combination with TLS) 43 | * Instructions for [installing](#install)/[deploying](#deploy) in/to different Platforms 44 | * [Docker Image](#via-docker-image) 45 | * [Helm Chart](#via-helm) 46 | * [Terraform Provider](https://github.com/autonubil/terraform-provider-ciao) (Maintainer: [@autonubil](https://github.com/autonubil)) 47 | 48 | 49 | ## Configuration 50 | 51 | ciao is configured via ENVIRONMENT variables following the [12-factor app methodology](https://12factor.net/config). 52 | 53 | - `SECRET_KEY_BASE` will be auto-generated if you omit it 54 | - Time zone is configurable per `TIME_ZONE` variable (default: `UTC`) eg. `TIME_ZONE="Vienna"` - you can find all possible values by executing `docker run --rm brotandgames/ciao rake time:zones` (since version 1.2.0) 55 | - Check [SMTP Configuration](smtp_configuration.md) for all possible configuration variables, notes and example configurations for Gmail, Sendgrid etc. 56 | - Check [Webhook Configuration](webhook_configuration.md) for instructions how to send (webhook) notifications to RocketChat, Slack etc. (since version 1.4.0) 57 | - You can enable HTTP Basic auth for ciao by defining `BASIC_AUTH_USERNAME` and `BASIC_AUTH_PASSWORD` eg. `BASIC_AUTH_USERNAME="ciao-admin"` and `BASIC_AUTH_PASSWORD="sensitive_password"` (since version 1.3.0) 58 | - You can enable a Prometheus Metrics endpoint served under `/metrics` by setting `PROMETHEUS_ENABLED=true` - furthermore you can enable HTTP Basic auth for this endpoint by defining `PROMETHEUS_BASIC_AUTH_USERNAME="ciao-metrics"` and `PROMETHEUS_BASIC_AUTH_PASSWORD="sensitive_password"` (since version 1.5.0) 59 | - Log level is configurable via `CIAO_LOG_LEVEL` variable (default: `WARN`) - levels: DEBUG, INFO, WARN, ERROR, FATAL (since version 1.7.0) 60 | 61 | ## Install 62 | 63 | You can install ciao via the official Docker image `brotandgames/ciao` or using Git and installing the dependencies manually. 64 | 65 | By mounting a Docker volume you can avoid loosing data on restart or upgrade. 66 | 67 | IMPORTANT: Be sure to enable authentication (eg. HTTP Basic auth) and TLS certificates if you serve ciao publicly. 68 | 69 | ### Via Docker image 70 | 71 | ```sh 72 | docker run \ 73 | --name ciao \ 74 | -p 8090:3000 \ 75 | -e SECRET_KEY_BASE="sensitive_secret_key_base" \ 76 | -e SMTP_ADDRESS=smtp.yourhost.com \ 77 | -e SMTP_EMAIL_FROM="ciao@yourhost.com" \ 78 | -e SMTP_EMAIL_TO="you@yourhost.com" \ 79 | -e SMTP_PORT=587 \ 80 | -e SMTP_DOMAIN=smtp.yourhost.com \ 81 | -e SMTP_AUTHENTICATION=plain \ 82 | -e SMTP_ENABLE_STARTTLS_AUTO=true \ 83 | -e SMTP_USERNAME=ciao \ 84 | -e SMTP_PASSWORD="sensitive_password" \ 85 | -v /opt/ciao/data:/app/db/sqlite \ 86 | brotandgames/ciao 87 | ``` 88 | 89 | Open localhost:8090 in your webbrowser. 90 | 91 | ### Via Docker-compose 92 | 93 | Create docker-compose.yml file 94 | 95 | ```yaml 96 | version: "3" 97 | services: 98 | ciao: 99 | image: brotandgames/ciao 100 | container_name: ciao 101 | ports: 102 | - '8090:3000' 103 | environment: 104 | - SECRET_KEY_BASE=sensitive_secret_key_base 105 | - SMTP_ADDRESS=smtp.yourhost.com 106 | - SMTP_EMAIL_FROM=ciao@yourhost.com 107 | - SMTP_EMAIL_TO=you@yourhost.com 108 | - SMTP_PORT=587 109 | - SMTP_AUTHENTICATION=plain 110 | - SMTP_DOMAIN=smtp.yourhost.com 111 | - SMTP_ENABLE_STARTTLS_AUTO=true 112 | - SMTP_USERNAME=ciao 113 | - SMTP_PASSWORD=sensitive_password 114 | volumes: 115 | - /opt/ciao/data:/app/db/sqlite/ 116 | ``` 117 | 118 | Pull and run 119 | 120 | ```sh 121 | docker-compose pull 122 | docker-compose up -d 123 | ``` 124 | 125 | Open localhost:8090 in the webbrowser. 126 | 127 | *Note: if you have problems with environment variables (quoting, spaces etc), take a look at these GitHub issues ([1](https://github.com/brotandgames/ciao/issues/40), [2](https://github.com/docker/compose/issues/2854)) and these Stack Overflow questions ([1](https://stackoverflow.com/questions/53082932/yaml-docker-compose-spaces-quotes), [2](https://stackoverflow.com/questions/41988809/docker-compose-how-to-escape-environment-variables))*. 128 | 129 | ### Via Git clone 130 | 131 | ```sh 132 | # Clone repo 133 | git clone https://github.com/brotandgames/ciao 134 | 135 | cd ciao 136 | 137 | # Install all dependencies (rubygems) 138 | RAILS_ENV=production bundle install 139 | 140 | # Configure 141 | export SECRET_KEY_BASE="sensitive_secret_key_base" \ 142 | SMTP_ADDRESS=smtp.yourhost.com \ 143 | SMTP_EMAIL_FROM="ciao@yourhost.com" \ 144 | SMTP_EMAIL_TO="you@yourhost.com" \ 145 | SMTP_PORT=587 \ 146 | SMTP_DOMAIN=smtp.yourhost.com \ 147 | SMTP_AUTHENTICATION=plain \ 148 | SMTP_ENABLE_STARTTLS_AUTO=true \ 149 | SMTP_USERNAME=ciao \ 150 | SMTP_PASSWORD="sensitive_password" 151 | 152 | # Precompile assets 153 | rails assets:precompile 154 | 155 | # Run start script - basically this is check SECRET_KEY_BASE, database init/migrate and rails server 156 | RAILS_ENV=production ./start.sh 157 | ``` 158 | 159 | Open localhost:3000 in the webbrowser. 160 | 161 | ## REST API 162 | 163 | **GET /checks.json** 164 | 165 | Show collection (array) of all checks 166 | 167 | ```sh 168 | curl -X GET -H "Content-type: application/json" /checks.json 169 | ``` 170 | 171 | **GET /checks/<:id>.json** 172 | 173 | Show a specific check 174 | 175 | ```sh 176 | curl -X GET -H "Content-type: application/json" /checks/<:id>.json 177 | ``` 178 | 179 | **POST /checks.json** 180 | 181 | Create a check 182 | 183 | ```sh 184 | curl -X POST -H "Content-type: application/json" /checks.json \ 185 | -d '{ "name": "brotandgames.com", "active": true, "url": "https://brotandgames.com", "cron": "* * * *"}' 186 | ``` 187 | 188 | **PATCH/PUT /checks/<:id>.json** 189 | 190 | Update a check 191 | 192 | ```sh 193 | curl -X PUT -H "Content-type: application/json" /checks/<:id>.json \ 194 | -d '{ "name": "brotandgames.com", "active": false, "url": "https://brotandgames.com", "cron": "* * * *"}' 195 | ``` 196 | 197 | **DELETE /checks/<:id>.json** 198 | 199 | Delete a check 200 | 201 | ```sh 202 | curl -X DELETE -H "Content-type: application/json" /checks/<:id>.json 203 | ``` 204 | 205 | ## Backup & Restore 206 | 207 | State is stored in an internal SQLite database located in `db/sqlite/production.sqlite3`. 208 | 209 | *Note: Prior to version 1.1.0 the database was located in `db/` (missing sqlite subfolder). From 1.1.0 onwards the location is `db/sqlite/` to enable docker to use a volume.* 210 | 211 | ### Backup 212 | 213 | ```sh 214 | docker cp ciao:/app/db/sqlite/production.sqlite3 production.sqlite3.backup 215 | ``` 216 | 217 | ### Restore 218 | 219 | ```sh 220 | docker cp production.sqlite3.backup ciao:/app/db/sqlite/production.sqlite3 221 | docker restart ciao 222 | ``` 223 | *Prior to version 1.2.0: visit `/checks/admin` and recreate the background jobs for active checks.* 224 | 225 | 226 | ## Upgrade 227 | 228 | 1. [Backup](#backup) the database 229 | 2. Run container with new version 230 | 3. [Restore](#restore) the database 231 | 232 | ## Deploy 233 | 234 | Here you'll find instructions for deploying ciao to different platforms like Kubernetes or Dokku. 235 | 236 | By mounting a Docker or Kubernetes volume you can avoid loosing data on restart or upgrade. 237 | 238 | IMPORTANT: Be sure to enable authentication (eg. HTTP Basic auth) and TLS certificates if you serve ciao publicly. 239 | 240 | ### Kubernetes 241 | 242 | #### Via Helm 243 | 244 | 1. Install ciao via `helm upgrade --install` 245 | 246 | Quickstart (without configuring) 247 | 248 | ```sh 249 | helm upgrade --install --namespace ciao ciao https://github.com/brotandgames/ciao/raw/master/helm-chart/ciao-0.5.0.tgz 250 | ``` 251 | 252 | With [configuration](#configuration) 253 | 254 | ```sh 255 | helm upgrade --install --namespace ciao ciao https://github.com/brotandgames/ciao/raw/master/helm-chart/ciao-0.5.0.tgz \ 256 | --set env.SECRET_KEY_BASE="sensitive_secret_key_base" \ 257 | --set env.SMTP_ADDRESS=smtp.yourhost.com \ 258 | --set env.SMTP_EMAIL_FROM="ciao@yourhost.com" \ 259 | --set env.SMTP_EMAIL_TO="you@yourhost.com" \ 260 | --set env.SMTP_PORT=587 \ 261 | --set env.SMTP_DOMAIN=smtp.yourhost.com \ 262 | --set env.SMTP_AUTHENTICATION=plain \ 263 | --set env.SMTP_ENABLE_STARTTLS_AUTO=true \ 264 | --set env.SMTP_USERNAME=ciao \ 265 | --set env.SMTP_PASSWORD="sensitive_password" 266 | ``` 267 | 268 | #### Via kubectl 269 | 270 | The following code snippet will create a Kubernetes 271 | 272 | * Namespace `ciao`, 273 | * Secret `ciao`, 274 | * Deployment `ciao` and 275 | * Service `ciao`. 276 | 277 | `kubectl apply -f k8s.yaml` 278 | 279 | ```yaml 280 | # k8s.yaml 281 | apiVersion: v1 282 | kind: Namespace 283 | metadata: 284 | name: ciao 285 | --- 286 | apiVersion: v1 287 | kind: Secret 288 | metadata: 289 | name: ciao 290 | namespace: ciao 291 | data: 292 | # all values should be base64 encoded 293 | # so some_secret would be c29tZV9zZWNyZXQ= 294 | SECRET_KEY_BASE: some_secret 295 | SMTP_ADDRESS: smtp_address 296 | SMTP_EMAIL_FROM: noreply@somedomain.com 297 | SMTP_EMAIL_TO: monitoring@somedomain.com 298 | SMTP_PORT: 465 299 | SMTP_DOMAIN: mail.somedomain.com 300 | SMTP_AUTHENTICATION: plain 301 | SMTP_ENABLE_STARTTLS_AUTO: true 302 | SMTP_USERNAME: smtp_some_username 303 | SMTP_PASSWORD: smtp_some_password 304 | SMTP_SSL: true 305 | BASIC_AUTH_USERNAME: auth_some_username 306 | BASIC_AUTH_PASSWORD: auth_some_password 307 | --- 308 | apiVersion: apps/v1beta1 309 | kind: Deployment 310 | metadata: 311 | name: ciao 312 | namespace: ciao 313 | spec: 314 | replicas: 1 315 | template: 316 | metadata: 317 | labels: 318 | app: ciao 319 | spec: 320 | containers: 321 | - image: brotandgames/ciao:latest 322 | imagePullPolicy: IfNotPresent 323 | name: ciao 324 | volumeMounts: # Emit if you do not have persistent volumes 325 | - mountPath: /app/db/sqlite/ 326 | name: persistent-volume 327 | subPath: ciao 328 | ports: 329 | - containerPort: 3000 330 | resources: 331 | requests: 332 | memory: 256Mi 333 | cpu: 200m 334 | limits: 335 | memory: 512Mi 336 | cpu: 400m 337 | envFrom: 338 | - secretRef: 339 | name: ciao 340 | --- 341 | apiVersion: v1 342 | kind: Service 343 | metadata: 344 | name: ciao 345 | namespace: ciao 346 | spec: 347 | ports: 348 | - port: 80 349 | targetPort: 3000 350 | protocol: TCP 351 | type: ClusterIP 352 | selector: 353 | app: ciao 354 | ``` 355 | 356 | ### Dokku 357 | 358 | 1. Create app 359 | 360 | ```sh 361 | dokku apps:create ciao 362 | ``` 363 | 364 | 2. Configure 365 | 366 | ```sh 367 | dokku config:set --no-restart ciao \ 368 | SECRET_KEY_BASE="sensitive_secret_key_base" \ 369 | SMTP_ADDRESS=smtp.yourhost.com \ 370 | SMTP_EMAIL_FROM="ciao@yourhost.com" \ 371 | SMTP_EMAIL_TO="you@yourhost.com" \ 372 | SMTP_PORT=587 \ 373 | SMTP_DOMAIN=smtp.yourhost.com \ 374 | SMTP_AUTHENTICATION=plain \ 375 | SMTP_ENABLE_STARTTLS_AUTO=true \ 376 | SMTP_USERNAME=ciao \ 377 | SMTP_PASSWORD="sensitive_password" 378 | ``` 379 | 380 | 3. Deploy ciao using your deployment method eg. [Dockerfile Deployment](http://dokku.viewdocs.io/dokku/deployment/methods/dockerfiles/), [Docker Image Deployment](http://dokku.viewdocs.io/dokku/deployment/methods/images/) etc. 381 | 382 | 4. Protect your ciao instance by enabling HTTP Basic auth (using [dokku-http-auth](https://github.com/dokku/dokku-http-auth)) and installing Lets Encrypt certificates via [dokku-letsencrypt](https://github.com/dokku/dokku-letsencrypt). 383 | 384 | 385 | ## Contributing 386 | 387 | We encourage you to contribute to this project in whatever way you like! 388 | 389 | Report bugs/feature requests in the [issues](https://github.com/brotandgames/ciao/issues/new/choose) section. 390 | 391 | When contributing to this repository, please first discuss the change you wish to make via issue with the owners of this repository before making a change. 392 | 393 | ## Versioning 394 | 395 | [Semantic Versioning 2.x](https://semver.org/) 396 | 397 | In a nutshell: 398 | 399 | > Given a version number MAJOR.MINOR.PATCH, increment the: 400 | > 401 | > 1. MAJOR version when you make incompatible API changes, 402 | > 2. MINOR version when you add functionality in a backwards-compatible manner, and 403 | > 3. PATCH version when you make backwards-compatible bug fixes. 404 | > 405 | > Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format. 406 | 407 | ## License 408 | 409 | ciao is released under the [MIT License](https://opensource.org/licenses/MIT). 410 | 411 | ## Guestbook 412 | 413 | Why not reinvent the [guestbook](guestbook.md)? 414 | 415 | ## Maintainer 416 | 417 | https://github.com/brotandgames 418 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Add your own tasks in files placed in lib/tasks ending in .rake, 4 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 5 | 6 | require_relative "config/application" 7 | 8 | Rails.application.load_tasks 9 | -------------------------------------------------------------------------------- /app/assets/builds/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brotandgames/ciao/d1b0d7818d1a9620440b14fd79b4fcd01765e865/app/assets/builds/.keep -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../stylesheets .css 3 | //= link_tree ../builds 4 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brotandgames/ciao/d1b0d7818d1a9620440b14fd79b4fcd01765e865/app/assets/images/.keep -------------------------------------------------------------------------------- /app/assets/stylesheets/application.scss: -------------------------------------------------------------------------------- 1 | // Custom tabler variables must be set or imported *before* bootstrap and tabler. 2 | @import "tabler/variables"; 3 | @import "bootstrap"; 4 | @import "tabler"; 5 | @import "tabler/icons/browser"; -------------------------------------------------------------------------------- /app/assets/stylesheets/checks.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the checks controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ApplicationCable 4 | class Channel < ActionCable::Channel::Base 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ApplicationCable 4 | class Connection < ActionCable::Connection::Base 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationController < ActionController::Base 4 | before_action :authenticate 5 | protect_from_forgery unless: -> { request.format.json? } 6 | 7 | def authenticate 8 | basic_auth_username = ENV.fetch("BASIC_AUTH_USERNAME", "") 9 | basic_auth_password = ENV.fetch("BASIC_AUTH_PASSWORD", "") 10 | 11 | return true if basic_auth_username.empty? 12 | 13 | authenticate_or_request_with_http_basic("Ciao Application") do |username, password| 14 | username == basic_auth_username && password == basic_auth_password 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/controllers/checks_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ChecksController < ApplicationController 4 | before_action :set_check, only: %i[show edit update destroy] 5 | 6 | # GET /checks 7 | # GET /checks.json 8 | def index 9 | @checks = Check.all 10 | end 11 | 12 | # GET /checks/1 13 | # GET /checks/1.json 14 | def show 15 | end 16 | 17 | # GET /checks/new 18 | def new 19 | @check = Check.new 20 | end 21 | 22 | # GET /checks/1/edit 23 | def edit 24 | end 25 | 26 | # POST /checks 27 | # POST /checks.json 28 | def create 29 | @check = Check.new(check_params) 30 | 31 | respond_to do |format| 32 | if @check.save 33 | format.html do 34 | redirect_to @check, notice: "Check was successfully created." 35 | end 36 | format.json { render :show, status: :created, location: @check } 37 | else 38 | format.html { render :new } 39 | format.json do 40 | render json: @check.errors, status: :unprocessable_entity 41 | end 42 | end 43 | end 44 | end 45 | 46 | # PATCH/PUT /checks/1 47 | # PATCH/PUT /checks/1.json 48 | def update 49 | respond_to do |format| 50 | if @check.update(check_params) 51 | format.html do 52 | redirect_to @check, notice: "Check was successfully updated." 53 | end 54 | format.json { render :show, status: :ok, location: @check } 55 | else 56 | format.html { render :edit } 57 | format.json do 58 | render json: @check.errors, status: :unprocessable_entity 59 | end 60 | end 61 | end 62 | end 63 | 64 | # DELETE /checks/1 65 | # DELETE /checks/1.json 66 | def destroy 67 | @check.destroy 68 | respond_to do |format| 69 | format.html do 70 | redirect_to checks_url, notice: "Check was successfully destroyed." 71 | end 72 | format.json { head :no_content } 73 | end 74 | end 75 | 76 | # GET /dashboard 77 | def dashboard 78 | @checks = Check.all 79 | end 80 | 81 | # GET /checks/1/job 82 | # GET /checks/1/job.json 83 | def job 84 | Rails.logger.info "ciao-scheduler Database conn. pool stat: #{ActiveRecord::Base.connection_pool.stat}" 85 | @check = Check.find(params[:check_id]) 86 | @job = Rufus::Scheduler.singleton.job(@check.job) 87 | respond_to do |format| 88 | if @job 89 | format.html 90 | format.json { render :job, status: :ok } 91 | else 92 | format.html { render :job, status: 404 } 93 | format.json { render json: "Job not found", status: 404 } 94 | end 95 | end 96 | end 97 | 98 | # GET /checks/jobs/recreate 99 | # GET /checks/jobs/recreate.json 100 | def jobs_recreate 101 | Check.active.each do |check| 102 | check.unschedule_job if check.job 103 | check.create_job 104 | end 105 | Rails.logger.info "ciao-scheduler Database conn. pool stat: #{ActiveRecord::Base.connection_pool.stat}" 106 | respond_to do |format| 107 | format.html do 108 | redirect_to checks_url, 109 | notice: "Check jobs were successfully recreated." 110 | end 111 | format.json do 112 | render json: "Check jobs were successfully recreated.", status: 200 113 | end 114 | end 115 | end 116 | 117 | # GET /checks/admin 118 | def admin 119 | end 120 | 121 | private 122 | 123 | # Use callbacks to share common setup or constraints between actions. 124 | def set_check 125 | @check = Check.find(params[:id]) 126 | end 127 | 128 | # Never trust parameters from the scary internet, only allow the white list through. 129 | def check_params 130 | params.require(:check).permit(:name, :cron, :url, :active) 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brotandgames/ciao/d1b0d7818d1a9620440b14fd79b4fcd01765e865/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ApplicationHelper 4 | end 5 | -------------------------------------------------------------------------------- /app/helpers/checks_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ChecksHelper 4 | # Converts the status to the corresponding CSS class 5 | # to control HTML element classes based on status 6 | # @param status [String] this is either the HTTP status code or an error, `1XX..5XX` or `e` 7 | # @return [String] the CSS class for the corresponding HTTP status 8 | def class_for_status(status) 9 | case status.to_i 10 | when 100..199 11 | "secondary" 12 | when 200..299 13 | "success" 14 | when 300..399 15 | "info" 16 | when 400..499 17 | "warning" 18 | else 19 | "danger" 20 | end 21 | end 22 | 23 | # Converts the tls_expires_in_days to the corresponding CSS class 24 | # to control HTML element classes based on tls_expires_in_days 25 | # @param tls_expires_in_days [Integer] TLS certificate expiration in days 26 | # @return [String] the CSS class for the corresponding tls_expires_in_days 27 | def class_for_tls_expires_in_days(tls_expires_in_days) 28 | case tls_expires_in_days 29 | when -Float::INFINITY..7 30 | "text-danger" 31 | when 8..30 32 | "text-warning" 33 | when 31..Float::INFINITY 34 | "text" 35 | end 36 | end 37 | 38 | # Converts the healthcheck's active flag to CSS class 39 | # to control HTML element color 40 | # @param active [Boolean] the healthcheck's active flag, `true` or `false` 41 | # @return [String] the corresponding color (CSS class) 42 | def class_for_active(active) 43 | active ? "text-green" : "text-red" 44 | end 45 | 46 | # Converts the healthcheck's active flag to CSS class 47 | # to control checkbox icon 48 | # @param active [Boolean] the healthcheck's active flag, `true` or `false` 49 | # @return [String] the corresponding checkbox icon (CSS class) 50 | def class_for_active_checkbox(active) 51 | active ? "fe-check" : "fe-minus" 52 | end 53 | 54 | # Converts the healthy percentage to CSS class 55 | # to control healthy/unhealthy colors 56 | # @param percent [Float] the healthy percentage 57 | # @return [String] the corresponding color (CSS class) 58 | def class_for_healthy(percent) 59 | (percent == 100) ? "text-green" : "text-red" 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /app/javascript/application.js: -------------------------------------------------------------------------------- 1 | // Entry point for the build script in your package.json 2 | -------------------------------------------------------------------------------- /app/javascript/channels/consumer.js: -------------------------------------------------------------------------------- 1 | // Action Cable provides the framework to deal with WebSockets in Rails. 2 | // You can generate new channels where WebSocket features live using the `rails generate channel` command. 3 | 4 | import { createConsumer } from "@rails/actioncable" 5 | 6 | export default createConsumer() 7 | -------------------------------------------------------------------------------- /app/javascript/channels/index.js: -------------------------------------------------------------------------------- 1 | // Load all the channels within this directory and all subdirectories. 2 | // Channel files must be named *_channel.js. 3 | 4 | const channels = require.context('.', true, /_channel\.js$/) 5 | channels.keys().forEach(channels) 6 | -------------------------------------------------------------------------------- /app/javascript/packs/application.js: -------------------------------------------------------------------------------- 1 | // This file is automatically compiled by Webpack, along with any other files 2 | // present in this directory. You're encouraged to place your actual application logic in 3 | // a relevant structure within app/javascript and only use these pack files to reference 4 | // that code so it'll be compiled. 5 | 6 | require("@rails/ujs").start() 7 | require("turbolinks").start() 8 | require("@rails/activestorage").start() 9 | require("channels") 10 | 11 | 12 | // Uncomment to copy all static images under ../images to the output folder and reference 13 | // them with the image_pack_tag helper in views (e.g <%= image_pack_tag 'rails.png' %>) 14 | // or the `imagePath` JavaScript helper below. 15 | // 16 | // const images = require.context('../images', true) 17 | // const imagePath = (name) => images(name, true) 18 | -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationJob < ActiveJob::Base 4 | # Automatically retry jobs that encountered a deadlock 5 | # retry_on ActiveRecord::Deadlocked 6 | 7 | # Most jobs are safe to ignore if the underlying records are no longer available 8 | # discard_on ActiveJob::DeserializationError 9 | end 10 | -------------------------------------------------------------------------------- /app/lib/ciao/notifications/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Ciao 4 | module Notifications 5 | class Base 6 | def initialize(endpoint = nil, 7 | payload_template = nil, 8 | payload_renderer_cls = Ciao::Renderers::ReplaceRenderer) 9 | @endpoint = endpoint 10 | @payload_renderer = payload_renderer_cls.new(payload_template) 11 | end 12 | 13 | def notify(_payload_data = {}) 14 | raise NotImplementedError, 15 | "You can not call Ciao::Notifications::Base#notify directly" 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/lib/ciao/notifications/mail_notification.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Ciao 4 | module Notifications 5 | class MailNotification < Base 6 | def notify(payload_data = {}) 7 | CheckMailer.with(payload_data).change_status_mail.deliver 8 | end 9 | end 10 | 11 | class MailNotificationTlsExpires < Base 12 | def notify(payload_data = {}) 13 | CheckMailer.with(payload_data).tls_expires_mail.deliver 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/lib/ciao/notifications/webhook_notification.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Ciao 4 | module Notifications 5 | class WebhookNotification < Base 6 | def notify(payload_data = {}) 7 | uri = URI.parse(@endpoint) 8 | http = Net::HTTP.new(uri.host, uri.port) 9 | http.use_ssl = uri.scheme == "https" 10 | 11 | request = Net::HTTP::Post.new( 12 | uri.request_uri, 13 | "Content-Type" => "application/json" 14 | ) 15 | request.body = @payload_renderer.render(payload_data) 16 | http.request(request) 17 | rescue *NET_HTTP_ERRORS => e 18 | Rails.logger.error "Ciao::Notifications::WebhookNotification#notify Could not notify webhook(#{@endpoint}) - #{e}" 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/lib/ciao/parsers/webhook_parser.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Ciao 4 | module Parsers 5 | class WebhookParser 6 | WEBHOOKS_ENDPOINT_PREFIX = "CIAO_WEBHOOK_ENDPOINT_" 7 | WEBHOOKS_PAYLOAD_PREFIX = "CIAO_WEBHOOK_PAYLOAD_" 8 | WEBHOOKS_PAYLOAD_TLS_EXPIRES_PREFIX = "CIAO_WEBHOOK_PAYLOAD_TLS_EXPIRES_" 9 | 10 | WEBHOOKS_ENDPOINT_FORMAT = "#{WEBHOOKS_ENDPOINT_PREFIX}%s".freeze 11 | WEBHOOKS_PAYLOAD_FORMAT = "#{WEBHOOKS_PAYLOAD_PREFIX}%s".freeze 12 | WEBHOOKS_PAYLOAD_TLS_EXPIRES_FORMAT = "#{WEBHOOKS_PAYLOAD_TLS_EXPIRES_PREFIX}%s".freeze 13 | 14 | WEBHOOKS_FORMAT_REGEX = /^#{WEBHOOKS_ENDPOINT_PREFIX}(?[A-Z0-9_]+)$/ 15 | 16 | def self.webhooks 17 | names.map do |check_name| 18 | { 19 | endpoint: ENV.fetch(WEBHOOKS_ENDPOINT_FORMAT % check_name, ""), 20 | payload: ENV.fetch(WEBHOOKS_PAYLOAD_FORMAT % check_name, ""), 21 | payload_tls_expires: ENV.fetch(WEBHOOKS_PAYLOAD_TLS_EXPIRES_FORMAT % check_name, "") 22 | } 23 | end 24 | end 25 | 26 | def self.names 27 | matches.map { |match| match[:name] } 28 | end 29 | 30 | def self.matches 31 | ENV.map do |k, _v| 32 | k.match(WEBHOOKS_FORMAT_REGEX) 33 | end.compact 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /app/lib/ciao/renderers/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Ciao 4 | module Renderers 5 | class Base 6 | def initialize(template) 7 | @template = template 8 | end 9 | 10 | def render(_data) 11 | raise NotImplementedError, 12 | "You can not call Ciao::Renderers::Base#render directly" 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/lib/ciao/renderers/replace_renderer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Ciao 4 | module Renderers 5 | class ReplaceRenderer < Base 6 | CHECK_NAME_PLACEHOLDER = "__name__" 7 | STATUS_AFTER_PLACEHOLDER = "__status_after__" 8 | STATUS_BEFORE_PLACEHOLDER = "__status_before__" 9 | URL_PLACEHOLDER = "__url__" 10 | CHECK_URL_PLACEHOLDER = "__check_url__" 11 | TLS_EXPIRES_AT_PLACEHOLDER = "__tls_expires_at__" 12 | TLS_EXPIRES_IN_DAYS_PLACEHOLDER = "__tls_expires_in_days__" 13 | 14 | def render(data) 15 | return "" if @template.nil? 16 | 17 | @template 18 | .gsub(CHECK_NAME_PLACEHOLDER, data.fetch(:name, "").to_s) 19 | .gsub(STATUS_AFTER_PLACEHOLDER, data.fetch(:status_after, "").to_s) 20 | .gsub(STATUS_BEFORE_PLACEHOLDER, data.fetch(:status_before, "").to_s) 21 | .gsub(URL_PLACEHOLDER, data.fetch(:url, "").to_s) 22 | .gsub(CHECK_URL_PLACEHOLDER, data.fetch(:check_url, "").to_s) 23 | .gsub(TLS_EXPIRES_AT_PLACEHOLDER, data.fetch(:tls_expires_at, "").to_s) 24 | .gsub(TLS_EXPIRES_IN_DAYS_PLACEHOLDER, data.fetch(:tls_expires_in_days, "").to_s) 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationMailer < ActionMailer::Base 4 | layout "mailer" 5 | end 6 | -------------------------------------------------------------------------------- /app/mailers/check_mailer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Mailer to send mails about checkhealth updates/changes. 4 | # `From:`, `To:` and other SMTP options are configured at config/environments/production.rb 5 | class CheckMailer < ApplicationMailer 6 | # Sends mail to inform the receiver about a 7 | # healthcheck status change 8 | # @param name [String] the name of the check 9 | # @param url [String] the URL of the check 10 | # @param status_before [String] the old status, `1XX..5XX` or `e` 11 | # @param status_after [String] the new status, `1XX..5XX` or `e` 12 | def change_status_mail 13 | @name = params[:name] 14 | @url = params[:url] 15 | @status_before = params[:status_before] 16 | @status_after = params[:status_after] 17 | mail(subject: "[ciao] #{@name}: Status changed (#{@status_after})") 18 | end 19 | 20 | # Sends mail to inform the receiver about a 21 | # expiration of TLS certificate 22 | # @param name [String] the name of the check 23 | # @param url [String] the URL of the check 24 | # @param tls_expires_at [DateTime] DateTime when the TLS certificate expires 25 | # @param tls_expires_in_days [Integer] Days until the TLS certificate expires 26 | def tls_expires_mail 27 | @name = params[:name] 28 | @url = params[:url] 29 | @tls_expires_at = params[:tls_expires_at] 30 | @tls_expires_in_days = params[:tls_expires_in_days] 31 | mail(subject: "[ciao] #{@name}: TLS certificate expires in #{@tls_expires_in_days} days") 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationRecord < ActiveRecord::Base 4 | self.abstract_class = true 5 | end 6 | -------------------------------------------------------------------------------- /app/models/check.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @attr [string] name name 4 | # @attr [string] cron cron schedule format 5 | # @attr [datetime] created_at when the record was created in database 6 | # @attr [datetime] updated_at when the record was last updated in database 7 | # @attr [string] url URL to ping for healthchecking 8 | # @attr [string] status this is either the HTTP status code 1XX..5XX or an error e 9 | # @attr [boolean] active is healthcheck active or not? 10 | # @attr [string] job rufus-scheduler's last run job ID 11 | # @attr [datetime] last_contact_at when the healthcheck was last run 12 | # @attr [datetime] next_contact_at when the healthcheck will next run 13 | class Check < ApplicationRecord 14 | has_many :status_changes, dependent: :destroy 15 | 16 | after_create :create_job, :create_tls_job, if: :active? 17 | after_update :update_routine 18 | after_destroy :unschedule_job, :unschedule_tls_job, if: :active? 19 | 20 | validates :name, presence: true 21 | validates :url, presence: true 22 | validates :url, http_url: true 23 | validates :cron, presence: true 24 | validates :cron, cron: true 25 | 26 | scope :active, -> { where(active: true) } 27 | scope :inactive, -> { where(active: false) } 28 | scope :healthy, -> { where("status LIKE ? AND active = ?", "2%", true) } 29 | scope :unhealthy, -> { where.not("status LIKE ? AND active = ?", "2%", true) } 30 | scope :status_1xx, -> { where("status LIKE ? AND active = ?", "1%", true) } 31 | scope :status_2xx, -> { where("status LIKE ? AND active = ?", "2%", true) } 32 | scope :status_3xx, -> { where("status LIKE ? AND active = ?", "3%", true) } 33 | scope :status_4xx, -> { where("status LIKE ? AND active = ?", "4%", true) } 34 | scope :status_5xx, -> { where("status LIKE ? AND active = ?", "5%", true) } 35 | scope :status_err, -> { where("NOT (status LIKE ? OR status LIKE ? OR status LIKE ? OR status LIKE ? OR status LIKE ?) AND active = ?", "1%", "2%", "3%", "4%", "5%", true) } 36 | 37 | def self.percentage_active 38 | if !active.empty? 39 | ((active.count * 1.0 / count * 1.0) * 100.0).round(0) 40 | else 41 | 0.0 42 | end 43 | end 44 | 45 | def self.percentage_healthy 46 | if !active.empty? 47 | ((healthy.count * 1.0 / active.count * 1.0) * 100.0).round(0) 48 | else 49 | 0.0 50 | end 51 | end 52 | 53 | def create_job 54 | job = 55 | Rufus::Scheduler.singleton.cron cron, job: true do 56 | url = URI.parse(self.url) 57 | begin 58 | response = Net::HTTP.get_response(url) 59 | http_code = response.code 60 | rescue *NET_HTTP_ERRORS => e 61 | status = e.to_s.tr('"', "'") 62 | end 63 | status = http_code unless e 64 | last_contact_at = Time.current 65 | Rails.logger.info "ciao-scheduler Checked '#{url}' at '#{last_contact_at}' and got '#{status}'" 66 | status_before = status_after = "" 67 | ActiveRecord::Base.connection_pool.with_connection do 68 | status_before = self.status 69 | update_columns(status: status, last_contact_at: last_contact_at, next_contact_at: job.next_times(1).first.to_local_time) 70 | status_after = self.status 71 | end 72 | if status_before != status_after 73 | ActiveRecord::Base.connection_pool.with_connection do 74 | status_changes.create(status: status) 75 | end 76 | Rails.logger.info "ciao-scheduler Check '#{name}': Status changed from '#{status_before}' to '#{status_after}'" 77 | NOTIFICATIONS.each do |notification| 78 | notification.notify( 79 | name: name, 80 | status_before: status_before, 81 | status_after: status_after, 82 | url: url, 83 | check_url: Rails.application.routes.url_helpers.check_path(self) 84 | ) 85 | end 86 | end 87 | end 88 | if job 89 | Rails.logger.info "ciao-scheduler Created job '#{job.id}'" 90 | update_columns(job: job.id, next_contact_at: job.next_times(1).first.to_local_time) 91 | else 92 | Rails.logger.error "ciao-scheduler Could not create job" 93 | end 94 | job 95 | end 96 | 97 | def unschedule_job 98 | job = Rufus::Scheduler.singleton.job(self.job) 99 | 100 | if job 101 | job.unschedule 102 | Rails.logger.info "ciao-scheduler Unscheduled job '#{job.id}'" 103 | else 104 | Rails.logger.info "ciao-scheduler Could not unschedule job: '#{self.job}' not found" 105 | end 106 | end 107 | 108 | # rubocop:disable Metrics/MethodLength 109 | # rubocop:disable Metrics/AbcSize 110 | def create_tls_job 111 | uri = URI.parse(url) 112 | return unless uri.scheme == "https" 113 | 114 | tls_job = 115 | Rufus::Scheduler.singleton.cron "0 12 * * *", job: true do 116 | http = Net::HTTP.new(uri.host, uri.port) 117 | http.use_ssl = true 118 | http.verify_mode = OpenSSL::SSL::VERIFY_NONE 119 | tls_expires_at = nil 120 | begin 121 | http.start do |h| 122 | tls_expires_at = h.peer_cert.not_after 123 | end 124 | rescue *NET_HTTP_ERRORS => e 125 | tls_expires_error = e.to_s.tr('"', "'") 126 | end 127 | if tls_expires_error 128 | Rails.logger.info "ciao-scheduler Checked TLS certificate of '#{url}' and got '#{tls_expires_error}'" 129 | else 130 | Rails.logger.info "ciao-scheduler Checked TLS certificate of '#{url}' and got '#{tls_expires_at}'" 131 | tls_expires_in_days = (tls_expires_at - Time.now).to_i / (24 * 60 * 60) 132 | ActiveRecord::Base.connection_pool.with_connection do 133 | update_columns(tls_expires_at: tls_expires_at, tls_expires_in_days: tls_expires_in_days) 134 | end 135 | 136 | if tls_expires_in_days < 30 137 | NOTIFICATIONS_TLS_EXPIRES.each do |notification| 138 | notification.notify( 139 | name: name, 140 | url: url, 141 | check_url: Rails.application.routes.url_helpers.check_path(self), 142 | tls_expires_at: tls_expires_at, 143 | tls_expires_in_days: tls_expires_in_days 144 | ) 145 | end 146 | end 147 | end 148 | end 149 | if tls_job 150 | Rails.logger.info "ciao-scheduler Created tls_job '#{tls_job.id}'" 151 | update_columns(tls_job: tls_job.id) 152 | else 153 | Rails.logger.error "ciao-scheduler Could not create tls_job" 154 | end 155 | tls_job 156 | end 157 | 158 | def unschedule_tls_job 159 | tls_job = Rufus::Scheduler.singleton.job(self.tls_job) 160 | 161 | if tls_job 162 | tls_job.unschedule 163 | Rails.logger.info "ciao-scheduler Unscheduled tls_job '#{tls_job.id}'" 164 | else 165 | Rails.logger.info "ciao-scheduler Could not unschedule tls_job: '#{self.tls_job}' not found" 166 | end 167 | end 168 | 169 | private 170 | 171 | def update_routine 172 | if saved_change_to_attribute?(:active) 173 | if active 174 | create_job 175 | create_tls_job 176 | else 177 | unschedule_job 178 | unschedule_tls_job 179 | update_columns(next_contact_at: nil, job: nil, tls_job: nil) 180 | end 181 | elsif saved_change_to_attribute?(:cron) || saved_change_to_attribute?(:url) 182 | Rails.logger.info "ciao-scheduler Check '#{name}' updates to cron or URL triggered job and tls_job update" 183 | unschedule_job 184 | unschedule_tls_job 185 | create_job if active? 186 | create_tls_job if active? 187 | end 188 | end 189 | end 190 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brotandgames/ciao/d1b0d7818d1a9620440b14fd79b4fcd01765e865/app/models/concerns/.keep -------------------------------------------------------------------------------- /app/models/status_change.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class StatusChange < ApplicationRecord 4 | belongs_to :check 5 | end 6 | -------------------------------------------------------------------------------- /app/validators/cron_validator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CronValidator < ActiveModel::EachValidator 4 | def self.compliant?(value) 5 | cron = ::Fugit::Cron.parse(value) 6 | cron.present? 7 | rescue => e 8 | Rails.logger.info "CronValidator Exception: #{e}" 9 | false 10 | end 11 | 12 | def validate_each(record, attribute, value) 13 | return if value.present? && self.class.compliant?(value) 14 | 15 | record.errors.add( 16 | attribute, 17 | "is not a valid cron. Check your cron schedule expression here: https://crontab.guru" 18 | ) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/validators/http_url_validator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class HttpUrlValidator < ActiveModel::EachValidator 4 | def self.compliant?(value) 5 | escaped_address = URI::DEFAULT_PARSER.escape(value) 6 | uri = URI.parse(escaped_address) 7 | uri.is_a?(URI::HTTP) && !uri.host.nil? 8 | rescue URI::InvalidURIError 9 | false 10 | end 11 | 12 | def validate_each(record, attribute, value) 13 | return if value.present? && self.class.compliant?(value) 14 | 15 | record.errors.add(attribute, "is not a valid HTTP URL") 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/views/check_mailer/change_status_mail.text.erb: -------------------------------------------------------------------------------- 1 | ciao 2 | 3 | The status of '<%= @name %>' check changed 4 | 5 | from: <%= @status_before %> 6 | to: <%= @status_after %> 7 | 8 | Url: <%= @url %> 9 | -------------------------------------------------------------------------------- /app/views/check_mailer/tls_expires_mail.text.erb: -------------------------------------------------------------------------------- 1 | ciao 2 | 3 | The TLS certificate of '<%= @name %>' expires at <%= @tls_expires_at %> (in <%= @tls_expires_in_days %> days) 4 | 5 | Url: <%= @url %> 6 | -------------------------------------------------------------------------------- /app/views/checks/_check.json.jbuilder: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | json.extract! check, :id, :active, :name, :cron, :url, :status, :job, 4 | :last_contact_at, :next_contact_at, :created_at, :updated_at, 5 | :tls_expires_at, :tls_expires_in_days 6 | json.check_url check_url(check, format: :json) 7 | -------------------------------------------------------------------------------- /app/views/checks/_form.html.erb: -------------------------------------------------------------------------------- 1 | 2 | <%= simple_form_for(@check) do |f| %> 3 | <%= f.error_notification %> 4 | <%= f.error_notification message: f.object.errors[:base].to_sentence if f.object.errors[:base].present? %> 5 | 6 |
7 | <%= f.input :name %> 8 | <%= f.input :active %> 9 | <%= f.input :url %> 10 | <%= f.input :cron %> 11 |
12 | 13 |
14 | <%= f.button :submit, class: 'btn btn-primary' %> 15 |
16 | <% end %> 17 | -------------------------------------------------------------------------------- /app/views/checks/admin.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |

Administration

6 |
7 |
8 |
9 |
10 |
<%= notice %>
11 |

12 | Recreate all scheduled background jobs of active checks 13 |

14 |

This is useful after a (re)start.

15 |

16 | <%= link_to ' Recreate'.html_safe, jobs_recreate_checks_path, class: 'btn btn-info' %> 17 |

18 |

19 | It can be also accomplished using the REST API endpoint: 20 |

21 |

22 | 23 | curl -X GET -H "Content-type: application/json" /checks/jobs/recreate.json 24 | 25 |

26 |
27 |
28 |
29 |
30 |

Debugging information

31 |
32 |
33 |
34 |
35 |

36 | 37 | Configuration 38 | 39 |

40 |

Time zone: <%= ENV.fetch('TIME_ZONE', 'not configured') %>

41 |

Notifications SMTP: <%= ENV.fetch('SMTP_ADDRESS', 'not configured') %>

42 |

Notifications Webhooks: <%= Ciao::Parsers::WebhookParser.names %> 43 |

Notifications count: <%= NOTIFICATIONS.length %>

44 |

45 |

HTTP Basic auth username: <%= ENV.fetch('BASIC_AUTH_USERNAME', 'not configured') %>

46 |

Prometheus enabled: <%= ENV.fetch('PROMETHEUS_ENABLED', 'not configured') %>

47 |

Prometheus HTTP Basic auth username: <%= ENV.fetch('PROMETHEUS_BASIC_AUTH_USERNAME', 'not configured') %>

48 |

49 | 50 | Database 51 | 52 |

53 |

Dabase conn. pool stat.: <%= ActiveRecord::Base.connection_pool.stat %>

54 |
55 |
56 |
57 |
58 | -------------------------------------------------------------------------------- /app/views/checks/dashboard.html.erb: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 |
6 |
7 | 100% valid 8 |
9 |
<%= @checks.count %>
10 |
Checks
11 |
12 |
13 |
14 | 15 |
16 |
17 |
18 |
19 | <%= @checks.percentage_active %>% active 20 |
21 |
<%= @checks.active.count %>
22 |
Checks active
23 |
24 |
25 |
26 | 27 |
28 |
29 |
30 |
31 | <%= @checks.percentage_healthy %>% healthy 32 |
33 |
<%= @checks.healthy.count %>
34 |
Checks healthy
35 |
36 |
37 |
38 | 39 |
40 | 41 |
42 |
43 |
44 |
45 |

Active checks

46 |
47 | 48 | 49 | 50 | 51 | 54 | 55 | 56 | 57 | 60 | 61 | 62 | 63 | 66 | 67 | 68 | 69 | 72 | 73 | 74 | 75 | 78 | 79 | 80 | 81 | 84 | 85 | 86 |
Status 1xx 52 | <%= @checks.status_1xx.count %> 53 |
Status 2xx 58 | <%= @checks.status_2xx.count %> 59 |
Status 3xx 64 | <%= @checks.status_3xx.count %> 65 |
Status 4xx 70 | <%= @checks.status_4xx.count %> 71 |
Status 5xx 76 | <%= @checks.status_5xx.count %> 77 |
Status error 82 | <%= @checks.status_err.count %> 83 |
87 |
88 |
89 |
90 | -------------------------------------------------------------------------------- /app/views/checks/edit.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |

Editing Check

6 |
7 | 8 |
9 | <%= render 'form', check: @check %> 10 |
11 |
12 |
13 |
-------------------------------------------------------------------------------- /app/views/checks/index.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Checks

4 |
5 | <%= notice %> 6 | <%= link_to 'JSON', 7 | checks_path(format: :json), class: 'btn btn-success btn-sm ml-1'%> 8 |
9 |
10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | <% @checks.each do |check| %> 26 | 27 | 28 | 29 | 37 | 47 | 51 | 61 | 78 | 83 | 84 | <% end %> 85 | 86 |
ActiveNameCronUrlStatusTLS
<%= link_to ('').html_safe, edit_check_path(check) %> 30 |
<%= link_to check.name, check %>
31 |
32 | 33 | <%= truncate(check.status, :length => 10) %> 34 | <%= time_ago_in_words(check.last_contact_at) + " ago" if check.last_contact_at %> 35 |
36 |
38 |
39 | <%= check.cron %> 40 | <%= "in " + distance_of_time_in_words(Time.current, check.next_contact_at) if check.next_contact_at %> 41 | 42 |
43 |
44 | <%= "Next: " + check.next_contact_at.to_s if check.next_contact_at %> 45 |
46 |
48 |
<%= link_to check.url, check.url %>
49 |
<%= "Last: " + check.last_contact_at.to_s if check.last_contact_at %>
50 |
52 | <% if check.status.to_i.between?(100, 599) %> 53 | <%= check.status %> 54 | <% else %> 55 |
56 | <%= truncate(check.status, :length => 37) %> 57 |
58 | <% end %> 59 |
<%= "Changes: " + check.status_changes.count.to_s %>
60 |
62 |
63 | <% if !check.active %> 64 | Check not active 65 | <% else %> 66 | <% if URI.parse(check.url).scheme == 'https' %> 67 | <% if check.tls_expires_at? %> 68 | Expires in <%= check.tls_expires_in_days %> days 69 | <% else %> 70 | Waiting for TLS check 71 | <% end %> 72 | <% else %> 73 | No TLS certificate 74 | <% end %> 75 | <% end %> 76 |
77 |
79 | <%= button_to check, class: "btn btn-outline-danger btn-sm", method: :delete do %> 80 | 81 | <% end %> 82 |
87 |
88 |
89 | -------------------------------------------------------------------------------- /app/views/checks/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | json.array! @checks, partial: "checks/check", as: :check 4 | -------------------------------------------------------------------------------- /app/views/checks/job.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Debug job for <%= @check.name %>

4 |
5 |
6 | 7 | <% if @job %> 8 | 9 |

Job id: <%= @job.id %>

10 |

Scheduled at: <%= @job.scheduled_at %>

11 |

Last work time: <%= @job.last_work_time %>

12 |

Mean work time: <%= @job.mean_work_time %>

13 |

Last time: <%= @job.last_time %>

14 |

Previous time: <%= @job.previous_time %>

15 |

16 | Next times: 17 |

    18 | <% @job.next_times(10).each do |time| %> 19 |
  • <%= time.to_s %>
  • 20 | <% end %> 21 |
22 |

23 |

24 | Threads: <%= @job.threads.count %> 25 |

    26 | <% @job.threads do |thread| %> 27 |
  • <%= thread.to_s %>
  • 28 | <% end %> 29 |
30 |

31 |

Inspect 32 |

<%= @job.inspect %>

33 |

34 | 35 | <% else %> 36 | 37 |

Job not found.

38 | 39 | <% end %> 40 | 41 |
42 |
-------------------------------------------------------------------------------- /app/views/checks/job.json.jbuilder: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | json.extract! @job, :id 4 | -------------------------------------------------------------------------------- /app/views/checks/new.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |

New Check

6 |
7 | 8 |
9 | <%= render 'form', check: @check %> 10 |
11 |
12 |
13 |
-------------------------------------------------------------------------------- /app/views/checks/show.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |

Check <%= @check.name %>

6 |
7 | <%= link_to 'JSON', 8 | check_path(@check, format: :json), class: 'btn btn-success btn-sm'%> 9 |
10 |
11 |
12 |

13 | <%= link_to ' Edit'.html_safe, edit_check_path(@check), class: 'btn btn-outline-info btn-sm' %> 14 |

<%= notice %>
15 |

16 | 17 |

18 | Active: 19 | <%= @check.active %> 20 |

21 | 22 |

23 | Cron: 24 | <%= @check.cron %> 25 | <%= link_to ' crontab.guru'.html_safe, "https://crontab.guru/##{@check.cron.split(' ').join('_')}", target: :_blank %> 26 |

27 | 28 |

29 | URL: 30 | <%= link_to @check.url, @check.url %> 31 |

32 | 33 |

34 | Status: 35 | 36 | <%= @check.status %> 37 | 38 |

39 | 40 |

41 | Next contact: 42 | <%= @check.next_contact_at %> 43 |

44 | 45 |

46 | Last contact: 47 | <%= @check.last_contact_at %> 48 |

49 | 50 | <% if URI.parse(@check.url).scheme == 'https' && @check.tls_expires_at? %> 51 |

52 | TLS expires at: 53 | 54 | <%= @check.tls_expires_at %> (in <%= @check.tls_expires_in_days %> days) 55 | 56 |

57 | <% end %> 58 | 59 |

60 | Status changes: <%= @check.status_changes.count %> 61 |

    62 | <% @check.status_changes.order(created_at: :desc).each do |status_change| %> 63 |
  • <%= status_change.created_at %> 64 | <% if status_change.status.to_i.between?(100, 599) %> 65 | <%= status_change.status %> 66 | <% else %> 67 | <%= status_change.status %> 68 | <% end %> 69 |
  • 70 | <% end %> 71 |
72 |

73 | 74 |

75 | Debug: <%= link_to @check.job, check_job_path(@check) %> 76 |

77 | 78 |

79 | <%= button_to @check, class: "btn btn-outline-danger btn-sm", method: :delete do %> 80 | Delete 81 | <% end %> 82 |

83 |
84 |
85 |
86 |
87 | -------------------------------------------------------------------------------- /app/views/checks/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | json.partial! "checks/check", check: @check 4 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ciao 5 | 6 | 7 | 11 | <%= csrf_meta_tags %> 12 | <%= csp_meta_tag %> 13 | <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> 14 | <%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %> 15 | 16 | 17 | 18 |
19 |

20 |
21 |
22 |

23 | <%= link_to 'ciao', root_path, class: 'btn btn-success' %> 24 | <%= link_to 'Checks', checks_path, class: 'btn btn-outline-info' %> 25 | <%= link_to ' Check'.html_safe, 26 | new_check_path, class: 'btn btn-outline-success'%> 27 | <%= link_to ''.html_safe, 28 | admin_checks_path, class: 'btn btn-icon btn-outline-secondary'%> 29 |

30 |
31 |
32 |
33 |
34 | <%= yield %> 35 |
36 | <%= link_to 'ciao', 'https://brotandgames.com/ciao/' %> 37 | <%= File.read(File.join(Rails.public_path, 'version')) %> 38 |
39 |

40 |
41 |
42 |
43 | 44 | 45 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | var validEnv = ['development', 'test', 'production'] 3 | var currentEnv = api.env() 4 | var isDevelopmentEnv = api.env('development') 5 | var isProductionEnv = api.env('production') 6 | var isTestEnv = api.env('test') 7 | 8 | if (!validEnv.includes(currentEnv)) { 9 | throw new Error( 10 | 'Please specify a valid `NODE_ENV` or ' + 11 | '`BABEL_ENV` environment variables. Valid values are "development", ' + 12 | '"test", and "production". Instead, received: ' + 13 | JSON.stringify(currentEnv) + 14 | '.' 15 | ) 16 | } 17 | 18 | return { 19 | presets: [ 20 | isTestEnv && [ 21 | require('@babel/preset-env').default, 22 | { 23 | targets: { 24 | node: 'current' 25 | } 26 | } 27 | ], 28 | (isProductionEnv || isDevelopmentEnv) && [ 29 | require('@babel/preset-env').default, 30 | { 31 | forceAllTransforms: true, 32 | useBuiltIns: 'entry', 33 | modules: false, 34 | exclude: ['transform-typeof-symbol'] 35 | } 36 | ] 37 | ].filter(Boolean), 38 | plugins: [ 39 | require('babel-plugin-macros'), 40 | require('@babel/plugin-syntax-dynamic-import').default, 41 | isTestEnv && require('babel-plugin-dynamic-import-node'), 42 | require('@babel/plugin-transform-destructuring').default, 43 | [ 44 | require('@babel/plugin-proposal-class-properties').default, 45 | { 46 | loose: true 47 | } 48 | ], 49 | [ 50 | require('@babel/plugin-proposal-object-rest-spread').default, 51 | { 52 | useBuiltIns: true 53 | } 54 | ], 55 | [ 56 | require('@babel/plugin-transform-runtime').default, 57 | { 58 | helpers: false, 59 | regenerator: true 60 | } 61 | ], 62 | [ 63 | require('@babel/plugin-transform-regenerator').default, 64 | { 65 | async: false 66 | } 67 | ] 68 | ].filter(Boolean) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'bundle' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require 'rubygems' 12 | 13 | m = Module.new do 14 | module_function 15 | 16 | def invoked_as_script? 17 | File.expand_path($PROGRAM_NAME) == File.expand_path(__FILE__) 18 | end 19 | 20 | def env_var_version 21 | ENV['BUNDLER_VERSION'] 22 | end 23 | 24 | def cli_arg_version 25 | return unless invoked_as_script? # don't want to hijack other binstubs 26 | return unless 'update'.start_with?(ARGV.first || ' ') # must be running `bundle update` 27 | 28 | bundler_version = nil 29 | update_index = nil 30 | ARGV.each_with_index do |a, i| 31 | if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN # rubocop:disable Style/IfUnlessModifier 32 | bundler_version = a 33 | end 34 | next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ 35 | 36 | bundler_version = Regexp.last_match(1) || '>= 0.a' 37 | update_index = i 38 | end 39 | bundler_version 40 | end 41 | 42 | def gemfile 43 | gemfile = ENV['BUNDLE_GEMFILE'] 44 | return gemfile if gemfile && !gemfile.empty? 45 | 46 | File.expand_path('../Gemfile', __dir__) 47 | end 48 | 49 | def lockfile 50 | lockfile = 51 | case File.basename(gemfile) 52 | when 'gems.rb' then gemfile.sub(/\.rb$/, gemfile) 53 | else "#{gemfile}.lock" 54 | end 55 | File.expand_path(lockfile) 56 | end 57 | 58 | def lockfile_version 59 | return unless File.file?(lockfile) 60 | 61 | lockfile_contents = File.read(lockfile) 62 | return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ 63 | 64 | Regexp.last_match(1) 65 | end 66 | 67 | def bundler_version 68 | @bundler_version ||= env_var_version || cli_arg_version || 69 | lockfile_version || "#{Gem::Requirement.default}.a" 70 | end 71 | 72 | def load_bundler! 73 | ENV['BUNDLE_GEMFILE'] ||= gemfile 74 | 75 | # must dup string for RG < 1.8 compatibility 76 | activate_bundler(bundler_version.dup) 77 | end 78 | 79 | def activate_bundler(bundler_version) 80 | if Gem::Version.correct?(bundler_version) && Gem::Version.new(bundler_version).release < Gem::Version.new('2.0') # rubocop:disable Style/IfUnlessModifier 81 | bundler_version = '< 2' 82 | end 83 | gem_error = activation_error_handling do 84 | gem 'bundler', bundler_version 85 | end 86 | return if gem_error.nil? 87 | 88 | require_error = activation_error_handling do 89 | require 'bundler/version' 90 | end 91 | return if require_error.nil? && Gem::Requirement.new(bundler_version).satisfied_by?(Gem::Version.new(Bundler::VERSION)) 92 | 93 | warn "Activating bundler (#{bundler_version}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_version}'`" 94 | exit 42 95 | end 96 | 97 | def activation_error_handling 98 | yield 99 | nil 100 | rescue StandardError, LoadError => e 101 | e 102 | end 103 | end 104 | 105 | m.load_bundler! 106 | 107 | load Gem.bin_path('bundler', 'bundle') if m.invoked_as_script? 108 | -------------------------------------------------------------------------------- /bin/dev: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if ! command -v foreman &> /dev/null 4 | then 5 | echo "Installing foreman..." 6 | gem install foreman 7 | fi 8 | 9 | foreman start -f Procfile.dev "$@" 10 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | begin 5 | load File.expand_path('spring', __dir__) 6 | rescue LoadError => e 7 | raise unless e.message.include?('spring') 8 | end 9 | APP_PATH = File.expand_path('../config/application', __dir__) 10 | require_relative '../config/boot' 11 | require 'rails/commands' 12 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | begin 5 | load File.expand_path('spring', __dir__) 6 | rescue LoadError => e 7 | raise unless e.message.include?('spring') 8 | end 9 | require_relative '../config/boot' 10 | require 'rake' 11 | Rake.application.run 12 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'fileutils' 5 | 6 | # path to your application root. 7 | APP_ROOT = File.expand_path('..', __dir__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | FileUtils.chdir APP_ROOT do 14 | # This script is a way to setup or update your development environment automatically. 15 | # This script is idempotent, so that you can run it at anytime and get an expectable outcome. 16 | # Add necessary setup steps to this file. 17 | 18 | puts '== Installing dependencies ==' 19 | system! 'gem install bundler --conservative' 20 | system('bundle check') || system!('bundle install') 21 | 22 | # Install JavaScript dependencies 23 | # system('bin/yarn') 24 | 25 | # puts "\n== Copying sample files ==" 26 | # unless File.exist?('config/database.yml') 27 | # FileUtils.cp 'config/database.yml.sample', 'config/database.yml' 28 | # end 29 | 30 | puts "\n== Preparing database ==" 31 | system! 'bin/rails db:prepare' 32 | 33 | puts "\n== Removing old logs and tempfiles ==" 34 | system! 'bin/rails log:clear tmp:clear' 35 | 36 | puts "\n== Restarting application server ==" 37 | system! 'bin/rails restart' 38 | end 39 | -------------------------------------------------------------------------------- /bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # This file loads spring without using Bundler, in order to be fast. 5 | # It gets overwritten when you run the `spring binstub` command. 6 | 7 | unless defined?(Spring) 8 | require 'rubygems' 9 | require 'bundler' 10 | 11 | lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read) 12 | spring = lockfile.specs.detect { |spec| spec.name == 'spring' } 13 | if spring 14 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path 15 | gem 'spring', spring.version 16 | require 'spring/binstub' 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /bin/yarn: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | APP_ROOT = File.expand_path('..', __dir__) 5 | Dir.chdir(APP_ROOT) do 6 | exec 'yarnpkg', *ARGV 7 | rescue Errno::ENOENT 8 | warn 'Yarn executable was not detected in the system.' 9 | warn 'Download Yarn at https://yarnpkg.com/en/docs/install' 10 | exit 1 11 | end 12 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file is used by Rack-based servers to start the application. 4 | 5 | require_relative "config/environment" 6 | 7 | prometheus_enabled = ENV.fetch("PROMETHEUS_ENABLED", "false") 8 | 9 | if prometheus_enabled == "true" 10 | require "prometheus/middleware/collector" 11 | 12 | rackapp = Rack::Builder.app do 13 | use Prometheus::Middleware::Collector 14 | if ENV["PROMETHEUS_BASIC_AUTH_USERNAME"].present? 15 | map "/metrics" do 16 | use Rack::Auth::Basic, "Ciao Prometheus Metrics" do |username, password| 17 | Rack::Utils.secure_compare(ENV["PROMETHEUS_BASIC_AUTH_USERNAME"], username) && 18 | Rack::Utils.secure_compare(ENV["PROMETHEUS_BASIC_AUTH_PASSWORD"], password) 19 | end 20 | use Rack::Deflater 21 | use Yabeda::Prometheus::Exporter, path: "" 22 | run ->(_) { [500, {"Content-Type" => "text/html"}, ["Ciao Prometheus Metrics unreachable."]] } 23 | end 24 | else 25 | use Yabeda::Prometheus::Exporter 26 | end 27 | run Rails.application 28 | end 29 | run rackapp 30 | else 31 | run Rails.application 32 | end 33 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "boot" 4 | 5 | require "rails/all" 6 | 7 | # Require the gems listed in Gemfile, including any gems 8 | # you've limited to :test, :development, or :production. 9 | Bundler.require(*Rails.groups) 10 | 11 | module Ciao 12 | class Application < Rails::Application 13 | # Initialize configuration defaults for originally generated Rails version. 14 | config.load_defaults 6.0 15 | 16 | # Settings in config/environments/* take precedence over those specified here. 17 | # Application configuration can go into files in config/initializers 18 | # -- all .rb files in that directory are automatically loaded after loading 19 | # the framework and any gems in your application. 20 | 21 | # Default time_zone is UTC 22 | ENV["TIME_ZONE"] ||= "UTC" 23 | config.time_zone = ENV["TIME_ZONE"] 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 4 | 5 | require "bundler/setup" # Set up gems listed in the Gemfile. 6 | require "logger" # Fix concurrent-ruby removing logger dependency which Rails itself does not have:wq 7 | require "bootsnap/setup" # Speed up boot time by caching expensive operations. 8 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: test 6 | 7 | production: 8 | adapter: redis 9 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> 10 | channel_prefix: ciao_production 11 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | # 7 | default: &default 8 | adapter: sqlite3 9 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 10 } %> 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: db/sqlite/development.sqlite3 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run "rake". 18 | # Do not set this db to the same as development or production. 19 | test: 20 | <<: *default 21 | database: db/sqlite/test.sqlite3 22 | 23 | production: 24 | <<: *default 25 | database: db/sqlite/production.sqlite3 26 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Load the Rails application. 4 | require_relative "application" 5 | 6 | # Initialize the Rails application. 7 | Rails.application.initialize! 8 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # In the development environment your application's code is reloaded on 7 | # every request. This slows down response time but is perfect for development 8 | # since you don't have to restart the web server when you make code changes. 9 | config.cache_classes = false 10 | 11 | # Do not eager load code on boot. 12 | config.eager_load = false 13 | 14 | # Show full error reports. 15 | config.consider_all_requests_local = true 16 | 17 | # Enable/disable caching. By default caching is disabled. 18 | # Run rails dev:cache to toggle caching. 19 | if Rails.root.join("tmp", "caching-dev.txt").exist? 20 | config.action_controller.perform_caching = true 21 | config.action_controller.enable_fragment_cache_logging = true 22 | 23 | config.cache_store = :memory_store 24 | config.public_file_server.headers = { 25 | "Cache-Control" => "public, max-age=#{2.days.to_i}" 26 | } 27 | else 28 | config.action_controller.perform_caching = false 29 | 30 | config.cache_store = :null_store 31 | end 32 | 33 | # Store uploaded files on the local file system (see config/storage.yml for options). 34 | config.active_storage.service = :local 35 | 36 | # Don't care if the mailer can't send. 37 | config.action_mailer.raise_delivery_errors = false 38 | 39 | config.action_mailer.perform_caching = false 40 | 41 | # Print deprecation notices to the Rails logger. 42 | config.active_support.deprecation = :log 43 | 44 | # Raise an error on page load if there are pending migrations. 45 | config.active_record.migration_error = :page_load 46 | 47 | # Highlight code that triggered database queries in logs. 48 | config.active_record.verbose_query_logs = true 49 | 50 | # Debug mode disables concatenation and preprocessing of assets. 51 | # This option may cause significant delays in view rendering with a large 52 | # number of complex assets. 53 | config.assets.debug = true 54 | 55 | # Suppress logger output for asset requests. 56 | config.assets.quiet = true 57 | 58 | # Raises error for missing translations. 59 | # config.action_view.raise_on_missing_translations = true 60 | 61 | # Use an evented file watcher to asynchronously detect changes in source code, 62 | # routes, locales, etc. This feature depends on the listen gem. 63 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker 64 | 65 | # E-mail will pop up in your browser instead of being sent using letter_opener 66 | config.action_mailer.delivery_method = :letter_opener 67 | config.action_mailer.perform_deliveries = true 68 | config.action_mailer.default_options = {from: "from@example.com", to: "to@example.com"} 69 | end 70 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # Code is not reloaded between requests. 7 | config.cache_classes = true 8 | 9 | # Eager load code on boot. This eager loads most of Rails and 10 | # your application in memory, allowing both threaded web servers 11 | # and those relying on copy on write to perform better. 12 | # Rake tasks automatically ignore this option for performance. 13 | config.eager_load = true 14 | 15 | # Full error reports are disabled and caching is turned on. 16 | config.consider_all_requests_local = false 17 | config.action_controller.perform_caching = true 18 | 19 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] 20 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). 21 | config.require_master_key = false 22 | 23 | # Disable serving static files from the `/public` folder by default since 24 | # Apache or NGINX already handles this. 25 | config.public_file_server.enabled = true 26 | 27 | # Compress CSS using a preprocessor. 28 | # config.assets.css_compressor = :sass 29 | 30 | # Do not fallback to assets pipeline if a precompiled asset is missed. 31 | config.assets.compile = false 32 | 33 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 34 | # config.action_controller.asset_host = 'http://assets.example.com' 35 | 36 | # Specifies the header that your server uses for sending files. 37 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 38 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 39 | 40 | # Store uploaded files on the local file system (see config/storage.yml for options). 41 | config.active_storage.service = :local 42 | 43 | # Mount Action Cable outside main process or domain. 44 | # config.action_cable.mount_path = nil 45 | # config.action_cable.url = 'wss://example.com/cable' 46 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] 47 | 48 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 49 | # config.force_ssl = true 50 | 51 | # Use the lowest log level to ensure availability of diagnostic information 52 | # when problems arise. 53 | config.log_level = :debug 54 | 55 | # Prepend all log lines with the following tags. 56 | config.log_tags = [:request_id] 57 | 58 | # Use a different cache store in production. 59 | # config.cache_store = :mem_cache_store 60 | 61 | # Use a real queuing backend for Active Job (and separate queues per environment). 62 | # config.active_job.queue_adapter = :resque 63 | # config.active_job.queue_name_prefix = "ciao_production" 64 | 65 | config.action_mailer.perform_caching = false 66 | 67 | # Ignore bad email addresses and do not raise email delivery errors. 68 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 69 | # config.action_mailer.raise_delivery_errors = false 70 | 71 | # Mailer settings 72 | # config.action_mailer.smtp_settings = { 73 | # address: "smtp.gmail.com", 74 | # port: 587, 75 | # domain: "example.com", 76 | # authentication: "plain", 77 | # enable_starttls_auto: true, 78 | # user_name: ENV["GMAIL_USERNAME"], 79 | # password: ENV["GMAIL_PASSWORD"] 80 | # } 81 | if ENV["SMTP_ADDRESS"].present? 82 | config.action_mailer.delivery_method = :smtp 83 | config.action_mailer.default_options = {from: ENV["SMTP_EMAIL_FROM"], to: ENV["SMTP_EMAIL_TO"]} 84 | config.action_mailer.smtp_settings = { 85 | address: ENV["SMTP_ADDRESS"], 86 | port: ENV["SMTP_PORT"], 87 | domain: ENV["SMTP_DOMAIN"], 88 | authentication: ENV["SMTP_AUTHENTICATION"], 89 | enable_starttls_auto: ActiveModel::Type::Boolean.new.cast(ENV["SMTP_ENABLE_STARTTLS_AUTO"]), 90 | user_name: ENV["SMTP_USERNAME"], 91 | password: ENV["SMTP_PASSWORD"], 92 | ssl: ActiveModel::Type::Boolean.new.cast(ENV["SMTP_SSL"]) 93 | } 94 | end 95 | 96 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 97 | # the I18n.default_locale when a translation cannot be found). 98 | config.i18n.fallbacks = true 99 | 100 | # Send deprecation notices to registered listeners. 101 | config.active_support.deprecation = :notify 102 | 103 | # Use default logging formatter so that PID and timestamp are not suppressed. 104 | config.log_formatter = ::Logger::Formatter.new 105 | 106 | # Use a different logger for distributed setups. 107 | # require 'syslog/logger' 108 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') 109 | 110 | if ENV["RAILS_LOG_TO_STDOUT"] == "true" 111 | logger = ActiveSupport::Logger.new($stdout) 112 | logger.formatter = config.log_formatter 113 | config.logger = ActiveSupport::TaggedLogging.new(logger) 114 | config.log_level = ENV.fetch("CIAO_LOG_LEVEL", "WARN").downcase.strip.to_sym 115 | end 116 | 117 | # Do not dump schema after migrations. 118 | config.active_record.dump_schema_after_migration = false 119 | 120 | # Inserts middleware to perform automatic connection switching. 121 | # The `database_selector` hash is used to pass options to the DatabaseSelector 122 | # middleware. The `delay` is used to determine how long to wait after a write 123 | # to send a subsequent read to the primary. 124 | # 125 | # The `database_resolver` class is used by the middleware to determine which 126 | # database is appropriate to use based on the time delay. 127 | # 128 | # The `database_resolver_context` class is used by the middleware to set 129 | # timestamps for the last write to the primary. The resolver uses the context 130 | # class timestamps to determine how long to wait before reading from the 131 | # replica. 132 | # 133 | # By default Rails will store a last write timestamp in the session. The 134 | # DatabaseSelector middleware is designed as such you can define your own 135 | # strategy for connection switching and pass that into the middleware through 136 | # these configuration options. 137 | # config.active_record.database_selector = { delay: 2.seconds } 138 | # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver 139 | # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session 140 | end 141 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # The test environment is used exclusively to run your application's 4 | # test suite. You never need to work with it otherwise. Remember that 5 | # your test database is "scratch space" for the test suite and is wiped 6 | # and recreated between test runs. Don't rely on the data there! 7 | 8 | Rails.application.configure do 9 | # Settings specified here will take precedence over those in config/application.rb. 10 | 11 | config.cache_classes = false 12 | 13 | # Do not eager load code on boot. This avoids loading your whole application 14 | # just for the purpose of running a single test. If you are using a tool that 15 | # preloads Rails for running tests, you may have to set it to true. 16 | config.eager_load = false 17 | 18 | # Configure public file server for tests with Cache-Control for performance. 19 | config.public_file_server.enabled = true 20 | config.public_file_server.headers = { 21 | "Cache-Control" => "public, max-age=#{1.hour.to_i}" 22 | } 23 | 24 | # Show full error reports and disable caching. 25 | config.consider_all_requests_local = true 26 | config.action_controller.perform_caching = false 27 | config.cache_store = :null_store 28 | 29 | # Raise exceptions instead of rendering exception templates. 30 | config.action_dispatch.show_exceptions = false 31 | 32 | # Disable request forgery protection in test environment. 33 | config.action_controller.allow_forgery_protection = false 34 | 35 | # Store uploaded files on the local file system in a temporary directory. 36 | config.active_storage.service = :test 37 | 38 | config.action_mailer.perform_caching = false 39 | 40 | # Tell Action Mailer not to deliver emails to the real world. 41 | # The :test delivery method accumulates sent emails in the 42 | # ActionMailer::Base.deliveries array. 43 | config.action_mailer.delivery_method = :test 44 | 45 | # Print deprecation notices to the stderr. 46 | config.active_support.deprecation = :stderr 47 | 48 | # Raises error for missing translations. 49 | # config.action_view.raise_on_missing_translations = true 50 | end 51 | -------------------------------------------------------------------------------- /config/initializers/app.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | NET_HTTP_ERRORS = [ 4 | EOFError, 5 | Errno::ECONNRESET, 6 | Errno::EINVAL, 7 | Errno::ECONNREFUSED, 8 | Net::HTTPBadResponse, 9 | Net::HTTPHeaderSyntaxError, 10 | Net::ProtocolError, 11 | Timeout::Error, 12 | SocketError, 13 | OpenSSL::SSL::SSLError 14 | ].freeze 15 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # ActiveSupport::Reloader.to_prepare do 6 | # ApplicationController.renderer.defaults.merge!( 7 | # http_host: 'example.org', 8 | # https: false 9 | # ) 10 | # end 11 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Version of your assets, change this if you want to expire all your assets. 6 | Rails.application.config.assets.version = "1.0" 7 | 8 | # Add additional assets to the asset load path. 9 | # Rails.application.config.assets.paths << Emoji.images_path 10 | # Add Yarn node_modules folder to the asset load path. 11 | Rails.application.config.assets.paths << Rails.root.join("node_modules") 12 | 13 | # Precompile additional assets. 14 | # application.js, application.css, and all non-JS/CSS in the app/assets 15 | # folder are already added. 16 | # Rails.application.config.assets.precompile += %w( admin.js admin.css ) 17 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 6 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 7 | 8 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 9 | # Rails.backtrace_cleaner.remove_silencers! 10 | -------------------------------------------------------------------------------- /config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Define an application-wide content security policy 6 | # For further information see the following documentation 7 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy 8 | 9 | # Rails.application.config.content_security_policy do |policy| 10 | # policy.default_src :self, :https 11 | # policy.font_src :self, :https, :data 12 | # policy.img_src :self, :https, :data 13 | # policy.object_src :none 14 | # policy.script_src :self, :https 15 | # policy.style_src :self, :https 16 | # # If you are using webpack-dev-server then specify webpack-dev-server host 17 | # policy.connect_src :self, :https, "http://localhost:3035", "ws://localhost:3035" if Rails.env.development? 18 | 19 | # # Specify URI for violation reports 20 | # # policy.report_uri "/csp-violation-report-endpoint" 21 | # end 22 | 23 | # If you are using UJS then enable automatic nonce generation 24 | # Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } 25 | 26 | # Report CSP violations to a specified URI 27 | # For further information see the following documentation: 28 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only 29 | # Rails.application.config.content_security_policy_report_only = true 30 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Specify a serializer for the signed and encrypted cookie jars. 6 | # Valid options are :json, :marshal, and :hybrid. 7 | Rails.application.config.action_dispatch.cookies_serializer = :json 8 | -------------------------------------------------------------------------------- /config/initializers/create_background_jobs.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Create all Rufus Scheduler Jobs for active checks on Application Start 4 | # Prevent the initializer to be run during rake tasks 5 | 6 | if defined?(Rails::Server) && ActiveRecord::Base.connection.table_exists?("checks") 7 | Check.active.each(&:create_job) 8 | Check.active.each(&:create_tls_job) 9 | end 10 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Configure sensitive parameters which will be filtered from the log file. 6 | Rails.application.config.filter_parameters += [:password] 7 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Add new inflection rules using the following format. Inflections 6 | # are locale specific, and you may define rules for as many different 7 | # locales as you wish. All of these examples are active by default: 8 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 9 | # inflect.plural /^(ox)$/i, '\1en' 10 | # inflect.singular /^(ox)en/i, '\1' 11 | # inflect.irregular 'person', 'people' 12 | # inflect.uncountable %w( fish sheep ) 13 | # end 14 | 15 | # These inflection rules are supported but not enabled by default: 16 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 17 | # inflect.acronym 'RESTful' 18 | # end 19 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Add new mime types for use in respond_to blocks: 6 | # Mime::Type.register "text/richtext", :rtf 7 | -------------------------------------------------------------------------------- /config/initializers/notifications.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Some time in the future Rails is not going to auto_load these for us :( 4 | # we have to explictly require it here 5 | Dir[Rails.root.join("app", "lib", "ciao", "**", "*.rb")].sort.each { |f| require f } 6 | 7 | # export CIAO_WEBHOOK_ENDPOINT_$NAME=https://chat.yourhost.net/***** 8 | # export CIAO_WEBHOOK_PAYLOAD_$NAME=#'{"username":"Brot & Games","icon_url":"https://avatars0.githubusercontent.com/u/43862266?s=400&v=4","text":"Example message","attachments":[{"title":"Rocket.Chat","title_link":"https://rocket.chat","text":"Rocket.Chat, the best open source chat","image_url":"/images/integration-attachment-example.png","color":"#764FA5"}]}' 9 | # `$NAME` can be any word `[A-Z0-9_]+` and must be unique as it is used as an identifier 10 | 11 | NOTIFICATIONS = Ciao::Parsers::WebhookParser.webhooks.map do |webhook| 12 | Ciao::Notifications::WebhookNotification.new( 13 | webhook[:endpoint], 14 | webhook[:payload], 15 | Ciao::Renderers::ReplaceRenderer 16 | ) 17 | end 18 | 19 | NOTIFICATIONS << Ciao::Notifications::MailNotification.new if ENV["SMTP_ADDRESS"].present? 20 | 21 | NOTIFICATIONS_TLS_EXPIRES = Ciao::Parsers::WebhookParser.webhooks.map do |webhook| 22 | Ciao::Notifications::WebhookNotification.new( 23 | webhook[:endpoint], 24 | webhook[:payload_tls_expires], 25 | Ciao::Renderers::ReplaceRenderer 26 | ) 27 | end 28 | 29 | NOTIFICATIONS_TLS_EXPIRES << Ciao::Notifications::MailNotificationTlsExpires.new if ENV["SMTP_ADDRESS"].present? && !ENV["SMTP_DISABLE_TLS_EXPIRES_NOTIFICATIONS"].present? 30 | -------------------------------------------------------------------------------- /config/initializers/rufus_scheduler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rufus-scheduler" 4 | 5 | scheduler = Rufus::Scheduler.singleton(max_work_threads: 2300) 6 | 7 | def scheduler.on_error(job, error) 8 | Rails.logger.error( 9 | "err#{error.object_id} rufus-scheduler intercepted #{error.inspect}" \ 10 | " in job #{job.inspect}" 11 | ) 12 | error.backtrace.each_with_index do |line, i| 13 | Rails.logger.error( 14 | "err#{error.object_id} #{i}: #{line}" 15 | ) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /config/initializers/simple_form.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # 4 | # Uncomment this and change the path if necessary to include your own 5 | # components. 6 | # See https://github.com/plataformatec/simple_form#custom-components to know 7 | # more about custom components. 8 | # Dir[Rails.root.join('lib/components/**/*.rb')].each { |f| require f } 9 | # 10 | # Use this setup block to configure all options available in SimpleForm. 11 | SimpleForm.setup do |config| 12 | # Wrappers are used by the form builder to generate a 13 | # complete input. You can remove any component from the 14 | # wrapper, change the order or even add your own to the 15 | # stack. The options given below are used to wrap the 16 | # whole input. 17 | config.wrappers :default, class: :input, 18 | hint_class: :field_with_hint, error_class: :field_with_errors, valid_class: :field_without_errors do |b| 19 | ## Extensions enabled by default 20 | # Any of these extensions can be disabled for a 21 | # given input by passing: `f.input EXTENSION_NAME => false`. 22 | # You can make any of these extensions optional by 23 | # renaming `b.use` to `b.optional`. 24 | 25 | # Determines whether to use HTML5 (:email, :url, ...) 26 | # and required attributes 27 | b.use :html5 28 | 29 | # Calculates placeholders automatically from I18n 30 | # You can also pass a string as f.input placeholder: "Placeholder" 31 | b.use :placeholder 32 | 33 | ## Optional extensions 34 | # They are disabled unless you pass `f.input EXTENSION_NAME => true` 35 | # to the input. If so, they will retrieve the values from the model 36 | # if any exists. If you want to enable any of those 37 | # extensions by default, you can change `b.optional` to `b.use`. 38 | 39 | # Calculates maxlength from length validations for string inputs 40 | # and/or database column lengths 41 | b.optional :maxlength 42 | 43 | # Calculate minlength from length validations for string inputs 44 | b.optional :minlength 45 | 46 | # Calculates pattern from format validations for string inputs 47 | b.optional :pattern 48 | 49 | # Calculates min and max from length validations for numeric inputs 50 | b.optional :min_max 51 | 52 | # Calculates readonly automatically from readonly attributes 53 | b.optional :readonly 54 | 55 | ## Inputs 56 | # b.use :input, class: 'input', error_class: 'is-invalid', valid_class: 'is-valid' 57 | b.use :label_input 58 | b.use :hint, wrap_with: {tag: :span, class: :hint} 59 | b.use :error, wrap_with: {tag: :span, class: :error} 60 | 61 | ## full_messages_for 62 | # If you want to display the full error message for the attribute, you can 63 | # use the component :full_error, like: 64 | # 65 | # b.use :full_error, wrap_with: { tag: :span, class: :error } 66 | end 67 | 68 | # The default wrapper to be used by the FormBuilder. 69 | config.default_wrapper = :default 70 | 71 | # Define the way to render check boxes / radio buttons with labels. 72 | # Defaults to :nested for bootstrap config. 73 | # inline: input + label 74 | # nested: label > input 75 | config.boolean_style = :nested 76 | 77 | # Default class for buttons 78 | config.button_class = "btn" 79 | 80 | # Method used to tidy up errors. Specify any Rails Array method. 81 | # :first lists the first message for each field. 82 | # Use :to_sentence to list all errors for each field. 83 | # config.error_method = :first 84 | 85 | # Default tag used for error notification helper. 86 | config.error_notification_tag = :div 87 | 88 | # CSS class to add for error notification helper. 89 | config.error_notification_class = "error_notification" 90 | 91 | # Series of attempts to detect a default label method for collection. 92 | # config.collection_label_methods = [ :to_label, :name, :title, :to_s ] 93 | 94 | # Series of attempts to detect a default value method for collection. 95 | # config.collection_value_methods = [ :id, :to_s ] 96 | 97 | # You can wrap a collection of radio/check boxes in a pre-defined tag, defaulting to none. 98 | # config.collection_wrapper_tag = nil 99 | 100 | # You can define the class to use on all collection wrappers. Defaulting to none. 101 | # config.collection_wrapper_class = nil 102 | 103 | # You can wrap each item in a collection of radio/check boxes with a tag, 104 | # defaulting to :span. 105 | # config.item_wrapper_tag = :span 106 | 107 | # You can define a class to use in all item wrappers. Defaulting to none. 108 | # config.item_wrapper_class = nil 109 | 110 | # How the label text should be generated altogether with the required text. 111 | # config.label_text = lambda { |label, required, explicit_label| "#{required} #{label}" } 112 | 113 | # You can define the class to use on all labels. Default is nil. 114 | # config.label_class = nil 115 | 116 | # You can define the default class to be used on forms. Can be overriden 117 | # with `html: { :class }`. Defaulting to none. 118 | # config.default_form_class = nil 119 | 120 | # You can define which elements should obtain additional classes 121 | # config.generate_additional_classes_for = [:wrapper, :label, :input] 122 | 123 | # Whether attributes are required by default (or not). Default is true. 124 | # config.required_by_default = true 125 | 126 | # Tell browsers whether to use the native HTML5 validations (novalidate form option). 127 | # These validations are enabled in SimpleForm's internal config but disabled by default 128 | # in this configuration, which is recommended due to some quirks from different browsers. 129 | # To stop SimpleForm from generating the novalidate option, enabling the HTML5 validations, 130 | # change this configuration to true. 131 | config.browser_validations = false 132 | 133 | # Collection of methods to detect if a file type was given. 134 | # config.file_methods = [ :mounted_as, :file?, :public_filename, :attached? ] 135 | 136 | # Custom mappings for input types. This should be a hash containing a regexp 137 | # to match as key, and the input type that will be used when the field name 138 | # matches the regexp as value. 139 | # config.input_mappings = { /count/ => :integer } 140 | 141 | # Custom wrappers for input types. This should be a hash containing an input 142 | # type as key and the wrapper that will be used for all inputs with specified type. 143 | # config.wrapper_mappings = { string: :prepend } 144 | 145 | # Namespaces where SimpleForm should look for custom input classes that 146 | # override default inputs. 147 | # config.custom_inputs_namespaces << "CustomInputs" 148 | 149 | # Default priority for time_zone inputs. 150 | # config.time_zone_priority = nil 151 | 152 | # Default priority for country inputs. 153 | # config.country_priority = nil 154 | 155 | # When false, do not use translations for labels. 156 | # config.translate_labels = true 157 | 158 | # Automatically discover new inputs in Rails' autoload path. 159 | # config.inputs_discovery = true 160 | 161 | # Cache SimpleForm inputs discovery 162 | # config.cache_discovery = !Rails.env.development? 163 | 164 | # Default class for inputs 165 | # config.input_class = nil 166 | 167 | # Define the default class of the input wrapper of the boolean input. 168 | config.boolean_label_class = "checkbox" 169 | 170 | # Defines if the default input wrapper class should be included in radio 171 | # collection wrappers. 172 | # config.include_default_input_wrapper_class = true 173 | 174 | # Defines which i18n scope will be used in Simple Form. 175 | # config.i18n_scope = 'simple_form' 176 | 177 | # Defines validation classes to the input_field. By default it's nil. 178 | # config.input_field_valid_class = 'is-valid' 179 | # config.input_field_error_class = 'is-invalid' 180 | end 181 | -------------------------------------------------------------------------------- /config/initializers/simple_form_bootstrap.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Please do not make direct changes to this file! 4 | # This generator is maintained by the community around simple_form-bootstrap: 5 | # https://github.com/rafaelfranca/simple_form-bootstrap 6 | # All future development, tests, and organization should happen there. 7 | # Background history: https://github.com/plataformatec/simple_form/issues/1561 8 | 9 | # Uncomment this and change the path if necessary to include your own 10 | # components. 11 | # See https://github.com/plataformatec/simple_form#custom-components 12 | # to know more about custom components. 13 | # Dir[Rails.root.join('lib/components/**/*.rb')].each { |f| require f } 14 | 15 | # Use this setup block to configure all options available in SimpleForm. 16 | SimpleForm.setup do |config| 17 | # Default class for buttons 18 | config.button_class = "btn" 19 | 20 | # Define the default class of the input wrapper of the boolean input. 21 | config.boolean_label_class = "form-check-label" 22 | 23 | # How the label text should be generated altogether with the required text. 24 | config.label_text = ->(label, required, _explicit_label) { "#{label} #{required}" } 25 | 26 | # Define the way to render check boxes / radio buttons with labels. 27 | config.boolean_style = :inline 28 | 29 | # You can wrap each item in a collection of radio/check boxes with a tag 30 | config.item_wrapper_tag = :div 31 | 32 | # Defines if the default input wrapper class should be included in radio 33 | # collection wrappers. 34 | config.include_default_input_wrapper_class = false 35 | 36 | # CSS class to add for error notification helper. 37 | config.error_notification_class = "alert alert-danger" 38 | 39 | # Method used to tidy up errors. Specify any Rails Array method. 40 | # :first lists the first message for each field. 41 | # :to_sentence to list all errors for each field. 42 | config.error_method = :to_sentence 43 | 44 | # add validation classes to `input_field` 45 | config.input_field_error_class = "is-invalid" 46 | config.input_field_valid_class = "is-valid" 47 | 48 | # vertical forms 49 | # 50 | # vertical default_wrapper 51 | config.wrappers :vertical_form, tag: "div", class: "form-group", error_class: "form-group-invalid", valid_class: "form-group-valid" do |b| 52 | b.use :html5 53 | b.use :placeholder 54 | b.optional :maxlength 55 | b.optional :minlength 56 | b.optional :pattern 57 | b.optional :min_max 58 | b.optional :readonly 59 | b.use :label, class: "form-control-label" 60 | b.use :input, class: "form-control", error_class: "is-invalid", valid_class: "is-valid" 61 | b.use :full_error, wrap_with: {tag: "div", class: "invalid-feedback"} 62 | b.use :hint, wrap_with: {tag: "small", class: "form-text text-muted"} 63 | end 64 | 65 | # vertical input for boolean 66 | config.wrappers :vertical_boolean, tag: "fieldset", class: "form-group", error_class: "form-group-invalid", valid_class: "form-group-valid" do |b| 67 | b.use :html5 68 | b.optional :readonly 69 | b.wrapper :form_check_wrapper, tag: "div", class: "form-check" do |bb| 70 | bb.use :input, class: "form-check-input", error_class: "is-invalid", valid_class: "is-valid" 71 | bb.use :label, class: "form-check-label" 72 | bb.use :full_error, wrap_with: {tag: "div", class: "invalid-feedback"} 73 | bb.use :hint, wrap_with: {tag: "small", class: "form-text text-muted"} 74 | end 75 | end 76 | 77 | # vertical input for radio buttons and check boxes 78 | config.wrappers :vertical_collection, item_wrapper_class: "form-check", tag: "fieldset", class: "form-group", error_class: "form-group-invalid", valid_class: "form-group-valid" do |b| 79 | b.use :html5 80 | b.optional :readonly 81 | b.wrapper :legend_tag, tag: "legend", class: "col-form-label pt-0" do |ba| 82 | ba.use :label_text 83 | end 84 | b.use :input, class: "form-check-input", error_class: "is-invalid", valid_class: "is-valid" 85 | b.use :full_error, wrap_with: {tag: "div", class: "invalid-feedback d-block"} 86 | b.use :hint, wrap_with: {tag: "small", class: "form-text text-muted"} 87 | end 88 | 89 | # vertical input for inline radio buttons and check boxes 90 | config.wrappers :vertical_collection_inline, item_wrapper_class: "form-check form-check-inline", tag: "fieldset", class: "form-group", error_class: "form-group-invalid", valid_class: "form-group-valid" do |b| 91 | b.use :html5 92 | b.optional :readonly 93 | b.wrapper :legend_tag, tag: "legend", class: "col-form-label pt-0" do |ba| 94 | ba.use :label_text 95 | end 96 | b.use :input, class: "form-check-input", error_class: "is-invalid", valid_class: "is-valid" 97 | b.use :full_error, wrap_with: {tag: "div", class: "invalid-feedback d-block"} 98 | b.use :hint, wrap_with: {tag: "small", class: "form-text text-muted"} 99 | end 100 | 101 | # vertical file input 102 | config.wrappers :vertical_file, tag: "div", class: "form-group", error_class: "form-group-invalid", valid_class: "form-group-valid" do |b| 103 | b.use :html5 104 | b.use :placeholder 105 | b.optional :maxlength 106 | b.optional :minlength 107 | b.optional :readonly 108 | b.use :label 109 | b.use :input, class: "form-control-file", error_class: "is-invalid", valid_class: "is-valid" 110 | b.use :full_error, wrap_with: {tag: "div", class: "invalid-feedback d-block"} 111 | b.use :hint, wrap_with: {tag: "small", class: "form-text text-muted"} 112 | end 113 | 114 | # vertical multi select 115 | config.wrappers :vertical_multi_select, tag: "div", class: "form-group", error_class: "form-group-invalid", valid_class: "form-group-valid" do |b| 116 | b.use :html5 117 | b.optional :readonly 118 | b.use :label, class: "form-control-label" 119 | b.wrapper tag: "div", class: "d-flex flex-row justify-content-between align-items-center" do |ba| 120 | ba.use :input, class: "form-control mx-1", error_class: "is-invalid", valid_class: "is-valid" 121 | end 122 | b.use :full_error, wrap_with: {tag: "div", class: "invalid-feedback d-block"} 123 | b.use :hint, wrap_with: {tag: "small", class: "form-text text-muted"} 124 | end 125 | 126 | # vertical range input 127 | config.wrappers :vertical_range, tag: "div", class: "form-group", error_class: "form-group-invalid", valid_class: "form-group-valid" do |b| 128 | b.use :html5 129 | b.use :placeholder 130 | b.optional :readonly 131 | b.optional :step 132 | b.use :label 133 | b.use :input, class: "form-control-range", error_class: "is-invalid", valid_class: "is-valid" 134 | b.use :full_error, wrap_with: {tag: "div", class: "invalid-feedback d-block"} 135 | b.use :hint, wrap_with: {tag: "small", class: "form-text text-muted"} 136 | end 137 | 138 | # horizontal forms 139 | # 140 | # horizontal default_wrapper 141 | config.wrappers :horizontal_form, tag: "div", class: "form-group row", error_class: "form-group-invalid", valid_class: "form-group-valid" do |b| 142 | b.use :html5 143 | b.use :placeholder 144 | b.optional :maxlength 145 | b.optional :minlength 146 | b.optional :pattern 147 | b.optional :min_max 148 | b.optional :readonly 149 | b.use :label, class: "col-sm-3 col-form-label" 150 | b.wrapper :grid_wrapper, tag: "div", class: "col-sm-9" do |ba| 151 | ba.use :input, class: "form-control", error_class: "is-invalid", valid_class: "is-valid" 152 | ba.use :full_error, wrap_with: {tag: "div", class: "invalid-feedback"} 153 | ba.use :hint, wrap_with: {tag: "small", class: "form-text text-muted"} 154 | end 155 | end 156 | 157 | # horizontal input for boolean 158 | config.wrappers :horizontal_boolean, tag: "div", class: "form-group row", error_class: "form-group-invalid", valid_class: "form-group-valid" do |b| 159 | b.use :html5 160 | b.optional :readonly 161 | b.wrapper tag: "label", class: "col-sm-3" do |ba| 162 | ba.use :label_text 163 | end 164 | b.wrapper :grid_wrapper, tag: "div", class: "col-sm-9" do |wr| 165 | wr.wrapper :form_check_wrapper, tag: "div", class: "form-check" do |bb| 166 | bb.use :input, class: "form-check-input", error_class: "is-invalid", valid_class: "is-valid" 167 | bb.use :label, class: "form-check-label" 168 | bb.use :full_error, wrap_with: {tag: "div", class: "invalid-feedback d-block"} 169 | bb.use :hint, wrap_with: {tag: "small", class: "form-text text-muted"} 170 | end 171 | end 172 | end 173 | 174 | # horizontal input for radio buttons and check boxes 175 | config.wrappers :horizontal_collection, item_wrapper_class: "form-check", tag: "div", class: "form-group row", error_class: "form-group-invalid", valid_class: "form-group-valid" do |b| 176 | b.use :html5 177 | b.optional :readonly 178 | b.use :label, class: "col-sm-3 form-control-label" 179 | b.wrapper :grid_wrapper, tag: "div", class: "col-sm-9" do |ba| 180 | ba.use :input, class: "form-check-input", error_class: "is-invalid", valid_class: "is-valid" 181 | ba.use :full_error, wrap_with: {tag: "div", class: "invalid-feedback d-block"} 182 | ba.use :hint, wrap_with: {tag: "small", class: "form-text text-muted"} 183 | end 184 | end 185 | 186 | # horizontal input for inline radio buttons and check boxes 187 | config.wrappers :horizontal_collection_inline, item_wrapper_class: "form-check form-check-inline", tag: "div", class: "form-group row", error_class: "form-group-invalid", valid_class: "form-group-valid" do |b| 188 | b.use :html5 189 | b.optional :readonly 190 | b.use :label, class: "col-sm-3 form-control-label" 191 | b.wrapper :grid_wrapper, tag: "div", class: "col-sm-9" do |ba| 192 | ba.use :input, class: "form-check-input", error_class: "is-invalid", valid_class: "is-valid" 193 | ba.use :full_error, wrap_with: {tag: "div", class: "invalid-feedback d-block"} 194 | ba.use :hint, wrap_with: {tag: "small", class: "form-text text-muted"} 195 | end 196 | end 197 | 198 | # horizontal file input 199 | config.wrappers :horizontal_file, tag: "div", class: "form-group row", error_class: "form-group-invalid", valid_class: "form-group-valid" do |b| 200 | b.use :html5 201 | b.use :placeholder 202 | b.optional :maxlength 203 | b.optional :minlength 204 | b.optional :readonly 205 | b.use :label, class: "col-sm-3 form-control-label" 206 | b.wrapper :grid_wrapper, tag: "div", class: "col-sm-9" do |ba| 207 | ba.use :input, error_class: "is-invalid", valid_class: "is-valid" 208 | ba.use :full_error, wrap_with: {tag: "div", class: "invalid-feedback d-block"} 209 | ba.use :hint, wrap_with: {tag: "small", class: "form-text text-muted"} 210 | end 211 | end 212 | 213 | # horizontal multi select 214 | config.wrappers :horizontal_multi_select, tag: "div", class: "form-group row", error_class: "form-group-invalid", valid_class: "form-group-valid" do |b| 215 | b.use :html5 216 | b.optional :readonly 217 | b.use :label, class: "col-sm-3 control-label" 218 | b.wrapper :grid_wrapper, tag: "div", class: "col-sm-9" do |ba| 219 | ba.wrapper tag: "div", class: "d-flex flex-row justify-content-between align-items-center" do |bb| 220 | bb.use :input, class: "form-control mx-1", error_class: "is-invalid", valid_class: "is-valid" 221 | end 222 | ba.use :full_error, wrap_with: {tag: "div", class: "invalid-feedback d-block"} 223 | ba.use :hint, wrap_with: {tag: "small", class: "form-text text-muted"} 224 | end 225 | end 226 | 227 | # horizontal range input 228 | config.wrappers :horizontal_range, tag: "div", class: "form-group row", error_class: "form-group-invalid", valid_class: "form-group-valid" do |b| 229 | b.use :html5 230 | b.use :placeholder 231 | b.optional :readonly 232 | b.optional :step 233 | b.use :label, class: "col-sm-3 form-control-label" 234 | b.wrapper :grid_wrapper, tag: "div", class: "col-sm-9" do |ba| 235 | ba.use :input, class: "form-control-range", error_class: "is-invalid", valid_class: "is-valid" 236 | ba.use :full_error, wrap_with: {tag: "div", class: "invalid-feedback d-block"} 237 | ba.use :hint, wrap_with: {tag: "small", class: "form-text text-muted"} 238 | end 239 | end 240 | 241 | # inline forms 242 | # 243 | # inline default_wrapper 244 | config.wrappers :inline_form, tag: "span", error_class: "form-group-invalid", valid_class: "form-group-valid" do |b| 245 | b.use :html5 246 | b.use :placeholder 247 | b.optional :maxlength 248 | b.optional :minlength 249 | b.optional :pattern 250 | b.optional :min_max 251 | b.optional :readonly 252 | b.use :label, class: "sr-only" 253 | 254 | b.use :input, class: "form-control", error_class: "is-invalid", valid_class: "is-valid" 255 | b.use :error, wrap_with: {tag: "div", class: "invalid-feedback"} 256 | b.optional :hint, wrap_with: {tag: "small", class: "form-text text-muted"} 257 | end 258 | 259 | # inline input for boolean 260 | config.wrappers :inline_boolean, tag: "span", class: "form-check flex-wrap justify-content-start mr-sm-2", error_class: "form-group-invalid", valid_class: "form-group-valid" do |b| 261 | b.use :html5 262 | b.optional :readonly 263 | b.use :input, class: "form-check-input", error_class: "is-invalid", valid_class: "is-valid" 264 | b.use :label, class: "form-check-label" 265 | b.use :error, wrap_with: {tag: "div", class: "invalid-feedback"} 266 | b.optional :hint, wrap_with: {tag: "small", class: "form-text text-muted"} 267 | end 268 | 269 | # bootstrap custom forms 270 | # 271 | # custom input for boolean 272 | config.wrappers :custom_boolean, tag: "fieldset", class: "form-group", error_class: "form-group-invalid", valid_class: "form-group-valid" do |b| 273 | b.use :html5 274 | b.optional :readonly 275 | b.wrapper :form_check_wrapper, tag: "div", class: "custom-control custom-checkbox" do |bb| 276 | bb.use :input, class: "custom-control-input", error_class: "is-invalid", valid_class: "is-valid" 277 | bb.use :label, class: "custom-control-label" 278 | bb.use :full_error, wrap_with: {tag: "div", class: "invalid-feedback"} 279 | bb.use :hint, wrap_with: {tag: "small", class: "form-text text-muted"} 280 | end 281 | end 282 | 283 | config.wrappers :custom_boolean_switch, tag: "fieldset", class: "form-group", error_class: "form-group-invalid", valid_class: "form-group-valid" do |b| 284 | b.use :html5 285 | b.optional :readonly 286 | b.wrapper :form_check_wrapper, tag: "div", class: "custom-control custom-checkbox-switch" do |bb| 287 | bb.use :input, class: "custom-control-input", error_class: "is-invalid", valid_class: "is-valid" 288 | bb.use :label, class: "custom-control-label" 289 | bb.use :full_error, wrap_with: {tag: "div", class: "invalid-feedback"} 290 | bb.use :hint, wrap_with: {tag: "small", class: "form-text text-muted"} 291 | end 292 | end 293 | 294 | # custom input for radio buttons and check boxes 295 | config.wrappers :custom_collection, item_wrapper_class: "custom-control", tag: "fieldset", class: "form-group", error_class: "form-group-invalid", valid_class: "form-group-valid" do |b| 296 | b.use :html5 297 | b.optional :readonly 298 | b.wrapper :legend_tag, tag: "legend", class: "col-form-label pt-0" do |ba| 299 | ba.use :label_text 300 | end 301 | b.use :input, class: "custom-control-input", error_class: "is-invalid", valid_class: "is-valid" 302 | b.use :full_error, wrap_with: {tag: "div", class: "invalid-feedback d-block"} 303 | b.use :hint, wrap_with: {tag: "small", class: "form-text text-muted"} 304 | end 305 | 306 | # custom input for inline radio buttons and check boxes 307 | config.wrappers :custom_collection_inline, item_wrapper_class: "custom-control custom-control-inline", tag: "fieldset", class: "form-group", error_class: "form-group-invalid", valid_class: "form-group-valid" do |b| 308 | b.use :html5 309 | b.optional :readonly 310 | b.wrapper :legend_tag, tag: "legend", class: "col-form-label pt-0" do |ba| 311 | ba.use :label_text 312 | end 313 | b.use :input, class: "custom-control-input", error_class: "is-invalid", valid_class: "is-valid" 314 | b.use :full_error, wrap_with: {tag: "div", class: "invalid-feedback d-block"} 315 | b.use :hint, wrap_with: {tag: "small", class: "form-text text-muted"} 316 | end 317 | 318 | # custom file input 319 | config.wrappers :custom_file, tag: "div", class: "form-group", error_class: "form-group-invalid", valid_class: "form-group-valid" do |b| 320 | b.use :html5 321 | b.use :placeholder 322 | b.optional :maxlength 323 | b.optional :minlength 324 | b.optional :readonly 325 | b.use :label, class: "form-control-label" 326 | b.wrapper :custom_file_wrapper, tag: "div", class: "custom-file" do |ba| 327 | ba.use :input, class: "custom-file-input", error_class: "is-invalid", valid_class: "is-valid" 328 | ba.use :label, class: "custom-file-label" 329 | ba.use :full_error, wrap_with: {tag: "div", class: "invalid-feedback"} 330 | end 331 | b.use :hint, wrap_with: {tag: "small", class: "form-text text-muted"} 332 | end 333 | 334 | # custom multi select 335 | config.wrappers :custom_multi_select, tag: "div", class: "form-group", error_class: "form-group-invalid", valid_class: "form-group-valid" do |b| 336 | b.use :html5 337 | b.optional :readonly 338 | b.use :label, class: "form-control-label" 339 | b.wrapper tag: "div", class: "d-flex flex-row justify-content-between align-items-center" do |ba| 340 | ba.use :input, class: "custom-select mx-1", error_class: "is-invalid", valid_class: "is-valid" 341 | end 342 | b.use :full_error, wrap_with: {tag: "div", class: "invalid-feedback d-block"} 343 | b.use :hint, wrap_with: {tag: "small", class: "form-text text-muted"} 344 | end 345 | 346 | # custom range input 347 | config.wrappers :custom_range, tag: "div", class: "form-group", error_class: "form-group-invalid", valid_class: "form-group-valid" do |b| 348 | b.use :html5 349 | b.use :placeholder 350 | b.optional :readonly 351 | b.optional :step 352 | b.use :label, class: "form-control-label" 353 | b.use :input, class: "custom-range", error_class: "is-invalid", valid_class: "is-valid" 354 | b.use :full_error, wrap_with: {tag: "div", class: "invalid-feedback d-block"} 355 | b.use :hint, wrap_with: {tag: "small", class: "form-text text-muted"} 356 | end 357 | 358 | # Input Group - custom component 359 | # see example app and config at https://github.com/rafaelfranca/simple_form-bootstrap 360 | # config.wrappers :input_group, tag: 'div', class: 'form-group', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b| 361 | # b.use :html5 362 | # b.use :placeholder 363 | # b.optional :maxlength 364 | # b.optional :minlength 365 | # b.optional :pattern 366 | # b.optional :min_max 367 | # b.optional :readonly 368 | # b.use :label, class: 'form-control-label' 369 | # b.wrapper :input_group_tag, tag: 'div', class: 'input-group' do |ba| 370 | # ba.optional :prepend 371 | # ba.use :input, class: 'form-control', error_class: 'is-invalid', valid_class: 'is-valid' 372 | # ba.optional :append 373 | # end 374 | # b.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback d-block' } 375 | # b.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' } 376 | # end 377 | 378 | # Floating Labels form 379 | # 380 | # floating labels default_wrapper 381 | config.wrappers :floating_labels_form, tag: "div", class: "form-label-group", error_class: "form-group-invalid", valid_class: "form-group-valid" do |b| 382 | b.use :html5 383 | b.use :placeholder 384 | b.optional :maxlength 385 | b.optional :minlength 386 | b.optional :pattern 387 | b.optional :min_max 388 | b.optional :readonly 389 | b.use :input, class: "form-control", error_class: "is-invalid", valid_class: "is-valid" 390 | b.use :label, class: "form-control-label" 391 | b.use :full_error, wrap_with: {tag: "div", class: "invalid-feedback"} 392 | b.use :hint, wrap_with: {tag: "small", class: "form-text text-muted"} 393 | end 394 | 395 | # custom multi select 396 | config.wrappers :floating_labels_select, tag: "div", class: "form-label-group", error_class: "form-group-invalid", valid_class: "form-group-valid" do |b| 397 | b.use :html5 398 | b.optional :readonly 399 | b.use :input, class: "custom-select custom-select-lg", error_class: "is-invalid", valid_class: "is-valid" 400 | b.use :label, class: "form-control-label" 401 | b.use :full_error, wrap_with: {tag: "div", class: "invalid-feedback"} 402 | b.use :hint, wrap_with: {tag: "small", class: "form-text text-muted"} 403 | end 404 | 405 | # The default wrapper to be used by the FormBuilder. 406 | config.default_wrapper = :vertical_form 407 | 408 | # Custom wrappers for input types. This should be a hash containing an input 409 | # type as key and the wrapper that will be used for all inputs with specified type. 410 | config.wrapper_mappings = { 411 | boolean: :vertical_boolean, 412 | check_boxes: :vertical_collection, 413 | date: :vertical_multi_select, 414 | datetime: :vertical_multi_select, 415 | file: :vertical_file, 416 | radio_buttons: :vertical_collection, 417 | range: :vertical_range, 418 | time: :vertical_multi_select 419 | } 420 | 421 | # enable custom form wrappers 422 | # config.wrapper_mappings = { 423 | # boolean: :custom_boolean, 424 | # check_boxes: :custom_collection, 425 | # date: :custom_multi_select, 426 | # datetime: :custom_multi_select, 427 | # file: :custom_file, 428 | # radio_buttons: :custom_collection, 429 | # range: :custom_range, 430 | # time: :custom_multi_select 431 | # } 432 | end 433 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # This file contains settings for ActionController::ParamsWrapper which 6 | # is enabled by default. 7 | 8 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 9 | ActiveSupport.on_load(:action_controller) do 10 | wrap_parameters format: [:json] 11 | end 12 | 13 | # To enable root element in JSON for ActiveRecord objects. 14 | # ActiveSupport.on_load(:active_record) do 15 | # self.include_root_in_json = true 16 | # end 17 | -------------------------------------------------------------------------------- /config/initializers/yabeda.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | prometheus_enabled = ENV.fetch("PROMETHEUS_ENABLED", "false") 4 | if prometheus_enabled == "true" 5 | Yabeda.configure do 6 | group :ciao do 7 | gauge :checks, comment: "Number of checks" 8 | gauge :checks_active, comment: "Number of active checks" 9 | gauge :checks_healthy, comment: "Number of healthy checks" 10 | gauge :checks_active_status_1xx, comment: "Number of active checks with status 1xx" 11 | gauge :checks_active_status_2xx, comment: "Number of active checks with status 2xx" 12 | gauge :checks_active_status_3xx, comment: "Number of active checks with status 3xx" 13 | gauge :checks_active_status_4xx, comment: "Number of active checks with status 4xx" 14 | gauge :checks_active_status_5xx, comment: "Number of active checks with status 5xx" 15 | gauge :checks_active_status_err, comment: "Number of active checks with status err" 16 | end 17 | # This block will be executed periodically few times in a minute 18 | # (by timer or external request depending on adapter you're using) 19 | # Keep it fast and simple! 20 | collect do 21 | ciao.checks.set({}, Check.count) 22 | ciao.checks_active.set({}, Check.active.count) 23 | ciao.checks_healthy.set({}, Check.healthy.count) 24 | ciao.checks_active_status_1xx.set({}, Check.status_1xx.count) 25 | ciao.checks_active_status_2xx.set({}, Check.status_2xx.count) 26 | ciao.checks_active_status_3xx.set({}, Check.status_3xx.count) 27 | ciao.checks_active_status_4xx.set({}, Check.status_4xx.count) 28 | ciao.checks_active_status_5xx.set({}, Check.status_5xx.count) 29 | ciao.checks_active_status_err.set({}, Check.status_err.count) 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # 'true': 'foo' 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at https://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | -------------------------------------------------------------------------------- /config/locales/simple_form.en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | simple_form: 3 | "yes": 'Yes' 4 | "no": 'No' 5 | required: 6 | text: 'required' 7 | mark: '*' 8 | # You can uncomment the line below if you need to overwrite the whole required html. 9 | # When using html, text and mark won't be used. 10 | # html: '*' 11 | error_notification: 12 | default_message: "Please review the problems below:" 13 | # Examples 14 | # labels: 15 | # defaults: 16 | # password: 'Password' 17 | # user: 18 | # new: 19 | # email: 'E-mail to sign in.' 20 | # edit: 21 | # email: 'E-mail.' 22 | # hints: 23 | # defaults: 24 | # username: 'User name to sign in.' 25 | # password: 'No special characters, please.' 26 | # include_blanks: 27 | # defaults: 28 | # age: 'Rather not say' 29 | # prompts: 30 | # defaults: 31 | # age: 'Select your age' 32 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Puma can serve each request in a thread from an internal thread pool. 4 | # The `threads` method setting takes two numbers: a minimum and maximum. 5 | # Any libraries that use thread pools should be configured to match 6 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 7 | # and maximum; this matches the default thread size of Active Record. 8 | # 9 | max_threads_count = ENV.fetch("RAILS_MAX_THREADS", 5) 10 | min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } 11 | threads min_threads_count, max_threads_count 12 | 13 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 14 | # 15 | port ENV.fetch("PORT", 3000) 16 | 17 | # Specifies the `environment` that Puma will run in. 18 | # 19 | environment ENV.fetch("RAILS_ENV", "development") 20 | 21 | # Specifies the number of `workers` to boot in clustered mode. 22 | # Workers are forked web server processes. If using threads and workers together 23 | # the concurrency of the application would be max `threads` * `workers`. 24 | # Workers do not work on JRuby or Windows (both of which do not support 25 | # processes). 26 | # 27 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 28 | 29 | # Use the `preload_app!` method when specifying a `workers` number. 30 | # This directive tells Puma to first boot the application and load code 31 | # before forking the application. This takes advantage of Copy On Write 32 | # process behavior so workers use less memory. 33 | # 34 | # preload_app! 35 | 36 | # Allow puma to be restarted by `rails restart` command. 37 | plugin :tmp_restart 38 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rails.application.routes.draw do 4 | root to: "checks#dashboard" 5 | resources :checks do 6 | get "job", to: "checks#job" 7 | get "jobs/recreate", to: "checks#jobs_recreate", on: :collection 8 | get "admin", to: "checks#admin", on: :collection 9 | end 10 | # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html 11 | end 12 | -------------------------------------------------------------------------------- /config/secrets.yaml: -------------------------------------------------------------------------------- 1 | development: 2 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 3 | test: 4 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 5 | production: 6 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 7 | -------------------------------------------------------------------------------- /config/spring.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Spring.watch( 4 | ".ruby-version", 5 | ".rbenv-vars", 6 | "tmp/restart.txt", 7 | "tmp/caching-dev.txt" 8 | ) 9 | -------------------------------------------------------------------------------- /config/storage.yml: -------------------------------------------------------------------------------- 1 | test: 2 | service: Disk 3 | root: <%= Rails.root.join("tmp/storage") %> 4 | 5 | local: 6 | service: Disk 7 | root: <%= Rails.root.join("storage") %> 8 | 9 | # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) 10 | # amazon: 11 | # service: S3 12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> 13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> 14 | # region: us-east-1 15 | # bucket: your_own_bucket 16 | 17 | # Remember not to checkin your GCS keyfile to a repository 18 | # google: 19 | # service: GCS 20 | # project: your_project 21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> 22 | # bucket: your_own_bucket 23 | 24 | # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) 25 | # microsoft: 26 | # service: AzureStorage 27 | # storage_account_name: your_account_name 28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> 29 | # container: your_container_name 30 | 31 | # mirror: 32 | # service: Mirror 33 | # primary: local 34 | # mirrors: [ amazon, google, microsoft ] 35 | -------------------------------------------------------------------------------- /config/webpack/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path") 2 | const webpack = require("webpack") 3 | const mode = process.env.NODE_ENV === 'development' ? 'development' : 'production'; 4 | 5 | module.exports = { 6 | mode, 7 | entry: { 8 | application: "./app/javascript/application.js" 9 | }, 10 | output: { 11 | filename: "[name].js", 12 | sourceMapFilename: "[name].js.map", 13 | path: path.resolve(__dirname, '..', '..', 'app/assets/builds') 14 | }, 15 | plugins: [ 16 | new webpack.optimize.LimitChunkCountPlugin({ 17 | maxChunks: 1 18 | }) 19 | ], 20 | optimization: { 21 | moduleIds: 'deterministic', 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /db/migrate/20190511005035_create_checks.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateChecks < ActiveRecord::Migration[6.0] 4 | def change 5 | create_table :checks do |t| 6 | t.string :name 7 | t.text :description 8 | t.string :cron 9 | 10 | t.timestamps 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20190511124347_add_url_to_checks.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddUrlToChecks < ActiveRecord::Migration[6.0] 4 | def change 5 | add_column :checks, :url, :string 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20190511124951_add_status_to_checks.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddStatusToChecks < ActiveRecord::Migration[6.0] 4 | def change 5 | add_column :checks, :status, :string 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20190511125018_add_active_to_checks.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddActiveToChecks < ActiveRecord::Migration[6.0] 4 | def change 5 | add_column :checks, :active, :boolean 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20190511134031_add_job_to_checks.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddJobToChecks < ActiveRecord::Migration[6.0] 4 | def change 5 | add_column :checks, :job, :string 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20190511231027_remove_description_from_checks.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class RemoveDescriptionFromChecks < ActiveRecord::Migration[6.0] 4 | def change 5 | remove_column :checks, :description, :text 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20190513113609_add_last_contact_at_to_checks.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddLastContactAtToChecks < ActiveRecord::Migration[6.0] 4 | def change 5 | add_column :checks, :last_contact_at, :datetime 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20190513131721_add_next_contact_at_to_checks.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddNextContactAtToChecks < ActiveRecord::Migration[6.0] 4 | def change 5 | add_column :checks, :next_contact_at, :datetime 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20221114215533_create_status_changes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateStatusChanges < ActiveRecord::Migration[6.1] 4 | def change 5 | create_table :status_changes do |t| 6 | t.string :status 7 | t.references :check, null: false, foreign_key: true 8 | 9 | t.timestamps 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20230613104350_add_tls_expires_at_to_checks.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddTlsExpiresAtToChecks < ActiveRecord::Migration[6.1] 4 | def change 5 | add_column :checks, :tls_expires_at, :datetime 6 | add_index :checks, :tls_expires_at 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20230613114013_add_tls_expires_in_days_to_checks.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddTlsExpiresInDaysToChecks < ActiveRecord::Migration[6.1] 4 | def change 5 | add_column :checks, :tls_expires_in_days, :integer 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20230615063542_add_tls_job_to_checks.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddTlsJobToChecks < ActiveRecord::Migration[6.1] 4 | def change 5 | add_column :checks, :tls_job, :string 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file is auto-generated from the current state of the database. Instead 4 | # of editing this file, please use the migrations feature of Active Record to 5 | # incrementally modify your database, and then regenerate this schema definition. 6 | # 7 | # This file is the source Rails uses to define your schema when running `bin/rails 8 | # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to 9 | # be faster and is potentially less error prone than running all of your 10 | # migrations from scratch. Old migrations may fail to apply correctly if those 11 | # migrations use external dependencies or application code. 12 | # 13 | # It's strongly recommended that you check this file into your version control system. 14 | 15 | ActiveRecord::Schema.define(version: 20_230_615_063_542) do 16 | create_table 'checks', force: :cascade do |t| 17 | t.string 'name' 18 | t.string 'cron' 19 | t.datetime 'created_at', precision: 6, null: false 20 | t.datetime 'updated_at', precision: 6, null: false 21 | t.string 'url' 22 | t.string 'status' 23 | t.boolean 'active' 24 | t.string 'job' 25 | t.datetime 'last_contact_at' 26 | t.datetime 'next_contact_at' 27 | t.datetime 'tls_expires_at' 28 | t.integer 'tls_expires_in_days' 29 | t.string 'tls_job' 30 | t.index ['tls_expires_at'], name: 'index_checks_on_tls_expires_at' 31 | end 32 | 33 | create_table 'status_changes', force: :cascade do |t| 34 | t.string 'status' 35 | t.integer 'check_id', null: false 36 | t.datetime 'created_at', precision: 6, null: false 37 | t.datetime 'updated_at', precision: 6, null: false 38 | t.index ['check_id'], name: 'index_status_changes_on_check_id' 39 | end 40 | 41 | add_foreign_key 'status_changes', 'checks' 42 | end 43 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file should contain all the record creation needed to seed the database with its default values. 4 | # The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup). 5 | # 6 | # Examples: 7 | # 8 | # movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) 9 | # Character.create(name: 'Luke', movie: movies.first) 10 | -------------------------------------------------------------------------------- /db/sqlite/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brotandgames/ciao/d1b0d7818d1a9620440b14fd79b4fcd01765e865/db/sqlite/.keep -------------------------------------------------------------------------------- /guestbook.md: -------------------------------------------------------------------------------- 1 | # Guestbook 2 | 3 | Do you remember the guestbook times? *missing running gifs* 4 | 5 | Listed here are some voices from the internet collected (copied & pasted) by the creator *@brotandgames* and maybe the first Guestbook via Pull Request. 6 | 7 | ## From the internet 8 | 9 | Listed here are some quotes from the internet. 10 | 11 | If you find yourself here, replace `*_user` by `@you` and open a PR. 12 | 13 | Also, if you want add something, open a PR. 14 | 15 | ### Hacker News 16 | 17 | Extracted from this [post](https://news.ycombinator.com/item?id=20460943). 18 | 19 | > Neat. *HackerNews_user* 20 | 21 | > Looks like a very cool solution if you can't use any of the common SaaS solutions out there like Pingdom or UptimeRobot. *HackerNews_user* 22 | 23 | > 'Ciao' has an interesting etymology: https://en.wikipedia.org/wiki/Ciao#Etymology *HackerNews_user* 24 | 25 | > Nice product! And thank you for making it opensource. *HackerNews_user* 26 | 27 | > Looks like at nice replacement for my current solution of a shellscript/cron combo (for monitoring a handful of internal endpoints) *HackerNews_user* 28 | 29 | > Looks great. *HackerNews_user* 30 | 31 | > Nice, I like the simplicity around the solution and how easy it is to use. *HackerNews_user* 32 | 33 | ### GitHub 34 | 35 | Extracted from the [Issues](https://github.com/brotandgames/ciao/issues?utf8=%E2%9C%93&q=) and [Pull Requests](https://github.com/brotandgames/ciao/pulls?utf8=%E2%9C%93&q=) of the [GitHub Repository](https://github.com/brotandgames/ciao). 36 | 37 | > Thanks for making this available, it is a nice replacement for my homebrew Go-based pinger that I have been using. *GitHub_user* 38 | 39 | > First of this is a really great tool. Thank you for creating it. *GitHub_user* 40 | 41 | > Nice project, thanks! *GitHub_user* 42 | 43 | > Very good work! *GitHub_user* 44 | 45 | ## Guestbook via Pull Request 46 | 47 | Open a PR and add something like this: 48 | 49 | `> Your opionion about ciao. (@you)` 50 | 51 | ## WTFAQ? 52 | 53 | **Q1** Guestbook? What's this? 54 | 55 | **A1** Leave you opionion about ciao via a PR - something you missed. 56 | 57 | **Q2** WTF is WTFAQ? 58 | 59 | **A2** A FAQ extension brought to you by @brotandgames. 60 | -------------------------------------------------------------------------------- /helm-chart/ciao-0.5.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brotandgames/ciao/d1b0d7818d1a9620440b14fd79b4fcd01765e865/helm-chart/ciao-0.5.0.tgz -------------------------------------------------------------------------------- /helm-chart/ciao/.gitignore: -------------------------------------------------------------------------------- 1 | values-test.yaml 2 | 3 | -------------------------------------------------------------------------------- /helm-chart/ciao/.helmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brotandgames/ciao/d1b0d7818d1a9620440b14fd79b4fcd01765e865/helm-chart/ciao/.helmignore -------------------------------------------------------------------------------- /helm-chart/ciao/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | name: ciao 3 | description: "Ciao - HTTP checks & tests (private & public) monitoring" 4 | version: 0.5.0 5 | appVersion: "latest" 6 | home: https://brotandgames.com/ciao/ 7 | icon: 8 | keywords: 9 | - ciao 10 | - http 11 | - checks 12 | - monitoring 13 | sources: 14 | - https://github.com/brotandgames/ciao 15 | maintainers: 16 | - name: "Brot & Games" 17 | email: github_username@gmail.com 18 | -------------------------------------------------------------------------------- /helm-chart/ciao/README.md: -------------------------------------------------------------------------------- 1 | # ciao Helm Chart 2 | 3 | **[ciao](https://www.brotandgames.com/ciao/)** - HTTP checks & tests (private & public) monitoring 4 | 5 | ## Development 6 | 7 | https://github.com/brotandgames/ciao/tree/master/helm-chart/ciao 8 | 9 | ## Install 10 | 11 | Check main [README.md](https://github.com/brotandgames/ciao/tree/master/README.md). 12 | 13 | ## Releases 14 | 15 | ### Release 0.5.0 16 | 17 | * Change initialDelaySeconds to 10 for livenessProbe and readinessProbe 18 | * Add support for Ingress apiVersion networking.k8s.io/v1 19 | 20 | ### Release 0.4.0 21 | 22 | * Fix k8s killing pods when basic auth is enabled (@bykvaadm) 23 | 24 | ### Release 0.3.0 25 | 26 | * Add ability to enable/disable and to configure liveness and readiness probes (@bykvaadm) 27 | 28 | Please read documentation for more information about this: 29 | https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#configure-probes 30 | 31 | ### Release 0.2.0 32 | 33 | * Add PVC (@brotandgames) 34 | * Add minimal README.md (@brotandgames) 35 | 36 | ### Release 0.1.0 37 | 38 | * Initial release with deployment, service and ingress (@brotandgames) 39 | 40 | ## Maintainer 41 | 42 | https://github.com/brotandgames 43 | -------------------------------------------------------------------------------- /helm-chart/ciao/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 1. Get the application URL by running these commands: 2 | {{- if .Values.ingress.enabled }} 3 | {{- range $host := .Values.ingress.hosts }} 4 | {{- range .paths }} 5 | http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }} 6 | {{- end }} 7 | {{- end }} 8 | {{- else if contains "NodePort" .Values.service.type }} 9 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "ciao.fullname" . }}) 10 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 11 | echo http://$NODE_IP:$NODE_PORT 12 | {{- else if contains "LoadBalancer" .Values.service.type }} 13 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 14 | You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "ciao.fullname" . }}' 15 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "ciao.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') 16 | echo http://$SERVICE_IP:{{ .Values.service.port }} 17 | {{- else if contains "ClusterIP" .Values.service.type }} 18 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "ciao.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") 19 | echo "Visit http://127.0.0.1:8080 to use your application" 20 | kubectl port-forward $POD_NAME 8080:3000 21 | {{- end }} 22 | -------------------------------------------------------------------------------- /helm-chart/ciao/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "ciao.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | If release name contains chart name it will be used as a full name. 13 | */}} 14 | {{- define "ciao.fullname" -}} 15 | {{- if .Values.fullnameOverride -}} 16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 17 | {{- else -}} 18 | {{- $name := default .Chart.Name .Values.nameOverride -}} 19 | {{- if contains $name .Release.Name -}} 20 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 21 | {{- else -}} 22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 23 | {{- end -}} 24 | {{- end -}} 25 | {{- end -}} 26 | 27 | {{/* 28 | Create chart name and version as used by the chart label. 29 | */}} 30 | {{- define "ciao.chart" -}} 31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 32 | {{- end -}} 33 | 34 | {{/* 35 | Common labels 36 | */}} 37 | {{- define "ciao.labels" -}} 38 | app.kubernetes.io/name: {{ include "ciao.name" . }} 39 | helm.sh/chart: {{ include "ciao.chart" . }} 40 | app.kubernetes.io/instance: {{ .Release.Name }} 41 | {{- if .Chart.AppVersion }} 42 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 43 | {{- end }} 44 | app.kubernetes.io/managed-by: {{ .Release.Service }} 45 | {{- end -}} 46 | 47 | {{- define "BasicAuthSecret" }} 48 | {{- printf "%s:%s" .Values.env.BASIC_AUTH_USERNAME .Values.env.BASIC_AUTH_PASSWORD | b64enc }} 49 | {{- end }} 50 | -------------------------------------------------------------------------------- /helm-chart/ciao/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "ciao.fullname" . }} 5 | labels: 6 | {{ include "ciao.labels" . | indent 4 }} 7 | spec: 8 | replicas: {{ .Values.replicaCount }} 9 | selector: 10 | matchLabels: 11 | app.kubernetes.io/name: {{ include "ciao.name" . }} 12 | app.kubernetes.io/instance: {{ .Release.Name }} 13 | template: 14 | metadata: 15 | labels: 16 | app.kubernetes.io/name: {{ include "ciao.name" . }} 17 | app.kubernetes.io/instance: {{ .Release.Name }} 18 | spec: 19 | {{- with .Values.imagePullSecrets }} 20 | imagePullSecrets: 21 | {{- toYaml . | nindent 8 }} 22 | {{- end }} 23 | containers: 24 | - name: {{ .Chart.Name }} 25 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" 26 | imagePullPolicy: {{ .Values.image.pullPolicy }} 27 | {{- if .Values.persistence.enabled }} 28 | volumeMounts: 29 | - name: sqlite 30 | mountPath: "/app/db/sqlite" 31 | {{- if .Values.persistence.subPath }} 32 | subPath: "{{ .Values.persistence.subPath }}" 33 | {{- end }} 34 | {{- end }} 35 | env: 36 | {{- range $key, $value := .Values.env }} 37 | - name: "{{ $key }}" 38 | value: "{{ $value }}" 39 | {{- end }} 40 | ports: 41 | - name: http 42 | containerPort: 3000 43 | protocol: TCP 44 | {{- if .Values.livenessProbe.enabled }} 45 | livenessProbe: 46 | httpGet: 47 | path: / 48 | port: http 49 | {{- if and .Values.env.BASIC_AUTH_USERNAME .Values.env.BASIC_AUTH_PASSWORD }} 50 | httpHeaders: 51 | - name: Authorization 52 | value: Basic {{ template "BasicAuthSecret" . }} 53 | {{- end }} 54 | initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }} 55 | timeoutSeconds: {{ .Values.livenessProbe.timeoutSeconds }} 56 | periodSeconds: {{ .Values.livenessProbe.periodSeconds }} 57 | failureThreshold: {{ .Values.livenessProbe.failureThreshold }} 58 | successThreshold: {{ .Values.livenessProbe.successThreshold }} 59 | {{- end }} 60 | {{- if .Values.readinessProbe.enabled }} 61 | readinessProbe: 62 | httpGet: 63 | path: / 64 | port: http 65 | {{- if and .Values.env.BASIC_AUTH_USERNAME .Values.env.BASIC_AUTH_PASSWORD }} 66 | httpHeaders: 67 | - name: Authorization 68 | value: Basic {{ template "BasicAuthSecret" . }} 69 | {{- end }} 70 | initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }} 71 | timeoutSeconds: {{ .Values.readinessProbe.timeoutSeconds }} 72 | periodSeconds: {{ .Values.readinessProbe.periodSeconds }} 73 | failureThreshold: {{ .Values.readinessProbe.failureThreshold }} 74 | successThreshold: {{ .Values.readinessProbe.successThreshold }} 75 | {{- end }} 76 | resources: 77 | {{- toYaml .Values.resources | nindent 12 }} 78 | {{- with .Values.nodeSelector }} 79 | nodeSelector: 80 | {{- toYaml . | nindent 8 }} 81 | {{- end }} 82 | {{- with .Values.affinity }} 83 | affinity: 84 | {{- toYaml . | nindent 8 }} 85 | {{- end }} 86 | {{- with .Values.tolerations }} 87 | tolerations: 88 | {{- toYaml . | nindent 8 }} 89 | {{- end }} 90 | {{- if .Values.persistence.enabled }} 91 | volumes: 92 | - name: sqlite 93 | persistentVolumeClaim: 94 | claimName: {{ .Values.persistence.existingClaim | default (include "ciao.fullname" .) }} 95 | {{- end }} 96 | -------------------------------------------------------------------------------- /helm-chart/ciao/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | {{- $fullName := include "ciao.fullname" . -}} 3 | {{- $svcPort := .Values.service.port -}} 4 | {{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} 5 | {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} 6 | {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} 7 | {{- end }} 8 | {{- end }} 9 | {{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} 10 | apiVersion: networking.k8s.io/v1 11 | {{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} 12 | apiVersion: networking.k8s.io/v1beta1 13 | {{- else -}} 14 | apiVersion: extensions/v1beta1 15 | {{- end }} 16 | kind: Ingress 17 | metadata: 18 | name: {{ $fullName }} 19 | labels: 20 | {{- include "ciao.labels" . | nindent 4 }} 21 | {{- with .Values.ingress.annotations }} 22 | annotations: 23 | {{- toYaml . | nindent 4 }} 24 | {{- end }} 25 | spec: 26 | {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} 27 | ingressClassName: {{ .Values.ingress.className }} 28 | {{- end }} 29 | {{- if .Values.ingress.tls }} 30 | tls: 31 | {{- range .Values.ingress.tls }} 32 | - hosts: 33 | {{- range .hosts }} 34 | - {{ . | quote }} 35 | {{- end }} 36 | secretName: {{ .secretName }} 37 | {{- end }} 38 | {{- end }} 39 | rules: 40 | {{- range .Values.ingress.hosts }} 41 | - host: {{ .host | quote }} 42 | http: 43 | paths: 44 | {{- range .paths }} 45 | - path: {{ .path }} 46 | {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} 47 | pathType: {{ .pathType }} 48 | {{- end }} 49 | backend: 50 | {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} 51 | service: 52 | name: {{ $fullName }} 53 | port: 54 | number: {{ $svcPort }} 55 | {{- else }} 56 | serviceName: {{ $fullName }} 57 | servicePort: {{ $svcPort }} 58 | {{- end }} 59 | {{- end }} 60 | {{- end }} 61 | {{- end }} 62 | -------------------------------------------------------------------------------- /helm-chart/ciao/templates/pvc.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) }} 2 | {{- $fullName := include "ciao.fullname" . -}} 3 | apiVersion: v1 4 | kind: PersistentVolumeClaim 5 | metadata: 6 | name: {{ $fullName }} 7 | labels: 8 | {{ include "ciao.labels" . | indent 4 }} 9 | spec: 10 | accessModes: 11 | - {{ .Values.persistence.accessMode | quote }} 12 | resources: 13 | requests: 14 | storage: {{ .Values.persistence.size | quote }} 15 | 16 | {{- if .Values.persistence.storageClass }} 17 | {{- if (eq "-" .Values.persistence.storageClass) }} 18 | storageClassName: "" 19 | {{- else }} 20 | storageClassName: "{{ .Values.persistence.storageClass }}" 21 | {{- end }} 22 | {{- end }} 23 | {{- if .Values.persistence.VolumeName }} 24 | volumeName: "{{ .Values.persistence.VolumeName }}" 25 | {{- end }} 26 | {{- end }} 27 | -------------------------------------------------------------------------------- /helm-chart/ciao/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "ciao.fullname" . }} 5 | labels: 6 | {{ include "ciao.labels" . | indent 4 }} 7 | spec: 8 | type: {{ .Values.service.type }} 9 | ports: 10 | - port: {{ .Values.service.port }} 11 | targetPort: http 12 | protocol: TCP 13 | name: http 14 | selector: 15 | app.kubernetes.io/name: {{ include "ciao.name" . }} 16 | app.kubernetes.io/instance: {{ .Release.Name }} 17 | -------------------------------------------------------------------------------- /helm-chart/ciao/templates/tests/test-connection.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: "{{ include "ciao.fullname" . }}-test-connection" 5 | labels: 6 | {{ include "ciao.labels" . | indent 4 }} 7 | annotations: 8 | "helm.sh/hook": test-success 9 | spec: 10 | containers: 11 | - name: wget 12 | image: busybox 13 | command: ['wget'] 14 | args: ['{{ include "ciao.fullname" . }}:{{ .Values.service.port }}'] 15 | restartPolicy: Never 16 | -------------------------------------------------------------------------------- /helm-chart/ciao/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for ciao. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | # Ciao is not designed to be horizontally scalable 6 | # due to architecture decisions (no external dependencies): 7 | # * in-memory (background) job scheduler: rufus-scheduler 8 | # * internal database: sqlite 9 | # replicaCount must be 1 10 | replicaCount: 1 11 | 12 | image: 13 | repository: brotandgames/ciao 14 | tag: latest 15 | pullPolicy: IfNotPresent 16 | 17 | imagePullSecrets: [] 18 | nameOverride: "" 19 | fullnameOverride: "" 20 | 21 | # All ciao configuration options are adjustable via ENV vars. 22 | # Ref: https://github.com/brotandgames/ciao#configuration 23 | env: {} 24 | # SECRET_KEY_BASE: "sensitive_secret_key_base" 25 | # SMTP_ADDRESS: smtp.yourhost.com 26 | # SMTP_EMAIL_FROM: "ciao@yourhost.com" 27 | # SMTP_EMAIL_TO: "you@yourhost.com" 28 | # SMTP_PORT: 587 29 | # SMTP_AUTHENTICATION: plain 30 | # SMTP_DOMAIN: smtp.yourhost.com 31 | # SMTP_ENABLE_STARTTLS_AUTO: true 32 | # SMTP_USERNAME: ciao 33 | # SMTP_PASSWORD: "sensitive_password" 34 | # 35 | # Check Reference for a full list of configuration variables. 36 | # Mind the ': ' instead of '=' when setting a variable and 37 | # don't forget to remove '{}'. 38 | 39 | # Enable persistence using Persistent Volume Claims 40 | # Ref: http://kubernetes.io/docs/user-guide/persistent-volumes/ 41 | persistence: 42 | enabled: false 43 | 44 | # A manually managed Persistent Volume and Claim 45 | # Requires persistence.enabled: true 46 | # If defined, PVC must be created manually before volume will be bound 47 | # existingClaim: 48 | 49 | # ciao data Persistent Volume Storage Class 50 | # If defined, storageClassName: 51 | # If set to "-", storageClassName: "", which disables dynamic provisioning 52 | # If undefined (the default) or set to null, no storageClassName spec is 53 | # set, choosing the default provisioner. (gp2 on AWS, standard on 54 | # GKE, AWS & OpenStack) 55 | # 56 | # Storage class of PV to bind. By default it looks for standard storage class. 57 | # If the PV uses a different storage class, specify that here. 58 | # storageClass: standard 59 | # VolumeName: "" 60 | accessMode: ReadWriteOnce 61 | size: 1Gi 62 | 63 | # If subPath is set mount a sub folder of a volume instead of the root of the volume. 64 | # This is especially handy for volume plugins that don't natively support sub mounting (like glusterfs). 65 | subPath: "" 66 | 67 | service: 68 | type: ClusterIP 69 | port: 80 70 | 71 | ingress: 72 | enabled: false 73 | className: "" 74 | annotations: {} 75 | # kubernetes.io/ingress.class: nginx 76 | # kubernetes.io/tls-acme: "true" 77 | hosts: 78 | - host: ciao.local 79 | paths: 80 | - path: / 81 | pathType: ImplementationSpecific 82 | tls: [] 83 | # - secretName: ciao-ingress-tls 84 | # hosts: 85 | # - ciao.local 86 | 87 | resources: {} 88 | # We usually recommend not to specify default resources and to leave this as a conscious 89 | # choice for the user. This also increases chances charts run on environments with little 90 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 91 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 92 | # limits: 93 | # cpu: 100m 94 | # memory: 128Mi 95 | # requests: 96 | # cpu: 100m 97 | # memory: 128Mi 98 | 99 | nodeSelector: {} 100 | 101 | tolerations: [] 102 | 103 | affinity: {} 104 | 105 | livenessProbe: 106 | enabled: true 107 | initialDelaySeconds: 10 108 | periodSeconds: 15 109 | successThreshold: 1 110 | failureThreshold: 4 111 | timeoutSeconds: 10 112 | readinessProbe: 113 | enabled: true 114 | initialDelaySeconds: 10 115 | periodSeconds: 15 116 | successThreshold: 1 117 | failureThreshold: 4 118 | timeoutSeconds: 10 119 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brotandgames/ciao/d1b0d7818d1a9620440b14fd79b4fcd01765e865/lib/assets/.keep -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brotandgames/ciao/d1b0d7818d1a9620440b14fd79b4fcd01765e865/lib/tasks/.keep -------------------------------------------------------------------------------- /lib/templates/erb/scaffold/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%# frozen_string_literal: true %> 2 | <%%= simple_form_for(@<%= singular_table_name %>) do |f| %> 3 | <%%= f.error_notification %> 4 | <%%= f.error_notification message: f.object.errors[:base].to_sentence if f.object.errors[:base].present? %> 5 | 6 |
7 | <%- attributes.each do |attribute| -%> 8 | <%%= f.<%= attribute.reference? ? :association : :input %> :<%= attribute.name %> %> 9 | <%- end -%> 10 |
11 | 12 |
13 | <%%= f.button :submit %> 14 |
15 | <%% end %> 16 | -------------------------------------------------------------------------------- /log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brotandgames/ciao/d1b0d7818d1a9620440b14fd79b4fcd01765e865/log/.keep -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ciao", 3 | "private": true, 4 | "dependencies": { 5 | "@rails/actioncable": "^6.1.5", 6 | "@rails/activestorage": "^6.1.5", 7 | "@rails/ujs": "^6.1.5", 8 | "turbolinks": "^5.2.0", 9 | "webpack": "^5.94.0", 10 | "webpack-cli": "^4.9.2" 11 | }, 12 | "version": "0.1.0", 13 | "devDependencies": {}, 14 | "scripts": { 15 | "build": "webpack --config ./config/webpack/webpack.config.js" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('postcss-import'), 4 | require('postcss-flexbugs-fixes'), 5 | require('postcss-preset-env')({ 6 | autoprefixer: { 7 | flexbox: 'no-2009' 8 | }, 9 | stage: 3 10 | }) 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

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

63 |
64 |

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

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

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

63 |
64 |

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

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

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

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brotandgames/ciao/d1b0d7818d1a9620440b14fd79b4fcd01765e865/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brotandgames/ciao/d1b0d7818d1a9620440b14fd79b4fcd01765e865/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brotandgames/ciao/d1b0d7818d1a9620440b14fd79b4fcd01765e865/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brotandgames/ciao/d1b0d7818d1a9620440b14fd79b4fcd01765e865/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brotandgames/ciao/d1b0d7818d1a9620440b14fd79b4fcd01765e865/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brotandgames/ciao/d1b0d7818d1a9620440b14fd79b4fcd01765e865/public/favicon.ico -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /public/version: -------------------------------------------------------------------------------- 1 | 1.9.6 2 | -------------------------------------------------------------------------------- /scripts/chart/cd.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | . ./ci.sh 4 | 5 | cd_dependencies=mc 6 | 7 | CHART_HELM_REPO=https://releases.brotandgames.com/helm-charts 8 | 9 | # Check dependencies 10 | for d in $(echo $cd_dependencies | tr "," "\n"); do 11 | command -v $d >/dev/null 2>&1 || { echo >&2 "== $self requires $d but it's not installed."; exit 1; } 12 | done 13 | 14 | echo "== $self Get current index.yaml from $CHART_HELM_REPO" 15 | curl -f -H 'Cache-Control: max-age=0,no-cache' "$CHART_HELM_REPO/index.yaml" -o /tmp/chart/repo_index.yaml || true 16 | 17 | if [ -f /tmp/chart/repo_index.yaml ]; then 18 | echo "== $self Update index.yaml" 19 | helm repo index --merge /tmp/chart/repo_index.yaml /tmp/chart 20 | rm /tmp/chart/repo_index.yaml 21 | else 22 | echo "== $self Create index.yaml" 23 | helm repo index /tmp/chart 24 | fi 25 | 26 | echo "== $self Upload index.yaml and *.tgz to $CHART_HELM_REPO" 27 | rm -rf /tmp/chart/$CHART_NAME 28 | mc cp -r /tmp/chart/ brotandgames/helm-charts/ 29 | -------------------------------------------------------------------------------- /scripts/chart/ci.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | self=${0##*/} 6 | ci_dependencies=helm 7 | 8 | echo "== $self" 9 | 10 | # Check dependencies 11 | for d in $(echo $ci_dependencies | tr "," "\n"); do 12 | command -v $d >/dev/null 2>&1 || { echo >&2 "== $self requires $d but it's not installed."; exit 1; } 13 | done 14 | 15 | CHART_NAME=ciao 16 | 17 | cd $(dirname $0)/.. 18 | 19 | rm -rf /tmp/chart 20 | mkdir -p /tmp/chart/ 21 | cp -Rf ../helm-chart/ciao /tmp/chart/$CHART_NAME 22 | 23 | echo "== $self Chart validate" 24 | helm lint /tmp/chart/$CHART_NAME 25 | 26 | echo "== $self Chart package" 27 | helm package -d /tmp/chart /tmp/chart/$CHART_NAME 28 | -------------------------------------------------------------------------------- /smtp_configuration.md: -------------------------------------------------------------------------------- 1 | # SMTP Configuration 2 | 3 | Following is a list of all possible configuration variables. 4 | 5 | ```` 6 | SMTP_ADDRESS=smtp.yourhost.com 7 | SMTP_EMAIL_FROM="ciao@yourhost.com" 8 | SMTP_EMAIL_TO="you@yourhost.com" 9 | SMTP_PORT=587 10 | SMTP_DOMAIN=smtp.yourhost.com 11 | SMTP_AUTHENTICATION=plain 12 | SMTP_ENABLE_STARTTLS_AUTO=true 13 | SMTP_USERNAME=ciao 14 | SMTP_PASSWORD="sensitive_password" 15 | SMTP_SSL=true 16 | ```` 17 | 18 | > New as of 1.9.3 19 | 20 | ``` 21 | # Disable TLS expiration notifications via E-Mail for all checks 22 | SMTP_DISABLE_TLS_EXPIRES_NOTIFICATIONS=true 23 | ``` 24 | 25 | ## Notes 26 | 27 | * When `SMTP_ADDRESS` variable is not set no e-mail notifications are sent 28 | * You can send emails to several addresses by separating them with a comma eg. `SMTP_EMAIL_TO="a@yourhost.com,b@yourhost.com"` 29 | 30 | ## Example configurations 31 | 32 | ### Gmail 33 | 34 | Don’t forget to change `you@gmail.com` to your email address and `sensitive_password` to your own password. 35 | 36 | If you encounter authentication errors, ensure you have allowed less secure apps to access the account. 37 | 38 | ```` 39 | SMTP_ADDRESS=smtp.gmail.com 40 | SMTP_EMAIL_FROM="you@gmail.com" 41 | SMTP_EMAIL_TO="you@yourhost.com" 42 | SMTP_PORT=587 43 | SMTP_DOMAIN=smtp.yourhost.com 44 | SMTP_AUTHENTICATION=plain 45 | SMTP_ENABLE_STARTTLS_AUTO=true 46 | SMTP_USERNAME="you@gmail.com" 47 | SMTP_PASSWORD="sensitive_password" 48 | ```` 49 | 50 | ### Sendgrid 51 | 52 | Port 587 (STARTTLS) 53 | 54 | ```` 55 | SMTP_ADDRESS=smtp.sendgrid.net 56 | SMTP_EMAIL_FROM="ciao@yourhost.com" 57 | SMTP_EMAIL_TO="you@yourhost.com" 58 | SMTP_PORT=587 59 | SMTP_DOMAIN=smtp.yourhost.com 60 | SMTP_AUTHENTICATION=plain 61 | SMTP_ENABLE_STARTTLS_AUTO=true 62 | SMTP_USERNAME=ciao 63 | SMTP_PASSWORD="sensitive_password" 64 | ```` 65 | 66 | Port 465 (explicit SSL/TLS) 67 | 68 | ```` 69 | SMTP_ADDRESS=smtp.sendgrid.net 70 | SMTP_EMAIL_FROM="ciao@yourhost.com" 71 | SMTP_EMAIL_TO="you@yourhost.com" 72 | SMTP_PORT=465 73 | SMTP_DOMAIN=smtp.yourhost.com 74 | SMTP_AUTHENTICATION=plain 75 | SMTP_ENABLE_STARTTLS_AUTO=true 76 | SMTP_USERNAME=ciao 77 | SMTP_PASSWORD="sensitive_password" 78 | SMTP_SSL=true 79 | ```` 80 | 81 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -euo pipefail 4 | 5 | self="$(basename "$0")" 6 | 7 | if [ -z "${SECRET_KEY_BASE:-}" ]; then 8 | echo "== $self WARNING: SECRET_KEY_BASE not set" 9 | echo "== $self It will be set to a random value using \`rake secret\`" 10 | export SECRET_KEY_BASE="$(rake secret)" 11 | fi 12 | 13 | rake db:migrate 14 | 15 | # Remove a potentially pre-existing server.pid for Rails. 16 | rm -f /app/tmp/pids/server.pid 17 | 18 | exec rails server -b 0.0.0.0 -p 3000 19 | -------------------------------------------------------------------------------- /storage/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brotandgames/ciao/d1b0d7818d1a9620440b14fd79b4fcd01765e865/storage/.keep -------------------------------------------------------------------------------- /test/application_system_test_case.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class ApplicationSystemTestCase < ActionDispatch::SystemTestCase 6 | driven_by :selenium, using: :chrome, screen_size: [1400, 1400] 7 | end 8 | -------------------------------------------------------------------------------- /test/channels/application_cable/connection_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module ApplicationCable 6 | class ConnectionTest < ActionCable::Connection::TestCase 7 | # test "connects with cookies" do 8 | # cookies.signed[:user_id] = 42 9 | # 10 | # connect 11 | # 12 | # assert_equal connection.user_id, "42" 13 | # end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/controllers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brotandgames/ciao/d1b0d7818d1a9620440b14fd79b4fcd01765e865/test/controllers/.keep -------------------------------------------------------------------------------- /test/controllers/checks_controller_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class ChecksControllerTest < ActionDispatch::IntegrationTest 6 | setup do 7 | @check = checks(:one) 8 | end 9 | 10 | test "should get index" do 11 | get checks_url 12 | assert_response :success 13 | end 14 | 15 | test "should get new" do 16 | get new_check_url 17 | assert_response :success 18 | end 19 | 20 | test "should create check" do 21 | assert_difference("Check.count") do 22 | post checks_url, params: { 23 | check: { 24 | cron: @check.cron, 25 | url: @check.url, 26 | name: @check.name 27 | } 28 | } 29 | end 30 | 31 | assert_redirected_to check_url(Check.last) 32 | end 33 | 34 | test "should show check" do 35 | get check_url(@check) 36 | assert_response :success 37 | end 38 | 39 | test "should get edit" do 40 | get edit_check_url(@check) 41 | assert_response :success 42 | end 43 | 44 | test "should update check" do 45 | patch check_url(@check), params: { 46 | check: { 47 | cron: @check.cron, 48 | url: @check.url, 49 | name: @check.name 50 | } 51 | } 52 | assert_redirected_to check_url(@check) 53 | end 54 | 55 | test "should destroy check" do 56 | assert_difference("Check.count", -1) do 57 | delete check_url(@check) 58 | end 59 | 60 | assert_redirected_to checks_url 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /test/fixtures/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brotandgames/ciao/d1b0d7818d1a9620440b14fd79b4fcd01765e865/test/fixtures/.keep -------------------------------------------------------------------------------- /test/fixtures/checks.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | name: Brot and Games 5 | url: https://brotandgames.com 6 | cron: "* * * * *" 7 | 8 | two: 9 | name: kubernete.sh 10 | url: https://kubernete.ch 11 | cron: "*/15 * * * *" 12 | -------------------------------------------------------------------------------- /test/fixtures/files/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brotandgames/ciao/d1b0d7818d1a9620440b14fd79b4fcd01765e865/test/fixtures/files/.keep -------------------------------------------------------------------------------- /test/fixtures/status_changes.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | status: MyString 5 | check: one 6 | 7 | two: 8 | status: MyString 9 | check: two 10 | -------------------------------------------------------------------------------- /test/helpers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brotandgames/ciao/d1b0d7818d1a9620440b14fd79b4fcd01765e865/test/helpers/.keep -------------------------------------------------------------------------------- /test/integration/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brotandgames/ciao/d1b0d7818d1a9620440b14fd79b4fcd01765e865/test/integration/.keep -------------------------------------------------------------------------------- /test/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brotandgames/ciao/d1b0d7818d1a9620440b14fd79b4fcd01765e865/test/mailers/.keep -------------------------------------------------------------------------------- /test/mailers/check_mailer_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class CheckMailerTest < ActionMailer::TestCase 6 | # test "the truth" do 7 | # assert true 8 | # end 9 | end 10 | -------------------------------------------------------------------------------- /test/mailers/previews/check_mailer_preview.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Preview all emails at http://localhost:3000/rails/mailers/check_mailer 4 | class CheckMailerPreview < ActionMailer::Preview 5 | end 6 | -------------------------------------------------------------------------------- /test/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brotandgames/ciao/d1b0d7818d1a9620440b14fd79b4fcd01765e865/test/models/.keep -------------------------------------------------------------------------------- /test/models/check_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class CheckTest < ActiveSupport::TestCase 6 | # test "the truth" do 7 | # assert true 8 | # end 9 | end 10 | -------------------------------------------------------------------------------- /test/models/status_change_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class StatusChangeTest < ActiveSupport::TestCase 6 | # test "the truth" do 7 | # assert true 8 | # end 9 | end 10 | -------------------------------------------------------------------------------- /test/system/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brotandgames/ciao/d1b0d7818d1a9620440b14fd79b4fcd01765e865/test/system/.keep -------------------------------------------------------------------------------- /test/system/checks_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "application_system_test_case" 4 | 5 | class ChecksTest < ApplicationSystemTestCase 6 | setup do 7 | @check = checks(:one) 8 | end 9 | 10 | test "visiting the index" do 11 | visit checks_url 12 | assert_selector "h1", text: "Checks" 13 | end 14 | 15 | test "creating a Check" do 16 | visit checks_url 17 | click_on "New Check" 18 | 19 | fill_in "Name", with: @check.name 20 | fill_in "Url", with: @check.url 21 | fill_in "Cron", with: @check.cron 22 | click_on "Create Check" 23 | 24 | assert_text "Check was successfully created" 25 | click_on "Back" 26 | end 27 | 28 | test "updating a Check" do 29 | visit checks_url 30 | click_on "Edit", match: :first 31 | 32 | fill_in "Name", with: @check.name 33 | fill_in "Url", with: @check.url 34 | fill_in "Cron", with: @check.cron 35 | click_on "Update Check" 36 | 37 | assert_text "Check was successfully updated" 38 | click_on "Back" 39 | end 40 | 41 | test "destroying a Check" do 42 | visit checks_url 43 | page.accept_confirm do 44 | click_on "Destroy", match: :first 45 | end 46 | 47 | assert_text "Check was successfully destroyed" 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ENV["RAILS_ENV"] ||= "test" 4 | require_relative "../config/environment" 5 | require "rails/test_help" 6 | require "mocha/minitest" 7 | require "webmock/minitest" 8 | 9 | module ActiveSupport 10 | class TestCase 11 | # Run tests in parallel with specified workers 12 | parallelize(workers: :number_of_processors) 13 | 14 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. 15 | fixtures :all 16 | 17 | # Add more helper methods to be used by all tests here... 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/unit/ciao/notifications/webhook_notification_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module Ciao 6 | module Notifications 7 | class WebhookNotificationTest < ActiveSupport::TestCase 8 | test "#initialize assigns @endpoint & @payload_renderer" do 9 | notification = Ciao::Notifications::WebhookNotification.new( 10 | "https://foo.bar", 11 | '{"foo": "bar"}', 12 | Ciao::Renderers::ReplaceRenderer 13 | ) 14 | assert_equal "https://foo.bar", 15 | notification.instance_variable_get(:@endpoint) 16 | assert_instance_of Ciao::Renderers::ReplaceRenderer, 17 | notification.instance_variable_get(:@payload_renderer) 18 | end 19 | 20 | test "#notify" do 21 | stub_request(:post, "https://foo.bar/").with( 22 | body: '{"name": "bar"}', 23 | headers: { 24 | "Accept" => "*/*", 25 | "Accept-Encoding" => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3", 26 | "Content-Type" => "application/json", 27 | "User-Agent" => "Ruby" 28 | } 29 | ).to_return(status: 200, body: '{"status":"success"}', headers: {}) 30 | 31 | notification = Ciao::Notifications::WebhookNotification.new( 32 | "https://foo.bar", 33 | '{"name": "__name__"}', 34 | Ciao::Renderers::ReplaceRenderer 35 | ) 36 | response = notification.notify(name: "bar") 37 | assert_equal '{"status":"success"}', response.body 38 | assert_equal "200", response.code 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test/unit/ciao/parsers/webhook_parser_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module Ciao 6 | module Parsers 7 | class WebhookParserTest < ActiveSupport::TestCase 8 | test "self.matches" do 9 | ENV["CIAO_WEBHOOK_ENDPOINT_1"] = "https://foo.bar" 10 | assert_equal "1", Ciao::Parsers::WebhookParser.matches.first[:name] 11 | end 12 | 13 | test "self.names" do 14 | Ciao::Parsers::WebhookParser.expects(:matches).returns([stub(:[] => "1")]) 15 | assert_equal ["1"], Ciao::Parsers::WebhookParser.names 16 | end 17 | 18 | test "self.webhooks" do 19 | ENV["CIAO_WEBHOOK_ENDPOINT_1"] = "https://foo.bar" 20 | ENV["CIAO_WEBHOOK_PAYLOAD_1"] = '{"foo":"bar"}' 21 | ENV["CIAO_WEBHOOK_PAYLOAD_TLS_EXPIRES_1"] = '{"foo":"bar"}' 22 | assert_equal [{ 23 | endpoint: "https://foo.bar", 24 | payload: '{"foo":"bar"}', 25 | payload_tls_expires: '{"foo":"bar"}' 26 | }], Ciao::Parsers::WebhookParser.webhooks 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/unit/ciao/renderers/replace_renderer_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module Ciao 6 | module Renderers 7 | class ReplaceRendererTest < ActiveSupport::TestCase 8 | test "#initialize assigns @template" do 9 | renderer = ReplaceRenderer.new('{"name": "__name__"}') 10 | assert_equal '{"name": "__name__"}', 11 | renderer.instance_variable_get(:@template) 12 | end 13 | 14 | test "#render replaces webhook placeholders" do 15 | renderer = ReplaceRenderer.new( 16 | '{"name": "__name__", "status_after":"__status_after__", "status_before":"__status_before__"}' 17 | ) 18 | assert_equal '{"name": "foo", "status_after":"500", "status_before":"200"}', 19 | renderer.render(name: "foo", status_after: "500", status_before: "200") 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brotandgames/ciao/d1b0d7818d1a9620440b14fd79b4fcd01765e865/tmp/.keep -------------------------------------------------------------------------------- /vendor/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brotandgames/ciao/d1b0d7818d1a9620440b14fd79b4fcd01765e865/vendor/.keep -------------------------------------------------------------------------------- /webhook_configuration.md: -------------------------------------------------------------------------------- 1 | # Webhook Configuration 2 | 3 | > New as of 1.4.0 4 | 5 | You can configure as many webhooks as you like. Each webhook consists of 2 ENV variables: 6 | 7 | * `CIAO_WEBHOOK_ENDPOINT_$NAME` 8 | * `CIAO_WEBHOOK_PAYLOAD_$NAME` 9 | 10 | `$NAME` can be any word `[A-Z0-9_]` and must be unique as it is used as an identifier. 11 | 12 | like: 13 | 14 | ``` 15 | # Webhook for Rocket.Chat 16 | CIAO_WEBHOOK_ENDPOINT_ROCKETCHAT="https://webhook.rocketchat.com/***/***" 17 | CIAO_WEBHOOK_PAYLOAD_ROCKETCHAT='{"text":"[ciao] __name__: Status changed (__status_after__)"}' 18 | 19 | # Webhook for Slack 20 | CIAO_WEBHOOK_ENDPOINT_SLACK="https://webhook.slack.com/***/***" 21 | CIAO_WEBHOOK_PAYLOAD_SLACK='{"text":"[ciao] __name__: Status changed (__status_after__)"}' 22 | 23 | etc. 24 | ``` 25 | 26 | There are 5 placeholders which you can use in the payload: 27 | 28 | * `__name__` 29 | * `__url__` 30 | * `__status_before__` 31 | * `__status_after__` 32 | * `__check_url__` 33 | 34 | ENV variable `CIAO_WEBHOOK_PAYLOAD_$NAME` has to be a valid JSON one-liner wrapped in single quotes like `'{"name":"__name__", "status_before":"__status_before__", "status_after":"__status_after__", "check_url":"__check_url__", "url":"__url__"}'` 35 | 36 | > New as of 1.9.0 37 | 38 | You can configure webhooks for TLS certificate expiration with one additional ENV variable: 39 | 40 | * `CIAO_WEBHOOK_PAYLOAD_TLS_EXPIRES_$NAME` 41 | 42 | like: 43 | 44 | ``` 45 | # Webhook payload for TLS certificate expiration for Rocket.Chat 46 | CIAO_WEBHOOK_PAYLOAD_TLS_EXPIRES_ROCKETCHAT='{"text": "[ciao] TLS certificate for __name__ expires at __tls_expires_at__ (in __tls_expires_in_days__ days)"}' 47 | 48 | 49 | # Webhook payload for TLS certificate expiration for Slack 50 | CIAO_WEBHOOK_PAYLOAD_TLS_EXPIRES_SLACK='{"text": "[ciao] TLS certificate for __name__ expires at __tls_expires_at__ (in __tls_expires_in_days__ days)"}' 51 | 52 | etc. 53 | ``` 54 | 55 | There are 5 placeholders which you can use in the payload for TLS certificate expiration: 56 | 57 | * `__name__` 58 | * `__url__` 59 | * `__tls_expires_at__` 60 | * `__tls_expires_in_days__` 61 | * `__check_url__` 62 | 63 | ## Notes 64 | 65 | * If you are using `docker-compose`, you have to omit the outer `""` and `''` in `*_ENDPOINT_*` and `*_PAYLOAD_*` - take a look at these GitHub issues ([1](https://github.com/brotandgames/ciao/issues/40), [2](https://github.com/docker/compose/issues/2854)) and these Stack Overflow questions ([1](https://stackoverflow.com/questions/53082932/yaml-docker-compose-spaces-quotes), [2](https://stackoverflow.com/questions/41988809/docker-compose-how-to-escape-environment-variables)) 66 | * You can add an Example configuration for a Service that's missing in the list via PR 67 | 68 | ## Example configurations 69 | 70 | ### Rocket.Chat 71 | 72 | ```` 73 | # Endpoint 74 | CIAO_WEBHOOK_ENDPOINT_ROCKETCHAT="https://chat.yourchat.net/hooks/****/****" 75 | 76 | # Payload 77 | CIAO_WEBHOOK_PAYLOAD_ROCKETCHAT='{"username":"Brot & Games","icon_url":"https://avatars0.githubusercontent.com/u/43862266?s=400&v=4","text":"[ciao] __name__: Status changed (__status_after__)"}' 78 | ```` 79 | 80 | ### Slack 81 | 82 | ```` 83 | # Endpoint 84 | - 'CIAO_WEBHOOK_ENDPOINT_SLACK=https://webhook.slack.com/***/***' 85 | OR 86 | - CIAO_WEBHOOK_ENDPOINT_SLACK=https://webhook.slack.com/***/*** 87 | 88 | # Payload 89 | - 'CIAO_WEBHOOK_PAYLOAD_SLACK={"text":"[ciao] __name__: Status changed (__status_after__)"}' 90 | ```` 91 | 92 | ### Office 365 Connector 93 | 94 | ``` 95 | # https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/connectors/connectors-using 96 | 97 | # Endpoint 98 | CIAO_WEBHOOK_ENDPOINT_OFFICE_365="https://outlook.office.com/webhook/****/IncomingWebhook/****/****" 99 | 100 | # Payload 101 | CIAO_WEBHOOK_PAYLOAD_OFFICE_365='{ "@context": "https://schema.org/extensions", "@type": "MessageCard", "themeColor": "0072C6", "title": "MySystem (__name__) status change", "text": "Status changed from (__status_before__) to (__status_after__)", "potentialAction": [ { "@type": "OpenUri", "name": "Learn More", "targets": [ { "os": "default", "uri": "__check_url__" } ] } ] }' 102 | ``` 103 | 104 | ### Telegram 105 | ``` 106 | # Endpoint 107 | CIAO_WEBHOOK_ENDPOINT_TELEGRAM="https://api.telegram.org/bot****/sendMessage" 108 | 109 | # Payload 110 | CIAO_WEBHOOK_PAYLOAD_TELEGRAM='{ "chat_id": ****, "disable_web_page_preview":1, "text": "[__name__] Status changed from (__status_before__) to (__status_after__)"}' 111 | ``` 112 | --------------------------------------------------------------------------------