diff --git a/.gitignore b/.gitignore index 4bbb9ed..75af97e 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ /test/dummy/tmp/development_secret.txt .byebug_history + +.DS_Store diff --git a/Gemfile.lock b/Gemfile.lock index 57d8452..f396eb4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,6 +3,7 @@ PATH specs: rake-ui (0.1.0) actionpack + activestorage activesupport railties rake diff --git a/README.md b/README.md index 45f4c5c..a00a1c1 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,20 @@ RakeUi.configuration do |config| end ``` +### Enabling ActiveStorage +In order to turn on/off active_storage for RakeUI gem, add configuration in initializer as follows + +```rb +RakeUi.configuration do |config| + config.active_storage = false +end +``` +if you want to specify active storage service, +update initializer with +``` +storage_service = :amazon +``` + We recommend adding guards in your route to ensure that the proper authentication is in place to ensure that users are authenticated so that if this were ever to be rendered in production, you would be covered. The best way for that is [router constraints](https://guides.rubyonrails.org/routing.html#specifying-constraints) ## Testing diff --git a/app/controllers/rake_ui/application_controller.rb b/app/controllers/rake_ui/application_controller.rb index 150a06e..f77b435 100644 --- a/app/controllers/rake_ui/application_controller.rb +++ b/app/controllers/rake_ui/application_controller.rb @@ -3,13 +3,51 @@ module RakeUi class ApplicationController < ActionController::Base before_action :black_hole_production + before_action :auth_validate + before_action :policy_validate + + # include Pundit::Authorization + + # before_action :authorize_pundit + # before_action :authorize! + + STAGING_OK = (Rails.env.staging? && RakeUi.configuration.allow_staging) + PROD_OK = RakeUi.configuration.allow_production private + def authorize_pundit + r = 3 + @current_user = authenticate_admin_user! + authorize(@current_user) + # binding.pry + # authorize :rake_tasks, :show? + end + def black_hole_production - return if Rails.env.test? || Rails.env.development? || RakeUi.configuration.allow_production + return if Rails.env.test? || Rails.env.development? || STAGING_OK || PROD_OK raise ActionController::RoutingError, "Not Found" end + + def auth_validate + return true unless RakeUi.configuration.auth_engine + + if defined?(RakeUi.configuration.auth_engine) + cb = RakeUi.configuration.auth_callback + return false unless cb && (cb.class == Proc) + RakeUi.configuration.auth_callback.call(self) + end + end + + def policy_validate + return true unless RakeUi.configuration.policy_engine + + if defined?(RakeUi.configuration.policy_engine) + cb = RakeUi.configuration.policy_callback + return false unless cb && (cb.class == Proc) + RakeUi.configuration.policy_callback.call(self) + end + end end end diff --git a/app/controllers/rake_ui/rake_task_logs_controller.rb b/app/controllers/rake_ui/rake_task_logs_controller.rb index 8cc1868..72f5992 100644 --- a/app/controllers/rake_ui/rake_task_logs_controller.rb +++ b/app/controllers/rake_ui/rake_task_logs_controller.rb @@ -12,8 +12,7 @@ class RakeTaskLogsController < ApplicationController :log_file_full_path].freeze def index - @rake_task_logs = RakeUi::RakeTaskLog.all.sort_by(&:id) - + @rake_task_logs = klass.all respond_to do |format| format.html format.json do @@ -25,8 +24,7 @@ def index end def show - @rake_task_log = RakeUi::RakeTaskLog.find_by_id(params[:id]) - + @rake_task_log = klass.find_by_id(params[:id]) @rake_task_log_content = @rake_task_log.file_contents.gsub("\n", "
") @rake_task_log_content_url = rake_task_log_path(@rake_task_log.id, format: :json) @is_rake_task_log_finished = @rake_task_log.finished? @@ -55,5 +53,10 @@ def rake_task_log_as_json(task) def rake_task_logs_as_json(tasks = []) tasks.map { |task| rake_task_log_as_json(task) } end + + def klass + RakeUi.configuration.active_storage ? ::RakeTaskLog : RakeUi::RakeTaskLog + end + end end diff --git a/app/helpers/rake_ui/rake_task_log_helper.rb b/app/helpers/rake_ui/rake_task_log_helper.rb new file mode 100644 index 0000000..5389e89 --- /dev/null +++ b/app/helpers/rake_ui/rake_task_log_helper.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module RakeUi + module RakeTaskLogHelper + def attributers_to_show + RakeUi.configuration.active_storage ? ::RakeTaskLog::ATTRIBUTES_TO_SHOW : [] + end + end +end diff --git a/app/models/concerns/rake_task_logs.rb b/app/models/concerns/rake_task_logs.rb new file mode 100644 index 0000000..9967137 --- /dev/null +++ b/app/models/concerns/rake_task_logs.rb @@ -0,0 +1,39 @@ +module RakeTaskLogs + extend ActiveSupport::Concern + ID_DATE_FORMAT = "%Y-%m-%d-%H-%M-%S%z" + REPOSITORY_DIR = Rails.root.join("tmp", "rake_ui") + FILE_DELIMITER = "____" + TASK_HEADER_OUTPUT_DELIMITER = "-------------------------------" + FILE_ITEM_SEPARATOR = ": " + FINISHED_STRING = "+++++ COMMAND FINISHED +++++" + INPROGRESS = "Task is in Progress...." + NOT_AVAILABLE = "Log File Not Avaialable..." + + + class_methods do + def create_tmp_file_dir + FileUtils.mkdir_p(REPOSITORY_DIR.to_s) + end + + def generate_task_attributes(raker_id:) + date = Time.now.strftime(ID_DATE_FORMAT) + id = "#{date}#{FILE_DELIMITER}#{raker_id}" + log_file_name = "#{id}.txt" + log_file_full_path = REPOSITORY_DIR.join(log_file_name).to_s + { + date:, + id:, + log_file_name:, + log_file_full_path: + } + end + end + + def rake_command_with_logging + "#{rake_command} 2>&1 >> #{log_file_full_path}" + end + + def command_to_mark_log_finished + "echo #{FINISHED_STRING} >> #{log_file_full_path}" + end +end diff --git a/app/models/rake_task_log.rb b/app/models/rake_task_log.rb new file mode 100644 index 0000000..9436fff --- /dev/null +++ b/app/models/rake_task_log.rb @@ -0,0 +1,55 @@ +class RakeTaskLog < ApplicationRecord + include RakeTaskLogs + + ATTRIBUTES_TO_SHOW = %w[id name date args environment rake_command rake_definition_file log_file_name log_file_full_path] + STORAGE_SERVICE = RakeUi.configuration.storage_service.presence || Rails.application.config.active_storage.service + has_one_attached :log_file, service: STORAGE_SERVICE + enum status: { in_progress: 0, finished: 1 } + + def self.build_new_for_command(name:, rake_definition_file:, rake_command:, raker_id:, args: nil, environment: nil) + create_tmp_file_dir + generate_file_content(log_file_full_path: generate_task_attributes(raker_id:)[:log_file_full_path]) + create_rake_task_log(name:, args:, environment:, rake_command:, rake_definition_file:, raker_id:) + end + + def self.generate_file_content(log_file_full_path:) + File.open(log_file_full_path, "w+") do |f| + f.puts TASK_HEADER_OUTPUT_DELIMITER.to_s + f.puts " INVOKED RAKE TASK OUTPUT BELOW" + f.puts TASK_HEADER_OUTPUT_DELIMITER.to_s + end + end + + def self.create_rake_task_log(name:, args:, environment:, rake_command:, rake_definition_file:, raker_id:) + attributes = generate_task_attributes(raker_id:) + + ::RakeTaskLog.create(name:, + args:, + environment:, + rake_command:, + rake_definition_file:, + log_file_name: attributes[:log_file_name], + log_file_full_path: attributes[:log_file_full_path], + raker_id:) + end + + def attach_file_with_rake_task_log + if File.exist?(log_file_full_path) + log_file.attach(io: File.open(log_file_full_path), filename: 'log.txt') + end + self.status = :finished + self.save! + File.delete(log_file_full_path) + end + + def file_contents + return INPROGRESS unless finished? + + log_file.download if log_file.attached? + end + + def date + created_at + end + +end diff --git a/app/models/rake_ui/rake_task.rb b/app/models/rake_ui/rake_task.rb index 7bab735..7531d45 100644 --- a/app/models/rake_ui/rake_task.rb +++ b/app/models/rake_ui/rake_task.rb @@ -82,8 +82,7 @@ def internal_task? def call(args: nil, environment: nil) rake_command = build_rake_command(args: args, environment: environment) - - rake_task_log = RakeUi::RakeTaskLog.build_new_for_command( + rake_task_log = klass.build_new_for_command( name: name, args: args, environment: environment, @@ -98,6 +97,7 @@ def call(args: nil, environment: nil) system(rake_task_log.rake_command_with_logging) system(rake_task_log.command_to_mark_log_finished) + rake_task_log.attach_file_with_rake_task_log if RakeUi.configuration.active_storage end rake_task_log @@ -122,5 +122,9 @@ def build_rake_command(args: nil, environment: nil) command end + + def klass + @klass ||= RakeUi.configuration.active_storage ? ::RakeTaskLog : RakeUi::RakeTaskLog + end end end diff --git a/app/models/rake_ui/rake_task_log.rb b/app/models/rake_ui/rake_task_log.rb index 861f14b..560792a 100644 --- a/app/models/rake_ui/rake_task_log.rb +++ b/app/models/rake_ui/rake_task_log.rb @@ -2,17 +2,7 @@ module RakeUi class RakeTaskLog < OpenStruct - # year-month-day-hour(24hour time)-minute-second-utc - ID_DATE_FORMAT = "%Y-%m-%d-%H-%M-%S%z" - REPOSITORY_DIR = Rails.root.join("tmp", "rake_ui") - FILE_DELIMITER = "____" - FINISHED_STRING = "+++++ COMMAND FINISHED +++++" - TASK_HEADER_OUTPUT_DELIMITER = "-------------------------------" - FILE_ITEM_SEPARATOR = ": " - - def self.create_tmp_file_dir - FileUtils.mkdir_p(REPOSITORY_DIR.to_s) - end + include RakeTaskLogs def self.truncate FileUtils.rm_rf(Dir.glob(REPOSITORY_DIR.to_s + "/*")) @@ -116,18 +106,10 @@ def log_file_full_path super || parsed_file_contents[:log_file_full_path] end - def rake_command_with_logging - "#{rake_command} 2>&1 >> #{log_file_full_path}" - end - def file_contents @file_contents ||= File.read(log_file_full_path) end - def command_to_mark_log_finished - "echo #{FINISHED_STRING} >> #{log_file_full_path}" - end - def finished? file_contents.include? FINISHED_STRING end diff --git a/app/views/rake_ui/rake_task_logs/_log_attributes.html.erb b/app/views/rake_ui/rake_task_logs/_log_attributes.html.erb new file mode 100644 index 0000000..99206fd --- /dev/null +++ b/app/views/rake_ui/rake_task_logs/_log_attributes.html.erb @@ -0,0 +1,4 @@ +
+ <%= "#{attr}: #{val}" %> +
+
diff --git a/app/views/rake_ui/rake_task_logs/show.html.erb b/app/views/rake_ui/rake_task_logs/show.html.erb index 10260b0..f21f8c5 100644 --- a/app/views/rake_ui/rake_task_logs/show.html.erb +++ b/app/views/rake_ui/rake_task_logs/show.html.erb @@ -4,6 +4,9 @@

Rake Task Log


+ <% attributers_to_show.each do |attr| %> + <%= render 'log_attributes', attr: attr, val: @rake_task_log.send(attr) %> + <% end %>
<%= @rake_task_log_content.html_safe %> diff --git a/db/migrate/20240906180222_create_active_storage_tables.active_storage.rb b/db/migrate/20240906180222_create_active_storage_tables.active_storage.rb new file mode 100644 index 0000000..8e0e407 --- /dev/null +++ b/db/migrate/20240906180222_create_active_storage_tables.active_storage.rb @@ -0,0 +1,49 @@ +# This migration comes from active_storage (originally 20170806125915) +class CreateActiveStorageTables < ActiveRecord::Migration[5.2] + def change + # Use Active Record's configured type for primary and foreign keys + primary_key_type, foreign_key_type = primary_and_foreign_key_types + + create_table :active_storage_blobs, id: primary_key_type, if_not_exists: true do |t| + t.string :key, null: false + t.string :filename, null: false + t.string :content_type + t.text :metadata + t.string :service_name, null: false + t.bigint :byte_size, null: false + t.string :checksum, null: false + t.datetime :created_at, null: false + + t.index [ :key ], unique: true + end + + create_table :active_storage_attachments, id: primary_key_type, if_not_exists: true do |t| + t.string :name, null: false + t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type + t.references :blob, null: false, type: foreign_key_type + + t.datetime :created_at, null: false + + t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true + t.foreign_key :active_storage_blobs, column: :blob_id + end + + create_table :active_storage_variant_records, id: primary_key_type, if_not_exists: true do |t| + t.belongs_to :blob, null: false, index: false, type: foreign_key_type + t.string :variation_digest, null: false + + t.index %i[ blob_id variation_digest ], name: "index_active_storage_variant_records_uniqueness", unique: true + t.foreign_key :active_storage_blobs, column: :blob_id + end + end + + private + + def primary_and_foreign_key_types + config = Rails.configuration.generators + setting = config.options[config.orm][:primary_key_type] + primary_key_type = setting || :primary_key + foreign_key_type = setting || :bigint + [primary_key_type, foreign_key_type] + end +end diff --git a/db/migrate/20240906183055_create_rake_task_logs.rb b/db/migrate/20240906183055_create_rake_task_logs.rb new file mode 100644 index 0000000..b6e3d09 --- /dev/null +++ b/db/migrate/20240906183055_create_rake_task_logs.rb @@ -0,0 +1,16 @@ +class CreateRakeTaskLogs < ActiveRecord::Migration[5.2] + def change + create_table :rake_task_logs, if_not_exists: true do |t| + t.integer :status, default: 0 + t.string :name + t.string :args + t.string :environment + t.string :rake_command + t.string :rake_definition_file + t.string :log_file_name + t.string :log_file_full_path + t.string :raker_id + t.timestamps + end + end +end diff --git a/lib/rake-ui.rb b/lib/rake-ui.rb index 770e16e..fcba2f0 100644 --- a/lib/rake-ui.rb +++ b/lib/rake-ui.rb @@ -4,7 +4,22 @@ module RakeUi mattr_accessor :allow_production + mattr_accessor :allow_staging + mattr_accessor :policy_engine + mattr_accessor :policy_callback + mattr_accessor :auth_engine + mattr_accessor :auth_callback + mattr_accessor :active_storage + mattr_accessor :storage_service + + self.active_storage = false self.allow_production = false + self.allow_staging = true + self.policy_engine = nil + self.policy_callback = nil + self.auth_engine = nil + self.auth_callback = nil + self.storage_service = nil def self.configuration yield(self) if block_given? diff --git a/lib/rake-ui/engine.rb b/lib/rake-ui/engine.rb index f29fe95..6f52ca9 100644 --- a/lib/rake-ui/engine.rb +++ b/lib/rake-ui/engine.rb @@ -7,5 +7,14 @@ module RakeUi class Engine < ::Rails::Engine isolate_namespace RakeUi + initializer "rake_ui.load_migrations" do |app| + if RakeUi.configuration.active_storage + unless app.root.to_s.match?(RakeUi::Engine.root.to_s) + app.config.paths['db/migrate'].concat( + RakeUi::Engine.paths['db/migrate'].existent + ) + end + end + end end end diff --git a/rake-ui.gemspec b/rake-ui.gemspec index c156fb1..bd7f15a 100644 --- a/rake-ui.gemspec +++ b/rake-ui.gemspec @@ -22,4 +22,5 @@ Gem::Specification.new do |spec| spec.add_dependency "rake" spec.add_development_dependency "standardrb" + spec.add_dependency 'activestorage' end diff --git a/test/dummy/config/storage.yml b/test/dummy/config/storage.yml new file mode 100644 index 0000000..a7f1763 --- /dev/null +++ b/test/dummy/config/storage.yml @@ -0,0 +1,4 @@ +# config/storage.yml +test: + service: Disk + root: <%= Rails.root.join("storage") %> diff --git a/test/dummy/db/schema.rb b/test/dummy/db/schema.rb new file mode 100644 index 0000000..6524e31 --- /dev/null +++ b/test/dummy/db/schema.rb @@ -0,0 +1,59 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema.define(version: 2024_09_06_183055) do + + create_table "active_storage_attachments", force: :cascade do |t| + t.string "name", null: false + t.string "record_type", null: false + t.bigint "record_id", null: false + t.bigint "blob_id", null: false + t.datetime "created_at", null: false + t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" + t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true + end + + create_table "active_storage_blobs", force: :cascade do |t| + t.string "key", null: false + t.string "filename", null: false + t.string "content_type" + t.text "metadata" + t.string "service_name", null: false + t.bigint "byte_size", null: false + t.string "checksum", null: false + t.datetime "created_at", null: false + t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true + end + + create_table "active_storage_variant_records", force: :cascade do |t| + t.bigint "blob_id", null: false + t.string "variation_digest", null: false + t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true + end + + create_table "rake_task_logs", force: :cascade do |t| + t.integer "status", default: 0 + t.string "name" + t.string "args" + t.string "environment" + t.string "rake_command" + t.string "rake_definition_file" + t.string "log_file_name" + t.string "log_file_full_path" + t.string "raker_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" + add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" +end diff --git a/test/dummy/fixtures/sample_log.txt b/test/dummy/fixtures/sample_log.txt new file mode 100644 index 0000000..3475010 --- /dev/null +++ b/test/dummy/fixtures/sample_log.txt @@ -0,0 +1 @@ +INVOKED RAKE TASK OUTPUT BELOW \ No newline at end of file diff --git a/test/integration/rake_task_logs_request_with_active_record_test.rb b/test/integration/rake_task_logs_request_with_active_record_test.rb new file mode 100644 index 0000000..b522ef7 --- /dev/null +++ b/test/integration/rake_task_logs_request_with_active_record_test.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require "test_helper" + +class RakeTaskLogsRequestWithActiveRecordTest < ActionDispatch::IntegrationTest + setup do + RakeUi.configuration.active_storage = true + + file = Rack::Test::UploadedFile.new(Rails.root.join('fixtures/sample_log.txt'), 'text/plain') + ::RakeTaskLog.create!( + status: 1, + name: 'Test Rake Task', + args: 'arg1,arg2', + environment: 'test', + rake_command: 'rake test', + rake_definition_file: 'definition_file.rake', + log_file: file, + log_file_name: 'sample_log.txt', + log_file_full_path: '/path/to/sample_log.txt', + raker_id: 'rake-id' + ) + end + + test "index html responds successfully" do + get "/rake-ui/rake_task_logs" + + assert_equal 200, status + end + + test "index json responds successfully" do + get "/rake-ui/rake_task_logs.json" + + assert_equal 200, status + assert_instance_of Array, json_response[:rake_task_logs] + end + + test "show html responds with the content" do + log = ::RakeTaskLog.all.first + get "/rake-ui/rake_task_logs/#{log.id}" + + assert_equal 200, status + assert_includes response.body, "INVOKED RAKE TASK OUTPUT BELOW" + end + + test "show json responds with the content and task log meta" do + log = ::RakeTaskLog.all.first + get "/rake-ui/rake_task_logs/#{log.id}.json" + assert_equal 200, status + assert_equal log.id, json_response[:rake_task_log][:id] + assert_equal log.log_file_name, json_response[:rake_task_log][:log_file_name] + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 046abc8..dfce709 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -6,6 +6,7 @@ require_relative "../test/dummy/config/environment" ActiveRecord::Migrator.migrations_paths = [File.expand_path("../test/dummy/db/migrate", __dir__)] ActiveRecord::Migrator.migrations_paths << File.expand_path("../db/migrate", __dir__) +ORIGINAL_CONFIGURATION = RakeUi.configuration.active_storage require "rails/test_help" require "minitest/mock" @@ -17,6 +18,12 @@ ActiveSupport::TestCase.fixtures :all end +class ActiveSupport::TestCase + teardown do + RakeUi.configuration.active_storage = ORIGINAL_CONFIGURATION + end +end + def json_response JSON.parse(response.body).with_indifferent_access end