How do you create pretty json in CHEF (ruby) - ruby

How would you make an erb template that has human readable json?
The following code works, but it makes a flat json file
default.rb
default['foo']['bar'] = { :herp => 'true', :derp => 42 }
recipe.rb
template "foo.json" do
source 'foo.json.erb'
variables :settings => node['foo']['bar'].to_json
action :create
end
foo.json.erb
<%= #settings %>
Similar SO questions
Chef and ruby templates - how to loop though key value pairs?
How can I "pretty" format my JSON output in Ruby on Rails?

As pointed out by this SO Answer .erb templates are great for HTML, and XML, but is not good for json.
Luckily, CHEF uses its own json library which has support for this using .to_json_pretty
#coderanger in IRC, pointed out that you can use this library right inside the recipe. This article shows more extensively how to use chef helpers in recipes.
default.rb
# if ['foo']['bar'] is null, to_json_pretty() will error
default['foo']['bar'] = {}
recipe/foo.rb
pretty_settings = Chef::JSONCompat.to_json_pretty(node['foo']['bar'])
template "foo.json" do
source 'foo.json.erb'
variables :settings => pretty_settings
action :create
end
Or more concise as pointed out by YMMV
default.rb
# if ['foo']['bar'] is null, to_json_pretty() will error
default['foo']['bar'] = {}
recipe/foo.rb
template "foo.json" do
source 'foo.json.erb'
variables :settings => node['foo']['bar']
action :create
end
templates/foo.json.erb
<%= Chef::JSONCompat.to_json_pretty(#settings) %>

Something like this would also work:
file "/var/my-file.json" do
content Chef::JSONCompat.to_json_pretty(node['foo']['bar'].to_hash)
end

<%= Chef::JSONCompat.to_json_pretty(#settings) %> Works like Charm !!

Related

Chef Custom Resources : mutable property or an alternative

I read that as of 2016 OpsCode recommends against LWRP and the alternative HWRP. They rather recommend to use custom resources.
While this makes sense it leaves a lot to be desired in terms of leveraging ruby (unless I've misunderstood?)
I would like to be able to modify an array based on some boolean properties. I can then use that array to pass to the template, as in:
property :get_iptables_info, [TrueClass, FalseClass], default: false
property :get_pkglist, [TrueClass, FalseClass], default: false
property :cw_logs, Array, default: [], required: false
action :create do
ruby_block 'cw_iptables' do
block do
new_resource.cw_logs.push({ "#{new_resource.custom_dir}/iptables/iptables.txt" => { "log_group_name" => new_resource.log_group_name+"/iptables"}})
end
action :run
only_if {new_resource.get_iptables_info}
end
template "my_template" do
variables ({:logstreams => cw_logs})
end
end
Then in my template:
<% #logstreams.each_pair do |path, _object| %>
["#{path}"]
log_group_name = _object["log_group_name"]
<% end %>
The problem is that properties are immutable. So I get the error:
RuntimeError
------------
ruby_block[cw_iptables] (/tmp/kitchen/cache/cookbooks/custom_cw/resources/logs.rb line 43) had an error: RuntimeError: can't modify frozen Array
What's the correct way to do this? What's the correct way to write ruby code within the resource so that it's more modular and uses methods/functions?
Mutating properties inside a custom resource is generally a bad idea (there are exceptions but this isn't one of them). Better would be to use a local variable. Related, you don't need to use a ruby_block like that when you're already in a custom resource's action:
action :create do
cw_logs = new_resource.cw_logs
if new_resource.get_iptables_info
cw_logs += [whatever extra stuff you want]
end
template "my_template" do
variables logstreams: cw_logs
end
end
Notably that's using += instead of push which doesn't mutate the original. If you used push you would want cw_logs = new_resource.cw_logs.dup or similar.

erb file with chef syntax

Trying to output the contents of
node['a'] = {:b "1" :c "2"}
by doing this:
a:
<% a = node['a'] %>
b: <% a[:b] %>
c: <% a[:c] %>
<% end %>
to generate this:
a:
b: 1
c: 2
However not entirely sure the correct syntax to do this being new to ruby, chef and erb.
Okay, so let's rewind a bit. The first thing is that you generally don't want to reference node attributes directly in templates. In some cases like attributes coming from Ohai it can be okay as a shorthand, but for important data I would also pass it in via the variables property like this:
template '/etc/whatever.conf' do
source 'whatever.conf.erb'
variables a: node['a']
end
With that in place we've now expose the data as a template variable. The second piece of improving this is to let Ruby do the heavy lifting of generating YAML. We can do this using the .to_yaml method in the template:
<%= #a.to_yaml %>
That should be all you need!

Test an ERB Template

I've written a template that does some stuff to a JSON object, as an example:
{
"list": [
<% ['a', 'b', 'c', 'd'].each do |letter| %>
<%= puts "{ \"letter\": \"" + letter + "\" }," %>
<% end %>
]
}
How can I write a unit test to check if the JSON output is valid? I'm new to Ruby so I don't really know the tools I would use for this. Also, is there a better way to make a list of JSON objects in an ERB template?
I don't have access to, or knowledge of the thing that's processing the template so I'm not really positive about which Ruby libraries I can consume.
NOTE: RAILS is not involved. Just vanilla Ruby.
If you are just trying to return some object or list as JSON, you can call render json: object from your controller. You can test this the same way you can test any other controller: http://guides.rubyonrails.org/testing.html#functional-tests-for-your-controllers
If you absolutely need it in ERB for whatever reason, you can call the .to_json method on the object between ERB tags: <%= object.to_json %>.
The .to_json method will always return valid JSON.

Sinatra's way to render multiple .erb files

I have a web project that will render an HTML file, that is based on various .erb files.
I'm not sure about the best way, to do this, since each .erb file need to get specific information, such as cookie content.
Currently I have used this concept:
I have a directory with all of my .erb files, which get rendered using:
ERB.new(template).result
the rendered HTML will get returned to the main .erb template, which will get again rendered by sinatra, using:
erb :main
the result is pretty good, but i don't have the chance to include content from session based cookies, since .erb can not access them
I am pretty sure, the sinatra framework provides a better way to do this. A good way would be...
require 'sinatra'
enable :sessions
get "/" do
content1 = erb :template1, :locals => { :cookie => session[:cookie] }
content2 = erb :template2, :locals => { :cookie => session[:cookie] }
erb :mainTemplate, :locals => { :content => [content1, content2] }
end
... but, unfortunately it doesn't work that easy :(
Does anybody has a better idea?
Here is what I did for mine:
get '/Login' do verbose = params['verbose']
cTest = MyTests.new("envo")
result = cTest.login()
if verbose == 'true'
erb :newResultPage, :locals => {:result => result}
elsif verbose == 'false'
erb :resultPage, :locals => {:result => result}
end
end
Basically, I use a conditional statement to determine which page to use. This one is based on parameters. You could also use return results, or what ever other conditions you like.

RDiscount :generate_toc with Sinatra

So I have sinatra setup with Rdiscount to render a markdown file with a HAML layout. This all works but I want RDiscount to generate a table of contents based on the headers in my haml file. I've tried setting it in the sinatra configuration.
set :markdown, :generate_toc => true
but that doesn't seem to work.
I've also tried doing it when I render the markdown like so:
markdown :PAGENAMEHERE, :layout => :'layouts/PAGENAMEHERE', :generate_toc => true
which also doesn't work.
Is this even possible? If it is, what am I doing wrong?
While #three's answer helped me a lot, I would like to show a perhaps more general solution:
class MDWithTOC < ::Tilt::RDiscountTemplate
def flags
[:generate_toc]
end
end
Tilt.register MDWithTOC, 'md'
Here we override the flags method of the RDiscount Tilt template handler and regiter it as a handler for the md format.
Now you can use the md helper as always and it will generate the TOC.
This should work:
get '/' do
text = File.read('README.md')
markdown = RDiscount.new(text, :generate_toc)
body = markdown.to_html
haml :home, :layout => true, :locals => {:body => body}
end
You create the body directly via RDiscount and include it as plain HTML in your HAML layout. Whatever prevented toc creation should work this way natively. I found the solution via https://github.com/mjijackson/markdown/blob/master/app.rb

Resources