grafana_sync 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ffc1428f36126020a5bf8ec2724ca40a262726f47775ca4f86c5fe22bdcc9606
4
+ data.tar.gz: 8ffa6b47005e7ba87b3f22e9f080d120421767bc113959cb36004a4f2955340b
5
+ SHA512:
6
+ metadata.gz: 7e6089aa7662c1ec68245a686c05360adae35f394a53d4ff6d42fd1bad29ce63a3abf2fa2964e626914fd0b0a9168f2f982b8c3e78013c19308477539080fe96
7
+ data.tar.gz: 6a2b0c3e8127bd6ff67fdc361446c079cb9f4edefd5b0d2797033884f5518d56f8eb21ccddfe05cda7f9fbccc104db81d0528fca904fec672a0bbfdeb9c1fd25
@@ -0,0 +1,32 @@
1
+ # This workflow uses actions that are not certified by GitHub.
2
+ # They are provided by a third-party and are governed by
3
+ # separate terms of service, privacy policy, and support
4
+ # documentation.
5
+ # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
6
+ # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
7
+
8
+ name: Ruby
9
+
10
+ on:
11
+ push:
12
+ branches: [ master ]
13
+ pull_request:
14
+ branches: [ master ]
15
+
16
+ jobs:
17
+ test:
18
+
19
+ runs-on: ubuntu-latest
20
+
21
+ steps:
22
+ - uses: actions/checkout@v2
23
+ - name: Set up Ruby
24
+ # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
25
+ # change this to (see https://github.com/ruby/setup-ruby#versioning):
26
+ uses: ruby/setup-ruby@v1
27
+ with:
28
+ ruby-version: 2.7
29
+ - name: Install dependencies
30
+ run: bundle install
31
+ - name: Run tests
32
+ run: bundle exec rspec
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /spec/examples.txt
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
@@ -0,0 +1 @@
1
+ ruby 2.7.1
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
@@ -0,0 +1,101 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ grafana_sync (1.0.0)
5
+ activesupport (~> 6.0)
6
+ diffy (~> 3.3)
7
+ http (~> 4.4)
8
+ httplog (~> 1.4)
9
+ methadone (~> 2.0)
10
+
11
+ GEM
12
+ remote: https://rubygems.org/
13
+ specs:
14
+ activesupport (6.0.2.2)
15
+ concurrent-ruby (~> 1.0, >= 1.0.2)
16
+ i18n (>= 0.7, < 2)
17
+ minitest (~> 5.1)
18
+ tzinfo (~> 1.1)
19
+ zeitwerk (~> 2.2)
20
+ addressable (2.7.0)
21
+ public_suffix (>= 2.0.2, < 5.0)
22
+ byebug (11.1.1)
23
+ coderay (1.1.2)
24
+ concurrent-ruby (1.1.6)
25
+ crack (0.4.3)
26
+ safe_yaml (~> 1.0.0)
27
+ diff-lcs (1.3)
28
+ diffy (3.3.0)
29
+ domain_name (0.5.20190701)
30
+ unf (>= 0.0.5, < 1.0.0)
31
+ ffi (1.12.2)
32
+ ffi-compiler (1.0.1)
33
+ ffi (>= 1.0.0)
34
+ rake
35
+ hashdiff (1.0.1)
36
+ http (4.4.1)
37
+ addressable (~> 2.3)
38
+ http-cookie (~> 1.0)
39
+ http-form_data (~> 2.2)
40
+ http-parser (~> 1.2.0)
41
+ http-cookie (1.0.3)
42
+ domain_name (~> 0.5)
43
+ http-form_data (2.3.0)
44
+ http-parser (1.2.1)
45
+ ffi-compiler (>= 1.0, < 2.0)
46
+ httplog (1.4.2)
47
+ rack (>= 1.0)
48
+ rainbow (>= 2.0.0)
49
+ i18n (1.8.2)
50
+ concurrent-ruby (~> 1.0)
51
+ methadone (2.0.2)
52
+ bundler
53
+ method_source (1.0.0)
54
+ minitest (5.14.0)
55
+ pry (0.13.1)
56
+ coderay (~> 1.1)
57
+ method_source (~> 1.0)
58
+ pry-byebug (3.9.0)
59
+ byebug (~> 11.0)
60
+ pry (~> 0.13.0)
61
+ public_suffix (4.0.4)
62
+ rack (2.2.2)
63
+ rainbow (3.0.0)
64
+ rake (13.0.1)
65
+ rspec (3.9.0)
66
+ rspec-core (~> 3.9.0)
67
+ rspec-expectations (~> 3.9.0)
68
+ rspec-mocks (~> 3.9.0)
69
+ rspec-core (3.9.1)
70
+ rspec-support (~> 3.9.1)
71
+ rspec-expectations (3.9.1)
72
+ diff-lcs (>= 1.2.0, < 2.0)
73
+ rspec-support (~> 3.9.0)
74
+ rspec-mocks (3.9.1)
75
+ diff-lcs (>= 1.2.0, < 2.0)
76
+ rspec-support (~> 3.9.0)
77
+ rspec-support (3.9.2)
78
+ safe_yaml (1.0.5)
79
+ thread_safe (0.3.6)
80
+ tzinfo (1.2.7)
81
+ thread_safe (~> 0.1)
82
+ unf (0.1.4)
83
+ unf_ext
84
+ unf_ext (0.0.7.7)
85
+ webmock (3.8.3)
86
+ addressable (>= 2.3.6)
87
+ crack (>= 0.3.2)
88
+ hashdiff (>= 0.4.0, < 2.0.0)
89
+ zeitwerk (2.3.0)
90
+
91
+ PLATFORMS
92
+ ruby
93
+
94
+ DEPENDENCIES
95
+ grafana_sync!
96
+ pry-byebug
97
+ rspec
98
+ webmock
99
+
100
+ BUNDLED WITH
101
+ 2.1.4
@@ -0,0 +1,52 @@
1
+ ![Gem version](https://img.shields.io/gem/v/grafana_sync?label=gem%20version)
2
+ ![GitHub Workflow Status](https://img.shields.io/github/workflow/status/staring-frog/grafana_sync/Ruby)
3
+
4
+ # Grafana Sync
5
+
6
+ Syncs dashboards between Grafana instances. Lifts the burden of migrating
7
+ changes between environment instances by hand. Unleashes the power of Linux
8
+ command-line tools on Grafana config files.
9
+
10
+ Tested against Grafana 6.
11
+
12
+ ## Installation and Usage
13
+
14
+ Suggested workflow is:
15
+ - for each separately deployed project create a repo based off of a `sample_repo`
16
+ and head to it
17
+ ```
18
+ cp sample_repo ../mars_shuttle
19
+ cd ../mars_shuttle
20
+ ```
21
+ - install GrafanaSync locally (with e.g. [asdf](https://github.com/asdf-vm/asdf))
22
+ ```
23
+ asdf install
24
+ gem install bundler -v 2.1.4
25
+ bundle
26
+ ```
27
+ or globally
28
+ ```
29
+ gem install grafana_sync
30
+ ```
31
+ - adjust `config.rb` to suit your needs. For each environment there has to be
32
+ specified Grafana URL and Grafana folder. One environment (say "staging") is
33
+ tweaked manually through Grafana web-interface, optionally stored in a VCS and
34
+ then deployed to other environments (say "production").
35
+ - tweak manually staging Grafana through web-interface to fit your needs
36
+ - fetch staging Grafana configs
37
+ ```
38
+ grafync staging pull
39
+ ```
40
+ - optionally review changes and commit
41
+ - see what are the changes to be applied to production
42
+ ```
43
+ grafync production diff
44
+ ```
45
+ same with paging
46
+ ```
47
+ grafync production diff | less -R
48
+ ```
49
+ - apply Grafana configs to production
50
+ ```
51
+ grafync production push
52
+ ```
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'grafana_sync'
4
+ require 'methadone'
5
+
6
+ include Methadone::Main
7
+ include Methadone::CLILogging
8
+
9
+ version GrafanaSync::VERSION
10
+ description 'Syncs dashboards between Grafana instances'
11
+ # TODO: bash completion
12
+ on('-f', '--make-folders', 'Make missing Grafana folders')
13
+ on('-d', '--debug', 'Turn on debugging messages')
14
+ arg :stage, 'One of the environments specified in config.rb to apply command for'
15
+ arg :command, '''pull: download remote <stage> dashboard configs into "dashboards/"
16
+ diff: preview what changes would be made by <push>
17
+ push: upload local dashboard configs to <stage>
18
+ '''
19
+
20
+ leak_exceptions true
21
+ main do |stage, command|
22
+ logger = Logger.new(STDERR,
23
+ level: (options[:debug]) ? Logger::DEBUG : Logger::INFO)
24
+ stage = GrafanaSync::Stage.new(stage: stage.to_sym,
25
+ make_folders: options['make-folders'],
26
+ debug: options[:debug],
27
+ logger: logger)
28
+
29
+ case command
30
+ when "pull"
31
+ stage.pull
32
+ when "push"
33
+ stage.push
34
+ when "diff"
35
+ stage.diff
36
+ else
37
+ GrafanaSync::die("Unknown command '#{command}'!")
38
+ end
39
+
40
+ 0
41
+ end
42
+
43
+ go!
@@ -0,0 +1,39 @@
1
+ require_relative 'lib/grafana_sync/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "grafana_sync"
5
+ spec.version = GrafanaSync::VERSION
6
+ spec.authors = ["Nikolay Epifanov"]
7
+ spec.email = ["nik.epifanov@gmail.com"]
8
+ spec.licenses = ["MIT"]
9
+
10
+ spec.summary = "Syncs dashboards between Grafana instances."
11
+ spec.description = <<-EOF
12
+ Grafana HTTP API tool to fetch, diff and create/update dashboards to
13
+ ease the burden of migrating changes between Grafana instances for each
14
+ environment.
15
+ EOF
16
+ spec.homepage = "https://github.com/funbox/grafana_sync"
17
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0")
18
+
19
+ spec.metadata["source_code_uri"] = spec.homepage
20
+ spec.metadata["changelog_uri"] = spec.homepage + "/blob/master/CHANGELOG.md"
21
+
22
+ # Specify which files should be added to the gem when it is released.
23
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
24
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
25
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(sample_repo|test|spec|features)/}) }
26
+ end
27
+ spec.bindir = "exe"
28
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
29
+ spec.require_paths = ["lib"]
30
+
31
+ spec.add_runtime_dependency 'activesupport', '~> 6.0'
32
+ spec.add_runtime_dependency 'methadone', '~> 2.0'
33
+ spec.add_runtime_dependency 'http', '~> 4.4'
34
+ spec.add_runtime_dependency 'httplog', '~> 1.4'
35
+ spec.add_runtime_dependency 'diffy', '~> 3.3'
36
+ spec.add_development_dependency 'pry-byebug'
37
+ spec.add_development_dependency 'rspec'
38
+ spec.add_development_dependency 'webmock'
39
+ end
@@ -0,0 +1,24 @@
1
+ require 'grafana_sync/stage'
2
+ require 'grafana_sync/version'
3
+
4
+ module GrafanaSync
5
+ class << self
6
+ def load_config
7
+ @load_config ||= load('config.rb')
8
+ end
9
+
10
+ def config
11
+ @config ||= {}
12
+ end
13
+
14
+ def merge_config(file_config)
15
+ config.merge!(file_config)
16
+ end
17
+
18
+ def die(msg)
19
+ puts("""Error: #{msg}
20
+ Use --debug option to get verbose output.""")
21
+ exit(false)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,256 @@
1
+ require 'active_support/all'
2
+ require 'diffy'
3
+ require 'http'
4
+ # Must be after 'http' gem.
5
+ require 'httplog'
6
+ require 'io/console'
7
+ require 'logger'
8
+
9
+ module GrafanaSync
10
+ class Stage
11
+ delegate :config, :die, to: GrafanaSync
12
+
13
+ DASHBOARDS_ROOT = "dashboards"
14
+ FILE_ENCODING = "UTF-8"
15
+ FILE_EXTENSION = ".json"
16
+ CREDENTIALS = File.expand_path("~/.grafana_sync_rc")
17
+
18
+ def initialize(stage:, make_folders: false, debug: false,
19
+ logger: Logger.new(STDERR, level: Logger::INFO))
20
+ @stage_name = stage
21
+ @make_folders = make_folders
22
+ @logger = logger
23
+ HttpLog.configure do |config|
24
+ config.logger = logger
25
+ config.log_connect = false
26
+ config.log_data = debug
27
+ config.log_response = debug
28
+ config.log_benchmark = false
29
+ end
30
+
31
+ GrafanaSync.load_config()
32
+ validate_config!
33
+ @base_url = stage_config[:url].chomp("/")
34
+ folder = stage_config[:folder]
35
+ # If there's no folder specified in Grafana dashboard config then it's "General".
36
+ @folder_name = (folder == "General") ? nil : folder
37
+ end
38
+
39
+ def pull
40
+ FileUtils.mkdir_p(DASHBOARDS_ROOT)
41
+ FileUtils.rm(dashboard_files)
42
+ remote_dashboards.each do |title, db|
43
+ @logger.info("Saving dashboard '#{title}'")
44
+ IO.write(File.join(DASHBOARDS_ROOT, title+FILE_EXTENSION),
45
+ JSON.pretty_generate(db), encoding: FILE_ENCODING, mode: "w")
46
+ end
47
+ end
48
+
49
+ def push
50
+ dashboards_to_delete.each do |title, _db|
51
+ @logger.info("Deleting dashboard '#{title}'")
52
+ uid = dashboard_uid(title)
53
+ http_delete("/api/dashboards/uid/#{uid}")
54
+ end
55
+
56
+ dashboards_to_update.each do |title, db|
57
+ db["folderId"] = (folder_id or make_folder)
58
+ db["overwrite"] = true
59
+ @logger.info("Updating dashboard '#{title}'")
60
+ http_post("/api/dashboards/db", json: db)
61
+ end
62
+ end
63
+
64
+ def diff
65
+ dashboards_to_delete.keys.each do |key|
66
+ puts("--- #{@stage_name}/#{key}")
67
+ puts("+++ /dev/null")
68
+ puts
69
+ end
70
+
71
+ dashboards_to_update.keys.sort.each do |key|
72
+ diff_str = Diffy::Diff.new(JSON.pretty_generate(remote_dashboards[key]),
73
+ JSON.pretty_generate(local_dashboards[key]),
74
+ context: 3, diff: '-w', include_diff_info: true).to_s(:color)
75
+ unless diff_str.chop.empty?
76
+ puts("--- #{@stage_name}/#{key}")
77
+ puts("+++ local/#{key}")
78
+ puts(diff_str)
79
+ end
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ def validate_config!
86
+ unless config.has_key?(@stage_name)
87
+ die("There's no environment ':#{@stage_name}' defined in config.rb!")
88
+ end
89
+
90
+ config.keys.each do |stage|
91
+ [:url, :folder].each {|key|
92
+ die("config.rb has no :#{key} specified for :#{stage}!") if config[stage][key].nil?
93
+ }
94
+
95
+ config[stage][:datasource_replace].try do |ds_hash|
96
+ if ds_hash.has_key?(nil) or ds_hash.has_value?(nil)
97
+ die("config.rb:#{stage}: nil value in :datasource_replace is not supported!")
98
+ end
99
+ end
100
+ end
101
+ end
102
+
103
+ def stage_config
104
+ config[@stage_name]
105
+ end
106
+
107
+ def dashboards_to_delete
108
+ remote_dashboards.slice(*(remote_dashboards.keys - local_dashboards.keys))
109
+ end
110
+
111
+ def dashboards_to_update
112
+ local_dashboards
113
+ end
114
+
115
+ def remote_dashboards
116
+ @remote_dashboards ||= dashboard_uids.lazy.map do |uid|
117
+ db = http_get("/api/dashboards/uid/#{uid}")
118
+ db.delete("meta")
119
+ ["id", "uid", "version"].each {|key|
120
+ db["dashboard"].delete(key)
121
+ }
122
+ [db["dashboard"]["title"], db]
123
+ end.to_h
124
+ end
125
+
126
+ def dashboard_uid(title)
127
+ index.find {|item| item["type"]=="dash-db" and item["title"]==title}
128
+ .try {|item| item["uid"]}.tap {|val|
129
+ die("Dashboard #{title} not found on #{@stage_name}!") if val.nil?
130
+ }
131
+ end
132
+
133
+ def dashboard_uids
134
+ index.filter {|item| item["type"]=="dash-db" and item["folderTitle"]==@folder_name}
135
+ .map {|item| item["uid"]}.tap {|val|
136
+ @logger.warn("No dashboards found on #{@stage_name}!") if val.empty?
137
+ }
138
+ end
139
+
140
+ def folder_id
141
+ @folder_id ||= if @folder_name.nil?
142
+ 0 # "General" folder ID is always 0.
143
+ else
144
+ index.find {|item|
145
+ item["type"]=="dash-folder" and item["title"]==@folder_name
146
+ }.try {|item| item["id"] }
147
+ end
148
+ end
149
+
150
+ def make_folder
151
+ die("""There is no folder '#{@folder_name}' for '#{@stage_name}'!
152
+ To create it add --make-folders option to command-line.""") unless @make_folders
153
+
154
+ @logger.info("Making Grafana folder '#{@folder_name}'")
155
+ response = http_post("/api/folders", json: {title: @folder_name})
156
+ response["id"].tap {
157
+ invalidate_index
158
+ }
159
+ end
160
+
161
+ def invalidate_index
162
+ @index = nil
163
+ end
164
+
165
+ def local_dashboards
166
+ @local_dashboards ||= dashboard_files.map do |path|
167
+ @logger.debug("Loading '#{path}'")
168
+ db = JSON.parse(IO.read(path, encoding: FILE_ENCODING))
169
+ title = db["dashboard"]["title"]
170
+ next if stage_config[:exclude].try { |array| array.include?(title) }
171
+ replace_datasources!(db)
172
+ [title, db]
173
+ end.compact.to_h
174
+ end
175
+
176
+ def dashboard_files
177
+ # dashboards/*.json
178
+ Dir.glob(File.join(DASHBOARDS_ROOT, '*'+FILE_EXTENSION))
179
+ end
180
+
181
+ def replace_datasources!(obj)
182
+ replaces = stage_config[:datasource_replace]
183
+ return if replaces.nil?
184
+
185
+ if obj.is_a?(Hash)
186
+ datasource = obj["datasource"]
187
+ if datasource
188
+ obj["datasource"] = replaces.fetch(datasource, datasource)
189
+ end
190
+ obj.each_value {|value| replace_datasources!(value)}
191
+ elsif obj.is_a?(Array)
192
+ obj.each {|value| replace_datasources!(value)}
193
+ end
194
+ end
195
+
196
+ def index
197
+ @index ||= http_get("/api/search")
198
+ end
199
+
200
+ def http_get(path)
201
+ url = @base_url + path
202
+ response = http.get(url)
203
+ die("Failed to GET #{url}!") if response.code != 200
204
+ JSON.parse(response.to_s)
205
+ end
206
+
207
+ def http_post(path, json: {})
208
+ url = @base_url + path
209
+ response = http.post(url, json: json)
210
+ die("Failed to POST #{url}!") if response.code != 200
211
+ JSON.parse(response.to_s)
212
+ end
213
+
214
+ def http_delete(path, json: {})
215
+ url = @base_url + path
216
+ response = http.delete(url, json: json)
217
+ die("Failed to DELETE #{url}!") if response.code != 200
218
+ JSON.parse(response.to_s)
219
+ end
220
+
221
+ def http
222
+ @http ||= HTTP.basic_auth(user: credentials[:login],
223
+ pass: credentials[:password]).follow
224
+ end
225
+
226
+ def credentials
227
+ @credentials ||= load_credentials or ask_credentials
228
+ end
229
+
230
+ def load_credentials
231
+ if File.exist?(CREDENTIALS)
232
+ JSON.parse(IO.read(CREDENTIALS, encoding: FILE_ENCODING)).transform_keys(&:to_sym)
233
+ else
234
+ nil
235
+ end
236
+ end
237
+
238
+ def ask_credentials
239
+ {login: ask_input("User: "),
240
+ password: ask_input("Password: ", hide: true)}.tap do |cred_hash|
241
+ IO.write(CREDENTIALS,
242
+ JSON.pretty_generate(cred_hash),
243
+ encoding: FILE_ENCODING, mode: "w")
244
+ end
245
+ end
246
+
247
+ def ask_input(invitation, hide: false)
248
+ print(invitation)
249
+ if hide
250
+ STDIN.noecho(&:gets).tap { puts }
251
+ else
252
+ STDIN.gets
253
+ end.strip
254
+ end
255
+ end
256
+ end
@@ -0,0 +1,3 @@
1
+ module GrafanaSync
2
+ VERSION = "1.0.0"
3
+ end
metadata ADDED
@@ -0,0 +1,173 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: grafana_sync
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Nikolay Epifanov
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2020-10-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '6.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '6.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: methadone
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: http
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '4.4'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '4.4'
55
+ - !ruby/object:Gem::Dependency
56
+ name: httplog
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.4'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.4'
69
+ - !ruby/object:Gem::Dependency
70
+ name: diffy
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.3'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.3'
83
+ - !ruby/object:Gem::Dependency
84
+ name: pry-byebug
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: webmock
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ description: |2
126
+ Grafana HTTP API tool to fetch, diff and create/update dashboards to
127
+ ease the burden of migrating changes between Grafana instances for each
128
+ environment.
129
+ email:
130
+ - nik.epifanov@gmail.com
131
+ executables:
132
+ - grafync
133
+ extensions: []
134
+ extra_rdoc_files: []
135
+ files:
136
+ - ".github/workflows/ruby.yml"
137
+ - ".gitignore"
138
+ - ".rspec"
139
+ - ".tool-versions"
140
+ - Gemfile
141
+ - Gemfile.lock
142
+ - README.md
143
+ - exe/grafync
144
+ - grafana_sync.gemspec
145
+ - lib/grafana_sync.rb
146
+ - lib/grafana_sync/stage.rb
147
+ - lib/grafana_sync/version.rb
148
+ homepage: https://github.com/funbox/grafana_sync
149
+ licenses:
150
+ - MIT
151
+ metadata:
152
+ source_code_uri: https://github.com/funbox/grafana_sync
153
+ changelog_uri: https://github.com/funbox/grafana_sync/blob/master/CHANGELOG.md
154
+ post_install_message:
155
+ rdoc_options: []
156
+ require_paths:
157
+ - lib
158
+ required_ruby_version: !ruby/object:Gem::Requirement
159
+ requirements:
160
+ - - ">="
161
+ - !ruby/object:Gem::Version
162
+ version: 2.7.0
163
+ required_rubygems_version: !ruby/object:Gem::Requirement
164
+ requirements:
165
+ - - ">="
166
+ - !ruby/object:Gem::Version
167
+ version: '0'
168
+ requirements: []
169
+ rubygems_version: 3.1.2
170
+ signing_key:
171
+ specification_version: 4
172
+ summary: Syncs dashboards between Grafana instances.
173
+ test_files: []