analytics_ops 0.1.0 → 0.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +76 -0
- data/CONTRIBUTING.md +2 -1
- data/README.md +232 -41
- data/SECURITY.md +8 -8
- data/docs/api-support-matrix.md +9 -8
- data/docs/architecture.md +22 -4
- data/docs/authentication.md +112 -51
- data/docs/commands.md +71 -17
- data/docs/configuration-schema-v1.json +53 -7
- data/docs/configuration.md +33 -9
- data/docs/google-client-compatibility.md +10 -6
- data/docs/live-smoke-test.md +159 -0
- data/docs/plan-format.md +4 -0
- data/docs/plan-schema-v1.json +113 -22
- data/docs/rails.md +12 -3
- data/docs/reports.md +44 -6
- data/docs/safety.md +18 -6
- data/docs/troubleshooting.md +42 -13
- data/lib/analytics_ops/applier.rb +2 -1
- data/lib/analytics_ops/cli/options.rb +157 -0
- data/lib/analytics_ops/cli/presenter.rb +221 -0
- data/lib/analytics_ops/cli.rb +172 -201
- data/lib/analytics_ops/clients/admin.rb +130 -129
- data/lib/analytics_ops/clients/admin_normalization.rb +95 -0
- data/lib/analytics_ops/clients/data.rb +120 -63
- data/lib/analytics_ops/clients/error_translation.rb +45 -0
- data/lib/analytics_ops/configuration/document.rb +1 -1
- data/lib/analytics_ops/configuration/loader.rb +38 -5
- data/lib/analytics_ops/configuration/schema.rb +51 -8
- data/lib/analytics_ops/configuration/validator.rb +43 -8
- data/lib/analytics_ops/configuration/writer.rb +105 -0
- data/lib/analytics_ops/configuration.rb +1 -0
- data/lib/analytics_ops/connection.rb +93 -0
- data/lib/analytics_ops/desired_state.rb +2 -2
- data/lib/analytics_ops/plan.rb +94 -33
- data/lib/analytics_ops/planner.rb +10 -2
- data/lib/analytics_ops/rails/railtie.rb +13 -18
- data/lib/analytics_ops/redaction.rb +14 -9
- data/lib/analytics_ops/reports/catalog.rb +22 -5
- data/lib/analytics_ops/reports/definition.rb +74 -37
- data/lib/analytics_ops/reports/overview.rb +55 -0
- data/lib/analytics_ops/reports/overview_result.rb +54 -0
- data/lib/analytics_ops/reports/result.rb +38 -9
- data/lib/analytics_ops/reports.rb +2 -0
- data/lib/analytics_ops/resources.rb +2 -1
- data/lib/analytics_ops/service_account.rb +171 -0
- data/lib/analytics_ops/setup.rb +171 -0
- data/lib/analytics_ops/snapshot.rb +20 -5
- data/lib/analytics_ops/version.rb +1 -1
- data/lib/analytics_ops/workspace.rb +41 -12
- data/lib/analytics_ops.rb +4 -0
- data/lib/generators/analytics_ops/install_generator.rb +2 -0
- data/lib/generators/analytics_ops/templates/analytics_ops.yml +2 -15
- data/sig/analytics_ops.rbs +117 -10
- metadata +26 -1
data/lib/analytics_ops/cli.rb
CHANGED
|
@@ -20,43 +20,100 @@ module AnalyticsOps
|
|
|
20
20
|
UNSUPPORTED = 78
|
|
21
21
|
STALE_PLAN = 79
|
|
22
22
|
PARTIAL_APPLY = 80
|
|
23
|
+
INTERRUPTED = 130
|
|
23
24
|
|
|
24
|
-
COMMANDS = %w[
|
|
25
|
+
COMMANDS = %w[
|
|
26
|
+
setup properties doctor discover snapshot audit plan apply verify overview report realtime schema
|
|
27
|
+
].freeze
|
|
25
28
|
FORMATS = %w[human json csv].freeze
|
|
26
29
|
LOG_LEVELS = %w[debug info warn error].freeze
|
|
27
|
-
NO_ARGUMENT_COMMANDS = %w[doctor discover snapshot audit plan verify schema].freeze
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
NO_ARGUMENT_COMMANDS = %w[setup properties doctor discover snapshot audit plan verify overview schema].freeze
|
|
31
|
+
CONNECTION_COMMANDS = %w[setup properties discover].freeze
|
|
32
|
+
|
|
33
|
+
def self.start(arguments, out: $stdout, err: $stderr, input: $stdin, workspace_loader: nil,
|
|
34
|
+
connection_loader: nil, service_account_loader: nil, service_account_store: nil)
|
|
35
|
+
new(
|
|
36
|
+
arguments,
|
|
37
|
+
out:,
|
|
38
|
+
err:,
|
|
39
|
+
input:,
|
|
40
|
+
workspace_loader:,
|
|
41
|
+
connection_loader:,
|
|
42
|
+
service_account_loader:,
|
|
43
|
+
service_account_store:
|
|
44
|
+
).call
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def initialize(arguments, out:, err:, input:, workspace_loader:, connection_loader:,
|
|
48
|
+
service_account_loader:, service_account_store:)
|
|
34
49
|
@arguments = arguments.dup
|
|
50
|
+
@json_requested = json_option_present?(@arguments)
|
|
35
51
|
@out = out
|
|
36
52
|
@err = err
|
|
37
53
|
@input = input
|
|
38
54
|
@workspace_loader = workspace_loader || method(:load_workspace)
|
|
39
|
-
@
|
|
55
|
+
@connection_loader = connection_loader || method(:load_connection)
|
|
56
|
+
@service_account_loader = service_account_loader || ServiceAccount.method(:load)
|
|
57
|
+
@service_account_store = service_account_store
|
|
40
58
|
end
|
|
41
59
|
|
|
42
60
|
def call
|
|
61
|
+
with_error_handling { execute_command }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def execute_command
|
|
43
67
|
command = @arguments.shift
|
|
44
|
-
|
|
45
|
-
|
|
68
|
+
if [nil, "help", "--help", "-h"].include?(command)
|
|
69
|
+
raise OptionParser::InvalidArgument, "help does not accept arguments" if @arguments.any?
|
|
70
|
+
|
|
71
|
+
return write(@out, help)
|
|
72
|
+
end
|
|
73
|
+
if ["version", "--version", "-v"].include?(command)
|
|
74
|
+
raise OptionParser::InvalidArgument, "version does not accept arguments" if @arguments.any?
|
|
75
|
+
|
|
76
|
+
return write(@out, AnalyticsOps::VERSION)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
options = Options.new(@arguments)
|
|
80
|
+
@options = options.values
|
|
81
|
+
options.parse!(command)
|
|
46
82
|
return unknown_command(command) unless COMMANDS.include?(command)
|
|
47
83
|
|
|
48
|
-
|
|
49
|
-
|
|
84
|
+
execute(command)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def execute(command)
|
|
50
88
|
return render(Configuration::SCHEMA, status: SUCCESS) if command == "schema"
|
|
51
89
|
|
|
90
|
+
if CONNECTION_COMMANDS.include?(command)
|
|
91
|
+
service_account = load_service_account
|
|
92
|
+
connection = @connection_loader.call(
|
|
93
|
+
service_account:,
|
|
94
|
+
transport: @options.fetch(:transport),
|
|
95
|
+
timeout: @options[:timeout],
|
|
96
|
+
logger: operation_logger
|
|
97
|
+
)
|
|
98
|
+
return dispatch_connection(command, connection, service_account)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
service_account = load_service_account
|
|
52
102
|
workspace = @workspace_loader.call(
|
|
53
103
|
config: @options.fetch(:config),
|
|
54
104
|
profile: @options.fetch(:profile),
|
|
105
|
+
service_account:,
|
|
55
106
|
transport: @options.fetch(:transport),
|
|
56
107
|
timeout: @options[:timeout],
|
|
57
108
|
logger: operation_logger
|
|
58
109
|
)
|
|
59
110
|
dispatch(command, workspace)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def with_error_handling
|
|
114
|
+
yield
|
|
115
|
+
rescue Interrupt
|
|
116
|
+
error_response(Interrupt.new("Interrupted by user"), INTERRUPTED)
|
|
60
117
|
rescue OptionParser::ParseError, ConfirmationRequiredError => error
|
|
61
118
|
error_response(error, USAGE_ERROR)
|
|
62
119
|
rescue ConfigurationError, InvalidPlanError, ConflictError => error
|
|
@@ -79,61 +136,11 @@ module AnalyticsOps
|
|
|
79
136
|
error_response(error, REMOTE_ERROR)
|
|
80
137
|
end
|
|
81
138
|
|
|
82
|
-
private
|
|
83
|
-
|
|
84
|
-
def default_options
|
|
85
|
-
{
|
|
86
|
-
config: "config/analytics_ops.yml",
|
|
87
|
-
profile: "production",
|
|
88
|
-
format: "human",
|
|
89
|
-
log_level: "warn",
|
|
90
|
-
transport: :grpc,
|
|
91
|
-
yes: false,
|
|
92
|
-
noninteractive: false
|
|
93
|
-
}
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def parse_options!
|
|
97
|
-
parser = OptionParser.new do |value|
|
|
98
|
-
value.on("-c", "--config PATH", "Configuration path") { |path| @options[:config] = path }
|
|
99
|
-
value.on("-p", "--profile NAME", "Configuration profile") { |name| @options[:profile] = name }
|
|
100
|
-
value.on("-f", "--format FORMAT", FORMATS, FORMATS.join(", ")) { |format| @options[:format] = format }
|
|
101
|
-
value.on("-o", "--output PATH", "Write a generated plan to PATH") { |path| @options[:output] = path }
|
|
102
|
-
value.on("--log-level LEVEL", LOG_LEVELS, LOG_LEVELS.join(", ")) { |level| @options[:log_level] = level }
|
|
103
|
-
value.on("--transport TRANSPORT", %w[grpc rest], "grpc or rest") do |transport|
|
|
104
|
-
@options[:transport] = transport.to_sym
|
|
105
|
-
end
|
|
106
|
-
value.on("--timeout SECONDS", Float, "Google API timeout") do |seconds|
|
|
107
|
-
raise OptionParser::InvalidArgument, "timeout must be positive" unless seconds.positive?
|
|
108
|
-
|
|
109
|
-
@options[:timeout] = seconds
|
|
110
|
-
end
|
|
111
|
-
value.on("--yes", "Approve every operation in a saved plan") { @options[:yes] = true }
|
|
112
|
-
value.on("--non-interactive", "Never prompt") { @options[:noninteractive] = true }
|
|
113
|
-
end
|
|
114
|
-
parser.parse!(@arguments)
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def validate_command!(command)
|
|
118
|
-
if NO_ARGUMENT_COMMANDS.include?(command) && @arguments.any?
|
|
119
|
-
raise OptionParser::InvalidArgument, "#{command} does not accept positional arguments"
|
|
120
|
-
end
|
|
121
|
-
raise OptionParser::InvalidArgument, "--output is only valid with plan" if @options[:output] && command != "plan"
|
|
122
|
-
if (@options[:yes] || @options[:noninteractive]) && command != "apply"
|
|
123
|
-
raise OptionParser::InvalidArgument, "--yes and --non-interactive are only valid with apply"
|
|
124
|
-
end
|
|
125
|
-
return unless @options.fetch(:format) == "csv" && !%w[report realtime].include?(command)
|
|
126
|
-
|
|
127
|
-
raise OptionParser::InvalidArgument, "CSV output is only valid for report results"
|
|
128
|
-
end
|
|
129
|
-
|
|
130
139
|
def dispatch(command, workspace)
|
|
131
140
|
case command
|
|
132
141
|
when "doctor"
|
|
133
142
|
result = workspace.doctor
|
|
134
143
|
render(result, status: result.success? ? SUCCESS : REMOTE_ERROR)
|
|
135
|
-
when "discover"
|
|
136
|
-
render(workspace.discover)
|
|
137
144
|
when "snapshot"
|
|
138
145
|
render(workspace.snapshot)
|
|
139
146
|
when "audit"
|
|
@@ -146,6 +153,8 @@ module AnalyticsOps
|
|
|
146
153
|
when "verify"
|
|
147
154
|
verification = workspace.verify
|
|
148
155
|
render(verification, status: verification.converged ? SUCCESS : DRIFT)
|
|
156
|
+
when "overview"
|
|
157
|
+
render(workspace.overview)
|
|
149
158
|
when "report"
|
|
150
159
|
render(workspace.report(required_report_name!("report")))
|
|
151
160
|
when "realtime"
|
|
@@ -153,6 +162,42 @@ module AnalyticsOps
|
|
|
153
162
|
end
|
|
154
163
|
end
|
|
155
164
|
|
|
165
|
+
def dispatch_connection(command, connection, service_account)
|
|
166
|
+
case command
|
|
167
|
+
when "discover"
|
|
168
|
+
render(connection.discover)
|
|
169
|
+
when "properties"
|
|
170
|
+
render_properties(connection.properties)
|
|
171
|
+
when "setup"
|
|
172
|
+
setup(connection, service_account)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def setup(connection, service_account)
|
|
177
|
+
result = Setup.new(
|
|
178
|
+
connection:,
|
|
179
|
+
config: @options.fetch(:config),
|
|
180
|
+
profile: @options.fetch(:profile),
|
|
181
|
+
property_id: @options[:property],
|
|
182
|
+
noninteractive: @options[:noninteractive],
|
|
183
|
+
input: @input,
|
|
184
|
+
out: @out
|
|
185
|
+
).call
|
|
186
|
+
service_account_store.write(service_account.path)
|
|
187
|
+
|
|
188
|
+
if human?
|
|
189
|
+
action = result.created? ? "Created" : "Using"
|
|
190
|
+
@out.puts "#{action} #{Redaction.message(result.config_path)}"
|
|
191
|
+
@out.puts "Connected #{Redaction.message(result.profile)} to " \
|
|
192
|
+
"#{Redaction.message(result.property.display_name)} " \
|
|
193
|
+
"(property #{Redaction.message(result.property.id)})."
|
|
194
|
+
@out.puts "Next: analytics-ops overview"
|
|
195
|
+
SUCCESS
|
|
196
|
+
else
|
|
197
|
+
render(result)
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
156
201
|
def render_plan(plan)
|
|
157
202
|
plan.write(@options.fetch(:output)) if @options[:output]
|
|
158
203
|
render(plan)
|
|
@@ -195,154 +240,51 @@ module AnalyticsOps
|
|
|
195
240
|
true
|
|
196
241
|
end
|
|
197
242
|
|
|
198
|
-
def load_workspace(config:, profile:, transport:, timeout:, logger:)
|
|
199
|
-
Workspace.load(config, profile:, transport:, timeout:, logger:)
|
|
243
|
+
def load_workspace(config:, profile:, service_account:, transport:, timeout:, logger:)
|
|
244
|
+
Workspace.load(config, profile:, service_account:, transport:, timeout:, logger:)
|
|
200
245
|
end
|
|
201
246
|
|
|
202
|
-
def
|
|
203
|
-
|
|
204
|
-
logger.level = Logger.const_get(@options.fetch(:log_level).upcase)
|
|
205
|
-
logger.formatter = ->(_severity, _time, _program, message) { "#{message}\n" }
|
|
206
|
-
end
|
|
247
|
+
def load_connection(service_account:, transport:, timeout:, logger:)
|
|
248
|
+
Connection.new(service_account:, transport:, timeout:, logger:)
|
|
207
249
|
end
|
|
208
250
|
|
|
209
|
-
def
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
@out.write(csv(value))
|
|
215
|
-
else
|
|
216
|
-
@out.puts(human(value))
|
|
217
|
-
end
|
|
218
|
-
status
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
def json(value)
|
|
222
|
-
payload = serializable(value)
|
|
223
|
-
"#{JSON.pretty_generate(Canonical.normalize(payload))}\n"
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
def csv(value)
|
|
227
|
-
unless value.is_a?(Reports::Result)
|
|
228
|
-
raise OptionParser::InvalidArgument, "CSV output is only valid for report results"
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
lines = [value.headers]
|
|
232
|
-
value.rows.each { |row| lines << value.headers.map { |header| csv_cell(row.fetch(header)) } }
|
|
233
|
-
"#{lines.map { |line| line.map { |cell| csv_field(cell) }.join(",") }.join("\n")}\n"
|
|
234
|
-
end
|
|
235
|
-
|
|
236
|
-
def csv_cell(value)
|
|
237
|
-
string = value.to_s
|
|
238
|
-
string.match?(/\A[=+\-@]/) ? "'#{string}" : string
|
|
251
|
+
def load_service_account
|
|
252
|
+
@service_account_loader.call(
|
|
253
|
+
path: @options[:service_account],
|
|
254
|
+
store: service_account_store
|
|
255
|
+
)
|
|
239
256
|
end
|
|
240
257
|
|
|
241
|
-
def
|
|
242
|
-
|
|
243
|
-
return string unless string.match?(/[",\r\n]/)
|
|
244
|
-
|
|
245
|
-
"\"#{string.gsub("\"", "\"\"")}\""
|
|
258
|
+
def service_account_store
|
|
259
|
+
@service_account_store ||= ServiceAccount::Store.new
|
|
246
260
|
end
|
|
247
261
|
|
|
248
|
-
def
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
when Hash
|
|
253
|
-
value.to_h { |key, item| [key.to_s, serializable(item)] }
|
|
254
|
-
else
|
|
255
|
-
value.respond_to?(:to_h) ? serializable(value.to_h) : value
|
|
262
|
+
def operation_logger
|
|
263
|
+
@operation_logger ||= Logger.new(@err).tap do |logger|
|
|
264
|
+
logger.level = Logger.const_get(@options.fetch(:log_level).upcase)
|
|
265
|
+
logger.formatter = ->(_severity, _time, _program, message) { "#{message}\n" }
|
|
256
266
|
end
|
|
257
267
|
end
|
|
258
268
|
|
|
259
|
-
def
|
|
260
|
-
|
|
261
|
-
when Plan
|
|
262
|
-
human_plan(value)
|
|
263
|
-
when Snapshot
|
|
264
|
-
"Snapshot #{value.fingerprint}\n#{JSON.pretty_generate(Canonical.normalize(value.to_h))}"
|
|
265
|
-
when Workspace::DoctorResult
|
|
266
|
-
human_doctor(value)
|
|
267
|
-
when Workspace::Verification
|
|
268
|
-
"#{value.converged ? "Converged" : "Drift found"}: #{value.plan.changes.length} planned changes"
|
|
269
|
-
when Applier::Result
|
|
270
|
-
"Apply #{value.status}: #{value.applied.length} changes completed"
|
|
271
|
-
when Reports::Result
|
|
272
|
-
human_report(value)
|
|
273
|
-
when Array
|
|
274
|
-
human_discovery(value)
|
|
275
|
-
else
|
|
276
|
-
JSON.pretty_generate(Canonical.normalize(serializable(value)))
|
|
277
|
-
end
|
|
269
|
+
def render(value, status: SUCCESS)
|
|
270
|
+
presenter.render(value, status:)
|
|
278
271
|
end
|
|
279
272
|
|
|
280
|
-
def
|
|
281
|
-
value
|
|
282
|
-
"#{check.fetch("status").upcase.ljust(11)} #{check.fetch("name")}: #{check.fetch("detail")}"
|
|
283
|
-
end.join("\n")
|
|
273
|
+
def json(value)
|
|
274
|
+
presenter.json(value)
|
|
284
275
|
end
|
|
285
276
|
|
|
286
277
|
def human_plan(plan, detailed: false)
|
|
287
|
-
|
|
288
|
-
"Plan for profile #{plan.profile} / property #{plan.property_id}",
|
|
289
|
-
"Snapshot: #{plan.snapshot_fingerprint}",
|
|
290
|
-
"Changes: #{plan.changes.length}; findings: #{plan.findings.length}"
|
|
291
|
-
]
|
|
292
|
-
plan.changes.each do |change|
|
|
293
|
-
lines << " #{change.operation.upcase.ljust(6)} #{change.resource_type} #{change.resource_identity}"
|
|
294
|
-
next unless detailed
|
|
295
|
-
|
|
296
|
-
lines << " before: #{JSON.generate(Canonical.normalize(change.before))}"
|
|
297
|
-
lines << " after: #{JSON.generate(Canonical.normalize(change.after))}"
|
|
298
|
-
lines << " rollback: #{change.rollback}"
|
|
299
|
-
end
|
|
300
|
-
plan.findings.each do |finding|
|
|
301
|
-
lines << " #{finding.severity.upcase.ljust(12)} #{finding.resource_identity}: #{finding.message}"
|
|
302
|
-
end
|
|
303
|
-
lines.join("\n")
|
|
304
|
-
end
|
|
305
|
-
|
|
306
|
-
def human_report(result)
|
|
307
|
-
lines = ["Report #{result.name} (#{result.kind}) — #{result.row_count} rows"]
|
|
308
|
-
return (lines << "No rows returned").join("\n") if result.rows.empty?
|
|
309
|
-
|
|
310
|
-
widths = result.headers.to_h do |header|
|
|
311
|
-
values = result.rows.map { |row| row.fetch(header).to_s }
|
|
312
|
-
[header, ([header.length] + values.map(&:length)).max.clamp(1, 60)]
|
|
313
|
-
end
|
|
314
|
-
lines << table_row(result.headers, widths)
|
|
315
|
-
lines << result.headers.map { |header| "-" * widths.fetch(header) }.join("-+-")
|
|
316
|
-
result.rows.each do |row|
|
|
317
|
-
lines << table_row(result.headers.map { |header| row.fetch(header) }, widths, headers: result.headers)
|
|
318
|
-
end
|
|
319
|
-
lines.join("\n")
|
|
320
|
-
end
|
|
321
|
-
|
|
322
|
-
def table_row(values, widths, headers: values)
|
|
323
|
-
values.zip(headers).map do |value, header|
|
|
324
|
-
value.to_s.slice(0, widths.fetch(header)).ljust(widths.fetch(header))
|
|
325
|
-
end.join(" | ")
|
|
278
|
+
presenter.human_plan(plan, detailed:)
|
|
326
279
|
end
|
|
327
280
|
|
|
328
|
-
def
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
accounts.flat_map do |account|
|
|
332
|
-
lines = ["Account #{account.id}: #{account.display_name}"]
|
|
333
|
-
account.properties.each do |property|
|
|
334
|
-
lines << " Property #{property.fetch("id")}: #{property.fetch("display_name")}"
|
|
335
|
-
property.fetch("streams").each do |stream|
|
|
336
|
-
lines << " Stream #{stream.fetch("id")}: #{stream.fetch("display_name")} (#{stream.fetch("type")})"
|
|
337
|
-
end
|
|
338
|
-
end
|
|
339
|
-
lines
|
|
340
|
-
end.join("\n")
|
|
281
|
+
def render_properties(accounts)
|
|
282
|
+
presenter.render_properties(accounts)
|
|
341
283
|
end
|
|
342
284
|
|
|
343
285
|
def render_error_result(error, status)
|
|
344
286
|
if json?
|
|
345
|
-
@err.write(json("error" => error_payload(error), "result" =>
|
|
287
|
+
@err.write(json("error" => error_payload(error), "result" => error.result))
|
|
346
288
|
else
|
|
347
289
|
@err.puts "#{error_name(error)}: #{Redaction.message(error.message)}"
|
|
348
290
|
@err.write(json(error.result))
|
|
@@ -368,11 +310,16 @@ module AnalyticsOps
|
|
|
368
310
|
end
|
|
369
311
|
|
|
370
312
|
def human?
|
|
371
|
-
@options
|
|
313
|
+
@options&.fetch(:format, "human") == "human"
|
|
372
314
|
end
|
|
373
315
|
|
|
374
316
|
def json?
|
|
375
|
-
@options
|
|
317
|
+
@json_requested || @options&.fetch(:format, "human") == "json"
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def presenter
|
|
321
|
+
format = json? ? "json" : @options&.fetch(:format, "human") || "human"
|
|
322
|
+
@presenter ||= Presenter.new(out: @out, format:)
|
|
376
323
|
end
|
|
377
324
|
|
|
378
325
|
def write(stream, message, status = SUCCESS)
|
|
@@ -381,10 +328,22 @@ module AnalyticsOps
|
|
|
381
328
|
end
|
|
382
329
|
|
|
383
330
|
def unknown_command(command)
|
|
331
|
+
if json?
|
|
332
|
+
error = OptionParser::InvalidArgument.new("Unknown command: #{Redaction.message(command)}")
|
|
333
|
+
return error_response(error, USAGE_ERROR)
|
|
334
|
+
end
|
|
335
|
+
|
|
384
336
|
@err.puts "Unknown command: #{Redaction.message(command)}"
|
|
385
337
|
write(@err, "Run `analytics-ops help` for available commands.", USAGE_ERROR)
|
|
386
338
|
end
|
|
387
339
|
|
|
340
|
+
def json_option_present?(arguments)
|
|
341
|
+
arguments.each_with_index.any? do |argument, index|
|
|
342
|
+
argument == "--json" || argument == "--format=json" || argument == "-fjson" ||
|
|
343
|
+
(%w[--format -f].include?(argument) && arguments[index + 1] == "json")
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
|
|
388
347
|
def help
|
|
389
348
|
<<~HELP
|
|
390
349
|
Analytics Ops #{AnalyticsOps::VERSION}
|
|
@@ -393,9 +352,14 @@ module AnalyticsOps
|
|
|
393
352
|
Usage:
|
|
394
353
|
analytics-ops COMMAND [options]
|
|
395
354
|
|
|
355
|
+
Start here:
|
|
356
|
+
setup Connect Google, choose a property, and create configuration
|
|
357
|
+
overview Show a useful five-section summary for the selected property
|
|
358
|
+
properties List accessible accounts and properties without configuration
|
|
359
|
+
|
|
396
360
|
Read-only commands:
|
|
397
361
|
doctor Validate local setup and Google API/property access
|
|
398
|
-
discover List accessible accounts, properties, and
|
|
362
|
+
discover List accessible accounts, properties, and streams without configuration
|
|
399
363
|
snapshot Print normalized remote configuration
|
|
400
364
|
audit Compare desired and remote state (exit 2 for drift)
|
|
401
365
|
plan Generate a deterministic plan; use --output to save it
|
|
@@ -412,16 +376,23 @@ module AnalyticsOps
|
|
|
412
376
|
version Print the installed version
|
|
413
377
|
|
|
414
378
|
Common options:
|
|
415
|
-
-c, --config PATH
|
|
416
|
-
-p, --profile NAME
|
|
417
|
-
-f, --format FORMAT
|
|
418
|
-
|
|
419
|
-
--
|
|
420
|
-
|
|
421
|
-
--
|
|
422
|
-
--
|
|
423
|
-
--
|
|
379
|
+
-c, --config PATH Default: config/analytics_ops.yml
|
|
380
|
+
-p, --profile NAME Default: production
|
|
381
|
+
-f, --format FORMAT human, json, or report-only csv
|
|
382
|
+
--json Shortcut for --format json
|
|
383
|
+
--csv Shortcut for --format csv
|
|
384
|
+
-o, --output PATH Save a generated plan (plan only)
|
|
385
|
+
--property ID Select a property without prompting (setup only)
|
|
386
|
+
--service-account PATH Google service-account JSON key (setup only)
|
|
387
|
+
--transport NAME grpc or rest
|
|
388
|
+
--timeout SECONDS Positive Google API timeout
|
|
389
|
+
--log-level LEVEL debug, info, warn, or error
|
|
390
|
+
--yes Required for non-interactive apply
|
|
391
|
+
--non-interactive Never prompt
|
|
424
392
|
HELP
|
|
425
393
|
end
|
|
426
394
|
end
|
|
427
395
|
end
|
|
396
|
+
|
|
397
|
+
require_relative "cli/presenter"
|
|
398
|
+
require_relative "cli/options"
|