From bac82b7ed5b30230b21b13dfc457b44f0450250f Mon Sep 17 00:00:00 2001 From: Jeff Ohrstrom Date: Wed, 18 Sep 2024 09:51:40 -0400 Subject: [PATCH] Starter ruby app (#999) * rename this file * refactor ruby tutorial to be a starter app. --- source/release-notes/v1.8-release-notes.rst | 2 +- source/tutorials/tutorials-passenger-apps.rst | 2 +- .../deploy-to-production.inc | 52 ++ .../nodejs-starter-app.rst | 2 + .../tutorials-passenger-apps/ps-to-quota.rst | 482 ------------------ .../python-starter-app.rst | 2 + .../ruby-starter-app.rst | 87 ++++ 7 files changed, 145 insertions(+), 484 deletions(-) create mode 100644 source/tutorials/tutorials-passenger-apps/deploy-to-production.inc delete mode 100644 source/tutorials/tutorials-passenger-apps/ps-to-quota.rst create mode 100644 source/tutorials/tutorials-passenger-apps/ruby-starter-app.rst diff --git a/source/release-notes/v1.8-release-notes.rst b/source/release-notes/v1.8-release-notes.rst index 6cac24fde..773494f93 100644 --- a/source/release-notes/v1.8-release-notes.rst +++ b/source/release-notes/v1.8-release-notes.rst @@ -268,7 +268,7 @@ Added Sinatra Ruby Gems into ondemand-gems for other apps to use ................................................................ Related Ruby gems for the micro-webframework "Sinatra" were added to the Dashboard Gemfile to ensure they are included in the ``ondemand-gems`` RPM. -Simple applications like the Passenger status app featured in the :ref:`Passenger app development tutorial ` can now be run without needing to install gem dependencies into the application directory. The following gems were added for this purpose: +Simple applications like the Passenger status app featured in the :ref:`Ruby Starter application ` can now be run without needing to install gem dependencies into the application directory. The following gems were added for this purpose: .. code-block:: ruby diff --git a/source/tutorials/tutorials-passenger-apps.rst b/source/tutorials/tutorials-passenger-apps.rst index 2bb8d1a54..e5c80897d 100644 --- a/source/tutorials/tutorials-passenger-apps.rst +++ b/source/tutorials/tutorials-passenger-apps.rst @@ -11,6 +11,6 @@ At the bottom of the page is a list of tutorials for developing Passenger apps f :maxdepth: 2 :caption: Tutorials - tutorials-passenger-apps/ps-to-quota + tutorials-passenger-apps/ruby-starter-app tutorials-passenger-apps/nodejs-starter-app tutorials-passenger-apps/python-starter-app \ No newline at end of file diff --git a/source/tutorials/tutorials-passenger-apps/deploy-to-production.inc b/source/tutorials/tutorials-passenger-apps/deploy-to-production.inc new file mode 100644 index 000000000..2463ec0b2 --- /dev/null +++ b/source/tutorials/tutorials-passenger-apps/deploy-to-production.inc @@ -0,0 +1,52 @@ +Publish App +----------- + +Publishing an app requires two steps: + +#. Updating the ``manifest.yml`` to specify the category and optionally subcategory, which indicates where in the dashboard menu the app appears. + +#. Having an administrator checkout a copy of the production version to a directory under ``/var/www/ood/apps/sys``. + + +Steps: + +#. Add category to manifest so the app appears in the Files menu: + + .. code-block:: diff + + name: Quota + description: Display quotas + icon: fa://hdd-o + +category: Files + +subcategory: Utilities + +#. Version these changes. Click **Shell** button on app details view, and then ``commit`` the changes: + + .. code-block:: sh + + git add . + git commit -m "update manifest for production" + + # if there is an external remote associated with this, push to that + git push origin master + +#. As the admin, ``sudo copy`` or ``git clone`` this repo to production + + .. code-block:: sh + + # as sudo on OnDemand host: + cd /var/www/ood/apps/sys + git clone /users/PZS0562/efranz/ondemand/dev/quota + + +#. **Reload** the dashboard. + +.. figure:: /images/app-dev-tutorial-ps-to-quota-published.png + :align: center + + Every user can now launch the Quota from the Files menu. + +.. warning:: + + Accessing this new app for the first time will cause your NGINX server to restart, + killing all websocket connections, which means resetting your active web-based OnDemand Shell sessions. \ No newline at end of file diff --git a/source/tutorials/tutorials-passenger-apps/nodejs-starter-app.rst b/source/tutorials/tutorials-passenger-apps/nodejs-starter-app.rst index f6ea4c409..3f4999883 100644 --- a/source/tutorials/tutorials-passenger-apps/nodejs-starter-app.rst +++ b/source/tutorials/tutorials-passenger-apps/nodejs-starter-app.rst @@ -91,5 +91,7 @@ There you should see this application at the top of the list. Clicking When the new tab opens you should see a blank page with the text ``Hello World``. This is your new `NodeJs`_ application! +.. include:: deploy-to-production.inc + .. _NodeJs: https://nodejs.org/en .. _Express: https://expressjs.com/ diff --git a/source/tutorials/tutorials-passenger-apps/ps-to-quota.rst b/source/tutorials/tutorials-passenger-apps/ps-to-quota.rst deleted file mode 100644 index db570cfc4..000000000 --- a/source/tutorials/tutorials-passenger-apps/ps-to-quota.rst +++ /dev/null @@ -1,482 +0,0 @@ -.. _app-development-tutorials-passenger-apps-ps-to-quota: - -Creating a Status App -===================== - -Overview of App ---------------- - -We will make a copy of a status app that displays the running Passenger -processes on the OnDemand host. We will use this as a starting point to -create a new status app that displays quota information in a table. - -The app we will be copying is: https://github.com/OSC/ood-example-ps. Running -this app looks like: - -.. figure:: /images/app-dev-tutorial-ps-to-quota-1.png - :align: center - - What app looks like after cloning and launching. - -After this tutorial the resulting app will be: - -.. figure:: /images/app-dev-tutorial-ps-to-quota-2.png - :align: center - - What app looks like after modifying in this tutorial. - -This assumes you have followed the directions to :ref:`enabling-development-mode` on the -Dashboard. - -#. The app uses the custom branded Bootstrap 3 that Job Composer and Active Jobs apps - use. -#. The navbar contains a link back to the dashboard. -#. On a request, the app runs a shell command, parses the output, and displays - the result in a table. -#. It is built in Ruby using the `Sinatra framework `__, a lightweight web framework - similar to `Python's Flask `__ and `Node.js's Express `__ - - -Benefits -........ - -This serves as a good starting point for any status app to build for OnDemand, -because - -#. the app has the branding matching other OnDemand apps -#. all status apps will do something similar on a request to the app: - - #. get raw data from a shell command or http request - #. parse the raw data into an intermediate object representation - #. use that intermediate object representation to display the data formatted - as a table or graph - -#. the app can be deployed without requiring a build step because gem - dependencies (specified in ``Gemfile`` and ``Gemfile.lock``) are pure ruby and - match those that are provided by the ondemand-gems rpm -#. most of the app can be modified without requiring a restart due to proper use - of Sinatra reloader extension -#. app has a built in scaffold for unit testing using minitest - - -OnDemand System Gems -.................... - -This app is able to run in OnDemand 1.8+ without installing the gems specified in the ``Gemfile``. - -All pre-installed Ruby gems used by OnDemand are available to make it easier to develop simple apps. -These include gems used by this example app: - -- ``sinatra`` -- ``sinatra-contrib`` -- ``erubi`` - -On the OnDemand web host, you can execute the command ``source scl_source enable ondemand`` and then ``gem list`` to -see all available gems. These gems are provided by a separate ``ondemand-gems`` rpm that is installed when -you do ``yum install ondemand``. The name of the RPM includes the OnDemand release version, such -as ``ondemand-gems-1.7.12-1.7.12-1.el7.x86_64.rpm``. This ensures that if you do ``yum update`` this gem will -not be removed - so apps can depend on the presence of these gems. - - - -Files and Their Purpose -....................... - -.. list-table:: Main files - :header-rows: 1 - - * - File - - Description - * - ``config.ru`` - - entry point of the Passenger Ruby app - * - ``app.rb`` - - Sinatra app config and routes; this in a separate file from ``config.ru`` so - that code reloading will work - * - ``command.rb`` - - class that defines an AppProcess struct, executes ``ps``, and parses the - output of the ps command producing an array of structs - * - ``test/test_command.rb`` - - a unit test of the parsing code - * - ``views/index.html`` - - the main section of the html page template using an implementation of `ERB `__ - called `erubi `__ - which auto-escapes output of ERB tags by default (for security) - * - ``views/layout.html`` - - the rendered HTML from ``views/index.html`` is inserted into this layout, - where css and javascript files are included - -.. list-table:: Other files - :header-rows: 1 - - * - File - - Description - * - ``Gemfile``, ``Gemfile.lock`` - - defines gem dependencies for the app (see `Bundler's Rationale `__) - * - ``tmp/`` - - tmp directory is kept so its easier to ``touch tmp/restart.txt`` when you - want to force Passenger to restart an app - * - ``public/`` - - serve up static assets like Bootstrap css; in OnDemand, NGINX auto-serves - all files under public/ directly, without going through the Passenger - process, which makes this much faster; as a result, each static file is - in a directory with an explicit version number, so if these files ever - change we change the version, which is one cache busting strategy - * - ``Rakefile`` - - this provides a default ``rake`` task for running the automated tests under - ``test/``, so you can run the tests by running the command ``rake`` - * - ``test/minitest_helper.rb`` - - contains setup code common between all tests - * - ``vendor/bundle`` - - This directory is added if you execute ``bin/bundle install --path vendor/bundle`` to store app specific gems. This is necessary if you want to add gems or specify specific gem versions used by the app that deviate from those provided by system gemset, or if you are using OnDemand 1.7 or earlier. - -Clone and Setup ---------------- - -#. Login to Open OnDemand, click "Develop" dropdown menu and click the "My Sandbox Apps (Development)" option. -#. Click "New App" and "Clone Existing App". -#. Fill out the form: - - #. Directory name: ``quota`` - #. Git remote: ``https://github.com/OSC/ood-example-ps`` - #. Check *"Create new Git Project from this?"* - #. Click **Submit** - -#. Launch the app by clicking the large blue **Launch** button. In a new browser - window/tab you will see the output of a ``ps`` command filtered using ``grep``. - -#. Switch browser tab/windows back to the dashboard Details view of the app and - click the **Files** button on the right to open the app's directory in the File - Explorer. - - -Edit to Run and Parse Quota ---------------------------- - -The app runs and parses this command: - -.. code-block:: sh - - ps aux | grep '[A]pp' - -We will change it to run and parse this command: - -.. code-block:: sh - - quota -spw - -Update ``test/test_command.rb`` -............................... - -Run the command to get example data. Copy and paste the output into the test, and -update the assertions to expect an array of "quotas" instead of "processes" -with appropriate attributes. - -Diff: - -.. code-block:: diff - - def test_command_output_parsing - output = <<-EOF - - - -efranz 30328 0.1 0.1 462148 28128 ? Sl 20:28 0:00 Passenger RackApp: /users/PZS0562/efranz/ondemand/dev/quota - - - +Disk quotas for user efranz (uid 10851): - + Filesystem blocks quota limit grace files quota limit grace - +10.11.200.32:/PZS0562/ 99616M 500G 500G 0 933k 1000k 1000k 0 - EOF - - processes = Command.new.parse(output) - + quotas = Command.new.parse(output) - - - assert_equal 1, processes.count - + assert_equal 1, quotas.count, "number of structs parsed should equal 1" - - - p = processes.first - + q = quotas.first - - - assert_equal "efranz", p.user - - assert_equal "462148", p.vsz - - assert_equal "28128", p.rss - - assert_equal "0:00", p.time - - assert_equal "Passenger RackApp: /users/PZS0562/efranz/ondemand/dev/quota", p.command - + assert_equal "10.11.200.32:/PZS0562/", q.filesystem, "expected filesystem value not correct" - + assert_equal "99616M", q.blocks, "expected blocks value not correct" - + assert_equal "500G", q.blocks_limit, "expected blocks_limit value not correct" - + assert_equal "933k", q.files, "expected files value not correct" - + assert_equal "0", q.files_grace, "expected files_grace value not correct" - end - - -Resulting test method: - -.. code-block:: ruby - - class TestCommand < Minitest::Test - - def test_command_output_parsing - output = <<-EOF - Disk quotas for user efranz (uid 10851): - Filesystem blocks quota limit grace files quota limit grace - 10.11.200.32:/PZS0562/ 99616M 500G 500G 0 933k 1000k 1000k 0 - EOF - quotas = Command.new.parse(output) - - assert_equal 1, quotas.count, "number of structs parsed should equal 1" - - q = quotas.first - - assert_equal "10.11.200.32:/PZS0562/", q.filesystem, "expected filesystem value not correct" - assert_equal "99616M", q.blocks, "expected blocks value not correct" - assert_equal "500G", q.blocks_limit, "expected blocks_limit value not correct" - assert_equal "933k", q.files, "expected files value not correct" - assert_equal "0", q.files_grace, "expected files_grace value not correct" - end - end - -Update ``command.rb`` -..................... - -Run the test by running the ``rake`` command and you will see it fail: - -.. code-block:: sh - - $ rake - Run options: --seed 58990 - - # Running: - - F - - Finished in 0.000943s, 1060.4569 runs/s, 1060.4569 assertions/s. - - 1) Failure: - TestCommand#test_command_output_parsing [/users/PZS0562/efranz/ondemand/dev/quota/test/test_command.rb:14]: - number of structs parsed should equal 1. - Expected: 1 - Actual: 3 - - 1 runs, 1 assertions, 1 failures, 0 errors, 0 skips - rake aborted! - Command failed with status (1) - - Tasks: TOP => default => test - (See full trace by running task with --trace) - -.. warning:: - - To run commands like ``rake`` through the shell you need to make sure you are on - a host that has the correct version of Ruby installed. For OnDemand that likely - means using Software Collections with the same packages used to install OnDemand. - - With SCL, running ``rake`` with ondemand SCL package looks like: - - ``scl enable ondemand -- rake`` - - You can avoid this by loading the SCL packages in your ``.bashrc`` or ``.bash_profile`` file. - For example, in my ``.bash_profile`` I have: - - .. code-block:: sh - - if [[ ${HOSTNAME%%.*} == webtest04* ]] - then - scl enable ondemand -- bash - fi - - This means when I login to the host ``webtest04.osc.edu`` the SCL packages will be enabled - in a new bash session. If you did the same you would replace ``webtest04`` with the hostname - of the node you are developing on. - -To get the unit test to pass we need to: - -#. Change the command we are using. - -#. Fix the command output parsing. - -#. Fix the struct definition. - -.. code-block:: diff - - class Command - def to_s - - "ps aux | grep '[A]pp'" - + "quota -spw" - end - - - AppProcess = Struct.new(:user, :pid, :pct_cpu, :pct_mem, :vsz, :rss, :tty, :stat, :start, :time, :command) - + Quota = Struct.new(:filesystem, :blocks, :blocks_quota, :blocks_limit, :blocks_grace, :files, :files_quota, :files_limit, :fil - - # Parse a string output from the `ps aux` command and return an array of - # AppProcess objects, one per process - def parse(output) - lines = output.strip.split("\n") - - lines.map do |line| - - AppProcess.new(*(line.split(" ", 11))) - + lines.drop(2).map do |line| - + Quota.new(*(line.split)) - end - end - -After the changes part of the ``command.rb`` will look like this: - -.. code-block:: ruby - - class Command - def to_s - "quota -spw" - end - - Quota = Struct.new(:filesystem, :blocks, :blocks_quota, :blocks_limit, :blocks_grace, :files, :files_quota, :files_limit, :files_grace) - - # Parse a string output from the `ps aux` command and return an array of - # AppProcess objects, one per process - def parse(output) - lines = output.strip.split("\n") - lines.drop(2).map do |line| - Quota.new(*(line.split)) - end - end - -Now when we run the test they pass: - -.. code-block:: sh - - $ rake - Run options: --seed 60317 - - # Running: - - . - - Finished in 0.000966s, 1035.1494 runs/s, 6210.8963 assertions/s. - - 1 runs, 6 assertions, 0 failures, 0 errors, 0 skips - -Update ``app.rb`` and ``view/index.html`` -......................................... - -Update ``app.rb``: - -.. code-block:: diff - - helpers do - def title - - "Passenger App Processes" - + "Quota" - end - end - - # Define a route at the root '/' of the app. - get '/' do - @command = Command.new - - @processes, @error = @command.exec - + @quotas, @error = @command.exec - - # Render the view - erb :index - end - - -In ``views/index.erb``, replace the table with this: - -.. code-block:: erb - - - - - - - - - - - - - - <% @quotas.each do |quota| %> - - - - - - - - - - - - <% end %> -
FilesystemBlocksBlocks QuotaBlocks LimitBlocks GraceFilesFiles QuotaFiles LimitFiles Grace
<%= quota.filesystem %><%= quota.blocks %><%= quota.blocks_quota %><%= quota.blocks_limit %><%= quota.blocks_grace %><%= quota.files %><%= quota.files_quota %><%= quota.files_limit %><%= quota.files_grace %>
- -These changes *should not require an app restart.* Go to the launched app and **reload the page** -to see the changes. - -Brand App ---------- - -The app is looking good, but the details page still shows the app title "Passenger App -Processes". To change this and the icon, edit the ``manifest.yml``: - -.. code-block:: diff - - -name: Passenger App Processes - -description: Display your running Passenger app processes in a table - +name: Quota - +description: Display quotas - +icon: fa://hdd-o - -* The icon follows format of ``fa://{FONTAWESOMENAME}`` where you replace ``{FONTAWESOMENAME}`` with an icon from https://fontawesome.com/icons/. - In this case we are using ``fa-hdd-o`` which we write in the manifest as ``fa://hdd-o``. - You can see details on this icon at https://fontawesome.com/icons/hdd?style=regular - -Publish App ------------ - -Publishing an app requires two steps: - -#. Updating the ``manifest.yml`` to specify the category and optionally subcategory, which indicates where in the dashboard menu the app appears. - -#. Having an administrator checkout a copy of the production version to a directory under ``/var/www/ood/apps/sys``. - - -Steps: - -#. Add category to manifest so the app appears in the Files menu: - - .. code-block:: diff - - name: Quota - description: Display quotas - icon: fa://hdd-o - +category: Files - +subcategory: Utilities - -#. Version these changes. Click **Shell** button on app details view, and then ``commit`` the changes: - - .. code-block:: sh - - git add . - git commit -m "update manifest for production" - - # if there is an external remote associated with this, push to that - git push origin master - -#. As the admin, ``sudo copy`` or ``git clone`` this repo to production - - .. code-block:: sh - - # as sudo on OnDemand host: - cd /var/www/ood/apps/sys - git clone /users/PZS0562/efranz/ondemand/dev/quota - - -#. **Reload** the dashboard. - -.. figure:: /images/app-dev-tutorial-ps-to-quota-published.png - :align: center - - Every user can now launch the Quota from the Files menu. - -.. warning:: - - Accessing this new app for the first time will cause your NGINX server to restart, - killing all websocket connections, which means resetting your active web-based OnDemand Shell sessions. - diff --git a/source/tutorials/tutorials-passenger-apps/python-starter-app.rst b/source/tutorials/tutorials-passenger-apps/python-starter-app.rst index 909fa5e48..4e318f9e8 100644 --- a/source/tutorials/tutorials-passenger-apps/python-starter-app.rst +++ b/source/tutorials/tutorials-passenger-apps/python-starter-app.rst @@ -138,5 +138,7 @@ virtual environment. Now, with the python wrapper script to load the environment for your application, it should boot up correctly. +.. include:: deploy-to-production.inc + .. _Python: https://www.python.org/ .. _Flask: https://flask.palletsprojects.com/en/3.0.x/ diff --git a/source/tutorials/tutorials-passenger-apps/ruby-starter-app.rst b/source/tutorials/tutorials-passenger-apps/ruby-starter-app.rst new file mode 100644 index 000000000..da095f3f7 --- /dev/null +++ b/source/tutorials/tutorials-passenger-apps/ruby-starter-app.rst @@ -0,0 +1,87 @@ +.. _app-development-tutorials-passenger-apps-starter-ruby-app: + +Starter Ruby Application +======================== + +This document walks through creating a hello world application in Ruby +with the `Sinatra`_ web framework. + +config.ru for Sinatra +--------------------- + +The first thing we need for OnDemand to recognize this directory is a ``config.ru`` file. +For `Sinatra`_ this is the ``config.ru`` that you need. + +.. code:: ruby + + # frozen_string_literal: true + + require_relative 'app' + run App + +config.ru for Ruby on Rails +--------------------------- + +This document does not cover `Ruby on Rails`_, but +this ``config.ru`` is given nonetheless for readers interested +in building Ruby apps based on `Ruby on Rails`_. + +.. code:: ruby + + # frozen_string_literal: true + + require_relative 'config/environment' + + run Rails.application + Rails.application.load_server + +Install dependencies +-------------------- + +The application won't boot with just the ``config.ru``, though it will try. +What you need now is to install the gems (the ruby dependencies). + +We need a ``Gemfile`` to tell ``bundler`` (Ruby's application for dependencies) +what gems to install. Here's that file. + +.. code:: ruby + + # frozen_string_literal: true + + source 'https://rubygems.org' + + gem 'sinatra' + +With the ``Gemfile`` written, we can now install the dependencies +into ``vendor/bundle``. Issue these commands to do that. + +.. code:: shell + + bundle config path --local vendor/bundle + bundle install + + +Write the app.rb file +--------------------- + +Still, the app will not boot at this point. The ``config.ru`` is looking +to load the ``app.rb`` file which does not exist yet. + +The ``app.rb`` file that will actually import `Sinatra`_ and implement your routes. +Here's the simplest version of this file returning Hello World on the root URL. + +.. code:: ruby + + require 'sinatra/base' + + class App < Sinatra::Base + get '/' do + 'Hello World' + end + end + + +.. include:: deploy-to-production.inc + +.. _Sinatra: https://sinatrarb.com/ +.. _Ruby on Rails: https://rubyonrails.org/ \ No newline at end of file