diff --git a/Gemfile b/Gemfile index 671940838..9e8f5ee7d 100644 --- a/Gemfile +++ b/Gemfile @@ -33,6 +33,7 @@ gem "octokit" gem "argon2-kdf" gem "googleauth" gem "simplecov" +gem "parse-cron" group :development do gem "brakeman" diff --git a/Gemfile.lock b/Gemfile.lock index 189f12b94..7ce44d80c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -157,6 +157,7 @@ GEM parallel (1.24.0) parallel_tests (4.5.2) parallel + parse-cron (0.1.4) parser (3.3.0.5) ast (~> 2.4.1) racc @@ -346,6 +347,7 @@ DEPENDENCIES nokogiri octokit pagerduty (>= 4.0) + parse-cron pry pry-byebug puma (>= 6.2.2) diff --git a/config.rb b/config.rb index fbad952ce..bd9080e57 100644 --- a/config.rb +++ b/config.rb @@ -142,6 +142,7 @@ def self.e2e_test? override :lantern_backup_bucket, "walg-dev-backups" override :e2e_test, "0" override :backup_retention_days, 7, int + optional :lantern_backend_database_url, string # GCP override :gcp_project_id, "lantern-development", string diff --git a/db.rb b/db.rb index cf731c5ba..88230811b 100644 --- a/db.rb +++ b/db.rb @@ -26,3 +26,11 @@ # DB.extension :date_arithmetic DB.extension :pg_json, :pg_auto_parameterize, :pg_timestamptz, :pg_range, :pg_array Sequel.extension :pg_range_ops + +module LanternBackend + @@db = Sequel.connect(Config.lantern_backend_database_url, max_connections: Config.db_pool, pool_timeout: Config.database_timeout) if Config.lantern_backend_database_url + + def self.db + @@db + end +end diff --git a/migrate/20240505_lantern_doctor.rb b/migrate/20240505_lantern_doctor.rb new file mode 100644 index 000000000..527bb1d23 --- /dev/null +++ b/migrate/20240505_lantern_doctor.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +Sequel.migration do + change do + # doctor + create_table(:lantern_doctor) do + column :id, :uuid, primary_key: true, default: nil + column :created_at, :timestamptz, null: false, default: Sequel.lit("now()") + column :updated_at, :timestamptz, null: false, default: Sequel.lit("now()") + end + alter_table(:lantern_resource) do + add_foreign_key :doctor_id, :lantern_doctor, type: :uuid, null: true + end + # queries + create_enum(:query_condition, %w[unknown healthy failed]) + create_enum(:query_type, %w[system user]) + create_table(:lantern_doctor_query) do + column :id, :uuid, primary_key: true, default: nil + foreign_key :parent_id, :lantern_doctor_query, type: :uuid + foreign_key :doctor_id, :lantern_doctor, type: :uuid + column :name, :text, null: true + column :db_name, :text, null: true + column :schedule, :text, null: true + column :condition, :query_condition, null: false, default: "unknown" + column :fn_label, :text, null: true + column :sql, :text, null: true + column :type, :query_type, null: false + column :severity, :severity, default: "error", null: true + column :last_checked, :timestamptz, null: true + column :created_at, :timestamptz, null: false, default: Sequel.lit("now()") + column :updated_at, :timestamptz, null: false, default: Sequel.lit("now()") + end + end +end diff --git a/migrate/20240506_lantern_doctor_queries.rb b/migrate/20240506_lantern_doctor_queries.rb new file mode 100644 index 000000000..b197ed9d3 --- /dev/null +++ b/migrate/20240506_lantern_doctor_queries.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +Sequel.migration do + change do + run "INSERT INTO lantern_doctor_query (id, name, db_name, schedule, condition, fn_label, type, severity) + VALUES ('4f916f44-3c7a-89b7-9795-1ccd417b45ba', 'Lantern Embedding Generation Job', '*', '*/30 * * * *', 'unknown', 'check_daemon_embedding_jobs', 'system', 'error')" + end +end diff --git a/model/lantern/lantern_doctor.rb b/model/lantern/lantern_doctor.rb new file mode 100644 index 000000000..ca25b5666 --- /dev/null +++ b/model/lantern/lantern_doctor.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require_relative "../../model" + +class LanternDoctor < Sequel::Model + one_to_one :strand, key: :id + one_to_one :resource, class: LanternResource, key: :doctor_id + one_to_many :queries, key: :doctor_id, class: LanternDoctorQuery + one_to_many :failed_queries, key: :doctor_id, class: LanternDoctorQuery, conditions: {condition: "failed"} + + plugin :association_dependencies, queries: :destroy + + include ResourceMethods + include SemaphoreMethods + + semaphore :destroy, :sync_system_queries + + def system_queries + @system_queries ||= LanternDoctorQuery.where(type: "system").all + end + + def has_system_query?(queries, query) + queries.any? { _1.parent_id == query.id } + end + + def sync_system_queries + doctor_query_list = queries + system_query_list = system_queries + + system_query_list.each { + if !has_system_query?(doctor_query_list, _1) + LanternDoctorQuery.create_with_id(parent_id: _1.id, doctor_id: id, condition: "unknown", type: "user") + end + } + end +end diff --git a/model/lantern/lantern_doctor_query.rb b/model/lantern/lantern_doctor_query.rb new file mode 100644 index 000000000..ee38c3a01 --- /dev/null +++ b/model/lantern/lantern_doctor_query.rb @@ -0,0 +1,139 @@ +# frozen_string_literal: true + +require "parse-cron" +require_relative "../../model" +require_relative "../../db" + +class LanternDoctorQuery < Sequel::Model + one_to_one :strand, key: :id + many_to_one :doctor, class: LanternDoctor, key: :doctor_id, primary_key: :id + many_to_one :parent, class: self, key: :parent_id + one_to_many :children, key: :parent_id, class: self + + plugin :association_dependencies, children: :destroy + dataset_module Pagination + + include ResourceMethods + include SemaphoreMethods + + semaphore :destroy + + def sql + parent&.sql || super + end + + def name + parent&.name || super + end + + def db_name + parent&.db_name || super + end + + def severity + parent&.severity || super + end + + def schedule + parent&.schedule || super + end + + def fn_label + parent&.fn_label || super + end + + def should_run? + CronParser.new(schedule).next(last_checked || Time.new - 61) <= Time.new + end + + def is_system? + !parent.nil? + end + + def user + return "postgres" if is_system? + doctor.resource.db_user + end + + def active_pages + tag = Page.generate_tag("LanternDoctorQueryFailed", id) + Page.active.where(Sequel.like(:tag, "%#{tag}-%")).all + end + + def all_pages + tag = Page.generate_tag("LanternDoctorQueryFailed", id) + Page.where(Sequel.like(:tag, "%#{tag}-%")).all + end + + def run + if !should_run? + return nil + end + + lantern_server = doctor.resource.representative_server + dbs = (db_name == "*") ? lantern_server.list_all_databases : [db_name] + query_user = user + + any_failed = false + dbs.each do |db| + err_msg = "" + + failed = false + begin + if is_system? && fn_label && LanternDoctorQuery.method_defined?(fn_label) + res = send(fn_label, db, query_user) + elsif sql + res = lantern_server.run_query(sql, db: db, user: query_user).strip + else + fail "BUG: non-system query without sql" + end + + if res != "f" + failed = true + any_failed = true + end + rescue => e + failed = true + any_failed = true + Clog.emit("LanternDoctorQuery failed") { {error: e, query_name: name, db: db, resource_name: doctor.resource.name} } + err_msg = e.message + end + + pg = Page.from_tag_parts("LanternDoctorQueryFailed", id, db) + + if failed && !pg + Prog::PageNexus.assemble_with_logs("Healthcheck: #{name} failed on #{doctor.resource.name} (#{db})", [ubid, doctor.ubid, lantern_server.ubid], {"stderr" => err_msg}, severity, "LanternDoctorQueryFailed", id, db) + elsif !failed && pg + pg.incr_resolve + end + end + + update(condition: any_failed ? "failed" : "healthy", last_checked: Time.new) + end + + def check_daemon_embedding_jobs(db, query_user) + if !LanternBackend.db + fail "No connection to lantern backend database specified" + end + + jobs = LanternBackend.db + .select(:schema, :table, :src_column, :dst_column) + .from(:embedding_generation_jobs) + .where(database_id: doctor.resource.name) + .where(Sequel.like(:db_connection, "%/#{db}")) + .where(Sequel.lit("init_finished_at IS NOT NULL")) + .all + + if jobs.empty? + return "f" + end + + lantern_server = doctor.resource.representative_server + failed = jobs.any? do |job| + res = lantern_server.run_query("SELECT (SELECT COUNT(*) FROM \"#{job[:schema]}\".\"#{job[:table]}\" WHERE \"#{job[:src_column]}\" IS NOT NULL AND \"#{job[:src_column]}\" != '' AND \"#{job[:dst_column]}\" IS NULL) > 1000", db: db, user: query_user).strip + res == "t" + end + + failed ? "t" : "f" + end +end diff --git a/model/lantern/lantern_resource.rb b/model/lantern/lantern_resource.rb index 913aeb194..c0b2fa03d 100644 --- a/model/lantern/lantern_resource.rb +++ b/model/lantern/lantern_resource.rb @@ -10,6 +10,7 @@ class LanternResource < Sequel::Model one_to_many :servers, class: LanternServer, key: :resource_id one_to_one :representative_server, class: LanternServer, key: :resource_id, conditions: Sequel.~(representative_at: nil) one_through_one :timeline, class: LanternTimeline, join_table: :lantern_server, left_key: :resource_id, right_key: :timeline_id + one_to_one :doctor, class: LanternDoctor, key: :id, primary_key: :doctor_id dataset_module Authorization::Dataset dataset_module Pagination diff --git a/model/lantern/lantern_server.rb b/model/lantern/lantern_server.rb index b86d7e3f7..98300bc34 100644 --- a/model/lantern/lantern_server.rb +++ b/model/lantern/lantern_server.rb @@ -47,12 +47,12 @@ def connection_string ).to_s end - def run_query(query) - vm.sshable.cmd("sudo lantern/bin/exec", stdin: query).chomp + def run_query(query, db: "postgres", user: "postgres") + vm.sshable.cmd("sudo docker compose -f /var/lib/lantern/docker-compose.yaml exec -T postgresql psql -U #{user} -t --csv #{db}", stdin: query).chomp end def run_query_all(query) - vm.sshable.cmd("sudo lantern/bin/exec_all", stdin: query).chomp + list_all_databases.map { [_1, run_query(query, db: _1)] } end def display_state @@ -196,6 +196,14 @@ def prewarm_indexes_query SQL end + def list_all_databases + vm.sshable.cmd("sudo docker compose -f /var/lib/lantern/docker-compose.yaml exec postgresql psql -U postgres -P \"footer=off\" -c 'SELECT datname from pg_database' | tail -n +3 | grep -v 'template0' | grep -v 'template1'") + .chomp + .strip + .split("\n") + .map { _1.strip } + end + # def failover_target # nil # end diff --git a/prog/lantern/lantern_doctor_nexus.rb b/prog/lantern/lantern_doctor_nexus.rb new file mode 100644 index 000000000..9251c2ba2 --- /dev/null +++ b/prog/lantern/lantern_doctor_nexus.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require "forwardable" + +class Prog::Lantern::LanternDoctorNexus < Prog::Base + subject_is :lantern_doctor + + extend Forwardable + def_delegators :lantern_doctor + + semaphore :destroy, :sync_system_queries + + def self.assemble + DB.transaction do + lantern_doctor = LanternDoctor.create_with_id + Strand.create(prog: "Lantern::LanternDoctorNexus", label: "start") { _1.id = lantern_doctor.id } + end + end + + def before_run + when_destroy_set? do + if strand.label != "destroy" + hop_destroy + end + end + end + + label def start + lantern_doctor.sync_system_queries + hop_wait_resource + end + + label def wait_resource + nap 5 if lantern_doctor.resource&.strand&.label != "wait" + hop_wait + end + + label def wait + if lantern_doctor.resource.nil? + hop_destroy + end + + when_sync_system_queries_set? do + hop_sync_system_queries + end + + lantern_doctor.queries.each { _1.run } + nap 60 + end + + label def sync_system_queries + decr_sync_system_queries + lantern_doctor.sync_system_queries + hop_wait + end + + label def destroy + decr_destroy + + lantern_doctor.failed_queries.each { + _1.active_pages.each { |pg| pg.incr_resolve } + } + + lantern_doctor.destroy + pop "lantern doctor is deleted" + end +end diff --git a/prog/lantern/lantern_resource_nexus.rb b/prog/lantern/lantern_resource_nexus.rb index 328356d4e..1d0c47ee8 100644 --- a/prog/lantern/lantern_resource_nexus.rb +++ b/prog/lantern/lantern_resource_nexus.rb @@ -74,12 +74,14 @@ def self.assemble(project_id:, location:, name:, target_vm_size:, target_storage end + lantern_doctor = Prog::Lantern::LanternDoctorNexus.assemble + lantern_resource = LanternResource.create( project_id: project_id, location: location, name: name, org_id: org_id, app_env: app_env, superuser_password: superuser_password, ha_type: ha_type, parent_id: parent_id, restore_target: restore_target, db_name: db_name, db_user: db_user, db_user_password: db_user_password, repl_user: repl_user, repl_password: repl_password, - label: label + label: label, doctor_id: lantern_doctor.id ) { _1.id = ubid.to_uuid } lantern_resource.associate_with_project(project) @@ -163,6 +165,7 @@ def before_run nap 5 end + lantern_resource.doctor.incr_destroy lantern_resource.dissociate_with_project(lantern_resource.project) lantern_resource.destroy diff --git a/routes/api/project/location/lantern.rb b/routes/api/project/location/lantern.rb index 8ed4d0b52..70ec3b605 100644 --- a/routes/api/project/location/lantern.rb +++ b/routes/api/project/location/lantern.rb @@ -12,6 +12,7 @@ class CloverApi end @pg = serialize(pg, :detailed) + r.hash_branches(:project_location_lantern_prefix) r.get true do Authorization.authorize(@current_user.id, "Postgres:view", pg.id) diff --git a/routes/api/project/location/lantern/doctor.rb b/routes/api/project/location/lantern/doctor.rb new file mode 100644 index 000000000..1404a02eb --- /dev/null +++ b/routes/api/project/location/lantern/doctor.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +class CloverApi + hash_branch(:project_location_lantern_prefix, "doctor") do |r| + @serializer = Serializers::Api::LanternDoctorQuery + @lantern_doctor = LanternResource[@pg[:id]].doctor + + r.get true do + result = LanternDoctorQuery.where(doctor_id: @lantern_doctor.id).paginated_result( + cursor: r.params["cursor"], + page_size: r.params["page_size"], + order_column: r.params["order_column"] + ) + + { + items: serialize(result[:records]), + next_cursor: result[:next_cursor], + count: result[:count] + } + end + + r.get "incidents" do + result = LanternDoctorQuery.where(doctor_id: @lantern_doctor.id, condition: "failed").paginated_result( + cursor: r.params["cursor"], + page_size: r.params["page_size"], + order_column: r.params["order_column"] + ) + + { + items: serialize(result[:records], :detailed), + next_cursor: result[:next_cursor], + count: result[:count] + } + end + + r.on String do |query_id| + query = LanternDoctorQuery[query_id] + + unless query + response.status = 404 + r.halt + end + + r.post "run" do + query.update(last_checked: nil) + response.status = 204 + r.halt + end + end + end +end diff --git a/serializers/api/lantern_doctor_query.rb b/serializers/api/lantern_doctor_query.rb new file mode 100644 index 000000000..c1f3b9e1b --- /dev/null +++ b/serializers/api/lantern_doctor_query.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class Serializers::Api::LanternDoctorQuery < Serializers::Base + def self.base(query) + { + id: query.id, + name: query.name, + type: query.type, + condition: query.condition, + last_checked: query.last_checked, + schedule: query.schedule, + db_name: query.db_name, + severity: query.severity + } + end + + structure(:default) do |query| + base(query) + end + + structure(:detailed) do |query| + base(query).merge({ + incidents: query.active_pages.map { + {summary: _1.summary, logs: _1.details["logs"]["stderr"]} + } + }) + end +end diff --git a/spec/model/lantern/lantern_doctor_query_spec.rb b/spec/model/lantern/lantern_doctor_query_spec.rb new file mode 100644 index 000000000..158675046 --- /dev/null +++ b/spec/model/lantern/lantern_doctor_query_spec.rb @@ -0,0 +1,368 @@ +# frozen_string_literal: true + +require_relative "../spec_helper" + +RSpec.describe LanternDoctorQuery do + subject(:lantern_doctor_query) { + described_class.new do |r| + r.sql = "SELECT 1" + r.name = "test" + r.db_name = "postgres" + r.severity = "error" + r.schedule = "*/1 * * * *" + r.id = "6181ddb3-0002-8ad0-9aeb-084832c9273b" + end + } + + let(:parent) { + instance_double( + described_class, + sql: "SELECT 2", + name: "test-parent", + db_name: "*", + severity: "critical", + schedule: "*/2 * * * *" + ) + } + + describe "#parent_properties" do + it "returns parent sql if parent_id is defined else self sql" do + expect(lantern_doctor_query).to receive(:parent).and_return(nil) + expect(lantern_doctor_query.sql).to be("SELECT 1") + + expect(lantern_doctor_query).to receive(:parent).and_return(parent) + expect(lantern_doctor_query.sql).to be("SELECT 2") + end + + it "returns parent name if parent_id is defined else self name" do + expect(lantern_doctor_query).to receive(:parent).and_return(nil) + expect(lantern_doctor_query.name).to be("test") + + expect(lantern_doctor_query).to receive(:parent).and_return(parent) + expect(lantern_doctor_query.name).to be("test-parent") + end + + it "returns parent db_name if parent_id is defined else self db_name" do + expect(lantern_doctor_query).to receive(:parent).and_return(nil) + expect(lantern_doctor_query.db_name).to be("postgres") + + expect(lantern_doctor_query).to receive(:parent).and_return(parent) + expect(lantern_doctor_query.db_name).to be("*") + end + + it "returns parent severity if parent_id is defined else self severity" do + expect(lantern_doctor_query).to receive(:parent).and_return(nil) + expect(lantern_doctor_query.severity).to be("error") + + expect(lantern_doctor_query).to receive(:parent).and_return(parent) + expect(lantern_doctor_query.severity).to be("critical") + end + + it "returns parent schedule if parent_id is defined else self schedule" do + expect(lantern_doctor_query).to receive(:parent).and_return(nil) + expect(lantern_doctor_query.schedule).to be("*/1 * * * *") + + expect(lantern_doctor_query).to receive(:parent).and_return(parent) + expect(lantern_doctor_query.schedule).to be("*/2 * * * *") + end + + it "returns parent fn_label if parent_id is defined else self fn_label" do + expect(lantern_doctor_query).to receive(:parent).and_return(nil) + expect(lantern_doctor_query.fn_label).to be_nil + + expect(lantern_doctor_query).to receive(:parent).and_return(parent) + expect(parent).to receive(:fn_label).and_return("test") + expect(lantern_doctor_query.fn_label).to be("test") + end + end + + describe "#should_run?" do + it "return false if not yet time for run" do + min = Time.new.min + modified_min = (min == 59) ? 0 : min + 1 + + expect(lantern_doctor_query).to receive(:schedule).and_return("#{modified_min} * * * *").at_least(:once) + expect(lantern_doctor_query.should_run?).to be(false) + end + + it "return true if it is the same time for run" do + ts = Time.new + min = ts.min + expect(Time).to receive(:new).and_return(ts).at_least(:once) + expect(lantern_doctor_query).to receive(:last_checked).and_return(nil) + expect(lantern_doctor_query).to receive(:schedule).and_return("#{min} * * * *").at_least(:once) + expect(lantern_doctor_query.should_run?).to be(true) + end + + it "return true if the running time was passed but didn't run yet" do + min = Time.new.min + modified_min = (min == 0) ? 59 : min - 1 + + expect(lantern_doctor_query).to receive(:last_checked).and_return(Time.new - 60 * 5).at_least(:once) + expect(lantern_doctor_query).to receive(:schedule).and_return("#{modified_min} * * * *").at_least(:once) + expect(lantern_doctor_query.should_run?).to be(true) + end + end + + describe "#is_system?" do + it "returns false if no parent" do + expect(lantern_doctor_query).to receive(:parent).and_return(nil) + expect(lantern_doctor_query.is_system?).to be(false) + end + + it "returns true if has parent" do + expect(lantern_doctor_query).to receive(:parent).and_return(parent) + expect(lantern_doctor_query.is_system?).to be(true) + end + end + + describe "#user" do + it "returns postgres if system query" do + expect(lantern_doctor_query).to receive(:is_system?).and_return(true) + expect(lantern_doctor_query.user).to be("postgres") + end + + it "returns db user if not system query" do + expect(lantern_doctor_query).to receive(:is_system?).and_return(false) + expect(lantern_doctor_query).to receive(:doctor).and_return(instance_double(LanternDoctor, resource: instance_double(LanternResource, db_user: "test"))) + expect(lantern_doctor_query.user).to be("test") + end + end + + describe "#run" do + it "returns if should not run yet" do + expect(lantern_doctor_query).to receive(:should_run?).and_return(false) + expect(lantern_doctor_query.run).to be_nil + end + + it "runs query on specified database" do + serv = instance_double(LanternServer) + resource = instance_double(LanternResource, representative_server: serv, db_user: "test") + doctor = instance_double(LanternDoctor, resource: resource) + expect(serv).to receive(:run_query).with(lantern_doctor_query.sql, db: lantern_doctor_query.db_name, user: resource.db_user).and_return("f") + expect(lantern_doctor_query).to receive(:doctor).and_return(doctor).at_least(:once) + expect(lantern_doctor_query).to receive(:should_run?).and_return(true) + expect(lantern_doctor_query).to receive(:update).with(hash_including(condition: "healthy")) + expect { lantern_doctor_query.run }.not_to raise_error + end + + it "throws error if no sql defined" do + serv = instance_double(LanternServer, ubid: "test-ubid") + resource = instance_double(LanternResource, representative_server: serv, db_user: "test", name: "test-res", id: "6181ddb3-0002-8ad0-9aeb-084832c9273b") + doctor = instance_double(LanternDoctor, resource: resource, ubid: "test-ubid") + + expect(lantern_doctor_query).to receive(:sql).and_return(nil).at_least(:once) + expect(lantern_doctor_query).to receive(:doctor).and_return(doctor).at_least(:once) + expect(lantern_doctor_query).to receive(:should_run?).and_return(true) + expect(lantern_doctor_query).to receive(:update).with(hash_including(condition: "failed")) + expect(Prog::PageNexus).to receive(:assemble_with_logs).with("Healthcheck: #{lantern_doctor_query.name} failed on #{doctor.resource.name} (postgres)", [lantern_doctor_query.ubid, doctor.ubid, serv.ubid], {"stderr" => "BUG: non-system query without sql"}, lantern_doctor_query.severity, "LanternDoctorQueryFailed", lantern_doctor_query.id, "postgres") + + expect { lantern_doctor_query.run }.not_to raise_error + end + + it "runs function for specified database" do + serv = instance_double(LanternServer) + resource = instance_double(LanternResource, representative_server: serv, db_user: "test") + doctor = instance_double(LanternDoctor, resource: resource) + + expect(parent).to receive(:db_name).and_return("postgres") + expect(lantern_doctor_query).to receive(:fn_label).and_return("check_daemon_embedding_jobs").at_least(:once) + expect(lantern_doctor_query).to receive(:parent).and_return(parent).at_least(:once) + + expect(lantern_doctor_query).to receive(:doctor).and_return(doctor).at_least(:once) + expect(lantern_doctor_query).to receive(:is_system?).and_return(true).at_least(:once) + expect(lantern_doctor_query).to receive(:should_run?).and_return(true).at_least(:once) + expect(lantern_doctor_query).to receive(:check_daemon_embedding_jobs).and_return("f") + + expect(lantern_doctor_query).to receive(:update).with(hash_including(condition: "healthy")) + + expect { lantern_doctor_query.run }.not_to raise_error + end + + it "runs query on all databases" do + serv = instance_double(LanternServer) + resource = instance_double(LanternResource, representative_server: serv, db_user: "test") + doctor = instance_double(LanternDoctor, resource: resource) + dbs = ["db1", "db2"] + + expect(serv).to receive(:list_all_databases).and_return(dbs) + + expect(serv).to receive(:run_query).with(lantern_doctor_query.sql, db: "db1", user: resource.db_user).and_return("f") + expect(serv).to receive(:run_query).with(lantern_doctor_query.sql, db: "db2", user: resource.db_user).and_return("f") + + expect(lantern_doctor_query).to receive(:db_name).and_return("*") + expect(lantern_doctor_query).to receive(:doctor).and_return(doctor).at_least(:once) + expect(lantern_doctor_query).to receive(:should_run?).and_return(true) + expect(lantern_doctor_query).to receive(:update).with(hash_including(condition: "healthy")) + + expect { lantern_doctor_query.run }.not_to raise_error + end + + it "runs query on all databases and errors" do + serv = instance_double(LanternServer, ubid: "test-ubid") + resource = instance_double(LanternResource, representative_server: serv, db_user: "test", name: "test-res", id: "6181ddb3-0002-8ad0-9aeb-084832c9273b") + doctor = instance_double(LanternDoctor, resource: resource, ubid: "test-ubid") + dbs = ["db1", "db2"] + + expect(serv).to receive(:list_all_databases).and_return(dbs) + + expect(serv).to receive(:run_query).with(lantern_doctor_query.sql, db: "db1", user: resource.db_user).and_raise("test-err") + expect(serv).to receive(:run_query).with(lantern_doctor_query.sql, db: "db2", user: resource.db_user).and_return("f") + + expect(lantern_doctor_query).to receive(:db_name).and_return("*") + expect(lantern_doctor_query).to receive(:doctor).and_return(doctor).at_least(:once) + expect(lantern_doctor_query).to receive(:should_run?).and_return(true) + expect(lantern_doctor_query).to receive(:update).with(hash_including(condition: "failed")) + expect(Prog::PageNexus).to receive(:assemble_with_logs).with("Healthcheck: #{lantern_doctor_query.name} failed on #{doctor.resource.name} (db1)", [lantern_doctor_query.ubid, doctor.ubid, serv.ubid], {"stderr" => "test-err"}, lantern_doctor_query.severity, "LanternDoctorQueryFailed", lantern_doctor_query.id, "db1") + + expect { lantern_doctor_query.run }.not_to raise_error + end + + it "runs query on all databases and fails" do + serv = instance_double(LanternServer, ubid: "test-ubid") + resource = instance_double(LanternResource, representative_server: serv, db_user: "test", name: "test-res", id: "6181ddb3-0002-8ad0-9aeb-084832c9273b") + doctor = instance_double(LanternDoctor, resource: resource, ubid: "test-ubid") + dbs = ["db1", "db2"] + + expect(serv).to receive(:list_all_databases).and_return(dbs) + + expect(serv).to receive(:run_query).with(lantern_doctor_query.sql, db: "db1", user: resource.db_user).and_return("t") + expect(serv).to receive(:run_query).with(lantern_doctor_query.sql, db: "db2", user: resource.db_user).and_return("f") + + expect(lantern_doctor_query).to receive(:db_name).and_return("*") + expect(lantern_doctor_query).to receive(:doctor).and_return(doctor).at_least(:once) + expect(lantern_doctor_query).to receive(:should_run?).and_return(true) + expect(lantern_doctor_query).to receive(:update).with(hash_including(condition: "failed")) + expect(Prog::PageNexus).to receive(:assemble_with_logs).with("Healthcheck: #{lantern_doctor_query.name} failed on #{doctor.resource.name} (db1)", [lantern_doctor_query.ubid, doctor.ubid, serv.ubid], {"stderr" => ""}, lantern_doctor_query.severity, "LanternDoctorQueryFailed", lantern_doctor_query.id, "db1") + + expect { lantern_doctor_query.run }.not_to raise_error + end + + it "does not create duplicate page" do + serv = instance_double(LanternServer, ubid: "test-ubid") + resource = instance_double(LanternResource, representative_server: serv, db_user: "test", name: "test-res", id: "6181ddb3-0002-8ad0-9aeb-084832c9273b") + doctor = instance_double(LanternDoctor, resource: resource, ubid: "test-ubid") + dbs = ["db1", "db2"] + + expect(serv).to receive(:list_all_databases).and_return(dbs) + + expect(serv).to receive(:run_query).with(lantern_doctor_query.sql, db: "db1", user: resource.db_user).and_return("t") + expect(serv).to receive(:run_query).with(lantern_doctor_query.sql, db: "db2", user: resource.db_user).and_return("f") + + expect(lantern_doctor_query).to receive(:db_name).and_return("*") + expect(lantern_doctor_query).to receive(:doctor).and_return(doctor).at_least(:once) + expect(lantern_doctor_query).to receive(:should_run?).and_return(true) + expect(lantern_doctor_query).to receive(:update).with(hash_including(condition: "failed")) + expect(Page).to receive(:from_tag_parts).with("LanternDoctorQueryFailed", lantern_doctor_query.id, "db1").and_return(instance_double(Page)) + expect(Page).to receive(:from_tag_parts).with("LanternDoctorQueryFailed", lantern_doctor_query.id, "db2").and_return(nil) + expect(Prog::PageNexus).not_to receive(:assemble_with_logs) + + expect { lantern_doctor_query.run }.not_to raise_error + end + + it "runs query on all databases and resolves previous error" do + serv = instance_double(LanternServer, ubid: "test-ubid") + resource = instance_double(LanternResource, representative_server: serv, db_user: "test", name: "test-res", id: "6181ddb3-0002-8ad0-9aeb-084832c9273b") + doctor = instance_double(LanternDoctor, resource: resource, ubid: "test-ubid") + dbs = ["db1", "db2"] + + expect(serv).to receive(:list_all_databases).and_return(dbs) + + expect(serv).to receive(:run_query).with(lantern_doctor_query.sql, db: "db1", user: resource.db_user).and_return("f") + expect(serv).to receive(:run_query).with(lantern_doctor_query.sql, db: "db2", user: resource.db_user).and_return("f") + + expect(lantern_doctor_query).to receive(:db_name).and_return("*") + expect(lantern_doctor_query).to receive(:doctor).and_return(doctor).at_least(:once) + expect(lantern_doctor_query).to receive(:should_run?).and_return(true) + expect(lantern_doctor_query).to receive(:update).with(hash_including(condition: "healthy")) + page = instance_double(Page) + expect(Page).to receive(:from_tag_parts).with("LanternDoctorQueryFailed", lantern_doctor_query.id, "db1").and_return(page) + expect(Page).to receive(:from_tag_parts).with("LanternDoctorQueryFailed", lantern_doctor_query.id, "db2").and_return(nil) + expect(page).to receive(:incr_resolve) + + expect { lantern_doctor_query.run }.not_to raise_error + end + end + + describe "#check_daemon_embedding_jobs" do + it "fails if not backend db connection" do + expect(LanternBackend).to receive(:db).and_return(nil) + expect { lantern_doctor_query.check_daemon_embedding_jobs "postgres", "postgres" }.to raise_error "No connection to lantern backend database specified" + end + + it "get jobs and fails" do + serv = instance_double(LanternServer, ubid: "test-ubid") + resource = instance_double(LanternResource, representative_server: serv, db_user: "test", name: "test-res", id: "6181ddb3-0002-8ad0-9aeb-084832c9273b") + doctor = instance_double(LanternDoctor, resource: resource, ubid: "test-ubid") + expect(lantern_doctor_query).to receive(:doctor).and_return(doctor).at_least(:once) + expect(LanternBackend).to receive(:db).and_return(DB).at_least(:once) + expect(LanternBackend.db).to receive(:select) + .and_return(instance_double(Sequel::Dataset, + from: instance_double(Sequel::Dataset, + where: instance_double(Sequel::Dataset, + where: instance_double(Sequel::Dataset, + where: instance_double(Sequel::Dataset, + all: [{schema: "public", table: "test", src_column: "test-src", dst_column: "test-dst"}])))))) + expect(serv).to receive(:run_query).with("SELECT (SELECT COUNT(*) FROM \"public\".\"test\" WHERE \"test-src\" IS NOT NULL AND \"test-src\" != '' AND \"test-dst\" IS NULL) > 1000", db: "postgres", user: "postgres").and_return("t") + expect(lantern_doctor_query.check_daemon_embedding_jobs("postgres", "postgres")).to eq("t") + end + + it "get jobs as empty" do + serv = instance_double(LanternServer, ubid: "test-ubid") + resource = instance_double(LanternResource, representative_server: serv, db_user: "test", name: "test-res", id: "6181ddb3-0002-8ad0-9aeb-084832c9273b") + doctor = instance_double(LanternDoctor, resource: resource, ubid: "test-ubid") + expect(lantern_doctor_query).to receive(:doctor).and_return(doctor).at_least(:once) + expect(LanternBackend).to receive(:db).and_return(DB).at_least(:once) + expect(LanternBackend.db).to receive(:select) + .and_return(instance_double(Sequel::Dataset, + from: instance_double(Sequel::Dataset, + where: instance_double(Sequel::Dataset, + where: instance_double(Sequel::Dataset, + where: instance_double(Sequel::Dataset, + all: [])))))) + expect(lantern_doctor_query.check_daemon_embedding_jobs("postgres", "postgres")).to eq("f") + end + end + + it "get jobs and succceds" do + serv = instance_double(LanternServer, ubid: "test-ubid") + resource = instance_double(LanternResource, representative_server: serv, db_user: "test", name: "test-res", id: "6181ddb3-0002-8ad0-9aeb-084832c9273b") + doctor = instance_double(LanternDoctor, resource: resource, ubid: "test-ubid") + expect(lantern_doctor_query).to receive(:doctor).and_return(doctor).at_least(:once) + expect(LanternBackend).to receive(:db).and_return(DB).at_least(:once) + expect(LanternBackend.db).to receive(:select) + .and_return(instance_double(Sequel::Dataset, + from: instance_double(Sequel::Dataset, + where: instance_double(Sequel::Dataset, + where: instance_double(Sequel::Dataset, + where: instance_double(Sequel::Dataset, + all: [{schema: "public", table: "test", src_column: "test-src", dst_column: "test-dst"}])))))) + expect(serv).to receive(:run_query).with("SELECT (SELECT COUNT(*) FROM \"public\".\"test\" WHERE \"test-src\" IS NOT NULL AND \"test-src\" != '' AND \"test-dst\" IS NULL) > 1000", db: "postgres", user: "postgres").and_return("f") + expect(lantern_doctor_query.check_daemon_embedding_jobs("postgres", "postgres")).to eq("f") + end + + describe "#page" do + it "lists active pages" do + p1 = Prog::PageNexus.assemble_with_logs("test", [lantern_doctor_query.ubid], {"stderr" => ""}, "error", "LanternDoctorQueryFailed", lantern_doctor_query.id, "postgres") + p2 = Prog::PageNexus.assemble_with_logs("test", [lantern_doctor_query.ubid], {"stderr" => ""}, "error", "LanternDoctorQueryFailed", lantern_doctor_query.id, "postgres2") + + Page[p2.id].update(resolved_at: Time.new) + + pages = lantern_doctor_query.active_pages + expect(pages.size).to be(1) + expect(pages[0].id).to eq(p1.id) + end + + it "lists all pages" do + p1 = Prog::PageNexus.assemble_with_logs("test", [lantern_doctor_query.ubid], {"stderr" => ""}, "error", "LanternDoctorQueryFailed", lantern_doctor_query.id, "postgres") + p2 = Prog::PageNexus.assemble_with_logs("test", [lantern_doctor_query.ubid], {"stderr" => ""}, "error", "LanternDoctorQueryFailed", lantern_doctor_query.id, "postgres2") + + Page[p2.id].update(resolved_at: Time.new) + + pages = lantern_doctor_query.all_pages + expect(pages.size).to be(2) + expect(pages[0].id).to eq(p1.id) + expect(pages[1].id).to eq(p2.id) + end + end +end diff --git a/spec/model/lantern/lantern_doctor_spec.rb b/spec/model/lantern/lantern_doctor_spec.rb new file mode 100644 index 000000000..584d628bb --- /dev/null +++ b/spec/model/lantern/lantern_doctor_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require_relative "../spec_helper" + +RSpec.describe LanternDoctor do + subject(:lantern_doctor) { + described_class.new do |r| + r.id = "6181ddb3-0002-8ad0-9aeb-084832c9273b" + end + } + + describe "#system_queries" do + it "returns cached queries" do + expect(lantern_doctor).to receive(:system_queries).and_return([instance_double(LanternDoctorQuery)]) + expect(lantern_doctor.system_queries.size).to be(1) + end + + it "fetches system queries" do + expect(LanternDoctorQuery).to receive(:where).with(type: "system").and_return(instance_double(Sequel::Dataset, all: [instance_double(LanternDoctorQuery), instance_double(LanternDoctorQuery)])) + expect(lantern_doctor.system_queries.size).to be(2) + end + end + + describe "#has_system_query?" do + it "returns true if system query exists in query list" do + system_query = instance_double(LanternDoctorQuery, id: "test-parent-id") + query_list = [instance_double(LanternDoctorQuery, parent_id: "test-parent-id")] + expect(lantern_doctor.has_system_query?(query_list, system_query)).to be(true) + end + + it "returns false if system query does not exist in query list" do + system_query = instance_double(LanternDoctorQuery, id: "test-parent-id") + query_list = [instance_double(LanternDoctorQuery, parent_id: "test-parent-id2"), instance_double(LanternDoctorQuery, parent_id: nil)] + expect(lantern_doctor.has_system_query?(query_list, system_query)).to be(false) + end + end + + describe "#sync_system_queries" do + it "creates new system query if not exists" do + system_queries = [instance_double(LanternDoctorQuery, id: "test-parent-id"), instance_double(LanternDoctorQuery, id: "test-parent-id2")] + query_list = [instance_double(LanternDoctorQuery, parent_id: "test-parent-id2"), instance_double(LanternDoctorQuery, parent_id: nil)] + expect(lantern_doctor).to receive(:queries).and_return(query_list) + expect(lantern_doctor).to receive(:system_queries).and_return(system_queries) + new_query = instance_double(LanternDoctorQuery, parent_id: "test-parent-id") + expect(LanternDoctorQuery).to receive(:create_with_id).with(parent_id: "test-parent-id", doctor_id: lantern_doctor.id, type: "user", condition: "unknown").and_return(new_query) + expect { lantern_doctor.sync_system_queries }.not_to raise_error + end + end +end diff --git a/spec/model/lantern/lantern_server_spec.rb b/spec/model/lantern/lantern_server_spec.rb index e91dd9833..844f4f070 100644 --- a/spec/model/lantern/lantern_server_spec.rb +++ b/spec/model/lantern/lantern_server_spec.rb @@ -115,13 +115,25 @@ end it "runs query on vm" do - expect(lantern_server.vm.sshable).to receive(:cmd).with("sudo lantern/bin/exec", stdin: "SELECT 1").and_return("1\n") + expect(lantern_server.vm.sshable).to receive(:cmd).with("sudo docker compose -f /var/lib/lantern/docker-compose.yaml exec -T postgresql psql -U postgres -t --csv postgres", stdin: "SELECT 1").and_return("1\n") expect(lantern_server.run_query("SELECT 1")).to eq("1") end + it "runs query on vm with different user and db" do + expect(lantern_server.vm.sshable).to receive(:cmd).with("sudo docker compose -f /var/lib/lantern/docker-compose.yaml exec -T postgresql psql -U lantern -t --csv db2", stdin: "SELECT 1").and_return("1\n") + expect(lantern_server.run_query("SELECT 1", db: "db2", user: "lantern")).to eq("1") + end + it "runs query on vm for all databases" do - expect(lantern_server.vm.sshable).to receive(:cmd).with("sudo lantern/bin/exec_all", stdin: "SELECT 1").and_return("1\n") - expect(lantern_server.run_query_all("SELECT 1")).to eq("1") + expect(lantern_server).to receive(:list_all_databases).and_return(["postgres", "db2"]) + expect(lantern_server.vm.sshable).to receive(:cmd).with("sudo docker compose -f /var/lib/lantern/docker-compose.yaml exec -T postgresql psql -U postgres -t --csv postgres", stdin: "SELECT 1").and_return("1\n") + expect(lantern_server.vm.sshable).to receive(:cmd).with("sudo docker compose -f /var/lib/lantern/docker-compose.yaml exec -T postgresql psql -U postgres -t --csv db2", stdin: "SELECT 1").and_return("2\n") + expect(lantern_server.run_query_all("SELECT 1")).to eq( + [ + ["postgres", "1"], + ["db2", "2"] + ] + ) end describe "#standby?" do @@ -587,4 +599,11 @@ expect(lantern_server.prewarm_indexes_query).to eq(query) end end + + describe "#list_all_databases" do + it "returns list of all databases" do + expect(lantern_server.vm.sshable).to receive(:cmd).with("sudo docker compose -f /var/lib/lantern/docker-compose.yaml exec postgresql psql -U postgres -P \"footer=off\" -c 'SELECT datname from pg_database' | tail -n +3 | grep -v 'template0' | grep -v 'template1'").and_return("postgres\ndb2\n") + expect(lantern_server.list_all_databases).to eq(["postgres", "db2"]) + end + end end diff --git a/spec/prog/lantern/lantern_doctor_nexus_spec.rb b/spec/prog/lantern/lantern_doctor_nexus_spec.rb new file mode 100644 index 000000000..9b3898d87 --- /dev/null +++ b/spec/prog/lantern/lantern_doctor_nexus_spec.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +require_relative "../../model/spec_helper" + +RSpec.describe Prog::Lantern::LanternDoctorNexus do + subject(:nx) { described_class.new(Strand.create(id: "6ae7e513-c34a-8039-a72a-7be45b53f2a0", prog: "Lantern::LanternDoctorNexus", label: "start")) } + + let(:lantern_doctor) { + instance_double( + LanternDoctor, + ubid: "6ae7e513-c34a-8039-a72a-7be45b53f2a0", + id: "6ae7e513-c34a-8039-a72a-7be45b53f2a0" + ) + } + + before do + allow(nx).to receive(:lantern_doctor).and_return(lantern_doctor) + end + + describe ".assemble" do + it "creates lantern doctor" do + st = described_class.assemble + doctor = LanternDoctor[st.id] + expect(doctor).not_to be_nil + end + end + + describe "#start" do + it "hops to wait resource" do + expect(lantern_doctor).to receive(:sync_system_queries) + expect { nx.start }.to hop("wait_resource") + end + end + + describe "#wait_resource" do + it "naps if no resource yet" do + expect(lantern_doctor).to receive(:resource).and_return(nil) + expect { nx.wait_resource }.to nap(5) + end + + it "naps if no resource strand yet" do + expect(lantern_doctor).to receive(:resource).and_return(instance_double(LanternResource, strand: nil)) + expect { nx.wait_resource }.to nap(5) + end + + it "naps if resource is not available" do + expect(lantern_doctor).to receive(:resource).and_return(instance_double(LanternResource, strand: instance_double(Strand, label: "start"))) + expect { nx.wait_resource }.to nap(5) + end + + it "hops to wait" do + expect(lantern_doctor).to receive(:resource).and_return(instance_double(LanternResource, strand: instance_double(Strand, label: "wait"))) + expect { nx.wait_resource }.to hop("wait") + end + end + + describe "#wait" do + it "hops to destroy" do + expect(lantern_doctor).to receive(:resource).and_return(nil) + expect { nx.wait }.to hop("destroy") + end + + it "syncs system queries" do + expect(lantern_doctor).to receive(:resource).and_return(instance_double(LanternResource, strand: nil)) + expect(nx).to receive(:when_sync_system_queries_set?).and_yield + expect { nx.wait }.to hop("sync_system_queries") + end + + it "runs queries and naps" do + expect(lantern_doctor).to receive(:resource).and_return(instance_double(LanternResource, strand: nil)) + queries = [instance_double(LanternDoctorQuery)] + expect(queries[0]).to receive(:run) + expect(lantern_doctor).to receive(:queries).and_return(queries) + expect { nx.wait }.to nap(60) + end + end + + describe "#before_run" do + it "hops to destroy" do + expect(nx).to receive(:when_destroy_set?).and_yield + expect { nx.before_run }.to hop("destroy") + end + + it "does not hop to destroy as strand label is not destroy" do + expect(nx).to receive(:when_destroy_set?).and_yield + expect(nx).to receive(:strand).and_return(instance_double(Strand, label: "destroy")) + expect(nx.before_run).to be_nil + end + + it "does not hop to destroy" do + expect(nx.before_run).to be_nil + end + end + + describe "#sync_system_queries" do + it "calls sync_system_queries" do + expect(lantern_doctor).to receive(:sync_system_queries) + expect(nx).to receive(:decr_sync_system_queries) + expect { nx.sync_system_queries }.to hop("wait") + end + end + + describe "#destroy" do + it "exits with message" do + expect(nx).to receive(:decr_destroy) + page = instance_double(Page) + query = instance_double(LanternDoctorQuery, active_pages: [page]) + expect(page).to receive(:incr_resolve) + expect(lantern_doctor).to receive(:failed_queries).and_return([query]) + expect(lantern_doctor).to receive(:destroy) + expect { nx.destroy }.to exit({"msg" => "lantern doctor is deleted"}) + end + end +end diff --git a/spec/routes/api/project/location/lantern_doctor_spec.rb b/spec/routes/api/project/location/lantern_doctor_spec.rb new file mode 100644 index 000000000..89cb2b861 --- /dev/null +++ b/spec/routes/api/project/location/lantern_doctor_spec.rb @@ -0,0 +1,126 @@ +# frozen_string_literal: true + +require_relative "../../spec_helper" + +RSpec.describe Clover, "lantern-doctor" do + let(:user) { create_account } + + let(:project) { user.create_project_with_default_policy("project-1", provider: "gcp") } + + let(:project_wo_permissions) { user.create_project_with_default_policy("project-2", provider: "gcp", policy_body: []) } + + let(:pg) do + st = Prog::Lantern::LanternResourceNexus.assemble( + project_id: project.id, + location: "us-central1", + name: "instance-1", + target_vm_size: "n1-standard-2", + target_storage_size_gib: 100, + org_id: 0 + ) + LanternResource[st.id] + end + + describe "authenticated" do + before do + login_api(user.email) + Project.create_with_id(name: "default", provider: "gcp").tap { _1.associate_with_project(_1) } + end + + describe "list" do + it "lists empty" do + get "/api/project/#{project.ubid}/location/#{pg.location}/lantern/#{pg.name}/doctor" + + expect(last_response.status).to eq(200) + expect(JSON.parse(last_response.body)["items"]).to eq([]) + end + + it "lists queries" do + system_query = LanternDoctorQuery.create_with_id( + name: "test system query", + db_name: "postgres", + schedule: "*/30 * * * *", + condition: "unknown", + sql: "SELECT 1<2", + type: "system", + severity: "error" + ) + pg.doctor.sync_system_queries + + get "/api/project/#{project.ubid}/location/#{pg.location}/lantern/#{pg.name}/doctor" + expect(last_response.status).to eq(200) + items = JSON.parse(last_response.body)["items"] + expect(items.size).to eq(1) + first_item = items[0] + expect(first_item["id"]).not_to be_nil + expect(first_item["name"]).to eq(system_query.name) + expect(first_item["db_name"]).to eq(system_query.db_name) + expect(first_item["schedule"]).to eq(system_query.schedule) + expect(first_item["type"]).to eq("user") + expect(first_item["severity"]).to eq(system_query.severity) + end + end + + describe "incidents" do + it "lists active incidents" do + system_query = LanternDoctorQuery.create_with_id( + name: "test system query", + db_name: "postgres", + schedule: "*/30 * * * *", + condition: "unknown", + sql: "SELECT 1<2", + type: "system", + severity: "error" + ) + pg.doctor.sync_system_queries + pg.doctor.queries + first_query = LanternDoctorQuery[doctor_id: pg.doctor.id] + first_query.update(condition: "failed") + summary = "Healthcheck: #{first_query.name} failed on #{pg.name} (postgres)" + Prog::PageNexus.assemble_with_logs(summary, [first_query.ubid, pg.doctor.ubid, "test"], {"stderr" => ""}, system_query.severity, "LanternDoctorQueryFailed", first_query.id, "postgres") + + get "/api/project/#{project.ubid}/location/#{pg.location}/lantern/#{pg.name}/doctor/incidents" + expect(last_response.status).to eq(200) + items = JSON.parse(last_response.body)["items"] + expect(items.size).to eq(1) + first_item = items[0] + expect(first_item["condition"]).to eq("failed") + incidents = first_item["incidents"] + expect(incidents.size).to eq(1) + expect(incidents[0]["summary"]).to eq(summary) + expect(incidents[0]["logs"]).to eq("") + end + + it "changes check time of query to run on next loop" do + LanternDoctorQuery.create_with_id( + name: "test system query", + db_name: "postgres", + schedule: "*/30 * * * *", + condition: "unknown", + sql: "SELECT 1<2", + type: "system", + severity: "error" + ) + pg.doctor.sync_system_queries + pg.doctor.queries + first_query = LanternDoctorQuery[doctor_id: pg.doctor.id] + t = Time.new + first_query.update(last_checked: t) + + post "/api/project/#{project.ubid}/location/#{pg.location}/lantern/#{pg.name}/doctor/#{first_query.id}/run" + expect(last_response.status).to eq(204) + get "/api/project/#{project.ubid}/location/#{pg.location}/lantern/#{pg.name}/doctor" + expect(last_response.status).to eq(200) + items = JSON.parse(last_response.body)["items"] + expect(items.size).to eq(1) + first_item = items[0] + expect(first_item["last_checked"]).to be_nil + end + + it "returns 404" do + get "/api/project/#{project.ubid}/location/#{pg.location}/lantern/#{pg.name}/doctor/fbdad2ba-b61e-89b7-b7e5-d3414b94c541/run" + expect(last_response.status).to eq(404) + end + end + end +end diff --git a/ubid.rb b/ubid.rb index 53e762e85..b35b5ad0d 100644 --- a/ubid.rb +++ b/ubid.rb @@ -35,6 +35,8 @@ class UBID TYPE_LANTERN_RESOURCE = "1r" TYPE_LANTERN_SERVER = "1s" TYPE_LANTERN_TIMELINE = "1t" + TYPE_LANTERN_DOCTOR = "1d" + TYPE_LANTERN_DOCTOR_QUERY = "dq" TYPE_PROJECT = "pj" TYPE_ACCESS_TAG = "tg" TYPE_ACCESS_POLICY = "pc" diff --git a/views/lantern/create.erb b/views/lantern/create.erb index 36c44aae2..66c7546f5 100644 --- a/views/lantern/create.erb +++ b/views/lantern/create.erb @@ -49,7 +49,6 @@ name: "label", label: "Label", attributes: { - required: false, placeholder: "Enter label" } }