glazed 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 +19 -0
- data/lib/glazed/collector.rb +10 -6
- data/lib/glazed/command/base.rb +3 -18
- data/lib/glazed/command/orchestrate.rb +1 -1
- data/lib/glazed/command/run.rb +1 -1
- data/lib/glazed/command/send_signal.rb +21 -13
- data/lib/glazed/config.rb +50 -0
- data/lib/glazed/coordinator/task_runner.rb +189 -0
- data/lib/glazed/coordinator.rb +252 -0
- data/lib/glazed/dialect/run.rb +9 -3
- data/lib/glazed/dialect/sensitive_pattern.rb +7 -2
- data/lib/glazed/orchestrator/policy.rb +28 -0
- data/lib/glazed/orchestrator.rb +15 -65
- data/lib/glazed/proto.rb +1 -0
- data/lib/glazed/redactor.rb +65 -26
- data/lib/glazed/secret.rb +12 -37
- data/lib/glazed/secrets/command.rb +66 -0
- data/lib/glazed/secrets.rb +27 -5
- data/lib/glazed/secrets_resolver.rb +85 -0
- data/lib/glazed/shell_command.rb +41 -6
- data/lib/glazed/sources/command.rb +11 -8
- data/lib/glazed/state_backends/base.rb +11 -1
- data/lib/glazed/state_backends/sqlite.rb +38 -1
- data/lib/glazed/step_context.rb +26 -8
- data/lib/glazed/steps/{batch_shell.rb → batch_command.rb} +15 -15
- data/lib/glazed/steps/{shell.rb → command.rb} +21 -18
- data/lib/glazed/steps/fetch.rb +1 -1
- data/lib/glazed/steps/redact.rb +3 -1
- data/lib/glazed/storage_context.rb +22 -0
- data/lib/glazed/storages/base.rb +3 -2
- data/lib/glazed/storages/filesystem.rb +3 -2
- data/lib/glazed/storages/git.rb +25 -8
- data/lib/glazed/storages/github.rb +22 -8
- data/lib/glazed/storages/memory.rb +3 -2
- data/lib/glazed/storages.rb +6 -4
- data/lib/glazed/transports/base.rb +14 -2
- data/lib/glazed/transports/ssh.rb +55 -2
- data/lib/glazed/transports/telnet.rb +4 -2
- data/lib/glazed/v1/config_pb.rb +2 -1
- data/lib/glazed/v1/coordinator_pb.rb +34 -0
- data/lib/glazed/v1/inventory_pb.rb +5 -1
- data/lib/glazed/v1/orchestrator_pb.rb +1 -1
- data/lib/glazed/v1/redaction_pb.rb +2 -1
- data/lib/glazed/v1/secret_pb.rb +3 -1
- data/lib/glazed/v1/state_pb.rb +2 -1
- data/lib/glazed/v1/storage_pb.rb +1 -1
- data/lib/glazed/v1/workflow_pb.rb +3 -3
- data/lib/glazed/version.rb +1 -1
- metadata +10 -3
|
@@ -34,7 +34,7 @@ module Glazed
|
|
|
34
34
|
class SchemaMismatch < Glazed::Error; end
|
|
35
35
|
|
|
36
36
|
# Version of the database layout.
|
|
37
|
-
SCHEMA_VERSION =
|
|
37
|
+
SCHEMA_VERSION = 2 #: Integer
|
|
38
38
|
# Attempts at a change that keeps conflicting with other processes.
|
|
39
39
|
MAX_ATTEMPTS = 5 #: Integer
|
|
40
40
|
# Milliseconds to wait for another process holding the database lock.
|
|
@@ -113,6 +113,32 @@ module Glazed
|
|
|
113
113
|
end
|
|
114
114
|
end
|
|
115
115
|
|
|
116
|
+
# @yieldparam record [Proto::V1::Wakeup, nil] the current record, or nil when there is none
|
|
117
|
+
# @yieldreturn [Proto::V1::Wakeup, nil] the new record, or nil to delete it
|
|
118
|
+
# @return [Proto::V1::Wakeup, nil] the record held afterwards
|
|
119
|
+
# @raise [InvalidMessage] when the block returns an invalid record
|
|
120
|
+
# @raise [Storages::Base::Conflict] when other processes keep changing the database
|
|
121
|
+
#: () { (Proto::V1::Wakeup? record) -> Proto::V1::Wakeup? } -> Proto::V1::Wakeup?
|
|
122
|
+
def update_wakeup(&block)
|
|
123
|
+
write do |db|
|
|
124
|
+
stored = db.get_first_value("select record from wakeups where name = 'default'") #: String?
|
|
125
|
+
updated = block.call(stored && Proto::V1::Wakeup.decode(stored))
|
|
126
|
+
if updated
|
|
127
|
+
Validation.validate!(updated)
|
|
128
|
+
encoded = Proto::V1::Wakeup.encode(updated)
|
|
129
|
+
unless encoded == stored
|
|
130
|
+
db.execute(<<~SQL, [SQLite3::Blob.new(encoded)])
|
|
131
|
+
insert into wakeups (name, record) values ('default', ?)
|
|
132
|
+
on conflict (name) do update set record = excluded.record
|
|
133
|
+
SQL
|
|
134
|
+
end
|
|
135
|
+
elsif stored
|
|
136
|
+
db.execute("delete from wakeups where name = 'default'")
|
|
137
|
+
end
|
|
138
|
+
updated
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
116
142
|
# @param workflow [String] workflow name
|
|
117
143
|
# @param now [Time] the time; a lease ending at or before it has expired
|
|
118
144
|
# @param limit [Integer, nil] records returned at most; all when nil
|
|
@@ -288,12 +314,23 @@ module Glazed
|
|
|
288
314
|
) strict
|
|
289
315
|
SQL
|
|
290
316
|
db.execute('create index schedules_due on schedules (workflow, due_at_ms)')
|
|
317
|
+
create_wakeups(db)
|
|
291
318
|
db.execute('insert into glazed_schema (version) values (?)', [SCHEMA_VERSION])
|
|
319
|
+
when 1
|
|
320
|
+
create_wakeups(db)
|
|
321
|
+
db.execute('update glazed_schema set version = ?', [SCHEMA_VERSION])
|
|
292
322
|
else
|
|
293
323
|
raise SchemaMismatch, "#{@path} has state schema version #{version}, while this glazed uses #{SCHEMA_VERSION}"
|
|
294
324
|
end
|
|
295
325
|
end
|
|
296
326
|
end
|
|
327
|
+
|
|
328
|
+
# @param db [SQLite3::Database] the database, in a transaction
|
|
329
|
+
# @return [void]
|
|
330
|
+
#: (SQLite3::Database db) -> void
|
|
331
|
+
private def create_wakeups(db)
|
|
332
|
+
db.execute('create table wakeups (name text primary key, record blob not null) strict')
|
|
333
|
+
end
|
|
297
334
|
end
|
|
298
335
|
end
|
|
299
336
|
end
|
data/lib/glazed/step_context.rb
CHANGED
|
@@ -6,34 +6,52 @@
|
|
|
6
6
|
require 'google/protobuf'
|
|
7
7
|
require 'glazed'
|
|
8
8
|
require 'glazed/proto'
|
|
9
|
+
require 'glazed/secrets_resolver'
|
|
10
|
+
require 'glazed/storage_context'
|
|
9
11
|
require 'glazed/storages'
|
|
10
12
|
|
|
11
13
|
module Glazed
|
|
12
|
-
# Named storages shared by the steps of a workflow,
|
|
14
|
+
# Named storages shared by the steps of a workflow, the resolver of the
|
|
15
|
+
# secrets they refer to, and where steps report progress.
|
|
13
16
|
class StepContext
|
|
14
17
|
# Raised when a step names a storage that is not configured.
|
|
15
18
|
class UnknownStorage < Glazed::Error; end
|
|
16
19
|
|
|
20
|
+
# @return [Proto::V1::Redaction, nil] redaction settings shared by redact steps
|
|
21
|
+
#: Proto::V1::Redaction?
|
|
22
|
+
attr_reader :redaction
|
|
23
|
+
|
|
17
24
|
# @param storages [Hash{String => Storages::Base}] storages by name
|
|
25
|
+
# @param redaction [Proto::V1::Redaction, nil] redaction settings shared by redact steps
|
|
18
26
|
# @param listener [Proc] receives progress events from steps, possibly from several threads at once
|
|
19
|
-
|
|
20
|
-
|
|
27
|
+
# @param secrets [SecretsResolver] reads secrets of nodes and settings
|
|
28
|
+
#: (storages: Hash[String, Storages::Base], ?redaction: Proto::V1::Redaction?, ?listener: Runner::listener, ?secrets: SecretsResolver) -> void
|
|
29
|
+
def initialize(storages:, redaction: nil, listener: ->(_event) {}, secrets: SecretsResolver.new)
|
|
21
30
|
@storages = storages #: Hash[String, Storages::Base]
|
|
31
|
+
@redaction = redaction #: Proto::V1::Redaction?
|
|
22
32
|
@listener = listener #: Runner::listener
|
|
33
|
+
@secrets = secrets #: SecretsResolver
|
|
23
34
|
end
|
|
24
35
|
|
|
25
|
-
#
|
|
36
|
+
# @return [SecretsResolver] reads secrets of nodes and settings
|
|
37
|
+
#: SecretsResolver
|
|
38
|
+
attr_reader :secrets
|
|
39
|
+
|
|
40
|
+
# Builds every configured storage, with one secrets resolver for the storages and the steps.
|
|
26
41
|
#
|
|
27
42
|
# @param storages [Google::Protobuf::Map] storage configs by name, from {Proto::V1::Config#storages}
|
|
43
|
+
# @param redaction [Proto::V1::Redaction, nil] redaction settings shared by redact steps, from {Proto::V1::Config#redaction}
|
|
28
44
|
# @param listener [Proc] receives progress events from steps, possibly from several threads at once
|
|
29
45
|
# @return [StepContext] the context
|
|
30
46
|
# @raise [Storages::MissingBackend] when a storage config sets no backend
|
|
31
|
-
#: (Google::Protobuf::Map[String, Proto::V1::StorageConfig] storages, ?listener: Runner::listener) -> StepContext
|
|
32
|
-
def self.from_proto(storages, listener: ->(_event) {})
|
|
47
|
+
#: (Google::Protobuf::Map[String, Proto::V1::StorageConfig] storages, ?redaction: Proto::V1::Redaction?, ?listener: Runner::listener) -> StepContext
|
|
48
|
+
def self.from_proto(storages, redaction: nil, listener: ->(_event) {})
|
|
49
|
+
secrets = SecretsResolver.new
|
|
50
|
+
storage_context = StorageContext.new(secrets:)
|
|
33
51
|
# Map#to_h would turn the messages into hashes.
|
|
34
52
|
built = {} #: Hash[String, Storages::Base]
|
|
35
|
-
storages.each { |name, config| built[name] = Storages.build(config) }
|
|
36
|
-
new(storages: built, listener:)
|
|
53
|
+
storages.each { |name, config| built[name] = Storages.build(config, context: storage_context) }
|
|
54
|
+
new(storages: built, redaction:, listener:, secrets:)
|
|
37
55
|
end
|
|
38
56
|
|
|
39
57
|
# Reports progress of a step.
|
|
@@ -11,41 +11,41 @@ require 'glazed/steps'
|
|
|
11
11
|
|
|
12
12
|
module Glazed
|
|
13
13
|
module Steps
|
|
14
|
-
# Batch: runs a command with /bin/sh -c
|
|
15
|
-
# {Proto::V1::BatchState} of every node that got
|
|
16
|
-
# stdin, for hooks such as notifications.
|
|
14
|
+
# Batch: runs a command line with /bin/sh -c, or a program with its
|
|
15
|
+
# arguments, once with the {Proto::V1::BatchState} of every node that got
|
|
16
|
+
# this far as ProtoJSON on stdin, for hooks such as notifications.
|
|
17
17
|
#
|
|
18
18
|
# @example Step definition
|
|
19
|
-
# {"name": "notify", "
|
|
20
|
-
class
|
|
19
|
+
# {"name": "notify", "batch_command": {"run": "bin/notify-changes"}}
|
|
20
|
+
class BatchCommand < BatchStep
|
|
21
21
|
# Seconds a command may run when the definition sets none.
|
|
22
22
|
DEFAULT_TIMEOUT = 300 #: Integer
|
|
23
23
|
|
|
24
24
|
# @param name [String] step name
|
|
25
|
-
# @param run [String] command line
|
|
25
|
+
# @param run [String, Array<String>] command line for /bin/sh -c, or program and its arguments
|
|
26
26
|
# @param timeout [Numeric] seconds before the command is killed
|
|
27
|
-
#: (name: String, run: String, ?timeout: Numeric) -> void
|
|
27
|
+
#: (name: String, run: String | Array[String], ?timeout: Numeric) -> void
|
|
28
28
|
def initialize(name:, run:, timeout: DEFAULT_TIMEOUT)
|
|
29
29
|
super(name:)
|
|
30
|
-
@run = run #: String
|
|
30
|
+
@run = run #: String | Array[String]
|
|
31
31
|
@timeout = timeout #: Numeric
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
# Builds the step from {Proto::V1::
|
|
34
|
+
# Builds the step from {Proto::V1::BatchCommandStep}.
|
|
35
35
|
#
|
|
36
36
|
# @param name [String] step name
|
|
37
|
-
# @param settings [Google::Protobuf::MessageExts] a {Proto::V1::
|
|
37
|
+
# @param settings [Google::Protobuf::MessageExts] a {Proto::V1::BatchCommandStep}
|
|
38
38
|
# @param context [StepContext] named storages, unused
|
|
39
|
-
# @return [
|
|
40
|
-
#: (String name, Google::Protobuf::MessageExts settings, context: StepContext) ->
|
|
39
|
+
# @return [BatchCommand] the step
|
|
40
|
+
#: (String name, Google::Protobuf::MessageExts settings, context: StepContext) -> BatchCommand
|
|
41
41
|
def self.from_proto(name, settings, context:)
|
|
42
|
-
|
|
43
|
-
new(name:, run:
|
|
42
|
+
command = Must.be_a(settings, Proto::V1::BatchCommandStep)
|
|
43
|
+
new(name:, run: ShellCommand.run_of(command.run, command.exec), timeout: command.timeout_seconds.zero? ? DEFAULT_TIMEOUT : command.timeout_seconds)
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
# @param batch [Proto::V1::BatchState] node states
|
|
47
47
|
# @return [Proto::V1::BatchState] the same states
|
|
48
|
-
# @raise [ShellCommand::Failed] when the command exits non-zero
|
|
48
|
+
# @raise [ShellCommand::Failed] when the command cannot start or exits non-zero
|
|
49
49
|
# @raise [TimeoutError] when the command does not finish in time
|
|
50
50
|
#: (Proto::V1::BatchState batch) -> Proto::V1::BatchState
|
|
51
51
|
def call(batch)
|
|
@@ -12,28 +12,31 @@ require 'glazed/storages'
|
|
|
12
12
|
|
|
13
13
|
module Glazed
|
|
14
14
|
module Steps
|
|
15
|
-
# Per node: runs a command with /bin/sh -c
|
|
16
|
-
#
|
|
15
|
+
# Per node: runs a command line with /bin/sh -c, or a program with its
|
|
16
|
+
# arguments. The input artifact's content is on stdin and stdout becomes
|
|
17
|
+
# the output artifact; both are optional.
|
|
17
18
|
#
|
|
18
19
|
# @example Step definition
|
|
19
|
-
# {"name": "normalize", "
|
|
20
|
+
# {"name": "normalize", "command": {"run": "sed -e '/^ntp clock-period/d'",
|
|
20
21
|
# "input": "raw", "output": "normalized", "storage": "staging"}}
|
|
21
|
-
|
|
22
|
+
# @example Step definition without a shell
|
|
23
|
+
# {"name": "lint", "command": {"exec": ["bin/lint-config", "--strict"], "input": "raw"}}
|
|
24
|
+
class Command < NodeStep
|
|
22
25
|
# Seconds a command may run when the definition sets none.
|
|
23
26
|
DEFAULT_TIMEOUT = 300 #: Integer
|
|
24
27
|
|
|
25
28
|
# @param name [String] step name
|
|
26
|
-
# @param run [String] command line
|
|
29
|
+
# @param run [String, Array<String>] command line for /bin/sh -c, or program and its arguments
|
|
27
30
|
# @param input [String, nil] artifact written to stdin
|
|
28
31
|
# @param output [String, nil] artifact produced from stdout
|
|
29
32
|
# @param storage_name [String, nil] storage for the output, by name
|
|
30
33
|
# @param context [StepContext] named storages
|
|
31
34
|
# @param timeout [Numeric] seconds before the command is killed
|
|
32
35
|
# @raise [StepContext::UnknownStorage] when the storage is not configured
|
|
33
|
-
#: (name: String, run: String, input: String?, output: String?, storage_name: String?, context: StepContext, ?timeout: Numeric) -> void
|
|
36
|
+
#: (name: String, run: String | Array[String], input: String?, output: String?, storage_name: String?, context: StepContext, ?timeout: Numeric) -> void
|
|
34
37
|
def initialize(name:, run:, input:, output:, storage_name:, context:, timeout: DEFAULT_TIMEOUT)
|
|
35
38
|
super(name:)
|
|
36
|
-
@run = run #: String
|
|
39
|
+
@run = run #: String | Array[String]
|
|
37
40
|
@input = input #: String?
|
|
38
41
|
@output = output #: String?
|
|
39
42
|
@storage_name = storage_name #: String?
|
|
@@ -42,21 +45,21 @@ module Glazed
|
|
|
42
45
|
@timeout = timeout #: Numeric
|
|
43
46
|
end
|
|
44
47
|
|
|
45
|
-
# Builds the step from {Proto::V1::
|
|
48
|
+
# Builds the step from {Proto::V1::CommandStep}.
|
|
46
49
|
#
|
|
47
50
|
# @param name [String] step name
|
|
48
|
-
# @param settings [Google::Protobuf::MessageExts] a {Proto::V1::
|
|
51
|
+
# @param settings [Google::Protobuf::MessageExts] a {Proto::V1::CommandStep}
|
|
49
52
|
# @param context [StepContext] named storages
|
|
50
|
-
# @return [
|
|
51
|
-
#: (String name, Google::Protobuf::MessageExts settings, context: StepContext) ->
|
|
53
|
+
# @return [Command] the step
|
|
54
|
+
#: (String name, Google::Protobuf::MessageExts settings, context: StepContext) -> Command
|
|
52
55
|
def self.from_proto(name, settings, context:)
|
|
53
|
-
|
|
56
|
+
command = Must.be_a(settings, Proto::V1::CommandStep)
|
|
54
57
|
new(
|
|
55
|
-
name:, run:
|
|
56
|
-
input:
|
|
57
|
-
output:
|
|
58
|
-
storage_name:
|
|
59
|
-
timeout:
|
|
58
|
+
name:, run: ShellCommand.run_of(command.run, command.exec), context:,
|
|
59
|
+
input: command.input.empty? ? nil : command.input,
|
|
60
|
+
output: command.output.empty? ? nil : command.output,
|
|
61
|
+
storage_name: command.storage.empty? ? nil : command.storage,
|
|
62
|
+
timeout: command.timeout_seconds.zero? ? DEFAULT_TIMEOUT : command.timeout_seconds,
|
|
60
63
|
)
|
|
61
64
|
end
|
|
62
65
|
|
|
@@ -70,7 +73,7 @@ module Glazed
|
|
|
70
73
|
|
|
71
74
|
# @param state [Proto::V1::NodeState] the node and its artifacts
|
|
72
75
|
# @return [Proto::V1::NodeState] the state, with the output artifact when configured
|
|
73
|
-
# @raise [ShellCommand::Failed] when the command exits non-zero
|
|
76
|
+
# @raise [ShellCommand::Failed] when the command cannot start or exits non-zero
|
|
74
77
|
# @raise [TimeoutError] when the command does not finish in time
|
|
75
78
|
# @raise [MissingArtifact] when the input artifact is absent
|
|
76
79
|
#: (Proto::V1::NodeState state) -> Proto::V1::NodeState
|
data/lib/glazed/steps/fetch.rb
CHANGED
|
@@ -58,7 +58,7 @@ module Glazed
|
|
|
58
58
|
def call(state)
|
|
59
59
|
node = state.node or raise ArgumentError, "#{name}: state has no node"
|
|
60
60
|
progress = ->(message) { @context.notify(level: 'info', step: name, node: node.name, message:) } #: ^(String) -> void
|
|
61
|
-
config = Collector.new(node:, timeout: @timeout, progress:).call
|
|
61
|
+
config = Collector.new(node:, timeout: @timeout, progress:, secrets: @context.secrets).call
|
|
62
62
|
result = @storage.write(key: Storages.key_for(node), content: config, message: "#{node.name}: #{name}")
|
|
63
63
|
with_artifacts(state, @output => stored(@storage_name, result))
|
|
64
64
|
end
|
data/lib/glazed/steps/redact.rb
CHANGED
|
@@ -67,7 +67,9 @@ module Glazed
|
|
|
67
67
|
#: (Proto::V1::NodeState state) -> Proto::V1::NodeState
|
|
68
68
|
def call(state)
|
|
69
69
|
node = state.node or raise ArgumentError, "#{name}: state has no node"
|
|
70
|
-
redactor = Redactor.from_proto(
|
|
70
|
+
redactor = Redactor.from_proto(
|
|
71
|
+
@redaction, patterns: Dialects.find(node.dialect).sensitive_patterns, global: @context.redaction, secrets: @context.secrets,
|
|
72
|
+
)
|
|
71
73
|
content = redactor.call(@context.read(artifact(state, @input)))
|
|
72
74
|
result = @storage.write(key: Storages.key_for(node), content:, message: "#{node.name}: #{name}")
|
|
73
75
|
with_artifacts(state, @output => stored(@storage_name, result))
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
|
|
6
|
+
require 'glazed'
|
|
7
|
+
require 'glazed/secrets_resolver'
|
|
8
|
+
|
|
9
|
+
module Glazed
|
|
10
|
+
# What storages are built with, besides their own settings.
|
|
11
|
+
class StorageContext
|
|
12
|
+
# @param secrets [SecretsResolver] reads secrets the storages refer to
|
|
13
|
+
#: (?secrets: SecretsResolver) -> void
|
|
14
|
+
def initialize(secrets: SecretsResolver.new)
|
|
15
|
+
@secrets = secrets #: SecretsResolver
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @return [SecretsResolver] reads secrets the storages refer to
|
|
19
|
+
#: SecretsResolver
|
|
20
|
+
attr_reader :secrets
|
|
21
|
+
end
|
|
22
|
+
end
|
data/lib/glazed/storages/base.rb
CHANGED
|
@@ -64,9 +64,10 @@ module Glazed
|
|
|
64
64
|
# Builds a storage from its settings message.
|
|
65
65
|
#
|
|
66
66
|
# @param settings [Google::Protobuf::MessageExts] backend settings from {Proto::V1::StorageConfig}
|
|
67
|
+
# @param context [StorageContext] secrets the settings refer to
|
|
67
68
|
# @return [Base] the storage
|
|
68
|
-
#: (Google::Protobuf::MessageExts settings) -> Base
|
|
69
|
-
def self.from_proto(settings) = raise NotImplementedError
|
|
69
|
+
#: (Google::Protobuf::MessageExts settings, context: StorageContext) -> Base
|
|
70
|
+
def self.from_proto(settings, context:) = raise NotImplementedError
|
|
70
71
|
|
|
71
72
|
# Writes one file.
|
|
72
73
|
#
|
|
@@ -33,10 +33,11 @@ module Glazed
|
|
|
33
33
|
# Builds a storage from {Proto::V1::FilesystemStorage}.
|
|
34
34
|
#
|
|
35
35
|
# @param settings [Google::Protobuf::MessageExts] a {Proto::V1::FilesystemStorage}
|
|
36
|
+
# @param context [StorageContext] secrets the settings refer to
|
|
36
37
|
# @return [Filesystem] the storage
|
|
37
38
|
# @raise [Must::Violation] when settings is another message
|
|
38
|
-
#: (Google::Protobuf::MessageExts settings) -> Filesystem
|
|
39
|
-
def self.from_proto(settings)
|
|
39
|
+
#: (Google::Protobuf::MessageExts settings, context: StorageContext) -> Filesystem
|
|
40
|
+
def self.from_proto(settings, context:)
|
|
40
41
|
filesystem = Must.be_a(settings, Proto::V1::FilesystemStorage)
|
|
41
42
|
new(path: filesystem.path)
|
|
42
43
|
end
|
data/lib/glazed/storages/git.rb
CHANGED
|
@@ -34,24 +34,29 @@ module Glazed
|
|
|
34
34
|
# @param remote [String, nil] remote name or URL to pull from and push to
|
|
35
35
|
# @param author_name [String] commit author and committer name
|
|
36
36
|
# @param author_email [String] commit author and committer email
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
# @param subdirectory [String, nil] directory within the repository holding the files; the root when nil
|
|
38
|
+
# @raise [InvalidKey] for an unsafe subdirectory
|
|
39
|
+
#: (path: String, ?branch: String, ?remote: String?, ?author_name: String, ?author_email: String, ?subdirectory: String?) -> void
|
|
40
|
+
def initialize(path:, branch: DEFAULT_BRANCH, remote: nil, author_name: DEFAULT_AUTHOR_NAME, author_email: DEFAULT_AUTHOR_EMAIL, subdirectory: nil)
|
|
39
41
|
super()
|
|
40
42
|
@path = path #: String
|
|
41
43
|
@branch = branch #: String
|
|
42
44
|
@remote = remote #: String?
|
|
43
45
|
@author_name = author_name #: String
|
|
44
46
|
@author_email = author_email #: String
|
|
47
|
+
@subdirectory = subdirectory&.then { validate_key(it.chomp('/')) } #: String?
|
|
45
48
|
@lock = Mutex.new #: Thread::Mutex
|
|
46
49
|
end
|
|
47
50
|
|
|
48
51
|
# Builds a storage from {Proto::V1::GitStorage}.
|
|
49
52
|
#
|
|
50
53
|
# @param settings [Google::Protobuf::MessageExts] a {Proto::V1::GitStorage}
|
|
54
|
+
# @param context [StorageContext] secrets the settings refer to
|
|
51
55
|
# @return [Git] the storage
|
|
52
56
|
# @raise [Must::Violation] when settings is another message
|
|
53
|
-
|
|
54
|
-
|
|
57
|
+
# @raise [InvalidKey] for an unsafe subdirectory
|
|
58
|
+
#: (Google::Protobuf::MessageExts settings, context: StorageContext) -> Git
|
|
59
|
+
def self.from_proto(settings, context:)
|
|
55
60
|
git = Must.be_a(settings, Proto::V1::GitStorage)
|
|
56
61
|
new(
|
|
57
62
|
path: git.path,
|
|
@@ -59,6 +64,7 @@ module Glazed
|
|
|
59
64
|
remote: git.remote.empty? ? nil : git.remote,
|
|
60
65
|
author_name: git.author_name.empty? ? DEFAULT_AUTHOR_NAME : git.author_name,
|
|
61
66
|
author_email: git.author_email.empty? ? DEFAULT_AUTHOR_EMAIL : git.author_email,
|
|
67
|
+
subdirectory: git.subdirectory.empty? ? nil : git.subdirectory,
|
|
62
68
|
)
|
|
63
69
|
end
|
|
64
70
|
|
|
@@ -71,6 +77,9 @@ module Glazed
|
|
|
71
77
|
# @return [String, nil] remote name or URL
|
|
72
78
|
#: String?
|
|
73
79
|
attr_reader :remote
|
|
80
|
+
# @return [String, nil] directory within the repository holding the files
|
|
81
|
+
#: String?
|
|
82
|
+
attr_reader :subdirectory
|
|
74
83
|
|
|
75
84
|
# Writes files in one commit. Nothing is committed when no content
|
|
76
85
|
# changed, and the pointers then name the existing HEAD.
|
|
@@ -83,22 +92,25 @@ module Glazed
|
|
|
83
92
|
#: (Array[Base::file] files, message: Base::message_builder) -> Array[WriteResult]
|
|
84
93
|
def write_all(files, message:)
|
|
85
94
|
keys = files.map { |key, _| validate_key(key) }
|
|
95
|
+
paths = keys.map { repository_path(it) }
|
|
86
96
|
@lock.synchronize do
|
|
87
97
|
prepare
|
|
88
98
|
files.each do |key, content|
|
|
89
|
-
file = File.join(path, key)
|
|
99
|
+
file = File.join(path, repository_path(key))
|
|
90
100
|
FileUtils.mkdir_p(File.dirname(file))
|
|
91
101
|
File.write(file, content)
|
|
92
102
|
end
|
|
93
|
-
git(['add', '--', *
|
|
94
|
-
|
|
103
|
+
git(['add', '--', *paths])
|
|
104
|
+
changed_paths = git(['diff', '--cached', '--name-only', '-z', '--', *paths]).split("\0")
|
|
105
|
+
changed = keys.select { changed_paths.include?(repository_path(it)) }
|
|
95
106
|
unless changed.empty?
|
|
96
107
|
git(['commit', '--quiet', '--no-verify', '-m', message.call(changed)])
|
|
97
108
|
git(['push', '--quiet', Must.expect(remote), "HEAD:refs/heads/#{branch}"]) if remote
|
|
98
109
|
end
|
|
99
110
|
commit = git(%w[rev-parse HEAD]).strip
|
|
100
111
|
keys.map do |key|
|
|
101
|
-
|
|
112
|
+
location = Proto::V1::GitLocation.new(path: repository_path(key), commit:)
|
|
113
|
+
WriteResult.new(pointer: pointer_to(location), changed: changed.include?(key))
|
|
102
114
|
end
|
|
103
115
|
end
|
|
104
116
|
end
|
|
@@ -115,6 +127,11 @@ module Glazed
|
|
|
115
127
|
git(['show', "#{location.commit}:#{validate_key(location.path)}"])
|
|
116
128
|
end
|
|
117
129
|
|
|
130
|
+
# @param key [String] validated file key
|
|
131
|
+
# @return [String] path of the file within the repository
|
|
132
|
+
#: (String key) -> String
|
|
133
|
+
private def repository_path(key) = subdirectory ? "#{subdirectory}/#{key}" : key
|
|
134
|
+
|
|
118
135
|
# Initialises the repository when missing, switches to the branch and
|
|
119
136
|
# pulls it from the remote when it exists there.
|
|
120
137
|
#
|
|
@@ -51,8 +51,10 @@ module Glazed
|
|
|
51
51
|
# @param api_url [String] API endpoint
|
|
52
52
|
# @param author_name [String, nil] commit author name; the token owner when nil
|
|
53
53
|
# @param author_email [String, nil] commit author email; the token owner when nil
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
# @param subdirectory [String, nil] directory within the repository holding the files; the root when nil
|
|
55
|
+
# @raise [InvalidKey] for an unsafe subdirectory
|
|
56
|
+
#: (repository: String, branch: String, token: Secret, ?base_branch: String?, ?api_url: String, ?author_name: String?, ?author_email: String?, ?subdirectory: String?) -> void
|
|
57
|
+
def initialize(repository:, branch:, token:, base_branch: nil, api_url: DEFAULT_API_URL, author_name: nil, author_email: nil, subdirectory: nil)
|
|
56
58
|
super()
|
|
57
59
|
@repository = repository #: String
|
|
58
60
|
@branch = branch #: String
|
|
@@ -60,26 +62,30 @@ module Glazed
|
|
|
60
62
|
@base_branch = base_branch #: String?
|
|
61
63
|
@api_url = api_url.chomp('/') #: String
|
|
62
64
|
@author = (author_name && author_email ? {name: author_name, email: author_email} : nil) #: Hash[Symbol, String]?
|
|
65
|
+
@subdirectory = subdirectory&.then { validate_key(it.chomp('/')) } #: String?
|
|
63
66
|
@http = nil #: HTTPX::Session?
|
|
64
67
|
end
|
|
65
68
|
|
|
66
69
|
# Builds a storage from {Proto::V1::GithubStorage}.
|
|
67
70
|
#
|
|
68
71
|
# @param settings [Google::Protobuf::MessageExts] a {Proto::V1::GithubStorage}
|
|
72
|
+
# @param context [StorageContext] secrets the settings refer to
|
|
69
73
|
# @return [Github] the storage
|
|
70
74
|
# @raise [Must::Violation] when settings is another message
|
|
71
75
|
# @raise [ArgumentError] when the token is missing
|
|
72
|
-
|
|
73
|
-
|
|
76
|
+
# @raise [InvalidKey] for an unsafe subdirectory
|
|
77
|
+
#: (Google::Protobuf::MessageExts settings, context: StorageContext) -> Github
|
|
78
|
+
def self.from_proto(settings, context:)
|
|
74
79
|
github = Must.be_a(settings, Proto::V1::GithubStorage)
|
|
75
80
|
new(
|
|
76
81
|
repository: github.repository,
|
|
77
82
|
branch: github.branch,
|
|
78
|
-
token:
|
|
83
|
+
token: context.secrets.maybe(github.token) || raise(ArgumentError, 'github storage needs a token'),
|
|
79
84
|
base_branch: github.base_branch.empty? ? nil : github.base_branch,
|
|
80
85
|
api_url: github.api_url.empty? ? DEFAULT_API_URL : github.api_url,
|
|
81
86
|
author_name: github.author_name.empty? ? nil : github.author_name,
|
|
82
87
|
author_email: github.author_email.empty? ? nil : github.author_email,
|
|
88
|
+
subdirectory: github.subdirectory.empty? ? nil : github.subdirectory,
|
|
83
89
|
)
|
|
84
90
|
end
|
|
85
91
|
|
|
@@ -89,6 +95,9 @@ module Glazed
|
|
|
89
95
|
# @return [String] branch to commit to
|
|
90
96
|
#: String
|
|
91
97
|
attr_reader :branch
|
|
98
|
+
# @return [String, nil] directory within the repository holding the files
|
|
99
|
+
#: String?
|
|
100
|
+
attr_reader :subdirectory
|
|
92
101
|
|
|
93
102
|
# Writes the changed files in one commit. Nothing is committed when no
|
|
94
103
|
# content changed, and the pointers then name the existing head.
|
|
@@ -103,7 +112,7 @@ module Glazed
|
|
|
103
112
|
keys = files.map { |key, _| validate_key(key) }
|
|
104
113
|
commit, changed = with_retries { commit_files(files, message) }
|
|
105
114
|
keys.map do |key|
|
|
106
|
-
pointer = pointer_to(Proto::V1::GithubLocation.new(repository:, path: key, commit:))
|
|
115
|
+
pointer = pointer_to(Proto::V1::GithubLocation.new(repository:, path: repository_path(key), commit:))
|
|
107
116
|
WriteResult.new(pointer:, changed: changed.include?(key))
|
|
108
117
|
end
|
|
109
118
|
end
|
|
@@ -147,12 +156,12 @@ module Glazed
|
|
|
147
156
|
parent = head || ref_sha(base_branch) || raise(ApiError.new(status: 404, message: "#{repository}: base branch #{base_branch} not found"))
|
|
148
157
|
base_tree = json(get("/repos/#{repository}/git/commits/#{parent}")).dig('tree', 'sha')
|
|
149
158
|
existing = blob_shas(base_tree)
|
|
150
|
-
candidates = existing ? files.reject { |key, content| existing[key] == blob_sha(content) } : files
|
|
159
|
+
candidates = existing ? files.reject { |key, content| existing[repository_path(key)] == blob_sha(content) } : files
|
|
151
160
|
|
|
152
161
|
commit = parent
|
|
153
162
|
changed = [] #: Array[String]
|
|
154
163
|
unless candidates.empty?
|
|
155
|
-
entries = candidates.map { |key, content| {path: key, mode: '100644', type: 'blob', content:} }
|
|
164
|
+
entries = candidates.map { |key, content| {path: repository_path(key), mode: '100644', type: 'blob', content:} }
|
|
156
165
|
tree = json(post("/repos/#{repository}/git/trees", {base_tree:, tree: entries})).fetch('sha')
|
|
157
166
|
unless tree == base_tree
|
|
158
167
|
changed = candidates.map(&:first)
|
|
@@ -181,6 +190,11 @@ module Glazed
|
|
|
181
190
|
entries.select { it['type'] == 'blob' }.to_h { [it.fetch('path'), it.fetch('sha')] }
|
|
182
191
|
end
|
|
183
192
|
|
|
193
|
+
# @param key [String] validated file key
|
|
194
|
+
# @return [String] path of the file within the repository
|
|
195
|
+
#: (String key) -> String
|
|
196
|
+
private def repository_path(key) = subdirectory ? "#{subdirectory}/#{key}" : key
|
|
197
|
+
|
|
184
198
|
# @param content [String] file content
|
|
185
199
|
# @return [String] git blob SHA-1 of the content
|
|
186
200
|
#: (String content) -> String
|
|
@@ -32,10 +32,11 @@ module Glazed
|
|
|
32
32
|
# Builds a storage from {Proto::V1::MemoryStorage}.
|
|
33
33
|
#
|
|
34
34
|
# @param settings [Google::Protobuf::MessageExts] a {Proto::V1::MemoryStorage}
|
|
35
|
+
# @param context [StorageContext] secrets the settings refer to
|
|
35
36
|
# @return [Memory] the storage
|
|
36
37
|
# @raise [Must::Violation] when settings is another message
|
|
37
|
-
#: (Google::Protobuf::MessageExts settings) -> Memory
|
|
38
|
-
def self.from_proto(settings)
|
|
38
|
+
#: (Google::Protobuf::MessageExts settings, context: StorageContext) -> Memory
|
|
39
|
+
def self.from_proto(settings, context:)
|
|
39
40
|
Must.be_a(settings, Proto::V1::MemoryStorage)
|
|
40
41
|
new
|
|
41
42
|
end
|
data/lib/glazed/storages.rb
CHANGED
|
@@ -8,6 +8,7 @@ require 'glazed/finder'
|
|
|
8
8
|
require 'glazed/must'
|
|
9
9
|
require 'glazed/plugin_registry'
|
|
10
10
|
require 'glazed/proto'
|
|
11
|
+
require 'glazed/storage_context'
|
|
11
12
|
require 'glazed/storages/base'
|
|
12
13
|
require 'glazed/validation'
|
|
13
14
|
|
|
@@ -49,21 +50,22 @@ module Glazed
|
|
|
49
50
|
# Builds the storage a config selects.
|
|
50
51
|
#
|
|
51
52
|
# @param config [Proto::V1::StorageConfig] storage settings
|
|
53
|
+
# @param context [StorageContext] secrets the settings refer to
|
|
52
54
|
# @return [Base] the storage
|
|
53
55
|
# @raise [InvalidMessage] when the config breaks the rules of its schema
|
|
54
56
|
# @raise [Finder::NotFound] when the backend is not available
|
|
55
57
|
# @raise [PluginRegistry::UnknownPlugin] when no plugin is registered for the settings type
|
|
56
|
-
#: (Proto::V1::StorageConfig config) -> Base
|
|
57
|
-
def self.build(config)
|
|
58
|
+
#: (Proto::V1::StorageConfig config, ?context: StorageContext) -> Base
|
|
59
|
+
def self.build(config, context: StorageContext.new)
|
|
58
60
|
Validation.validate!(config)
|
|
59
61
|
backend = config.backend or raise MissingBackend, 'storage config sets no backend'
|
|
60
62
|
if (packed = config.plugin)
|
|
61
63
|
implementation, settings = PLUGINS.resolve(packed)
|
|
62
|
-
return storage_class(implementation).from_proto(settings)
|
|
64
|
+
return storage_class(implementation).from_proto(settings, context:)
|
|
63
65
|
end
|
|
64
66
|
|
|
65
67
|
settings = Must.be_a(config[backend.to_s], Google::Protobuf::MessageExts)
|
|
66
|
-
find(backend).from_proto(settings)
|
|
68
|
+
find(backend).from_proto(settings, context:)
|
|
67
69
|
end
|
|
68
70
|
|
|
69
71
|
# @param implementation [Module] registered implementation
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
# SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
|
|
6
6
|
require 'glazed'
|
|
7
|
+
require 'glazed/proto'
|
|
7
8
|
require 'glazed/secret'
|
|
9
|
+
require 'glazed/secrets_resolver'
|
|
8
10
|
|
|
9
11
|
module Glazed
|
|
10
12
|
module Transports
|
|
@@ -18,13 +20,17 @@ module Glazed
|
|
|
18
20
|
# @param username [String] login user name
|
|
19
21
|
# @param password [Secret, nil] login password
|
|
20
22
|
# @param connect_timeout [Numeric] seconds to wait for a connection
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
# @param transport_options [Proto::V1::TransportOptions, nil] per-transport connection options
|
|
24
|
+
# @param secrets [SecretsResolver] reads secrets in the transport options
|
|
25
|
+
#: (host: String, port: Integer?, username: String, password: Secret?, ?connect_timeout: Numeric, ?transport_options: Proto::V1::TransportOptions?, ?secrets: SecretsResolver) -> void
|
|
26
|
+
def initialize(host:, port:, username:, password:, connect_timeout: DEFAULT_CONNECT_TIMEOUT, transport_options: nil, secrets: SecretsResolver.new)
|
|
23
27
|
@host = host #: String
|
|
24
28
|
@port = port #: Integer?
|
|
25
29
|
@username = username #: String
|
|
26
30
|
@password = password #: Secret?
|
|
27
31
|
@connect_timeout = connect_timeout #: Numeric
|
|
32
|
+
@transport_options = transport_options #: Proto::V1::TransportOptions?
|
|
33
|
+
@secrets = secrets #: SecretsResolver
|
|
28
34
|
@pty_options = {} #: Hash[Symbol, untyped]
|
|
29
35
|
end
|
|
30
36
|
|
|
@@ -43,6 +49,12 @@ module Glazed
|
|
|
43
49
|
# @return [Numeric] seconds to wait for a connection
|
|
44
50
|
#: Numeric
|
|
45
51
|
attr_reader :connect_timeout
|
|
52
|
+
# @return [Proto::V1::TransportOptions, nil] per-transport connection options
|
|
53
|
+
#: Proto::V1::TransportOptions?
|
|
54
|
+
attr_reader :transport_options
|
|
55
|
+
# @return [SecretsResolver] reads secrets in the transport options
|
|
56
|
+
#: SecretsResolver
|
|
57
|
+
attr_reader :secrets
|
|
46
58
|
# @return [Hash{Symbol => Object}] terminal settings overriding the transport's defaults; ignored by transports without a PTY
|
|
47
59
|
#: Hash[Symbol, untyped]
|
|
48
60
|
attr_accessor :pty_options
|