textus 0.8.1 → 0.9.2
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 +224 -0
- data/README.md +50 -22
- data/SPEC.md +194 -63
- data/docs/architecture.md +22 -4
- data/docs/conventions.md +24 -17
- data/lib/textus/application/context.rb +68 -0
- data/lib/textus/application/reads/audit.rb +69 -0
- data/lib/textus/application/reads/blame.rb +79 -0
- data/lib/textus/application/reads/freshness.rb +77 -0
- data/lib/textus/application/reads/get.rb +62 -0
- data/lib/textus/application/reads/policy_explain.rb +39 -0
- data/lib/textus/application/refresh/all.rb +41 -0
- data/lib/textus/application/refresh/orchestrator.rb +68 -0
- data/lib/textus/application/refresh/worker.rb +79 -0
- data/lib/textus/application/writes/accept.rb +43 -0
- data/lib/textus/application/writes/build.rb +24 -0
- data/lib/textus/application/writes/delete.rb +37 -0
- data/lib/textus/application/writes/publish.rb +25 -0
- data/lib/textus/application/writes/put.rb +44 -0
- data/lib/textus/builder.rb +27 -14
- data/lib/textus/cli/group/policy.rb +11 -0
- data/lib/textus/cli/verb/accept.rb +2 -1
- data/lib/textus/cli/verb/audit.rb +31 -0
- data/lib/textus/cli/verb/blame.rb +17 -0
- data/lib/textus/cli/verb/build.rb +2 -1
- data/lib/textus/cli/verb/delete.rb +2 -1
- data/lib/textus/cli/verb/freshness.rb +17 -0
- data/lib/textus/cli/verb/get.rb +8 -1
- data/lib/textus/cli/verb/hook_run.rb +3 -3
- data/lib/textus/cli/verb/policy_explain.rb +15 -0
- data/lib/textus/cli/verb/policy_list.rb +25 -0
- data/lib/textus/cli/verb/put.rb +5 -4
- data/lib/textus/cli/verb/refresh.rb +2 -1
- data/lib/textus/cli/verb/refresh_stale.rb +19 -0
- data/lib/textus/cli/verb/reject.rb +15 -0
- data/lib/textus/cli.rb +16 -2
- data/lib/textus/composition.rb +71 -0
- data/lib/textus/doctor/check/handler_allowlist.rb +33 -0
- data/lib/textus/doctor/check/intake_registration.rb +46 -0
- data/lib/textus/doctor/check/legacy_intake_fields.rb +57 -0
- data/lib/textus/doctor/check/policy_ambiguity.rb +49 -0
- data/lib/textus/doctor.rb +4 -0
- data/lib/textus/domain/action.rb +9 -0
- data/lib/textus/domain/freshness/evaluator.rb +30 -0
- data/lib/textus/domain/freshness/policy.rb +18 -0
- data/lib/textus/domain/freshness/verdict.rb +12 -0
- data/lib/textus/domain/outcome.rb +10 -0
- data/lib/textus/domain/permission.rb +15 -0
- data/lib/textus/domain/policy/handler_allowlist.rb +17 -0
- data/lib/textus/domain/policy/matcher.rb +51 -0
- data/lib/textus/domain/policy/promote.rb +24 -0
- data/lib/textus/domain/policy/refresh.rb +48 -0
- data/lib/textus/domain/policy.rb +7 -0
- data/lib/textus/hooks/builtin.rb +5 -5
- data/lib/textus/hooks/dispatcher.rb +15 -1
- data/lib/textus/hooks/dsl.rb +18 -0
- data/lib/textus/hooks/registry.rb +12 -5
- data/lib/textus/infra/clock.rb +9 -0
- data/lib/textus/infra/event_bus.rb +27 -0
- data/lib/textus/infra/publisher.rb +73 -0
- data/lib/textus/infra/refresh/detached.rb +38 -0
- data/lib/textus/infra/refresh/lock.rb +44 -0
- data/lib/textus/init.rb +71 -28
- data/lib/textus/intro.rb +19 -11
- data/lib/textus/manifest/entry.rb +18 -9
- data/lib/textus/manifest/policies.rb +83 -0
- data/lib/textus/manifest.rb +30 -0
- data/lib/textus/proposal.rb +4 -21
- data/lib/textus/publisher.rb +4 -69
- data/lib/textus/refresh.rb +9 -44
- data/lib/textus/store/mover.rb +14 -9
- data/lib/textus/store/reader.rb +10 -8
- data/lib/textus/store/staleness.rb +4 -16
- data/lib/textus/store/validator.rb +46 -20
- data/lib/textus/store/view.rb +8 -19
- data/lib/textus/store/writer.rb +51 -14
- data/lib/textus/store.rb +29 -9
- data/lib/textus/version.rb +1 -1
- data/lib/textus.rb +1 -0
- metadata +46 -2
- data/lib/textus/cli/verb/stale.rb +0 -14
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Application
|
|
3
|
+
module Writes
|
|
4
|
+
class Publish
|
|
5
|
+
def initialize(ctx:, bus:)
|
|
6
|
+
@ctx = ctx
|
|
7
|
+
@bus = bus
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def call(source:, target:, key:)
|
|
11
|
+
Textus::Infra::Publisher.publish(
|
|
12
|
+
source: source,
|
|
13
|
+
target: target,
|
|
14
|
+
store_root: @ctx.store.root,
|
|
15
|
+
)
|
|
16
|
+
@bus.publish(:published,
|
|
17
|
+
key: key,
|
|
18
|
+
source: source,
|
|
19
|
+
target: target,
|
|
20
|
+
correlation_id: @ctx.correlation_id)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Application
|
|
3
|
+
module Writes
|
|
4
|
+
class Put
|
|
5
|
+
def initialize(ctx:, bus:)
|
|
6
|
+
@ctx = ctx
|
|
7
|
+
@bus = bus
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def call(key, meta: nil, body: nil, content: nil, if_etag: nil, suppress_events: false)
|
|
11
|
+
@ctx.store.manifest.validate_key!(key)
|
|
12
|
+
mentry, = @ctx.store.manifest.resolve(key)
|
|
13
|
+
|
|
14
|
+
unless @ctx.can_write?(mentry.zone)
|
|
15
|
+
raise WriteForbidden.new(key, mentry.zone,
|
|
16
|
+
writers: @ctx.store.manifest.zone_writers(mentry.zone))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
envelope = @ctx.store.writer.write_envelope_to_disk(
|
|
20
|
+
key,
|
|
21
|
+
mentry: mentry,
|
|
22
|
+
meta: meta,
|
|
23
|
+
body: body,
|
|
24
|
+
content: content,
|
|
25
|
+
if_etag: if_etag,
|
|
26
|
+
as: @ctx.role,
|
|
27
|
+
correlation_id: @ctx.correlation_id,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
unless suppress_events
|
|
31
|
+
store_view = Store::View.new(@ctx.store)
|
|
32
|
+
@bus.publish(:put,
|
|
33
|
+
store: store_view,
|
|
34
|
+
key: key,
|
|
35
|
+
envelope: envelope,
|
|
36
|
+
correlation_id: @ctx.correlation_id)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
envelope
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
data/lib/textus/builder.rb
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
require "fileutils"
|
|
2
2
|
|
|
3
|
+
# As of 0.9.1, Textus::Application::Writes::Build is the preferred public
|
|
4
|
+
# entry point. This class remains as the implementation home of materialization
|
|
5
|
+
# and projection logic; full extraction is deferred to 0.10.0.
|
|
3
6
|
module Textus
|
|
4
7
|
class Builder
|
|
5
8
|
def initialize(store)
|
|
@@ -35,21 +38,27 @@ module Textus
|
|
|
35
38
|
next unless row[:manifest_entry].equal?(mentry)
|
|
36
39
|
next if prefix && !row[:key].start_with?(prefix) && row[:key] != prefix
|
|
37
40
|
|
|
38
|
-
|
|
39
|
-
target_abs = File.expand_path(File.join(repo_root, target_rel))
|
|
40
|
-
unless target_abs.start_with?(File.expand_path(repo_root) + File::SEPARATOR)
|
|
41
|
-
raise PublishError.new(
|
|
42
|
-
"entry '#{mentry.key}': publish_each target '#{target_rel}' for key '#{row[:key]}' escapes repo root",
|
|
43
|
-
)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
Publisher.publish(source: row[:path], target: target_abs, store_root: @root)
|
|
47
|
-
out << { "key" => row[:key], "source" => row[:path], "target" => target_abs }
|
|
41
|
+
out << publish_leaf(mentry, row, repo_root)
|
|
48
42
|
end
|
|
49
43
|
end
|
|
50
44
|
out
|
|
51
45
|
end
|
|
52
46
|
|
|
47
|
+
def publish_leaf(mentry, row, repo_root)
|
|
48
|
+
target_rel = mentry.publish_target_for(row[:key])
|
|
49
|
+
target_abs = File.expand_path(File.join(repo_root, target_rel))
|
|
50
|
+
unless target_abs.start_with?(File.expand_path(repo_root) + File::SEPARATOR)
|
|
51
|
+
raise PublishError.new(
|
|
52
|
+
"entry '#{mentry.key}': publish_each target '#{target_rel}' for key '#{row[:key]}' escapes repo root",
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
Textus::Infra::Publisher.publish(source: row[:path], target: target_abs, store_root: @root)
|
|
57
|
+
@store.fire_event(:published, key: row[:key], envelope: @store.get(row[:key]),
|
|
58
|
+
source: row[:path], target: target_abs)
|
|
59
|
+
{ "key" => row[:key], "source" => row[:path], "target" => target_abs }
|
|
60
|
+
end
|
|
61
|
+
|
|
53
62
|
def derived_zone?(mentry)
|
|
54
63
|
writers = @manifest.zone_writers(mentry.zone)
|
|
55
64
|
writers.include?("build")
|
|
@@ -73,13 +82,17 @@ module Textus
|
|
|
73
82
|
end
|
|
74
83
|
|
|
75
84
|
def publish_and_fire(mentry, target_path)
|
|
85
|
+
envelope = @store.get(mentry.key)
|
|
86
|
+
repo_root = File.dirname(@root)
|
|
87
|
+
|
|
76
88
|
mentry.publish_to.each do |rel|
|
|
77
|
-
|
|
78
|
-
Publisher.publish(source: target_path, target:
|
|
89
|
+
target_abs = File.join(repo_root, rel)
|
|
90
|
+
Textus::Infra::Publisher.publish(source: target_path, target: target_abs, store_root: @root)
|
|
91
|
+
@store.fire_event(:published, key: mentry.key, envelope: envelope,
|
|
92
|
+
source: target_path, target: target_abs)
|
|
79
93
|
end
|
|
80
94
|
|
|
81
|
-
|
|
82
|
-
@store.fire_event(:build, key: mentry.key, envelope: envelope,
|
|
95
|
+
@store.fire_event(:built, key: mentry.key, envelope: envelope,
|
|
83
96
|
sources: Array(mentry.projection&.fetch("select", nil)).compact)
|
|
84
97
|
end
|
|
85
98
|
end
|
|
@@ -7,7 +7,8 @@ module Textus
|
|
|
7
7
|
def call(store)
|
|
8
8
|
key = positional.shift or raise UsageError.new("accept requires a key")
|
|
9
9
|
role = Role.resolve(flag: as_flag, env: ENV, root: store.root)
|
|
10
|
-
|
|
10
|
+
ctx = Textus::Composition.context(store, role: role)
|
|
11
|
+
emit(Textus::Composition.writes_accept(ctx).call(key))
|
|
11
12
|
end
|
|
12
13
|
end
|
|
13
14
|
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class CLI
|
|
3
|
+
class Verb
|
|
4
|
+
class Audit < Verb
|
|
5
|
+
option :key_filter, "--key=KEY"
|
|
6
|
+
option :zone, "--zone=Z"
|
|
7
|
+
option :role_filter, "--role=ROLE"
|
|
8
|
+
option :verb_filter, "--verb=V"
|
|
9
|
+
option :since, "--since=ISO8601|RELATIVE"
|
|
10
|
+
option :correlation_id, "--correlation-id=ID"
|
|
11
|
+
option :limit, "--limit=N"
|
|
12
|
+
|
|
13
|
+
def call(store)
|
|
14
|
+
role = Role.resolve(flag: nil, env: ENV, root: store.root)
|
|
15
|
+
ctx = Textus::Composition.context(store, role: role)
|
|
16
|
+
since_time = since && Textus::Application::Reads::Audit.parse_since(since, now: ctx.now)
|
|
17
|
+
rows = Textus::Composition.audit(ctx).call(
|
|
18
|
+
key: key_filter,
|
|
19
|
+
zone: zone,
|
|
20
|
+
role: role_filter,
|
|
21
|
+
verb: verb_filter,
|
|
22
|
+
since: since_time,
|
|
23
|
+
correlation_id: correlation_id,
|
|
24
|
+
limit: limit&.to_i,
|
|
25
|
+
)
|
|
26
|
+
emit({ "verb" => "audit", "rows" => rows })
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class CLI
|
|
3
|
+
class Verb
|
|
4
|
+
class Blame < Verb
|
|
5
|
+
option :limit, "--limit=N"
|
|
6
|
+
|
|
7
|
+
def call(store)
|
|
8
|
+
key = positional.shift or raise UsageError.new("blame requires a key")
|
|
9
|
+
role = Role.resolve(flag: nil, env: ENV, root: store.root)
|
|
10
|
+
ctx = Textus::Composition.context(store, role: role)
|
|
11
|
+
rows = Textus::Composition.blame(ctx).call(key: key, limit: limit&.to_i)
|
|
12
|
+
emit({ "verb" => "blame", "key" => key, "rows" => rows })
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -5,7 +5,8 @@ module Textus
|
|
|
5
5
|
option :prefix, "--prefix=K"
|
|
6
6
|
|
|
7
7
|
def call(store)
|
|
8
|
-
|
|
8
|
+
ctx = Textus::Composition.context(store, role: "build")
|
|
9
|
+
emit(Textus::Composition.writes_build(ctx).call(prefix: prefix))
|
|
9
10
|
end
|
|
10
11
|
end
|
|
11
12
|
end
|
|
@@ -8,7 +8,8 @@ module Textus
|
|
|
8
8
|
def call(store)
|
|
9
9
|
key = positional.shift or raise UsageError.new("delete requires a key")
|
|
10
10
|
role = Role.resolve(flag: as_flag, env: ENV, root: store.root)
|
|
11
|
-
|
|
11
|
+
ctx = Textus::Composition.context(store, role: role)
|
|
12
|
+
emit(Textus::Composition.writes_delete(ctx).call(key, if_etag: if_etag))
|
|
12
13
|
end
|
|
13
14
|
end
|
|
14
15
|
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class CLI
|
|
3
|
+
class Verb
|
|
4
|
+
class Freshness < Verb
|
|
5
|
+
option :prefix, "--prefix=KEY"
|
|
6
|
+
option :zone, "--zone=Z"
|
|
7
|
+
|
|
8
|
+
def call(store)
|
|
9
|
+
role = Role.resolve(flag: nil, env: ENV, root: store.root)
|
|
10
|
+
ctx = Textus::Composition.context(store, role: role)
|
|
11
|
+
rows = Textus::Composition.freshness(ctx).call(prefix: prefix, zone: zone)
|
|
12
|
+
emit({ "verb" => "freshness", "rows" => rows })
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
data/lib/textus/cli/verb/get.rb
CHANGED
|
@@ -2,9 +2,16 @@ module Textus
|
|
|
2
2
|
class CLI
|
|
3
3
|
class Verb
|
|
4
4
|
class Get < Verb
|
|
5
|
+
option :as_flag, "--as=ROLE"
|
|
6
|
+
|
|
5
7
|
def call(store)
|
|
6
8
|
key = positional.shift or raise UsageError.new("get requires a key")
|
|
7
|
-
|
|
9
|
+
role = Role.resolve(flag: as_flag, env: ENV, root: store.root)
|
|
10
|
+
ctx = Textus::Composition.context(store, role: role)
|
|
11
|
+
result = Textus::Composition.reads_get(ctx).call(key)
|
|
12
|
+
raise Textus::UnknownKey.new(key, suggestions: store.manifest.suggestions_for(key)) if result.nil?
|
|
13
|
+
|
|
14
|
+
emit(result)
|
|
8
15
|
end
|
|
9
16
|
end
|
|
10
17
|
end
|
|
@@ -23,16 +23,16 @@ module Textus
|
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
role = Role.resolve(flag: as_flag, env: ENV, root: store.root)
|
|
26
|
-
callable = store.registry.rpc_callable(:
|
|
26
|
+
callable = store.registry.rpc_callable(:intake, name)
|
|
27
27
|
view = Store::View.new(store, writable: true, as: role)
|
|
28
28
|
|
|
29
29
|
begin
|
|
30
|
-
Timeout.timeout(Textus::Refresh::FETCH_TIMEOUT_SECONDS) do
|
|
30
|
+
Timeout.timeout(Textus::Application::Refresh::Worker::FETCH_TIMEOUT_SECONDS) do
|
|
31
31
|
callable.call(config: {}, store: view, args: args)
|
|
32
32
|
end
|
|
33
33
|
rescue Timeout::Error
|
|
34
34
|
raise UsageError.new(
|
|
35
|
-
"hook run '#{name}' exceeded #{Textus::Refresh::FETCH_TIMEOUT_SECONDS}s timeout",
|
|
35
|
+
"hook run '#{name}' exceeded #{Textus::Application::Refresh::Worker::FETCH_TIMEOUT_SECONDS}s timeout",
|
|
36
36
|
)
|
|
37
37
|
rescue Textus::Error
|
|
38
38
|
raise
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class CLI
|
|
3
|
+
class Verb
|
|
4
|
+
class PolicyExplain < Verb
|
|
5
|
+
def call(store)
|
|
6
|
+
key = positional.shift or raise UsageError.new("policy explain requires a KEY")
|
|
7
|
+
role = Role.resolve(flag: nil, env: ENV, root: store.root)
|
|
8
|
+
ctx = Textus::Composition.context(store, role: role)
|
|
9
|
+
result = Textus::Composition.policy_explain(ctx).call(key: key)
|
|
10
|
+
emit({ "verb" => "policy_explain" }.merge(result.transform_keys(&:to_s)))
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class CLI
|
|
3
|
+
class Verb
|
|
4
|
+
class PolicyList < Verb
|
|
5
|
+
def call(store)
|
|
6
|
+
policies = store.manifest.policies.blocks.map do |b|
|
|
7
|
+
row = { "match" => b.match }
|
|
8
|
+
if b.refresh
|
|
9
|
+
row["refresh"] = {
|
|
10
|
+
"ttl_seconds" => b.refresh.ttl_seconds,
|
|
11
|
+
"on_stale" => b.refresh.on_stale,
|
|
12
|
+
"sync_budget_ms" => b.refresh.sync_budget_ms,
|
|
13
|
+
}
|
|
14
|
+
end
|
|
15
|
+
row["handler_allowlist"] = b.handler_allowlist.handlers if b.handler_allowlist
|
|
16
|
+
row["promote_requires"] = b.promote.requires if b.promote
|
|
17
|
+
row["retention"] = b.retention if b.retention
|
|
18
|
+
row
|
|
19
|
+
end
|
|
20
|
+
emit({ "verb" => "policy_list", "policies" => policies })
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/textus/cli/verb/put.rb
CHANGED
|
@@ -15,15 +15,15 @@ module Textus
|
|
|
15
15
|
raw = @stdin.read
|
|
16
16
|
payload =
|
|
17
17
|
if fetch_name
|
|
18
|
-
callable = store.registry.rpc_callable(:
|
|
18
|
+
callable = store.registry.rpc_callable(:intake, fetch_name)
|
|
19
19
|
result =
|
|
20
20
|
begin
|
|
21
|
-
Timeout.timeout(Textus::Refresh::FETCH_TIMEOUT_SECONDS) do
|
|
21
|
+
Timeout.timeout(Textus::Application::Refresh::Worker::FETCH_TIMEOUT_SECONDS) do
|
|
22
22
|
callable.call(config: { "bytes" => raw }, store: Textus::Store::View.new(store), args: {})
|
|
23
23
|
end
|
|
24
24
|
rescue Timeout::Error
|
|
25
25
|
raise UsageError.new(
|
|
26
|
-
"fetch '#{fetch_name}' exceeded #{Textus::Refresh::FETCH_TIMEOUT_SECONDS}s timeout",
|
|
26
|
+
"fetch '#{fetch_name}' exceeded #{Textus::Application::Refresh::Worker::FETCH_TIMEOUT_SECONDS}s timeout",
|
|
27
27
|
)
|
|
28
28
|
end
|
|
29
29
|
basename = key.split(".").last
|
|
@@ -42,7 +42,8 @@ module Textus
|
|
|
42
42
|
meta = payload["_meta"] || payload["frontmatter"] || {}
|
|
43
43
|
body = payload["body"] || ""
|
|
44
44
|
if_etag = payload["if_etag"]
|
|
45
|
-
|
|
45
|
+
ctx = Textus::Composition.context(store, role: role)
|
|
46
|
+
emit(Textus::Composition.writes_put(ctx).call(key, meta: meta, body: body, if_etag: if_etag))
|
|
46
47
|
end
|
|
47
48
|
end
|
|
48
49
|
end
|
|
@@ -7,7 +7,8 @@ module Textus
|
|
|
7
7
|
def call(store)
|
|
8
8
|
key = positional.shift or raise UsageError.new("refresh requires a key")
|
|
9
9
|
role = Role.resolve(flag: as_flag, env: ENV, root: store.root)
|
|
10
|
-
|
|
10
|
+
ctx = Textus::Composition.context(store, role: role)
|
|
11
|
+
emit(Textus::Composition.refresh_worker(ctx).run(key))
|
|
11
12
|
end
|
|
12
13
|
end
|
|
13
14
|
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class CLI
|
|
3
|
+
class Verb
|
|
4
|
+
class RefreshStale < Verb
|
|
5
|
+
option :prefix, "--prefix=KEY"
|
|
6
|
+
option :zone, "--zone=Z"
|
|
7
|
+
option :as_flag, "--as=ROLE"
|
|
8
|
+
|
|
9
|
+
def call(store)
|
|
10
|
+
role = Role.resolve(flag: as_flag, env: ENV, root: store.root)
|
|
11
|
+
ctx = Textus::Composition.context(store, role: role)
|
|
12
|
+
result = Textus::Application::Refresh::All.call(ctx, prefix: prefix, zone: zone)
|
|
13
|
+
emit(result)
|
|
14
|
+
exit(1) unless result["ok"]
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class CLI
|
|
3
|
+
class Verb
|
|
4
|
+
class Reject < Verb
|
|
5
|
+
option :as_flag, "--as=ROLE"
|
|
6
|
+
|
|
7
|
+
def call(store)
|
|
8
|
+
key = positional.shift or raise UsageError.new("reject requires a key")
|
|
9
|
+
role = Role.resolve(flag: as_flag, env: ENV, root: store.root)
|
|
10
|
+
emit(store.reject(key, as: role))
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
data/lib/textus/cli.rb
CHANGED
|
@@ -7,22 +7,27 @@ module Textus
|
|
|
7
7
|
# plus a new file under lib/textus/cli/.
|
|
8
8
|
VERBS = {
|
|
9
9
|
"accept" => Verb::Accept,
|
|
10
|
+
"audit" => Verb::Audit,
|
|
11
|
+
"blame" => Verb::Blame,
|
|
12
|
+
"reject" => Verb::Reject,
|
|
10
13
|
"build" => Verb::Build,
|
|
11
14
|
"delete" => Verb::Delete,
|
|
12
15
|
"deps" => Verb::Deps,
|
|
13
16
|
"doctor" => Verb::Doctor,
|
|
17
|
+
"freshness" => Verb::Freshness,
|
|
14
18
|
"get" => Verb::Get,
|
|
15
19
|
"hook" => Group::Hook,
|
|
16
20
|
"init" => Verb::Init,
|
|
17
21
|
"intro" => Verb::Intro,
|
|
18
22
|
"key" => Group::Key,
|
|
19
23
|
"list" => Verb::List,
|
|
24
|
+
"policy" => Group::Policy,
|
|
20
25
|
"published" => Verb::Published,
|
|
21
26
|
"put" => Verb::Put,
|
|
22
27
|
"rdeps" => Verb::Rdeps,
|
|
23
28
|
"refresh" => Verb::Refresh,
|
|
29
|
+
"refresh-stale" => Verb::RefreshStale,
|
|
24
30
|
"schema" => Group::Schema,
|
|
25
|
-
"stale" => Verb::Stale,
|
|
26
31
|
"where" => Verb::Where,
|
|
27
32
|
}.freeze
|
|
28
33
|
|
|
@@ -48,6 +53,10 @@ module Textus
|
|
|
48
53
|
0
|
|
49
54
|
when "--help", "-h" then print_help
|
|
50
55
|
0
|
|
56
|
+
when "stale"
|
|
57
|
+
raise UsageError.new(
|
|
58
|
+
"textus stale was removed in 0.9.2 — use `textus freshness` instead",
|
|
59
|
+
)
|
|
51
60
|
else
|
|
52
61
|
klass = VERBS[verb] or raise UsageError.new("unknown verb: #{verb}")
|
|
53
62
|
dispatch(klass, argv)
|
|
@@ -84,13 +93,18 @@ module Textus
|
|
|
84
93
|
textus where KEY
|
|
85
94
|
textus get KEY
|
|
86
95
|
textus put KEY --stdin [--fetch=NAME] --as=ROLE
|
|
87
|
-
textus
|
|
96
|
+
textus freshness [--prefix=KEY] [--zone=Z]
|
|
97
|
+
textus refresh-stale [--prefix=KEY] [--zone=Z]
|
|
98
|
+
textus audit [--key=K] [--zone=Z] [--role=R] [--verb=V] [--since=X] [--correlation-id=ID] [--limit=N]
|
|
99
|
+
textus blame KEY [--limit=N]
|
|
88
100
|
textus doctor
|
|
89
101
|
textus intro
|
|
90
102
|
|
|
91
103
|
textus key {mv,uid,migrate}
|
|
92
104
|
textus schema {show,init,diff,migrate}
|
|
93
105
|
textus hook {list,run}
|
|
106
|
+
textus policy {list,explain}
|
|
107
|
+
textus migrate {zones,policies}
|
|
94
108
|
HELP
|
|
95
109
|
end
|
|
96
110
|
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Composition
|
|
3
|
+
module_function
|
|
4
|
+
|
|
5
|
+
def context(store, role:, correlation_id: nil, dry_run: false)
|
|
6
|
+
Textus::Application::Context.new(
|
|
7
|
+
store: store,
|
|
8
|
+
role: role,
|
|
9
|
+
correlation_id: correlation_id,
|
|
10
|
+
dry_run: dry_run,
|
|
11
|
+
)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def reads_get(ctx)
|
|
15
|
+
Textus::Application::Reads::Get.new(ctx: ctx, orchestrator: refresh_orchestrator(ctx))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def freshness(ctx)
|
|
19
|
+
Textus::Application::Reads::Freshness.new(ctx: ctx)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def audit(ctx)
|
|
23
|
+
Textus::Application::Reads::Audit.new(ctx: ctx)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def blame(ctx)
|
|
27
|
+
Textus::Application::Reads::Blame.new(ctx: ctx)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def policy_explain(ctx)
|
|
31
|
+
Textus::Application::Reads::PolicyExplain.new(ctx: ctx)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def refresh_worker(ctx)
|
|
35
|
+
Textus::Application::Refresh::Worker.new(ctx: ctx, bus: ctx.store.bus)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def refresh_orchestrator(ctx)
|
|
39
|
+
Textus::Application::Refresh::Orchestrator.new(
|
|
40
|
+
worker: refresh_worker(ctx),
|
|
41
|
+
bus: ctx.store.bus,
|
|
42
|
+
store_root: ctx.store.root,
|
|
43
|
+
store: ctx.store,
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def writes_put(ctx)
|
|
48
|
+
Textus::Application::Writes::Put.new(ctx: ctx, bus: ctx.store.bus)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def writes_delete(ctx)
|
|
52
|
+
Textus::Application::Writes::Delete.new(ctx: ctx, bus: ctx.store.bus)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def writes_build(ctx)
|
|
56
|
+
Textus::Application::Writes::Build.new(ctx: ctx, bus: ctx.store.bus)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def writes_accept(ctx)
|
|
60
|
+
Textus::Application::Writes::Accept.new(ctx: ctx, bus: ctx.store.bus)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def writes_publish(ctx)
|
|
64
|
+
Textus::Application::Writes::Publish.new(ctx: ctx, bus: ctx.store.bus)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def event_bus(ctx)
|
|
68
|
+
Textus::Infra::EventBus.new(registry: ctx.store.registry)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Doctor
|
|
3
|
+
class Check
|
|
4
|
+
# For every entry with an `intake.handler`, look up its handler_allowlist
|
|
5
|
+
# policy (if any) and verify the declared handler is allowed. Emits a
|
|
6
|
+
# failure when the handler is rejected by policy.
|
|
7
|
+
class HandlerAllowlist < Check
|
|
8
|
+
def call
|
|
9
|
+
out = []
|
|
10
|
+
store.manifest.entries.each do |mentry|
|
|
11
|
+
handler = mentry.intake_handler
|
|
12
|
+
next if handler.nil?
|
|
13
|
+
|
|
14
|
+
allow = store.manifest.policies_for(mentry.key).handler_allowlist
|
|
15
|
+
next if allow.nil?
|
|
16
|
+
next if allow.allows?(handler)
|
|
17
|
+
|
|
18
|
+
out << {
|
|
19
|
+
"code" => "policy.handler_not_allowed",
|
|
20
|
+
"level" => "error",
|
|
21
|
+
"subject" => mentry.key,
|
|
22
|
+
"message" => "entry '#{mentry.key}' declares intake.handler='#{handler}' but the " \
|
|
23
|
+
"handler_allowlist policy permits only: #{allow.handlers.join(", ")}",
|
|
24
|
+
"fix" => "either change intake.handler to one of [#{allow.handlers.join(", ")}], " \
|
|
25
|
+
"or extend the handler_allowlist policy in .textus/manifest.yaml",
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
out
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Doctor
|
|
3
|
+
class Check
|
|
4
|
+
class IntakeRegistration < Check
|
|
5
|
+
BUILTIN = %i[json csv markdown-links ical-events rss].freeze
|
|
6
|
+
|
|
7
|
+
def call
|
|
8
|
+
declared = collect_declared_handlers
|
|
9
|
+
registered = store.registry.rpc_names(:intake).to_set
|
|
10
|
+
|
|
11
|
+
out = (declared - registered).map do |name|
|
|
12
|
+
{
|
|
13
|
+
"code" => "intake.handler_missing",
|
|
14
|
+
"level" => "error",
|
|
15
|
+
"subject" => name.to_s,
|
|
16
|
+
"message" => "manifest references intake handler '#{name}' but no Textus.intake(:#{name}) is registered",
|
|
17
|
+
"fix" => "create .textus/hooks/#{name}.rb with `Textus.intake(:#{name}) { ... }`",
|
|
18
|
+
}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
(registered - declared - BUILTIN.to_set).each do |name|
|
|
22
|
+
out << {
|
|
23
|
+
"code" => "intake.handler_orphan",
|
|
24
|
+
"level" => "warning",
|
|
25
|
+
"subject" => name.to_s,
|
|
26
|
+
"message" => "Textus.intake(:#{name}) is registered but no manifest entry references it",
|
|
27
|
+
"fix" => "remove the unused handler, or add an entry with `intake.handler: #{name}`",
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
out
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def collect_declared_handlers
|
|
37
|
+
set = Set.new
|
|
38
|
+
store.manifest.entries.each do |mentry|
|
|
39
|
+
set << mentry.intake_handler.to_sym if mentry.intake_handler
|
|
40
|
+
end
|
|
41
|
+
set
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|