grafana_sync 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []