diff --git a/lib/puppet/provider/grafana_team/grafana.rb b/lib/puppet/provider/grafana_team/grafana.rb index 37ca5bb..223ef73 100644 --- a/lib/puppet/provider/grafana_team/grafana.rb +++ b/lib/puppet/provider/grafana_team/grafana.rb @@ -1,231 +1,278 @@ # 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]) + dash = get_dashboard(resource[:home_dashboard], resource[:home_dashboard_folder]) 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_folder + preferences unless @preferences + dash = get_dashboard(@preferences[:home_dashboard]) + return dash[:folder_name] if dash + + nil + end + + def home_dashboard_folder=(value) + resource[:home_dashboard_folder] = value + save_preferences + 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 + def setup_search_path(ident, folder_id = nil) + query = if ident.is_a?(Numeric) || ident.match(%r{/^[0-9]*$/}) + { + dashboardIds: ident, + type: 'dash-db' + } + else + { + query: ident, + type: 'dash-db' + } + end + query[:folderIds] = folder_id unless folder_id.nil? + query end - def get_dashboard(ident) + def get_dashboard(ident, folder = nil) set_current_organization return { id: 0, name: 'Default' } if ident == 0 # rubocop:disable Style/NumericPredicate - search_path = setup_search_path(ident) + folder_id = nil + folder_id = get_dashboard_folder_id(folder) unless folder.nil? + + search_path = setup_search_path(ident, folder_id) 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'] + name: dashboard.first['title'], + folder_uid: dashboard.first['folderUid'], + folder_name: dashboard.first['folderTitle'], } end + def setup_folder_search_path(ident) + if ident.is_a?(Numeric) || ident.match(%r{/^[0-9]*$/}) + { + folderIds: ident, + type: 'dash-folder' + } + else + { + query: ident, + type: 'dash-folder' + } + end + end + + def get_dashboard_folder_id(ident) + return nil if ident.nil? + + set_current_organization + search_path = setup_folder_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) + return nil unless dashboard.first + dashboard.first['id'] + 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])) 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/lib/puppet/type/grafana_team.rb b/lib/puppet/type/grafana_team.rb index b623173..18209e8 100644 --- a/lib/puppet/type/grafana_team.rb +++ b/lib/puppet/type/grafana_team.rb @@ -1,80 +1,84 @@ # frozen_string_literal: true Puppet::Type.newtype(:grafana_team) do @doc = 'Manage teams in Grafana' ensurable newparam(:name, namevar: true) do desc 'The name of the team' end newparam(:grafana_api_path) do desc 'The absolute path to the API endpoint' defaultto '/api' validate do |value| unless value =~ %r{^/.*/?api$} raise ArgumentError, format('%s is not a valid API path', value) end end end newparam(:grafana_url) do desc 'The URL of the Grafana server' defaultto '' validate do |value| unless value =~ %r{^https?://} raise ArgumentError, format('%s is not a valid URL', value) end end end newparam(:grafana_user) do desc 'The username for the Grafana server' end newparam(:grafana_password) do desc 'The password for the Grafana server' end newparam(:organization) do desc 'The organization the team belongs to' defaultto 'Main Org.' end newparam(:email) do desc 'The email for the team' defaultto '' end + newproperty(:home_dashboard_folder) do + desc 'The UID or name of the home dashboard folder' + end + newproperty(:home_dashboard) do desc 'The id or name of the home dashboard' defaultto 'Default' end newproperty(:theme) do desc 'The theme to use for the team' end newproperty(:timezone) do desc 'The timezone to use for the team' end autorequire(:service) do 'grafana-server' end autorequire(:grafana_dashboard) do catalog.resources.select { |r| r.is_a?(Puppet::Type.type(:grafana_dashboard)) } end autorequire(:grafana_organization) do catalog.resources.select { |r| r.is_a?(Puppet::Type.type(:grafana_organization)) } end autorequire(:grafana_conn_validator) do 'grafana' end end diff --git a/spec/acceptance/grafana_team_spec.rb b/spec/acceptance/grafana_team_spec.rb index 833323a..9381ae6 100644 --- a/spec/acceptance/grafana_team_spec.rb +++ b/spec/acceptance/grafana_team_spec.rb @@ -1,159 +1,208 @@ 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_folder { 'example-folder': + ensure => present, + uid => 'example-folder', + grafana_url => 'http://localhost:3000', + grafana_user => 'admin', + grafana_password => 'admin', + } + -> grafana_dashboard { 'example-dashboard2': + ensure => present, + grafana_url => 'http://localhost:3000', + grafana_user => 'admin', + grafana_password => 'admin', + content => '{"uid": "niew0ahN"}', + folder => 'example-folder', + } grafana_team { 'example-team': ensure => present, grafana_url => 'http://localhost:3000', grafana_user => 'admin', grafana_password => 'admin', home_dashboard => 'example-dashboard', } + grafana_team { 'example-team2': + ensure => present, + grafana_url => 'http://localhost:3000', + grafana_user => 'admin', + grafana_password => 'admin', + home_dashboard_folder => 'example-folder', + home_dashboard => 'example-dashboard2', + } 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 + + it 'has updated the example team home dashboard with folder' do + shell('curl --user admin:admin http://localhost:3000/api/teams/2/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-team2': + 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_dashboard { 'example-dashboard2': + ensure => absent, + grafana_url => 'http://localhost:3000', + grafana_user => 'admin', + grafana_password => 'admin', + } + grafana_folder { 'example-folder': + ensure => absent, + uid => 'example-folder', + grafana_url => 'http://localhost:3000', + grafana_user => 'admin', + grafana_password => 'admin', + } grafana_organization { 'example-organization': - ensure => absent, + 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 diff --git a/spec/unit/puppet/type/grafana_team_type_spec.rb b/spec/unit/puppet/type/grafana_team_type_spec.rb index e4cea6a..a714091 100644 --- a/spec/unit/puppet/type/grafana_team_type_spec.rb +++ b/spec/unit/puppet/type/grafana_team_type_spec.rb @@ -1,61 +1,63 @@ require 'spec_helper' describe Puppet::Type.type(:grafana_team) do let(:gteam) do described_class.new( name: 'foo', grafana_url: 'http://example.com', grafana_user: 'admin', grafana_password: 'admin', + home_dashboard_folder: 'bar', home_dashboard: 'foo_dashboard', organization: 'foo_organization' ) end context 'when setting parameters' do it "fails if grafana_url isn't HTTP-based" do expect do described_class.new name: 'foo', grafana_url: 'example.com', content: '{}', ensure: :present end.to raise_error(Puppet::Error, %r{not a valid URL}) end it 'accepts valid parameters' do expect(gteam[:name]).to eq('foo') expect(gteam[:grafana_user]).to eq('admin') expect(gteam[:grafana_password]).to eq('admin') expect(gteam[:grafana_url]).to eq('http://example.com') + expect(gteam[:home_dashboard_folder]).to eq('bar') expect(gteam[:home_dashboard]).to eq('foo_dashboard') expect(gteam[:organization]).to eq('foo_organization') end # rubocop:enable RSpec/MultipleExpectations it 'autorequires the grafana-server for proper ordering' do catalog = Puppet::Resource::Catalog.new service = Puppet::Type.type(:service).new(name: 'grafana-server') catalog.add_resource service catalog.add_resource gteam relationship = gteam.autorequire.find do |rel| (rel.source.to_s == 'Service[grafana-server]') && (rel.target.to_s == gteam.to_s) end expect(relationship).to be_a Puppet::Relationship end it 'does not autorequire the service it is not managed' do catalog = Puppet::Resource::Catalog.new catalog.add_resource gteam expect(gteam.autorequire).to be_empty end it 'autorequires grafana_conn_validator' do catalog = Puppet::Resource::Catalog.new validator = Puppet::Type.type(:grafana_conn_validator).new(name: 'grafana') catalog.add_resource validator catalog.add_resource gteam relationship = gteam.autorequire.find do |rel| (rel.source.to_s == 'Grafana_conn_validator[grafana]') && (rel.target.to_s == gteam.to_s) end expect(relationship).to be_a Puppet::Relationship end end end