Move MathMan to being configured by Consul

Fixes: CNVS-35833

There is a lot more than just moving to Consul going on here. The whole
PrefixProxy business wouldn't be required for this change, but it will
be really useful as we move to adding cluster awareness.

Test Plan:
- Have MathMan running
- Update config/consul.yml to enable use_for_svg and
  use_for_mml under the math-man init values key
- Start Canvas
- Build an equation with the rich content editor
- The equation should be rendered as usual.

Change-Id: I650527ebaecb6224c6ee6ba26346d27dee33b9d7
Reviewed-on: https://gerrit.instructure.com/111543
QA-Review: Tucker McKnight <tmcknight@instructure.com>
Tested-by: Jenkins
Reviewed-by: Brent Burgoyne <bburgoyne@instructure.com>
Product-Review: Tyler Pickett <tpickett@instructure.com>
This commit is contained in:
Tyler Pickett 2017-05-05 11:58:22 -06:00
parent 8700cffa99
commit 823cda8924
19 changed files with 666 additions and 197 deletions

View File

@ -1,38 +0,0 @@
<%
# Copyright (C) 2016 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
%>
<%= fields_for :settings, OpenObject.new(settings) do |f| %>
<table style="width: 500px;" class="formtable">
<tr>
<td><%= f.blabel :base_url, t('Base url for MathMan service') %></td>
<td><%= f.text_field :base_url %></td>
</tr>
<tr>
<td>
<%= f.blabel :use_for_svg, t('Use MathMan to convert LaTeX to SVG?') %>
</td>
<td><%= f.check_box :use_for_svg %></td>
</tr>
<tr>
<td>
<%= f.blabel :use_for_mml, t('Use MathMan to convert LaTeX to MathML?') %>
</td>
<td><%= f.check_box :use_for_mml %></td>
</tr>
</table>
<% end %>

View File

@ -41,3 +41,8 @@ development:
live-events:
aws_endpoint: http://localhost:4567 # assumes kinesalite is running locally
kinesis_stream_name: live-events
init_values:
math-man:
base_url: 'http://mathman.docker'
use_for_svg: 'false'
use_for_mml: 'false'

View File

@ -16,3 +16,10 @@ development:
canvas:
encryption-secret: "astringthatisactually32byteslong"
signing-secret: "astringthatisactually32byteslong"
live-events:
aws_endpoint: http://kinesis.canvaslms.docker
kinesis_stream_name: live-events
math-man:
base_url: 'http://mathman.docker'
use_for_svg: 'false'
use_for_mml: 'false'

View File

@ -2,11 +2,13 @@
# host: 10.11.12.13
# port: 8500
# ssl: true
# environment: prod
# acl_token: "xxxxxxxx-yyyy-zzzz-1111-222222222222"
# test:
# host: consul
# port: 8500
# ssl: false
# environment: test
# init_values:
# canvas:
# signing-secret: astringthatisactually32byteslong
@ -24,7 +26,8 @@
# host: consul
# port: 8500
# ssl: false
# init_values:
# environment: "dev"
# init_values_witnout_env:
# canvas:
# signing-secret: astringthatisactually32byteslong
# encryption-secret: astringthatisactually32byteslong
@ -38,5 +41,10 @@
# app-host: http://les.docker
# sad-panda: null
# live-events:
# aws_endpoint: http://localhost:4567 # assumes kinesalite is running locally
# aws_endpoint: http://kinesis.canvaslms.docker
# kinesis_stream_name: live-events
# init_values:
# math-man:
# base_url: 'http://mathman.docker'
# use_for_svg: 'false'
# use_for_mml: 'false'

View File

@ -15,6 +15,9 @@
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
require_dependency 'canvas/dynamic_settings/cache'
require_dependency 'canvas/dynamic_settings/fallback_proxy'
require_dependency 'canvas/dynamic_settings/prefix_proxy'
require 'imperium'
module Canvas
@ -22,12 +25,14 @@ module Canvas
class Error < StandardError; end
class ConsulError < Error; end
class NoFallbackError < Error; end
CONSUL_READ_OPTIONS = %i{recurse stale}.freeze
KV_NAMESPACE = "config/canvas".freeze
class << self
attr_accessor :config, :cache, :environment, :fallback_data
attr_accessor :config, :environment
attr_reader :base_prefix_proxy, :fallback_data
def config=(conf_hash)
@config = conf_hash
@ -47,9 +52,26 @@ module Canvas
init_values(conf_hash.fetch("init_values", {}))
init_values(conf_hash.fetch("init_values_without_env", {}), use_env: false)
@base_prefix_proxy = DynamicSettings::PrefixProxy.new(
[KV_NAMESPACE, @environment.presence].compact.join('/'),
kv_client: Imperium::KV.default_client
)
else
@environment = @base_prefix_proxy = nil
end
end
# Set the fallback data to use in leiu of Consul
#
# This isn't really meant for use in production, but as a convenience for
# development where most won't want to run a consul agent/server.
def fallback_data=(value)
@fallback_data = value&.with_indifferent_access
end
# This is deprecated, use for_prefix to get a client that will fetch your
# values for you and squawks like a hash so you don't have to change much.
def find(key, use_env: true)
if config.nil?
return fallback_data.fetch(key) if fallback_data.present?
@ -59,18 +81,39 @@ module Canvas
end
end
# Build an object used to interacting with consul for the given
# keyspace prefix.
#
# If using fallback data for values it is queried by the returned object
# instead of a Consul agent/server. The decision between using fallback
# data or consul is driven by whether or not consul is configured.
#
# @param prefix [String] The portion to extend the base prefix with
# (base prefix: 'config/canvas/<environment>')
# @param default_ttl [ActiveSupport::Duration] How long to retain cached
# values
def for_prefix(prefix, default_ttl: DynamicSettings::PrefixProxy::DEFAULT_TTL)
if @base_prefix_proxy
@base_prefix_proxy.for_prefix(prefix, default_ttl: default_ttl)
elsif @fallback_data.present?
DynamicSettings::FallbackProxy.new(@fallback_data[prefix])
else
raise NoFallbackError, 'DynamicSettings.fallback_data is not set and'\
' consul is not configured, unable to supply configuration values.'
end
end
# This is deprecated, use for_prefix to get a client that will fetch your
# values for you and squawks like a hash so you don't have to change much.
#
# settings found this way with nil expiry will be cached in the process
# the first time they're asked for, and then can only be cleared with a SIGHUP
# or restart of the process. Make sure that's the behavior you want before
# you use this method, or specify a timeout
def from_cache(key, expires_in: nil, use_env: true)
reset_cache! if cache.nil?
cached_value = get_from_cache(key, expires_in)
return cached_value if cached_value.present?
# cache miss or timeout
value = self.find(key, use_env: use_env)
set_in_cache(key, value)
value
Cache.fetch(key, ttl: expires_in) do
self.find(key, use_env: use_env)
end
end
def kv_client
@ -78,24 +121,12 @@ module Canvas
end
def reset_cache!(hard: false)
@cache = {}
Cache.reset!
@strategic_reserve = {} if hard
end
private
def get_from_cache(key, timeout)
return nil unless cache.key?(key)
cache_entry = cache[key]
return cache_entry[:value] if timeout.nil?
threshold = (Time.zone.now - timeout).to_i
return cache_entry[:value] if cache_entry[:timestamp] > threshold
end
def set_in_cache(key, value)
cache[key] = {value: value, timestamp: Time.zone.now.to_i}
end
def init_values(hash, use_env: true)
hash.each do |parent_key, settings|
settings.each do |child_key, value|

View File

@ -0,0 +1,76 @@
# Copyright (C) 2017 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
module Canvas
class DynamicSettings
# A cache for values fetched from consul
module Cache
Value = Struct.new(:value, :expiration_time) do
def expired?
return false unless expiration_time
Time.zone.now >= expiration_time
end
end
# TODO: consider making this an L{R,f}U cache instead of a boundless one
@store = {}
class << self
attr_reader :store
end
# Get the cached value, if any, from the store regardless of expiration
#
# This is really only meant as an emergency fallback in the event that
# Consul can't be reached, not for normal operation.
def self.fallback_fetch(key)
@store[key]&.value
end
# Return the cached value for `key` or execute the supplied block to get it
#
# @param key [String] The key to cache the result under
# @param ttl [ActiveSupport::Duration] The length of time this key should
# be cached for, pass nil for no expiration.
def self.fetch(key, ttl: nil)
stored = @store[key]
if stored && !stored.expired?
stored.expiration_time = ttl&.from_now if stored.expiration_time.nil?
stored.value
else
yield.tap do |value|
insert(key, value, ttl: ttl) unless value.respond_to?(:not_found?) && value.not_found?
end
end
end
# Clear the cache store
def self.reset!
@store = {}
end
# Insert the supplied value into the cache using the supplied key
#
# @param key [String] The cache key to use
# @param value [Object] The value to store
# @param ttl [ActiveSupport::Duration] The length of time this key should
# be cached for, pass nil for no expiration.
def self.insert(key, value, ttl: nil)
@store[key] = Value.new(value, ttl&.from_now)
value
end
end
end
end

View File

@ -0,0 +1,41 @@
# Copyright (C) 2017 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
module Canvas
class DynamicSettings
class FallbackProxy
attr_reader :data
def initialize(data = nil)
@data = (data || {}).with_indifferent_access
end
def fetch(key, **_)
@data[key]
end
alias [] fetch
# TODO: Make this return something API compatible with
# Imperium::KVGETResponse once we're actually using Consul's metadata
def fetch_object(_, **_)
raise NotImplementedError, "Fetching full metadata objects from fallback data isn't implemented yet."
end
def for_prefix(prefix_extension, **_)
self.class.new(@data[prefix_extension])
end
end
end
end

View File

@ -0,0 +1,104 @@
# Copyright (C) 2017 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
module Canvas
class DynamicSettings
# A class for reading values from Consul
#
# @attr prefix [String] The prefix to be prepended to keys for querying.
class PrefixProxy
CONSUL_READ_OPTIONS = %i{stale}.freeze
private_constant :CONSUL_READ_OPTIONS
DEFAULT_TTL = 5.minutes
# The TTL for cached values if none is specified in the constructor
attr_reader :prefix
# Build a new prefix proxy
#
# @param prefix [String] The prefix to be prepended to keys for querying.
# @param default_ttl [ActiveSupport::Duration] The TTL to use for cached
# values when not specified to the fetch methods.
# @param kv_client [Imperium::KV] The client to use for connecting to
# Consul, defaults to Imperium::KV.default_client
def initialize(prefix, default_ttl: DEFAULT_TTL, kv_client: Imperium::KV.default_client)
@prefix = prefix
@default_ttl = default_ttl
@kv_client = kv_client
end
# Fetch the value at the requested key using the prefix passed to the
# initializer.
#
# This method is intended to retreive a single key from the keyspace and
# will not work for getting multiple values in a hash from the store. If
# you need to access values nested deeper in the keyspace use #for_prefix
# to move deeper in the nesting.
#
# @param key [String, Symbol] The key to fetch
# @param ttl [ActiveSupport::Duration] The TTL for the value in the cache,
# defaults to value supplied to the constructor.
# @return [String]
# @return [nil] When no value was found
def fetch(key, ttl: @default_ttl)
fetch_object(key, ttl: ttl)&.values
end
alias [] fetch
# Fetch the full object at the specified key including all metadata
#
# This method is intended to retreive a single key from the keyspace and
# will not work for getting multiple values in a hash from the store. If
# you need to access values nested deeper in the keyspace use #for_prefix
# to move deeper in the nesting.
#
# @param key [String, Symbol] The key to fetch
# @param ttl [ActiveSupport::Duration] The TTL for the value in the cache,
# defaults to value supplied to the constructor.
# @return [Imperium::KVGETResponse]
def fetch_object(key, ttl: @default_ttl)
full_key = "#{@prefix}/#{key}"
Cache.fetch(full_key, ttl: ttl) do
@kv_client.get(full_key, *CONSUL_READ_OPTIONS)
end
rescue Imperium::TimeoutError => exception
Cache.fallback_fetch(full_key).tap do |val|
if val
Canvas::Errors.capture_exception(:consul, exception)
val
else
raise
end
end
end
# Extend the prefix from this instance returning a new one.
#
# @param prefix_extension [String]
# @param default_ttl [ActiveSupport::Duration] The default TTL to use when
# fetching keys from the extended keyspace, defaults to the same value as
# the receiver
# @return [ProxyPrefix]
def for_prefix(prefix_extension, default_ttl: @default_ttl)
self.class.new(
"#{@prefix}/#{prefix_extension}",
default_ttl: default_ttl,
kv_client: @kv_client
)
end
end
end
end

View File

@ -140,19 +140,6 @@ Canvas::Plugin.register('kaltura', nil, {
:settings_partial => 'plugins/kaltura_settings',
:validator => 'KalturaValidator'
})
Canvas::Plugin.register('mathman', nil, {
:name => lambda{ t :name, 'MathMan' },
:description => lambda{ t :description, 'A simple microservice that converts LaTeX formulae to MathML and SVG'},
:author => 'Instructure',
:author_website => 'http://www.instructure.com',
:version => '1.0.0',
:settings_partial => 'plugins/mathman_settings',
:validator => 'MathmanValidator',
:settings => {
use_for_svg: false,
use_for_mml: false
}
})
Canvas::Plugin.register('wimba', :web_conferencing, {
:name => lambda{ t :name, "Wimba" },
:description => lambda{ t :description, "Wimba web conferencing support" },

View File

@ -1,29 +0,0 @@
#
# Copyright (C) 2011 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
module Canvas::Plugins::Validators::MathmanValidator
def self.validate(settings, plugin_setting)
base_url = settings[:base_url]
if base_url.match(URI.regexp)
settings.to_hash.with_indifferent_access
else
plugin_setting.errors.add(:base, I18n.t('Must provide a valid url.'))
false
end
end
end

View File

@ -15,9 +15,13 @@
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
require "addressable/uri"
module MathMan
def self.url_for(latex:, target:)
"#{base_url}/#{target}?tex=#{latex}"
uri = base_url.merge(path: "/#{target}")
uri.query = "tex=#{latex}"
uri.to_s
end
def self.use_for_mml?
@ -36,15 +40,24 @@ module MathMan
end
end
private
def self.base_url
with_plugin_settings do |plugin_settings|
plugin_settings[:base_url].sub(/\/$/, '')
class << self
private
def base_url
with_plugin_settings do |plugin_settings|
Addressable::URI.parse(plugin_settings[:base_url])
end
end
def with_plugin_settings
plugin_settings = Canvas::DynamicSettings.for_prefix('math-man')
yield plugin_settings
rescue Canvas::DynamicSettings::NoFallbackError
if Rails.env.production?
raise
else
yield({})
end
end
end
def self.with_plugin_settings
plugin_settings = Canvas::Plugin.find(:mathman).settings
yield plugin_settings
end
end

View File

@ -103,13 +103,10 @@ describe ConsulInitializer do
describe "just from loading" do
it "clears the DynamicSettings cache on reload" do
Canvas::DynamicSettings.reset_cache!
Canvas::DynamicSettings.cache["key"] = {
value: "value",
timestamp: Time.zone.now.to_i
}
Canvas::DynamicSettings::Cache.insert('key', 'value')
expect(Canvas::DynamicSettings.from_cache("key")).to eq("value")
Canvas::Reloader.reload!
expect(Canvas::DynamicSettings.cache).to eq({})
expect(Canvas::DynamicSettings::Cache.store).to eq({})
end
end
end

View File

@ -0,0 +1,102 @@
#
# Copyright (C) 2015 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
require 'spec_helper'
require_dependency "canvas/dynamic_settings"
require 'imperium/testing' # Not loaded by default
module Canvas
class DynamicSettings
RSpec.describe Cache do
before do
Cache.reset!
end
describe '.fallback_fetch(key)' do
it 'must return the stored value even if expired' do
Cache.fetch('foobar', ttl: 2.seconds) do
42
end
Timecop.travel(1.minute.from_now) do
expect(Cache.fallback_fetch('foobar')).to eq 42
end
end
end
describe '.fetch(key, ttl: nil)' do
it 'must return the value returned by the supplied block' do
val = Cache.fetch('foobar') do
42
end
expect(val).to eq 42
end
it 'must capture the return value from the block in the store' do
Cache.fetch('foobar') do
42
end
expect(Cache.store).to include 'foobar' => Cache::Value.new(42)
end
it 'must return the stored value rather than calling the block again on future calls' do
Cache.fetch('foobar') do
42
end
val = Cache.fetch('foobar') do
51
end
expect(val).to eq 42
end
it 'must call the block again when the ttl has elapsed' do
called = false
Cache.fetch('foobar', ttl: 5.minutes) do
42
end
Timecop.travel(10.minutes.from_now) do
Cache.fetch('foobar', ttl: 5.minutes) do
called = true
end
expect(called).to eq true
end
end
it 'must not cache not found responses' do
Cache.fetch('foobar', ttl: 5.minutes) do
Imperium::Testing.kv_not_found_response
end
expect(Cache.store).to be_empty
end
it 'must update the TTL on the cached value if it was previously nil' do
Cache.insert('foo', 'bar')
Cache.fetch('foo', ttl: 3.minutes)
expect(Cache.store['foo'].expiration_time).to_not be_nil
end
end
describe '.reset!' do
it 'must clear the stored values' do
described_class.store['foo/bar'] = 'value'
described_class.reset!
expect(described_class.store).to be_empty
end
end
end
end
end

View File

@ -0,0 +1,62 @@
# Copyright (C) 2017 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
require 'spec_helper'
require_dependency "canvas/dynamic_settings"
module Canvas
class DynamicSettings
RSpec.describe FallbackProxy do
let(:fallback_data) do
{
foo: 'bar',
baz: {
qux: 42
}
}
end
let(:proxy) { FallbackProxy.new(fallback_data) }
describe '#initalize' do
it 'must store an empty hash when initialized with nil' do
proxy = FallbackProxy.new(nil)
expect(proxy.data).to eq({})
end
end
describe '#fetch(key, ttl: nil)' do
it 'must return the value from the data hash' do
expect(proxy.fetch('foo')).to eq 'bar'
end
it 'must return nil when the val' do
expect(proxy.fetch('nx-key')).to be_nil
end
end
describe '#fetch_object(key, ttl: nil)' do
it 'must return a thing that squawks like an Imperium::KVGETResponse'
end
describe '#for_prefix(key, default_ttl: nil)' do
it 'must return a new instance populated with the sub hash found at the specified key' do
new_proxy = proxy.for_prefix('baz')
expect(new_proxy.data).to eq({qux: 42}.with_indifferent_access)
end
end
end
end
end

View File

@ -0,0 +1,116 @@
# Copyright (C) 2017 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
require 'spec_helper'
require_dependency "canvas/dynamic_settings"
require 'imperium/testing' # Not loaded by default
module Canvas
class DynamicSettings
RSpec.describe PrefixProxy do
let(:client) { instance_double(Imperium::KV) }
let(:proxy) { PrefixProxy.new('foo/bar', default_ttl: 3.minutes, kv_client: client) }
after(:each) do
Cache.reset!
end
describe '.fetch(key, ttl: @default_ttl)' do
it 'must return nil when no value was found' do
allow(client).to receive(:get)
.and_return(
Imperium::Testing.kv_not_found_response(options: [:stale])
)
expect(proxy.fetch('baz')).to be_nil
end
it 'must return the value for the specified key' do
allow(client).to receive(:get)
.and_return(
Imperium::Testing.kv_get_response(
body: [
{ Key: "foo/bar/baz", Value: 'qux'},
],
options: [:stale],
)
)
expect(proxy.fetch('baz')).to eq 'qux'
end
end
describe 'fetch_object(key, ttl: @default_ttl)' do
it 'must fetch the value from consul using the prefix and supplied key' do
expect(client).to receive(:get).with('foo/bar/baz', an_instance_of(Symbol))
proxy.fetch_object('baz')
end
it 'must use the dynamic settings cache for previously fetched values' do
expect(Cache).to receive(:fetch).with('foo/bar/baz', ttl: 3.minutes)
proxy.fetch_object('baz')
end
it "must fall back to expired cached values when consul can't be contacted" do
Cache.store['foo/bar/baz'] = Cache::Value.new('qux', 3.minutes.ago)
expect(client).to receive(:get).and_raise(Imperium::TimeoutError)
val = proxy.fetch_object('baz')
expect(val).to eq 'qux'
end
it "must log the connection failure when consul can't be contacted" do
Cache.store['foo/bar/baz'] = Cache::Value.new('qux', 3.minutes.ago)
expect(Canvas::Errors).to receive(:capture_exception).
with(:consul, an_instance_of(Imperium::TimeoutError))
allow(client).to receive(:get).and_raise(Imperium::TimeoutError)
proxy.fetch_object('baz')
end
it "must raise an exception when consul can't be reached and no previous value is found" do
expect(client).to receive(:get).and_raise(Imperium::TimeoutError)
expect { proxy.fetch_object('baz') }.to raise_error(Imperium::TimeoutError)
end
end
describe 'for_prefix(prefix_extension, default_ttl: @default_ttl)' do
it 'must instantiate a new proxy with the supplied prefix extnsion appended to the current prefix' do
new_proxy = proxy.for_prefix('baz')
expect(new_proxy).to be_a PrefixProxy
expect(new_proxy.prefix).to eq 'foo/bar/baz'
end
it "must pass on the current instance's default ttl if not supplied" do
proxy
expect(PrefixProxy).to receive(:new)
.with(an_instance_of(String), a_hash_including(default_ttl: 3.minutes))
proxy.for_prefix('baz')
end
it 'must pass on the supplied default ttl' do
proxy
expect(PrefixProxy).to receive(:new)
.with(an_instance_of(String), a_hash_including(default_ttl: 5.minutes))
proxy.for_prefix('baz', default_ttl: 5.minutes)
end
it 'must pass on the kv client from the receiving proxy' do
proxy
expect(PrefixProxy).to receive(:new)
.with(an_instance_of(String), a_hash_including(kv_client: client))
proxy.for_prefix('baz')
end
end
end
end
end

View File

@ -117,6 +117,27 @@ module Canvas
end
end
describe '.fallback_data =' do
before(:each) do
@original_fallback = DynamicSettings.fallback_data
end
after(:each) do
DynamicSettings.fallback_data = @original_fallback
end
it 'must convert the supplied hash to one with indifferent access' do
DynamicSettings.fallback_data = {}
expect(DynamicSettings.fallback_data).to be_a(ActiveSupport::HashWithIndifferentAccess)
end
it 'must clear the fallback data when passed nil' do
DynamicSettings.fallback_data = {}
DynamicSettings.fallback_data = nil
expect(DynamicSettings.fallback_data).to be_nil
end
end
describe ".find" do
describe "with consul config" do
# we don't need to interact with a real consul for unit tests
@ -250,6 +271,38 @@ module Canvas
end
end
describe '.for_prefix' do
before(:each) do
@original_fallback = DynamicSettings.fallback_data
end
after(:each) do
DynamicSettings.config = nil
DynamicSettings.fallback_data = @original_fallback
end
it 'must return a PrefixProxy when consul is configured' do
DynamicSettings.config = valid_config
proxy = DynamicSettings.for_prefix('foo')
expect(proxy).to be_a(DynamicSettings::PrefixProxy)
end
it 'must raise an error when neither consul or fallback data have been configured' do
DynamicSettings.config = nil
DynamicSettings.fallback_data = nil
expect { DynamicSettings.for_prefix('foo') }.to raise_error(
DynamicSettings::NoFallbackError,
/fallback_data is not set/
)
end
it 'must return a FallbackProxy when consul is not configured' do
DynamicSettings.fallback_data = {'foo' => {bar: 'baz'}}
proxy = DynamicSettings.for_prefix('foo')
expect(proxy).to be_a(DynamicSettings::FallbackProxy)
end
end
describe ".from_cache" do
before(:each){ DynamicSettings.config = valid_config } # just to be not nil
after(:each){ DynamicSettings.reset_cache! }
@ -323,16 +376,6 @@ module Canvas
end
end
it "accepts a timeout on a previously inifinity key" do
stub_consul_with("rce.insops.net")
value = DynamicSettings.from_cache(parent_key)
Timecop.travel(Time.zone.now + 11.minutes) do
stub_consul_with("CHANGED VALUE")
value = DynamicSettings.from_cache(parent_key, expires_in: 10.minutes)
expect(value["app-host"]).to eq("CHANGED VALUE")
end
end
context "using catastrophic cache fallback" do
let!(:now) { Time.zone.now }

View File

@ -1,61 +0,0 @@
#
# Copyright (C) 2016 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
require 'spec_helper'
describe Canvas::Plugins::Validators::MathmanValidator do
describe '.validate' do
let(:plugin_setting) do
PluginSetting.new(
name: 'mathman',
settings: PluginSetting.settings_for_plugin('mathman')
)
end
let(:settings) do
{
base_url: 'http://mathman.docker'
}
end
subject(:validator) do
Canvas::Plugins::Validators::MathmanValidator.validate(settings, plugin_setting)
end
it 'should return provided settings when base_url is a valid url' do
expect(validator).to eq settings.with_indifferent_access
end
context 'when base_url is invalid' do
let(:settings) do
{
base_url: 'wooper'
}
end
it 'returns false' do
expect(validator).to be_falsey
end
it 'adds errors to plugin_setting' do
expect(plugin_setting.errors[:base]).to be_empty, 'precondition'
validator
expect(plugin_setting.errors[:base]).not_to be_empty
end
end
end
end

View File

@ -26,14 +26,18 @@ describe MathMan do
let(:use_for_svg) { false }
before do
PluginSetting.create(
name: 'mathman',
settings: {
@original_fallback = Canvas::DynamicSettings.fallback_data
Canvas::DynamicSettings.fallback_data = {
'math-man': {
base_url: service_url,
use_for_mml: use_for_mml,
use_for_svg: use_for_svg
}
)
}
end
after do
Canvas::DynamicSettings.fallback_data = @original_fallback
end
describe '.url_for' do

View File

@ -114,15 +114,16 @@ describe "RequestContextGenerator" do
before(:each) do
Thread.current[:context] = nil
Canvas::DynamicSettings.reset_cache!
Canvas::DynamicSettings.cache['canvas'] = {
timetamp: Time.zone.now.to_i,
value: { "signing-secret" => shared_secret }
Canvas::DynamicSettings.fallback_data = {
canvas: {
'signing-secret' => shared_secret
}
}
env['HTTP_X_REQUEST_CONTEXT_ID'] = Canvas::Security.base64_encode(remote_request_context_id)
env['HTTP_X_REQUEST_CONTEXT_SIGNATURE'] = Canvas::Security.base64_encode(remote_signature)
end
after(:each){ Canvas::DynamicSettings.reset_cache! }
after(:each){ Canvas::DynamicSettings.fallback_data = {} }
def run_middleware
_, headers, _msg = RequestContextGenerator.new(->(_){ [200, {}, []] }).call(env)