Skip to content

Commit 636bd54

Browse files
committed
Initial commit
1 parent ff7de23 commit 636bd54

File tree

4 files changed

+268
-1
lines changed

4 files changed

+268
-1
lines changed

bin/datadog_backup

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ def prereqs(defaults) # rubocop:disable Metrics/AbcSize
5454
opts.on('--synthetics-only') do
5555
result[:resources] = [DatadogBackup::Synthetics]
5656
end
57+
opts.on('--workflows-only') do
58+
result[:resources] = [DatadogBackup::Workflows]
59+
end
5760
opts.on(
5861
'--json',
5962
'format backups as JSON instead of YAML. Does not impact `diffs` nor `restore`, but do not mix formats in the same backup-dir.'
@@ -86,7 +89,7 @@ defaults = {
8689
action: nil,
8790
backup_dir: File.join(ENV.fetch('PWD'), 'backup'),
8891
diff_format: :color,
89-
resources: [DatadogBackup::Dashboards, DatadogBackup::Monitors, DatadogBackup::SLOs, DatadogBackup::Synthetics],
92+
resources: [DatadogBackup::Dashboards, DatadogBackup::Monitors, DatadogBackup::SLOs, DatadogBackup::Synthetics, DatadogBackup::Workflows],
9093
output_format: :yaml,
9194
force_restore: false,
9295
disable_array_sort: false

lib/datadog_backup.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
require_relative 'datadog_backup/monitors'
1111
require_relative 'datadog_backup/slos'
1212
require_relative 'datadog_backup/synthetics'
13+
require_relative 'datadog_backup/workflows'
1314
require_relative 'datadog_backup/thread_pool'
1415
require_relative 'datadog_backup/version'
1516
require_relative 'datadog_backup/deprecations'

lib/datadog_backup/workflows.rb

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# frozen_string_literal: true
2+
3+
module DatadogBackup
4+
# Workflow specific overrides for backup and restore.
5+
class Workflows < Resources
6+
def all
7+
get_all
8+
end
9+
10+
def backup
11+
LOGGER.info("Starting workflows backup on #{::DatadogBackup::ThreadPool::TPOOL.max_length} threads")
12+
13+
futures = all.map do |workflow|
14+
Concurrent::Promises.future_on(::DatadogBackup::ThreadPool::TPOOL, workflow) do |wf|
15+
id = wf[id_keyname]
16+
get_and_write_file(id)
17+
end
18+
end
19+
20+
watcher = ::DatadogBackup::ThreadPool.watcher
21+
watcher.join if watcher.status
22+
23+
Concurrent::Promises.zip(*futures).value!
24+
end
25+
26+
def get_by_id(id)
27+
except(get(id))
28+
rescue Faraday::ResourceNotFound
29+
LOGGER.warn("Workflow #{id} not found")
30+
{}
31+
end
32+
33+
def initialize(options)
34+
super
35+
@banlist = %w[created_at modified_at last_executed_at].freeze
36+
end
37+
38+
# v2 API wraps all responses in 'data' key
39+
def body_with_2xx(response)
40+
raise "#{caller_locations(1, 1)[0].label} failed with error #{response.status}" unless response.status.to_s =~ /^2/
41+
42+
response.body.fetch('data')
43+
end
44+
45+
private
46+
47+
def api_version
48+
'v2'
49+
end
50+
51+
def api_resource_name
52+
'workflows'
53+
end
54+
55+
def id_keyname
56+
'id'
57+
end
58+
end
59+
end
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
describe DatadogBackup::Workflows do
6+
let(:stubs) { Faraday::Adapter::Test::Stubs.new }
7+
let(:api_client_double) { Faraday.new { |f| f.adapter :test, stubs } }
8+
let(:tempdir) { Dir.mktmpdir }
9+
let(:workflows) do
10+
workflows = described_class.new(
11+
action: 'backup',
12+
backup_dir: tempdir,
13+
output_format: :json,
14+
resources: []
15+
)
16+
allow(workflows).to receive(:api_service).and_return(api_client_double)
17+
return workflows
18+
end
19+
let(:workflow_abc_123) do
20+
{
21+
'id' => 'abc-123-def',
22+
'attributes' => {
23+
'name' => 'Test Workflow',
24+
'description' => 'A test workflow for CI/CD',
25+
'steps' => [
26+
{
27+
'name' => 'step_1',
28+
'action' => 'com.datadoghq.http',
29+
'params' => {
30+
'url' => 'https://example.com/api',
31+
'method' => 'POST'
32+
}
33+
}
34+
],
35+
'triggers' => [
36+
{
37+
'type' => 'schedule',
38+
'schedule' => '0 9 * * 1-5'
39+
}
40+
]
41+
},
42+
'created_at' => '2024-01-01T00:00:00Z',
43+
'modified_at' => '2024-01-02T00:00:00Z',
44+
'last_executed_at' => '2024-01-03T00:00:00Z'
45+
}
46+
end
47+
let(:workflow_xyz_456) do
48+
{
49+
'id' => 'xyz-456-ghi',
50+
'attributes' => {
51+
'name' => 'Another Workflow',
52+
'description' => 'Another test workflow',
53+
'steps' => [],
54+
'triggers' => []
55+
},
56+
'created_at' => '2024-02-01T00:00:00Z',
57+
'modified_at' => '2024-02-02T00:00:00Z'
58+
}
59+
end
60+
let(:workflow_abc_123_clean) do
61+
{
62+
'id' => 'abc-123-def',
63+
'attributes' => {
64+
'name' => 'Test Workflow',
65+
'description' => 'A test workflow for CI/CD',
66+
'steps' => [
67+
{
68+
'name' => 'step_1',
69+
'action' => 'com.datadoghq.http',
70+
'params' => {
71+
'url' => 'https://example.com/api',
72+
'method' => 'POST'
73+
}
74+
}
75+
],
76+
'triggers' => [
77+
{
78+
'type' => 'schedule',
79+
'schedule' => '0 9 * * 1-5'
80+
}
81+
]
82+
}
83+
}
84+
end
85+
let(:workflow_xyz_456_clean) do
86+
{
87+
'id' => 'xyz-456-ghi',
88+
'attributes' => {
89+
'name' => 'Another Workflow',
90+
'description' => 'Another test workflow',
91+
'steps' => [],
92+
'triggers' => []
93+
}
94+
}
95+
end
96+
let(:fetched_workflows) do
97+
{
98+
'data' => [workflow_abc_123, workflow_xyz_456]
99+
}
100+
end
101+
let(:workflow_abc_123_response) do
102+
{ 'data' => workflow_abc_123 }
103+
end
104+
let(:workflow_xyz_456_response) do
105+
{ 'data' => workflow_xyz_456 }
106+
end
107+
let(:all_workflows) { respond_with200(fetched_workflows) }
108+
let(:example_workflow1) { respond_with200(workflow_abc_123_response) }
109+
let(:example_workflow2) { respond_with200(workflow_xyz_456_response) }
110+
111+
before do
112+
stubs.get('/api/v2/workflows') { all_workflows }
113+
stubs.get('/api/v2/workflows/abc-123-def') { example_workflow1 }
114+
stubs.get('/api/v2/workflows/xyz-456-ghi') { example_workflow2 }
115+
end
116+
117+
after do
118+
FileUtils.remove_entry tempdir
119+
end
120+
121+
describe '#backup' do
122+
subject { workflows.backup }
123+
124+
it 'is expected to create two files' do
125+
file1 = instance_double(File)
126+
allow(File).to receive(:open).with(workflows.filename('abc-123-def'), 'w').and_return(file1)
127+
allow(file1).to receive(:write)
128+
allow(file1).to receive(:close)
129+
130+
file2 = instance_double(File)
131+
allow(File).to receive(:open).with(workflows.filename('xyz-456-ghi'), 'w').and_return(file2)
132+
allow(file2).to receive(:write)
133+
allow(file2).to receive(:close)
134+
135+
workflows.backup
136+
expect(file1).to have_received(:write).with(::JSON.pretty_generate(workflow_abc_123_clean.deep_sort))
137+
expect(file2).to have_received(:write).with(::JSON.pretty_generate(workflow_xyz_456_clean.deep_sort))
138+
end
139+
end
140+
141+
describe '#filename' do
142+
subject { workflows.filename('abc-123-def') }
143+
144+
it { is_expected.to eq("#{tempdir}/workflows/abc-123-def.json") }
145+
end
146+
147+
describe '#get_by_id' do
148+
subject { workflows.get_by_id('abc-123-def') }
149+
150+
it { is_expected.to eq workflow_abc_123_clean }
151+
end
152+
153+
describe '#all' do
154+
subject { workflows.all }
155+
156+
it 'returns array of workflows' do
157+
expect(subject).to eq([workflow_abc_123, workflow_xyz_456])
158+
end
159+
end
160+
161+
describe '#diff' do
162+
it 'calls the api only once' do
163+
workflows.write_file('{"a":"b"}', workflows.filename('abc-123-def'))
164+
expect(workflows.diff('abc-123-def')).to eq(<<~EODASH
165+
---
166+
-attributes:
167+
- description: A test workflow for CI/CD
168+
- name: Test Workflow
169+
- steps:
170+
- - action: com.datadoghq.http
171+
- name: step_1
172+
- params:
173+
- method: POST
174+
- url: https://example.com/api
175+
- triggers:
176+
- - schedule: 0 9 * * 1-5
177+
- type: schedule
178+
-id: abc-123-def
179+
+a: b
180+
EODASH
181+
.chomp)
182+
end
183+
end
184+
185+
describe '#except' do
186+
subject { workflows.except({ :a => :b, 'created_at' => :c, 'modified_at' => :d, 'last_executed_at' => :e }) }
187+
188+
it { is_expected.to eq({ a: :b }) }
189+
end
190+
191+
describe 'private methods' do
192+
it 'uses v2 API' do
193+
expect(workflows.send(:api_version)).to eq('v2')
194+
end
195+
196+
it 'uses workflows resource name' do
197+
expect(workflows.send(:api_resource_name)).to eq('workflows')
198+
end
199+
200+
it 'uses id as id_keyname' do
201+
expect(workflows.send(:id_keyname)).to eq('id')
202+
end
203+
end
204+
end

0 commit comments

Comments
 (0)