How can you access Chef LWRP attributes in a recipe - ruby

With some of the default chef resources, it is possible to access some of their attributes after they have been called
# recipes/default.rb
f = file "/tmp/file_resource" do
owner "root"
group "root"
mode "0755"
action :create
end
log "Path to my file is #{f.path}" # outputs "/tmp/file_resource"
How can this be achieved in a custom LWRP (here is an example)
# resources/default.rb
actions :create
default_action :create
attribute :relative_path, :kind_of => String, :name_attribute => true
attribute :full_path, :kind_of => String
In this provider, I am trying to update the property of new_resource.full_path to be equal to the path of the file resource
# providers/default.rb
action :create do
f = file "/path/to/my/resource/#{new_resource.relative_path}" do
owner "root"
group "root"
mode "0755"
action :create
end
new_resource.full_path(f.path)
new_resource.updated_by_last_action(f.updated_by_last_action?)
end
However when I try to access resource.full_path in the recipe, it is nil rather than the expected /path/to/my/resource/relative/path
# recipes/default.rb
resource = my_awesome_lwrp "relative/path" do
action :create
end
log "Full path for my resource: #{resource.full_path}" # outputs "Full path for my resource:"
This example is rather contrived I know, the real world application/reason for this can be seen in the default resource/provider here https://github.com/peterjmit/chef-ssl-cert

Related

Chef custom resource action properties not working

I'm having an issue with a custom resource I created in a new cookbook i'm working on. When I do a kitchen converge on the following setup I get the following error:
error: NoMethodError: undefined method `repository_url' for PoiseApplicationGit::Resource
Info about my development machine:
Chef Development Kit Version: 2.1.11
chef-client version: 13.2.20
delivery version: master (73ebb72a6c42b3d2ff5370c476be800fee7e5427)
berks version: 6.3.0
kitchen version: 1.17.0
inspec version: 1.33.1
Here's my custom resource under resources/deploy.rb:
resource_name :opsworks_deploy
property :app_path, String, name_property: true
property :app_name, String, required: true
property :repository_url, String, required: true
property :repository_key, String, required: true
property :short_name, String, required: true
property :app_type, String, required: true
property :app, Object, required: true
property :permission, String, required: true
action :deploy do
apt_update 'update'
# Install NGinx
package 'nginx' do
not_if { ::File.exist?("/etc/init.d/nginx") }
end
# Setup the app directory
directory my_app_path do
owner 'www-data'
group 'www-data'
mode '0755'
recursive true
end
slack_notify "notify_deployment_end" do
message "App #{app_name} deployed successfully"
action :nothing
end
slack_notify "notify_nginx_reload" do
message "Nginx has reloaded"
action :nothing
end
slack_notify "notify_nginx_config" do
message "Nginx site config has been updated for #{app_name}"
action :nothing
end
slack_notify "notify_git_deploy" do
message "App #{app_name} has been checkout out from git"
action :nothing
end
slack_notify "notify_file_permissions" do
message "App #{app_name} has been given proper file permissions"
action :nothing
end
# Deploy git repo from opsworks app
application app_path do
owner 'www-data'
group 'www-data'
git do
user 'root'
group 'root'
repository my_repo_url
deploy_key my_repo_key
notifies :notify, "slack_notify[notify_git_deploy]", :immediately
end
execute "chown-data-www" do
command "chown -R www-data:www-data #{my_app_path}"
user "root"
action :run
notifies :notify, "slack_notify[notify_file_permissions]", :immediately
end
# Setup the nginx config file for the site
template "/etc/nginx/sites-enabled/#{my_short_name}" do
source "#{my_app_type}.erb"
owner "root"
group "root"
mode 0644
variables( :app => my_app )
notifies :notify, "slack_notify[notify_nginx_config]", :immediately
end
# Reload nginx
service "nginx" do
supports :status => true, :restart => true, :reload => true, :stop => true, :start => true
action :reload
notifies :notify, "slack_notify[notify_nginx_reload]", :immediately
end
end
end
Here's my recipe under recipes/default.rb (all variables are set properly and to the proper types, i'm using test data bags and they are correctly passing):
command = search(:aws_opsworks_command).first
deploy_app = command[:args][:app_ids].first
app = search(:aws_opsworks_app, "app_id:#{deploy_app}").first
app_path = "/var/www/" + app[:shortname]
opsworks_deploy app_path do
app_name app[:name]
app_type app[:environment][:APP_TYPE]
repository_url app[:app_source][:url]
repository_key app[:app_source][:ssh_key]
short_name app[:shortname]
app app
permission '0755'
end
The only way I can get it working is if I add this at the top of the action declaration and update the properties within to match:
action :deploy do
my_repo_url = repository_url.dup
my_repo_key = repository_key.dup
my_app_path = app_path.dup
my_app_type = app_type.dup
my_short_name = short_name.dup
my_app = app.dup
...
Any reason why this is the case? Should I have to redeclare them like that in the action?
Use new_resource.repository_url and similar. The magic aliasing doesn't work correctly in a lot of circumstances and we've formally deprecated it as of 13.1 (though it's been not recommended for a while).

How do I use a Chef Resource in a Library

How do I include a Chef Directory resource in a library? When executing the recipe below, the variables are assigned but the directories aren't created. I don't receive any error messages.
This is a simplified example. Trying to create multiple directories by passing the directory name to a library.
# create directories
# /var/www/domain
# /var/www/domain/production
# /var/www/domain/staging
The library
# directory library
class DirectoryStructure
attr_accessor :domain, :production, :staging
def initialize(domain)
#domain = "/var/www/#{domain}"
#staging = "##domain/staging"
#production = "##domain/production"
end
def create(dir_type,directory_path,username,group)
dir = Chef::Resource::Directory.new(:create)
dir.name(directory_path)
dir.owner(username)
dir.group(group)
dir.mode(2750)
dir
end
end
The recipe
web_dirs = DirectoryStructure.new(domain)
site_dirs.create(web_dirs.domain,username,deploy_group)
site_dirs.create(web_dirs.production,username,deploy_group)
site_dirs.create(web_dirs.staging,username,deploy_group)
I have tried to use this example but I am obviously missing something.
enter link description herehttps://docs.chef.io/lwrp_custom_resource_library.html
This looks like you should be creating a resource, either an LWRP or a normal Ruby class. In most cases the LWRP will be simpler and is probably what you want in this case.
cookbooks/mycook/resources/directory_structure.rb:
default_action :create
attribute :domain, name_attribute: true
attribute :owner
attribute :group
cookbooks/mycook/providers/directory_structure.rb:
action :create do
directory "/var/www/#{#new_resource.domain}" do
owner new_resource.owner
group new_resource.group
end
directory "/var/www/#{#new_resource.domain}/production" do
owner new_resource.owner
group new_resource.group
end
directory "/var/www/#{#new_resource.domain}/staging" do
owner new_resource.owner
group new_resource.group
end
end
cookbooks/mycook/providers/directory_structure.rb:
mycook_directory_structure 'example.com' do
owner 'me'
group 'mygroup'
end

Chef : Pass parameters to a ruby_block in Chef

How do you pass parameters to a ruby_block in chef.
If I have
notifies :create, "ruby_block[createErb]", :immediately
and I want pass a parameter (fileToConvert) to this ruby_block (createErb) at the time that I notify.
ruby_block "createErb" do
block do
ErbCreator.new(fileToConvert)
end
action :nothing
end
How would I do this?
Short answer - you can't.
RubyBlock is a Chef resource, so it does not accept arbitrary parameters. In your example, I would recommend creating a Chef Extension (LWRP or HWRP):
In your resource:
# resources/erb_create.rb
actions :create
default_action :create
attribute :filename, name_attribute: true
# more attributes
And in your provider:
# providers/erb_create.rb
action(:create) do
ErbCreator.new(new_resource.filename)
# ... etc
end
Then in a recipe:
# recipes/default.rb
cookbook_erb_create 'filename'
You can read more about LWRPs on the Chef Docs.

How to define conditional abilities using cancan

I am using cancan gem for authorization. I have defined ability.rb module to initialize roles and their permissions for different modules.
`def initialize(user)
user ||= User.new # guest user (not logged in)
members = user.members
members.each do |member|
role = member.role
if role.presence
permissions = role.permissions
permissions.each do |permission|
actions = Array.new
actions = permission.activity_name.split(",") if permission.activity_name.presence
app_module = permission.application_module
actions.each do |action|
if app_module.module_name == "Project"
Rails.logger.debug "in if"
can action.downcase.to_sym, app_module.module_name.constantize, :id => member.project_id if action.presence
elsif app_module.module_name == "Sequence"
Rails.logger.debug "in sequence and project id is #{member.project_id} and action is #{action}"
can action.downcase.to_sym, app_module.module_name.constantize, :project_id => 0..1 if action.presence
else
Rails.logger.debug "Module name is #{app_module.module_name}"
can action.downcase.to_sym, app_module.module_name.constantize if action.presence
end
end
end
end
end
end`
Here user has assigned a project and project has many sequences.
If user has assigned a project with id 1 then I have to fetch only those sequences whose project id is 1
In controller I have written -
load_and_authorize_resource
The permissions for project controller is properly fetched but for sequence controller if I pass project id also it was not restricting to other project.
Can anyone please help me
Conditional abilities would be in your ability.rb file.
You would do something like this in your ability.rb file:
can [:<actions>], Sequence do |sequence|
sequence.project.user_id == user.id
end

Find or create record through factory_girl association

I have a User model that belongs to a Group. Group must have unique name attribute. User factory and group factory are defined as:
Factory.define :user do |f|
f.association :group, :factory => :group
# ...
end
Factory.define :group do |f|
f.name "default"
end
When the first user is created a new group is created too. When I try to create a second user it fails because it wants to create same group again.
Is there a way to tell factory_girl association method to look first for an existing record?
Note: I did try to define a method to handle this, but then I cannot use f.association. I would like to be able to use it in Cucumber scenarios like this:
Given the following user exists:
| Email | Group |
| test#email.com | Name: mygroup |
and this can only work if association is used in Factory definition.
You can to use initialize_with with find_or_create method
FactoryGirl.define do
factory :group do
name "name"
initialize_with { Group.find_or_create_by_name(name)}
end
factory :user do
association :group
end
end
It can also be used with id
FactoryGirl.define do
factory :group do
id 1
attr_1 "default"
attr_2 "default"
...
attr_n "default"
initialize_with { Group.find_or_create_by_id(id)}
end
factory :user do
association :group
end
end
For Rails 4
The correct way in Rails 4 is Group.find_or_create_by(name: name), so you'd use
initialize_with { Group.find_or_create_by(name: name) }
instead.
I ended up using a mix of methods found around the net, one of them being inherited factories as suggested by duckyfuzz in another answer.
I did following:
# in groups.rb factory
def get_group_named(name)
# get existing group or create new one
Group.where(:name => name).first || Factory(:group, :name => name)
end
Factory.define :group do |f|
f.name "default"
end
# in users.rb factory
Factory.define :user_in_whatever do |f|
f.group { |user| get_group_named("whatever") }
end
You can also use a FactoryGirl strategy to achieve this
module FactoryGirl
module Strategy
class Find
def association(runner)
runner.run
end
def result(evaluation)
build_class(evaluation).where(get_overrides(evaluation)).first
end
private
def build_class(evaluation)
evaluation.instance_variable_get(:#attribute_assigner).instance_variable_get(:#build_class)
end
def get_overrides(evaluation = nil)
return #overrides unless #overrides.nil?
evaluation.instance_variable_get(:#attribute_assigner).instance_variable_get(:#evaluator).instance_variable_get(:#overrides).clone
end
end
class FindOrCreate
def initialize
#strategy = FactoryGirl.strategy_by_name(:find).new
end
delegate :association, to: :#strategy
def result(evaluation)
found_object = #strategy.result(evaluation)
if found_object.nil?
#strategy = FactoryGirl.strategy_by_name(:create).new
#strategy.result(evaluation)
else
found_object
end
end
end
end
register_strategy(:find, Strategy::Find)
register_strategy(:find_or_create, Strategy::FindOrCreate)
end
You can use this gist.
And then do the following
FactoryGirl.define do
factory :group do
name "name"
end
factory :user do
association :group, factory: :group, strategy: :find_or_create, name: "name"
end
end
This is working for me, though.
I had a similar problem and came up with this solution. It looks for a group by name and if it is found it associates the user with that group. Otherwise it creates a group by that name and then associates with it.
factory :user do
group { Group.find_by(name: 'unique_name') || FactoryBot.create(:group, name: 'unique_name') }
end
I hope this can be useful to someone :)
To ensure FactoryBot's build and create still behaves as it should, we should only override the logic of create, by doing:
factory :user do
association :group, factory: :group
# ...
end
factory :group do
to_create do |instance|
instance.id = Group.find_or_create_by(name: instance.name).id
instance.reload
end
name { "default" }
end
This ensures build maintains it's default behavior of "building/initializing the object" and does not perform any database read or write so it's always fast. Only logic of create is overridden to fetch an existing record if exists, instead of attempting to always create a new record.
I wrote an article explaining this.
I was looking for a way that doesn't affect the factories. Creating a Strategy is the way to go, as pointed out by #Hiasinho. However, that solution didn't work for me anymore, probably the API changed. Came up with this:
module FactoryBot
module Strategy
# Does not work when passing objects as associations: `FactoryBot.find_or_create(:entity, association: object)`
# Instead do: `FactoryBot.find_or_create(:entity, association_id: id)`
class FindOrCreate
def initialize
#build_strategy = FactoryBot.strategy_by_name(:build).new
end
delegate :association, to: :#build_strategy
def result(evaluation)
attributes = attributes_shared_with_build_result(evaluation)
evaluation.object.class.where(attributes).first || FactoryBot.strategy_by_name(:create).new.result(evaluation)
end
private
# Here we handle possible mismatches between initially provided attributes and actual model attrbiutes
# For example, devise's User model is given a `password` and generates an `encrypted_password`
# In this case, we shouldn't use `password` in the `where` clause
def attributes_shared_with_build_result(evaluation)
object_attributes = evaluation.object.attributes
evaluation.hash.filter { |k, v| object_attributes.key?(k.to_s) }
end
end
end
register_strategy(:find_or_create, Strategy::FindOrCreate)
end
And use it like this:
org = FactoryBot.find_or_create(:organization, name: 'test-org')
user = FactoryBot.find_or_create(:user, email: 'test#test.com', password: 'test', organization: org)
Usually I just make multiple factory definitions. One for a user with a group and one for a groupless user:
Factory.define :user do |u|
u.email "email"
# other attributes
end
Factory.define :grouped_user, :parent => :user do |u|
u.association :group
# this will inherit the attributes of :user
end
THen you can use these in your step definitions to create users and groups seperatly and join them together at will. For example you could create one grouped user and one lone user and join the lone user to the grouped users team.
Anyway, you should take a look at the pickle gem which will allow you to write steps like:
Given a user exists with email: "hello#email.com"
And a group exists with name: "default"
And the user: "hello#gmail.com" has joined that group
When somethings happens....
I'm using exactly the Cucumber scenario you described in your question:
Given the following user exists:
| Email | Group |
| test#email.com | Name: mygroup |
You can extend it like:
Given the following user exists:
| Email | Group |
| test#email.com | Name: mygroup |
| foo#email.com | Name: mygroup |
| bar#email.com | Name: mygroup |
This will create 3 users with the group "mygroup". As it used like this uses 'find_or_create_by' functionality, the first call creates the group, the next two calls finds the already created group.
Another way to do it (that will work with any attribute and work with associations):
# config/initializers/factory_bot.rb
#
# Example use:
#
# factory :my_factory do
# change_factory_to_find_or_create
#
# some_attr { 7 }
# other_attr { "hello" }
# end
#
# FactoryBot.create(:my_factory) # creates
# FactoryBot.create(:my_factory) # finds
# FactoryBot.create(:my_factory, other_attr: "new value") # creates
# FactoryBot.create(:my_factory, other_attr: "new value") # finds
module FactoryBotEnhancements
def change_factory_to_find_or_create
to_create do |instance|
# Note that this will ignore nil value attributes, to avoid auto-generated attributes such as id and timestamps
attributes = instance.class.find_or_create_by(instance.attributes.compact).attributes
instance.attributes = attributes.except('id')
instance.id = attributes['id'] # id can't be mass-assigned
instance.instance_variable_set('#new_record', false) # marks record as persisted
end
end
end
# This makes the module available to all factory definition blocks
class FactoryBot::DefinitionProxy
include FactoryBotEnhancements
end
The only caveat is that you can't find by nil values. Other than that, it works like a dream

Resources