diff --git a/lib/puppet/provider/grafana_dashboard/grafana.rb b/lib/puppet/provider/grafana_dashboard/grafana.rb index 0add4c2..bdfd77e 100644 --- a/lib/puppet/provider/grafana_dashboard/grafana.rb +++ b/lib/puppet/provider/grafana_dashboard/grafana.rb @@ -1,167 +1,172 @@ # Copyright 2015 Mirantis, Inc. # require 'json' require File.expand_path(File.join(File.dirname(__FILE__), '..', 'grafana')) # Note: this class doesn't implement the self.instances and self.prefetch # methods because the Grafana API doesn't allow to retrieve the dashboards and # all their properties in a single call. Puppet::Type.type(:grafana_dashboard).provide(:grafana, parent: Puppet::Provider::Grafana) do desc 'Support for Grafana dashboards stored into Grafana' defaultfor kernel: 'Linux' def organization resource[:organization] end def grafana_api_path resource[:grafana_api_path] end def fetch_organizations response = send_request('GET', format('%s/orgs', resource[:grafana_api_path])) if response.code != '200' raise format('Fail to retrieve organizations (HTTP response: %s/%s)', response.code, response.body) end begin fetch_organizations = JSON.parse(response.body) fetch_organizations.map { |x| x['id'] }.map do |id| response = send_request 'GET', format('%s/orgs/%s', resource[:grafana_api_path], id) if response.code != '200' raise format('Failed to retrieve organization %d (HTTP response: %s/%s)', id, response.code, response.body) end fetch_organization = JSON.parse(response.body) { id: fetch_organization['id'], name: fetch_organization['name'] } end rescue JSON::ParserError raise format('Failed to parse response: %s', response.body) end end def fetch_organization unless @fetch_organization @fetch_organization = if resource[:organization].is_a?(Numeric) || resource[:organization].match(%r{^[0-9]*$}) fetch_organizations.find { |x| x[:id] == resource[:organization] } else fetch_organizations.find { |x| x[:name] == resource[:organization] } end end @fetch_organization end def folders response = send_request('GET', format('%s/folders', resource[:grafana_api_path])) if response.code != '200' raise format('Fail to retrieve the folders (HTTP response: %s/%s)', response.code, response.body) end begin @folders = JSON.parse(response.body) rescue JSON::ParserError raise format('Fail to parse folders (HTTP response: %s/%s)', response.code, response.body) end end def find_folder folders unless @folders begin @folders.each do |folder| @folder = folder if folder['title'] == resource[:folder] end raise format('Folder not found: %s', resource[:folder]) unless @folder['title'] == resource[:folder] rescue JSON::ParserError raise format('Fail to parse folder %s: %s', resource[:folder], response.body) end end # Return the list of dashboards def dashboards + # change organizations + response = send_request 'POST', format('%s/user/using/%s', resource[:grafana_api_path], fetch_organization[:id]) + unless response.code == '200' + raise format('Failed to switch to org %s (HTTP response: %s/%s)', fetch_organization[:id], response.code, response.body) + end response = send_request('GET', format('%s/search', resource[:grafana_api_path]), nil, q: '', starred: false) if response.code != '200' raise format('Fail to retrieve the dashboards (HTTP response: %s/%s)', response.code, response.body) end begin JSON.parse(response.body) rescue JSON::ParserError raise format('Fail to parse dashboards (HTTP response: %s/%s)', response.code, response.body) end end # Return the dashboard matching with the resource's title def find_dashboard return unless dashboards.find { |x| x['title'] == resource[:title] } response = send_request('GET', format('%s/dashboards/db/%s', resource[:grafana_api_path], slug)) if response.code != '200' raise format('Fail to retrieve dashboard %s (HTTP response: %s/%s)', resource[:title], response.code, response.body) end begin # Cache the dashboard's content @dashboard = JSON.parse(response.body)['dashboard'] rescue JSON::ParserError raise format('Fail to parse dashboard %s: %s', resource[:title], response.body) end end def save_dashboard(dashboard) find_folder if resource[:folder] # change organizations response = send_request 'POST', format('%s/user/using/%s', resource[:grafana_api_path], fetch_organization[:id]) unless response.code == '200' raise format('Failed to switch to org %s (HTTP response: %s/%s)', fetch_organization[:id], response.code, response.body) end data = { dashboard: dashboard.merge('title' => resource[:title], 'id' => @dashboard ? @dashboard['id'] : nil, 'version' => @dashboard ? @dashboard['version'] + 1 : 0), folderId: @folder ? @folder['id'] : nil, overwrite: !@dashboard.nil? } response = send_request('POST', format('%s/dashboards/db', resource[:grafana_api_path]), data) return unless (response.code != '200') && (response.code != '412') raise format('Fail to save dashboard %s (HTTP response: %s/%s', resource[:name], response.code, response.body) end def slug resource[:title].downcase.gsub(%r{[ \+]+}, '-').gsub(%r{[^\w\- ]}, '') end def content @dashboard.reject { |k, _| k =~ %r{^id|version|title$} } end def content=(value) save_dashboard(value) end def create save_dashboard(resource[:content]) end def destroy response = send_request('DELETE', format('%s/dashboards/db/%s', resource[:grafana_api_path], slug)) return unless response.code != '200' raise Puppet::Error, format('Failed to delete dashboard %s (HTTP response: %s/%s', resource[:title], response.code, response.body) end def exists? find_dashboard end end diff --git a/lib/puppet/provider/grafana_team/grafana.rb b/lib/puppet/provider/grafana_team/grafana.rb index 218df7e..37ca5bb 100644 --- a/lib/puppet/provider/grafana_team/grafana.rb +++ b/lib/puppet/provider/grafana_team/grafana.rb @@ -1,233 +1,231 @@ # frozen_string_literal: true require 'json' require File.expand_path(File.join(File.dirname(__FILE__), '..', 'grafana')) Puppet::Type.type(:grafana_team).provide(:grafana, parent: Puppet::Provider::Grafana) do desc 'Support for Grafana permissions' defaultfor kernel: 'Linux' def raise_on_error(code, message) raise message if code != '200' end def parse_response(data) JSON.parse(data) rescue JSON::ParserError raise format('Fail to parse response: %s', response.body) end def map_organizations(ids) ids.map do |id| response = send_request 'GET', format('%s/orgs/%s', resource[:grafana_api_path], id) raise_on_error(response.code, format('Failed to retrieve organization %d (HTTP response: %s/%s)', id, response.code, response.body)) organization = parse_response(response.body) { id: organization['id'], name: organization['name'] } end end def organizations response = send_request('GET', format('%s/orgs', resource[:grafana_api_path])) raise_on_error(response.code, format('Fail to retrieve organizations (HTTP response: %s/%s)', response.code, response.body)) organizations = JSON.parse(response.body) map_organizations(organizations.map { |x| x['id'] }) end def organization return @organization if @organization org = resource[:organization] key = org.is_a?(Numeric) || org.match(%r{/^[0-9]*$/}) ? :id : :name @organization = organizations.find { |x| x[key] == org } end def map_teams(teams) teams['teams'].map do |team| { id: team['id'], name: team['name'], organization: team['orgId'], membercount: team['membercount'], permission: team['permission'], email: team['email'] } end end def teams return [] unless organization set_current_organization response = send_request('GET', format('%s/teams/search', resource[:grafana_api_path])) raise_on_error(response.code, format('Fail to retrieve teams (HTTP response: %s/%s)', response.code, response.body)) teams = parse_response(response.body) map_teams(teams) end def team @team ||= teams.find { |x| x[:name] == resource[:name] } end def map_preferences(preferences) { theme: preferences['theme'], home_dashboard: preferences['homeDashboardId'], timezone: preferences['timezone'] } end def preferences team unless @team return if @preferences response = send_request('GET', format('%s/teams/%s/preferences', resource[:grafana_api_path], @team[:id])) raise_on_error(response.code, format('Fail to retrieve teams (HTTP response: %s/%s)', response.code, response.body)) preferences = parse_response(response.body) @preferences = map_preferences(preferences) end def setup_save_preferences_data endpoint = format('%s/teams/%s/preferences', resource[:grafana_api_path], @team[:id]) dash = get_dashboard(resource[:home_dashboard]) request_data = { theme: resource[:theme], homeDashboardId: dash[:id], timezone: resource[:timezone] } ['PUT', endpoint, request_data] end def save_preferences team unless @team set_current_organization setup_save_preferences_data response = send_request(*setup_save_preferences_data) # TODO: Raise on error? return if response.code == '200' || response.code == '412' raise format('Failed to update team %s, (HTTP response: %s/%s)', resource, response.code, response.body) end def set_current_organization response = send_request 'POST', format('%s/user/using/%s', resource[:grafana_api_path], organization[:id]) return if response.code == '200' raise format('Failed to switch to org %s (HTTP response: %s/%s)', organization[:id], response.code, response.body) end def home_dashboard preferences unless @preferences dash = get_dashboard(@preferences[:home_dashboard]) return dash[:name] if dash nil end def home_dashboard=(value) resource[:home_dashboard] = value save_preferences end def setup_search_path(ident) if ident.is_a?(Numeric) || ident.match(%r{/^[0-9]*$/}) { dashboardIds: ident, type: 'dash-db' } else { query: ident, type: 'dash-db' } end end def get_dashboard(ident) set_current_organization return { id: 0, name: 'Default' } if ident == 0 # rubocop:disable Style/NumericPredicate search_path = setup_search_path(ident) response = send_request('GET', format('%s/search', resource[:grafana_api_path]), nil, search_path) raise_on_error(response.code, format('Fail to retrieve dashboars (HTTP response: %s/%s)', response.code, response.body)) dashboard = parse_response(response.body) format_dashboard(dashboard) end def format_dashboard(dashboard) return { id: 0, name: 'Default' } unless dashboard.first { id: dashboard.first['id'], name: dashboard.first['title'] } end def theme preferences unless @preferences return @preferences[:theme] if @preferences nil end def theme=(value) resource[:theme] = value save_preferences end def timezone preferences unless @preferences return @preferences[:timezone] if @preferences nil end def timezone=(value) resource[:timezone] = value save_preferences end def setup_save_team_data verb = 'POST' endpoint = format('%s/teams', resource[:grafana_api_path]) request_data = { name: resource[:name], email: resource[:email] } if exists? verb = 'PUT' endpoint = format('%s/teams/%s', resource[:grafana_api_path], @team[:id]) end [verb, endpoint, request_data] end def save_team set_current_organization response = send_request(*setup_save_team_data) raise_on_error(response.code, format('Failed to update team %s, (HTTP response: %s/%s)', resource, response.code, response.body)) end def create save_team save_preferences end def destroy return unless team response = send_request('DELETE', format('%s/teams/%s', resource[:grafana_api_path], @team[:id])) - return unless response.code != '200' - - raise Puppet::Error, format('Failed to delete team %s (HTTP response: %s/%s)', resource, response.code, response.body) + raise_on_error(response.code, format('Failed to delete team %s (HTTP response: %s/%s)', resource, response.code, response.body)) end def exists? team return true if @team && @team[:name] == resource[:name] false end end diff --git a/spec/acceptance/grafana_team_spec.rb b/spec/acceptance/grafana_team_spec.rb new file mode 100644 index 0000000..833323a --- /dev/null +++ b/spec/acceptance/grafana_team_spec.rb @@ -0,0 +1,159 @@ +require 'spec_helper_acceptance' + +describe 'grafana_team' do + context 'setup grafana server' do + it 'runs successfully' do + pp = <<-EOS + class { 'grafana': + cfg => { + security => { + admin_user => 'admin', + admin_password => 'admin' + } + } + } + EOS + apply_manifest(pp, catch_failures: true) + apply_manifest(pp, catch_changes: true) + end + end + + context 'create team resource on `Main Org.`' do + it 'creates the team' do + pp = <<-EOS + include grafana::validator + grafana_team { 'example-team': + ensure => present, + grafana_url => 'http://localhost:3000', + grafana_user => 'admin', + grafana_password => 'admin', + } + EOS + apply_manifest(pp, catch_failures: true) + apply_manifest(pp, catch_changes: true) + end + + it 'has created the example team' do + shell('curl --user admin:admin http://localhost:3000/api/teams/search?name=example-team') do |f| + expect(f.stdout).to match(%r{example-team}) + end + end + + it 'has set default home dashboard' do + shell('curl --user admin:admin http://localhost:3000/api/teams/1/preferences') do |f| + data = JSON.parse(f.stdout) + expect(data).to include('homeDashboardId' => 0) + end + end + end + + context 'updates team resource' do + it 'creates dashboard and sets team home dashboard' do + pp = <<-EOS + include grafana::validator + grafana_dashboard { 'example-dashboard': + ensure => present, + grafana_url => 'http://localhost:3000', + grafana_user => 'admin', + grafana_password => 'admin', + content => '{"uid": "zyx986bc"}', + } + grafana_team { 'example-team': + ensure => present, + grafana_url => 'http://localhost:3000', + grafana_user => 'admin', + grafana_password => 'admin', + home_dashboard => 'example-dashboard', + } + EOS + apply_manifest(pp, catch_failures: true) + apply_manifest(pp, catch_changes: true) + end + + it 'has updated the example team home dashboard' do + shell('curl --user admin:admin http://localhost:3000/api/teams/1/preferences') do |f| + data = JSON.parse(f.stdout) + expect(data['homeDashboardId']).not_to eq(0) + end + end + end + + context 'create team resource on seperate organization' do + it 'creates organization and team' do + pp = <<-EOS + include grafana::validator + grafana_organization { 'example-organization': + ensure => present, + grafana_url => 'http://localhost:3000', + grafana_user => 'admin', + grafana_password => 'admin', + } + grafana_team { 'example-team-on-org': + ensure => present, + grafana_url => 'http://localhost:3000', + grafana_user => 'admin', + grafana_password => 'admin', + organization => 'example-organization', + } + EOS + apply_manifest(pp, catch_failures: true) + apply_manifest(pp, catch_changes: true) + end + + it 'creates team on organization' do + shell('curl --user admin:admin -X POST http://localhost:3000/api/user/using/2 && '\ + 'curl --user admin:admin http://localhost:3000/api/teams/search?name=example-team-on-org') do |f| + expect(f.stdout).to match(%r{example-team-on-org}) + end + end + end + + context 'destroy resources' do + it 'destroys the teams, dashboard, and organization' do + pp = <<-EOS + include grafana::validator + grafana_team { 'example-team': + ensure => absent, + grafana_url => 'http://localhost:3000', + grafana_user => 'admin', + grafana_password => 'admin', + } + grafana_team { 'example-team-on-org': + ensure => absent, + grafana_url => 'http://localhost:3000', + grafana_user => 'admin', + grafana_password => 'admin', + organization => 'example-organization', + } + grafana_dashboard { 'example-dashboard': + ensure => absent, + grafana_url => 'http://localhost:3000', + grafana_user => 'admin', + grafana_password => 'admin', + } + grafana_organization { 'example-organization': + ensure => absent, + grafana_url => 'http://localhost:3000', + grafana_user => 'admin', + grafana_password => 'admin', + } + EOS + apply_manifest(pp, catch_failures: true) + apply_manifest(pp, catch_changes: true) + end + + it 'has no example-team' do + shell('curl --user admin:admin -X POST http://localhost:3000/api/user/using/1 && '\ + 'curl --user admin:admin http://localhost:3000/api/teams/search') do |f| + expect(f.stdout).not_to match(%r{example-team}) + end + end + + it 'has no example-team-on-org' do + shell('curl --user admin:admin -X POST http://localhost:3000/api/user/using/2 && '\ + 'curl --user admin:admin http://localhost:3000/api/teams') do |f| + expect(f.stdout).not_to match(%r{example-team-on-org}) + end + end + end +end