I'm implementing a custom resource which is basically a facade to an existing resource (in the example below its the vault_certificate resource).
Using the existing resource this code is valid:
certificate = vault_certificate 'a common name' do
combine_certificate_and_chain true
output_certificates false # Just to decrease the chef-client run output
vault_path "pki/issue/#{node['deployment']}"
end
template "a path" do
source 'nginx/dummy.conf.erb'
variables(
certificate: certificate.certificate_filename
key: certificate.key_filename
)
end
Notice I can invoke certificate.certificate_filename or certificate.key_filename. Or more generally I can read any property defined by the vault_certificate resource.
Now with the new resource (sort of a facade to vault_certificate)
provides :vault_certificate_handle_exceptions
unified_mode true
property :common_name, String, name_property: true
property :max_retries, Integer, default: 5
action :create do
require 'retries'
# new_resource.max_retries is being used inside the retry_options. I omitted that part as its not relevant for the question
with_retries(retry_options) do
begin
vault_certificate new_resource.common_name do
combine_certificate_and_chain true
output_certificates false # Just to decrease the chef-client run output
vault_path "pki/issue/#{node['deployment']}"
ignore_failure :quiet
end
rescue Vault::HTTPClientError => e
data = JSON.parse(e.errors)['data']
if data['error'] == 'Certificate not found locally'
# This error is one we can recover from (actually we are expecting it). This raise with VaultCertificateError will trigger the with_retries.
raise VaultCertificateError.new("Waiting for the certificate to appear in the store (because I'm not the leader)", data)
else
# Any other error means something really went wrong.
raise e
end
end
end
end
If I now use this resource and try to invoke .certificate_filename or .key_filename:
certificate = vault_certificate_handle_exceptions 'a common name' do
action :create
end
template "a path" do
source 'nginx/dummy.conf.erb'
variables(
certificate: certificate.certificate_filename
key: certificate.key_filename
)
end
I get an error saying the method certificate_filename (or key_filename) is not defined for vault_certificate_handle_exceptions. To solve it I resorted to this hack:
provides :vault_certificate_handle_exceptions
unified_mode true
property :common_name, String, name_property: true
property :max_retries, Integer, default: 5
action :create do
require 'retries'
# new_resource.max_retries is being used inside the retry_options. I omitted that part as its not relevant for the question
with_retries(retry_options) do
begin
cert = vault_certificate new_resource.common_name do
combine_certificate_and_chain true
output_certificates false # Just to decrease the chef-client run output
vault_path "pki/issue/#{node['deployment']}"
ignore_failure :quiet
end
# These lines ensure we can read the vault_certificate properties as if they were properties of this resource (vault_certificate_handle_exceptions)
Chef::ResourceResolver.resolve(cert.resource_name).properties.keys.each do |name|
new_resource.send(:define_singleton_method, name.to_sym) do
cert.send(name.to_sym)
end
end
rescue Vault::HTTPClientError => e
data = JSON.parse(e.errors)['data']
if data['error'] == 'Certificate not found locally'
# This error is one we can recover from (actually we are expecting it). This raise with VaultCertificateError will trigger the with_retries.
raise VaultCertificateError.new("Waiting for the certificate to appear in the store (because I'm not the leader)", data)
else
# Any other error means something really went wrong.
raise e
end
end
end
end
Is there a cleaner way to achieve this? If not, is there a more direct way to list all the properties of a resource? I thought cert.properties would work, but no luck there.
I inherited a cookbook utilizing another cookbook with a custom resource.
I'm trying to figure out the use of the new_resource in this code.
In the code below, path new_resource.fullpath causes a "non-defined method."
When I changed that line to simply path fullpath to reference the local variable, it worked just fine. I can't figure out what the previous intent was.
I'm sure this is an easy one. Looking for an ELI5. Thanks!
Full code Block.
resource_name :mb_worker_role
default_action :create
property :service_name, String, name_property: true
property :root_path, String, required: true
property :executable, String, required: true
property :wcf_port, [Integer, String], default: 808
property :health_port, [Integer, String], default: 8000
property :use_dummy, [TrueClass, FalseClass], default: true
action :create do
# Create firewall allow for WCF port
windows_firewall_rule "#{new_resource.service_name} - WCF" do
localport new_resource.wcf_port.to_s
protocol 'TCP'
firewall_action :allow
end
# Create firewall allow for health port
windows_firewall_rule "#{new_resource.service_name} - Health" do
localport new_resource.health_port.to_s
protocol 'TCP'
firewall_action :allow
end
# Full path to service executable at root_path\executable
fullpath = "#{new_resource.root_path}\\#{new_resource.executable}"
# Create directory for worker role application root
directory new_resource.root_path do
recursive true
action :create
end
# Set NetTCPPortSharing to start on demand
windows_service 'NetTCPPortSharing' do
action :configure_startup
startup_type :manual
end
# Stage the dummy worker role executable if requested
# Only used to verify this resource when testing
if property_is_set?(:use_dummy)
cookbook_file 'Stage dummy worker role executable' do
# path new_resource.fullpath
path fullpath
cookbook 'mb_worker_role'
source 'WorkerRole.Default.exe'
only_if { new_resource.use_dummy }
action :create
end
end
# Create windows service if it does not exist
powershell_script "Installing Windows service #{new_resource.service_name}" do
code <<-EOH
# Create the Windows service
sc.exe create "#{new_resource.service_name}" binPath="#{fullpath}"
EOH
not_if "(Get-Service -Name #{new_resource.service_name}).Name -eq '#{new_resource.service_name}'"
end
# Set service to automatic and make sure it's running
windows_service new_resource.service_name do
action [:configure_startup, :enable, :start]
startup_type :automatic
end
end
path fullpath is a magical alias system that we actively discourage at this point. It was a nice thing to try, but the syntax has a lot of problems that result in unexpected behavior. Use new_resource.fullpath, far fewer footguns.
I am using swagger doc to generate API document in grape api. API parameters are defined like:
desc "Get results by query"
params do
requires :type, type: String, desc: "search type"
requires :body, type: Hash, desc: "query body"
optional :user, type: Hash, desc: "support search as other user, otherwise search as current user" do
requires :id, type: String, desc: "user id"
requires :name, type: String, desc: "user name (dn)"
requires :directory, type: String, desc: "directory user belongs to"
end
end
post :query do
The generated JSON only shows the user related parameters. How can I define the hash parameters whose key is not restricted and could be generated in API document?
I've created an RSpec test to simply test if my model is valid with the given info, it should be, yet my test is still failing. I'm hoping someone can see why since I've stared at this all day yesterday.
I'm also using MongoDB (not sure if that matters).
models/stock.rb
class Stock
include Mongoid::Document
field :symbol, type: String
field :last_trade_price, type: Integer
field :ask, type: Integer
field :change, type: Integer
field :change_percent, type: String
field :market_cap, type: String
field :avg_volume, type: Integer
field :change_from_year_high, type: Integer
field :change_from_year_low, type: Integer
field :change_from_year_high_percent, type: Integer
field :change_from_year_low_percent, type: Integer
field :year_high, type: Integer
field :year_low, type: Integer
field :day_high, type: Integer
field :day_low, type: Integer
field :day_range, type: String
field :ebitda, type: String
field :eps_estimate_current_year, type: Integer
field :eps_estimate_next_year, type: Integer
field :eps_estimate_next_quarter, type: Integer
validates :symbol, :last_trade_price, :ask, :change, :change_percent, :market_cap,
:avg_volume, :change_from_year_high, :change_from_year_low, :change_from_year_high_percent,
:change_from_year_low_percent, :year_high, :year_low, :day_high, :day_low, :day_range,
:ebitda, :eps_estimate_current_year, :eps_estimate_next_year, :eps_estimate_next_quarter, presence: true
validates :last_trade_price, :ask, :change, :avg_volume,
:change_from_year_high, :change_from_year_low, :change_from_year_high_percent,
:change_from_year_low_percent, :year_high, :year_low, :day_high, :day_low,
:eps_estimate_current_year, :eps_estimate_next_year, :eps_estimate_next_quarter, numericality: true
validates_uniqueness_of :symbol
end
spec/factories.rb
FactoryGirl.define do
factory :stock do
symbol "AAPL"
last_trade_price 92.51
ask 92.78
change -0.91
change_percent "-0.91 - -0.97"
market_cap "512.93B"
avg_volume 37776500
change_from_year_high -40.46
change_from_year_low 0.66
change_from_year_high_percent -30.43
change_from_year_low_percent 0.72
year_high 132.97
year_low 91.85
day_high 93.57
day_low 92.46
day_range "92.46 - 93.57"
ebitda "82.79B"
eps_estimate_current_year 8.29
eps_estimate_next_year 9.15
eps_estimate_next_quarter 1.67
end
end
spec/models/stock_spec.rb
describe Stock do
let(:stock) { build(:stock) }
it "should be valid if all information is provided" do
expect(stock).to be_valid
end
end
My output from running the rspec test is:
Failures:
1) Stock should be valid if all information is provided
Failure/Error: expect(stock).to be_valid
expected `#<Stock _id: 5734dd60b8066872f6000000, symbol: "AAPL", last_trade_price: 92, ask: 92, change: 0, change_percent: "-0.91 - -0.97", market_cap: "512.93B", avg_volume: 37776500, change_from_year_high: -40, change_from_year_low: 0, change_from_year_high_percent: -30, change_from_year_low_percent: 0, year_high: 132, year_low: 91, day_high: 93, day_low: 92, day_range: "92.46 - 93.57", ebitda: "82.79B", eps_estimate_current_year: 8, eps_estimate_next_year: 9, eps_estimate_next_quarter: 1>.valid?` to return true, got false
# ./spec/models/stock_spec.rb:5:in `block (2 levels) in <top (required)>'
Finished in 0.02311 seconds (files took 1.72 seconds to load)
1 examples, 1 failure
Failed examples:
rspec ./spec/models/stock_spec.rb:4 # Stock should be valid if all information is provided
Randomized with seed 36574
From looking at the error, it seems that all of the information was built into the factory test object, so I'm unsure why the test is getting false instead of the true it's expecting.
Thanks for any help!
You can test what fields are giving an error by modifiying the spec:
describe Stock do
let(:stock) { build(:stock) }
it "should be valid if all information is provided" do
#expect(stock).to be_valid
stock.valid?
expect(stock.errors.full_messages).to eq []
end
end
However even as such the spec has very little actual value - you're just testing that your factory has all the required fields. If it didn't you would get failures in other specs anyways.
Also if you are grouping a bunch of similar validations by type you might want to use the longhand methods instead as it is much easier to read:
validates_presence_of :symbol, :last_trade_price, :ask, :change, :change_percent, :market_cap,
:avg_volume, :change_from_year_high, :change_from_year_low, :change_from_year_high_percent,
:change_from_year_low_percent, :year_high, :year_low, :day_high, :day_low, :day_range,
:ebitda, :eps_estimate_current_year, :eps_estimate_next_year, :eps_estimate_next_quarter
Added
When defining factories you should use sequences or computed properties to ensure that unique fields are unique - otherwise your validations will fail if you create more than one record from your factory!
FactoryGirl.define do
factory :stock do
sequence :symbol do |n|
"TEST-#{n}"
end
last_trade_price 92.51
ask 92.78
change -0.91
change_percent "-0.91 - -0.97"
market_cap "512.93B"
avg_volume 37776500
change_from_year_high -40.46
change_from_year_low 0.66
change_from_year_high_percent -30.43
change_from_year_low_percent 0.72
year_high 132.97
year_low 91.85
day_high 93.57
day_low 92.46
day_range "92.46 - 93.57"
ebitda "82.79B"
eps_estimate_current_year 8.29
eps_estimate_next_year 9.15
eps_estimate_next_quarter 1.67
end
end
Gems like FFaker are really helpful here. See the FactoryGirl docs for more info.
Also you should use a gem like database_cleaner (Yes it works for mongoid) to clean out your database between specs - the reason your validation is currently failing is that you have residual test state from some other test which is effecting the result.
# model.rb
validates :employee_id, presence: true, uniqueness: true
When left empty, the error message says "Employee can't be blank" when I want it to say "Employee ID can't be blank".
I resolved this by:
# model.rb
validates :employee_id, presence: { message: " ID can't be blank" }, uniqueness: true
which outputs "Employee ID can' be blank".
However, this isn't a really good solution IMO. I would like some means of customizing the entire message, including the attribute prefix.
Is there a simple way to do this?
There are several "correct" ways to go about this, but you definitely shouldn't do it via the validation itself, or by defining your own validation method.
On a model-by-model level, this is controlled by the class-level human_attribute_name method.
If you want your model's employee_id field to be a special case where the _id postfix isn't truncated, define that special case by overridding human_attribute_name:
class MyModel
validates :employee_id, presence: true
def self.human_attribute_name(attr, options = {})
attr == :employee_id ? 'Employee ID' : super
end
end
In broader terms, you can redefine human_attribute_name on ActiveRecord::Base to override handling of all _id attributes, but I doubt you want to do this. Generally, it's a good thing that Rails drops the _id postfix.
The second (and probably better) mechanism is to simply rely on localization. ActiveRecord ties into your locale YAML files for just about everything. If you want your employee_id field to humanize to Employee ID regardless of language, you'll need to edit your YAML files.
# config/locales/en.yml
en:
activerecord:
attributes:
employee_id: "Employee ID"
You can override human_attribute_name and always send default value with id
class MyModel
def self.human_attribute_name(attribute, options = {})
super(attribute, { default: attribute.to_s.humanize(keep_id_suffix: true) } )
end
end
You can write a custom validation. By doing it that way, you get to define the error message inside the validation method.
The relevant section from Rails Guides is here: Performing Custom Validations
Something like:
Class Paystub
validate :employee_id_is_not_blank
def employee_id_is_not_blank
errors[:base] << "Employee must be a part of the record.") if id.blank?
end
end
p = Paystub.create
p.errors.full_messages #=> ["Employee must be a part of the record."]
Section 7.4 in the Rails Guides specified using errors[:base]. Error messages shoveled into :base don't require an attribute to be tied to them.
Update: This is not the right answer. See #meagars answer above.