Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor grafana_datasource and add uid property #301

Merged
merged 2 commits into from Oct 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
234 changes: 112 additions & 122 deletions lib/puppet/provider/grafana_datasource/grafana.rb
Expand Up @@ -3,6 +3,7 @@
# Copyright 2015 Mirantis, Inc.
#
require 'json'
require 'erb'

require File.expand_path(File.join(File.dirname(__FILE__), '..', 'grafana'))

Expand Down Expand Up @@ -50,211 +51,200 @@ def fetch_organization
@fetch_organization
end

def datasources
response = send_request('GET', format('%s/datasources', resource[:grafana_api_path]))
raise format('Fail to retrieve datasources (HTTP response: %s/%s)', response.code, response.body) if response.code != '200'
def datasource_by_name
response = send_request('GET', format('%s/datasources/name/%s', resource[:grafana_api_path], ERB::Util.url_encode(resource[:name])))
return nil if response.code == '404'

begin
datasources = JSON.parse(response.body)

datasources.map { |x| x['id'] }.map do |id|
response = send_request 'GET', format('%s/datasources/%s', resource[:grafana_api_path], id)
raise format('Failed to retrieve datasource %d (HTTP response: %s/%s)', id, response.code, response.body) if response.code != '200'

datasource = JSON.parse(response.body)
raise Puppet::Error, format('Failed to retrieve datasource %s (HTTP response: %s/%s)', resource[:name], response.code, response.body) if response.code != '200'

{
id: datasource['id'],
name: datasource['name'],
url: datasource['url'],
type: datasource['type'],
user: datasource['user'],
password: datasource['password'],
database: datasource['database'],
access_mode: datasource['access'],
is_default: datasource['isDefault'] ? :true : :false,
with_credentials: datasource['withCredentials'] ? :true : :false,
basic_auth: datasource['basicAuth'] ? :true : :false,
basic_auth_user: datasource['basicAuthUser'],
basic_auth_password: datasource['basicAuthPassword'],
json_data: datasource['jsonData'],
secure_json_data: datasource['secureJsonData']
}
begin
JSON.parse(response.body).transform_values do |v|
case v
when true
:true
when false
:false
else
v
end
end
rescue JSON::ParserError
raise format('Failed to parse response: %s', response.body)
end
end

def datasource
@datasource ||= datasources.find { |x| x[:name] == resource[:name] }
@datasource ||= datasource_by_name
@datasource
end

attr_writer :datasource

def type
datasource[:type]
end
# Create setters for all properties just so they exist
mk_resource_methods # Creates setters for all properties

def type=(value)
resource[:type] = value
save_datasource
# Then override all of the getters
def type
datasource['type']
end

def url
datasource[:url]
end

def url=(value)
resource[:url] = value
save_datasource
datasource['url']
end

def access_mode
datasource[:access_mode]
end

def access_mode=(value)
resource[:access_mode] = value
save_datasource
datasource['access']
end

def database
datasource[:database]
end

def database=(value)
resource[:database] = value
save_datasource
datasource['database']
end

def user
datasource[:user]
end

def user=(value)
resource[:user] = value
save_datasource
datasource['user']
end

def password
datasource[:password]
end

def password=(value)
resource[:password] = value
save_datasource
datasource['password']
end

# rubocop:disable Naming/PredicateName
def is_default
datasource[:is_default]
datasource['isDefault']
end

def is_default=(value)
resource[:is_default] = value
save_datasource
end
# rubocop:enable Naming/PredicateName

def basic_auth
datasource[:basic_auth]
end

def basic_auth=(value)
resource[:basic_auth] = value
save_datasource
datasource['basicAuth']
end

def basic_auth_user
datasource[:basic_auth_user]
end

def basic_auth_user=(value)
resource[:basic_auth_user] = value
save_datasource
datasource['basicAuthUser']
end

def basic_auth_password
datasource[:basic_auth_password]
end

def basic_auth_password=(value)
resource[:basic_auth_password] = value
save_datasource
datasource['basicAuthPassword']
end

def with_credentials
datasource[:with_credentials]
datasource['withCredentials']
end

def with_credentials=(value)
resource[:with_credentials] = value
save_datasource
def json_data
datasource['jsonData']
end

def json_data
datasource[:json_data]
def id
datasource['id']
end

def json_data=(value)
resource[:json_data] = value
save_datasource
def uid
datasource['uid']
end

def secure_json_data
datasource[:secure_json_data]
# The API never returns `secure` data, so we won't ever be able to tell if the current state is correct.
# TODO: Figure this out!!
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😿

{}
end

def secure_json_data=(value)
resource[:secure_json_data] = value
save_datasource
end
def flush
return if resource['ensure'] == :absent

def save_datasource
# change organizations
response = send_request 'POST', format('%s/user/using/%s', resource[:grafana_api_path], fetch_organization[:id])
raise format('Failed to switch to org %s (HTTP response: %s/%s)', fetch_organization[:id], response.code, response.body) unless response.code == '200'

# Build the `data` to POST/PUT by first creating a hash with some defaults which will be used if we're _creating_ a datasource
data = {
name: resource[:name],
type: resource[:type],
url: resource[:url],
access: resource[:access_mode],
database: resource[:database],
user: resource[:user],
password: resource[:password],
isDefault: (resource[:is_default] == :true),
basicAuth: (resource[:basic_auth] == :true),
basicAuthUser: resource[:basic_auth_user],
basicAuthPassword: resource[:basic_auth_password],
withCredentials: (resource[:with_credentials] == :true),
jsonData: resource[:json_data],
secureJsonData: resource[:secure_json_data]
access: :direct,
isDefault: false,
basicAuth: false,
withCredentials: false,
}

# If we're updating a datasource, merge in the current state (overwriting the defaults above)
unless datasource.nil?
data.merge!(datasource.transform_keys(&:to_sym).slice(
:access,
:basicAuth,
:basicAuthUser,
:basicAuthPassword,
:database,
:isDefault,
:jsonData,
:type,
:url,
:user,
:password,
:withCredentials,
:uid
))
end

# Finally, merge in the properies the user has specified
data.merge!(
{
name: resource['name'],
access: resource['access_mode'],
basicAuth: resource['basic_auth'],
basicAuthUser: resource['basic_auth_user'],
basicAuthPassword: resource['basic_auth_password'],
database: resource['database'],
isDefault: resource['is_default'],
jsonData: resource['json_data'],
type: resource['type'],
url: resource['url'],
user: resource['user'],
password: resource['password'],
withCredentials: resource['with_credentials'],
secureJsonData: resource['secure_json_data'],
uid: resource['uid']
}.compact
)

# Puppet properties need to work with symbols, but the Grafana API will want to receive actual Booleans
data.transform_values! do |v|
case v
when :true
true
when :false
false
else
v
end
end

if datasource.nil?
Puppet.debug 'Creating datasource'
response = send_request('POST', format('%s/datasources', resource[:grafana_api_path]), data)
elsif uid.nil?
# This API call is deprecated in Grafana 9 so we only use it if our datasource doesn't have a uid (eg Grafana 6)
Puppet.debug 'Updating datasource by id'
response = send_request 'PUT', format('%s/datasources/%s', resource[:grafana_api_path], id), data
else
data[:id] = datasource[:id]
response = send_request 'PUT', format('%s/datasources/%s', resource[:grafana_api_path], datasource[:id]), data
Puppet.debug 'Updating datasource by uid'
response = send_request 'PUT', format('%s/datasources/uid/%s', resource[:grafana_api_path], uid), data
end
raise format('Failed to create save %s (HTTP response: %s/%s)', resource[:name], response.code, response.body) if response.code != '200'

raise format('Failed to create/update %s (HTTP response: %s/%s)', resource[:name], response.code, response.body) if response.code != '200'

self.datasource = nil
end

def delete_datasource
response = send_request 'DELETE', format('%s/datasources/%s', resource[:grafana_api_path], datasource[:id])
response = send_request 'DELETE', format('%s/datasources/name/%s', resource[:grafana_api_path], ERB::Util.url_encode(resource[:name]))

raise format('Failed to delete datasource %s (HTTP response: %s/%s', resource[:name], response.code, response.body) if response.code != '200'

self.datasource = nil
end

def create
save_datasource
# There's no sensible default for `type` when creating a new datasource so perform some validation here
# The actual creation happens when `flush` gets called.
raise Puppet::Error, 'type is required when creating a new datasource' if resource[:type].nil?
end

def destroy
Expand Down
23 changes: 17 additions & 6 deletions lib/puppet/type/grafana_datasource.rb
Expand Up @@ -37,6 +37,10 @@
desc 'The password for the Grafana server'
end

newproperty(:uid) do
desc 'An optional unique identifier for the datasource. Supported by grafana 7.3 onwards. If you do not specify this parameter, grafana will assign a uid for you'
end

newproperty(:url) do
desc 'The URL/Endpoint of the datasource'
end
Expand Down Expand Up @@ -66,35 +70,29 @@
newproperty(:access_mode) do
desc 'Whether the datasource is accessed directly or not by the clients'
newvalues(:direct, :proxy)
defaultto :direct
end

newproperty(:is_default) do
desc 'Whether the datasource is the default one'
newvalues(:true, :false)
defaultto :false
end

newproperty(:basic_auth) do
desc 'Whether basic auth is enabled or not'
newvalues(:true, :false)
defaultto :false
end

newproperty(:basic_auth_user) do
desc 'The username for basic auth if enabled'
defaultto ''
end

newproperty(:basic_auth_password) do
desc 'The password for basic auth if enabled'
defaultto ''
end

newproperty(:with_credentials) do
desc 'Whether credentials such as cookies or auth headers should be sent with cross-site requests'
newvalues(:true, :false)
defaultto :false
end

newproperty(:json_data) do
Expand All @@ -112,6 +110,19 @@
validate do |value|
raise ArgumentError, 'secure_json_data should be a Hash!' unless value.nil? || value.is_a?(Hash)
end

# Unwrap any sensitive values _in_ the hash. For instance those created by
# node_encrypt. The whole property is automatically marked as `sensitive`
# anyway and puppet won't unwrap the nested sensitive data for us.
munge do |hash|
hash.transform_values do |v|
if v.instance_of?(Puppet::Pops::Types::PSensitiveType::Sensitive)
v.unwrap
else
v
end
end
end
end

def set_sensitive_parameters(sensitive_parameters) # rubocop:disable Naming/AccessorMethodName
Expand Down