textus 0.8.1 → 0.10.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 +329 -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 +44 -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 +69 -0
- data/lib/textus/application/refresh/worker.rb +79 -0
- data/lib/textus/application/writes/accept.rb +44 -0
- data/lib/textus/application/writes/build.rb +116 -0
- data/lib/textus/application/writes/delete.rb +36 -0
- data/lib/textus/application/writes/publish.rb +25 -0
- data/lib/textus/application/writes/put.rb +43 -0
- data/lib/textus/builder/pipeline.rb +1 -1
- data/lib/textus/builder/renderer/json.rb +1 -1
- data/lib/textus/builder/renderer/markdown.rb +1 -1
- data/lib/textus/builder/renderer/text.rb +1 -1
- data/lib/textus/builder/renderer/yaml.rb +1 -1
- data/lib/textus/builder/renderer.rb +1 -1
- data/lib/textus/cli/group/policy.rb +11 -0
- data/lib/textus/cli/verb/accept.rb +2 -2
- data/lib/textus/cli/verb/audit.rb +30 -0
- data/lib/textus/cli/verb/blame.rb +16 -0
- data/lib/textus/cli/verb/build.rb +2 -1
- data/lib/textus/cli/verb/delete.rb +2 -2
- data/lib/textus/cli/verb/freshness.rb +16 -0
- data/lib/textus/cli/verb/get.rb +7 -1
- data/lib/textus/cli/verb/hook_run.rb +4 -4
- data/lib/textus/cli/verb/mv.rb +1 -2
- data/lib/textus/cli/verb/policy_explain.rb +14 -0
- data/lib/textus/cli/verb/policy_list.rb +25 -0
- data/lib/textus/cli/verb/put.rb +10 -8
- data/lib/textus/cli/verb/refresh.rb +2 -2
- data/lib/textus/cli/verb/refresh_stale.rb +18 -0
- data/lib/textus/cli/verb/reject.rb +14 -0
- data/lib/textus/cli/verb.rb +14 -0
- data/lib/textus/cli.rb +16 -2
- data/lib/textus/composition.rb +72 -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 +7 -1
- 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 +17 -14
- data/lib/textus/manifest/entry.rb +39 -13
- data/lib/textus/manifest/policies.rb +83 -0
- data/lib/textus/manifest.rb +30 -11
- data/lib/textus/projection.rb +1 -1
- data/lib/textus/proposal.rb +4 -21
- data/lib/textus/refresh.rb +9 -45
- data/lib/textus/store/mover.rb +14 -9
- data/lib/textus/store/reader.rb +10 -8
- data/lib/textus/store/staleness.rb +5 -17
- data/lib/textus/store/validator.rb +46 -20
- data/lib/textus/store/writer.rb +51 -14
- data/lib/textus/store.rb +30 -10
- data/lib/textus/version.rb +1 -1
- data/lib/textus.rb +1 -0
- metadata +46 -5
- data/lib/textus/builder.rb +0 -86
- data/lib/textus/cli/verb/stale.rb +0 -14
- data/lib/textus/publisher.rb +0 -71
- data/lib/textus/store/view.rb +0 -29
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Application
|
|
3
|
+
module Writes
|
|
4
|
+
class Delete
|
|
5
|
+
def initialize(ctx:, bus:)
|
|
6
|
+
@ctx = ctx
|
|
7
|
+
@bus = bus
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def call(key, 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
|
+
@ctx.store.writer.delete_envelope_from_disk(
|
|
20
|
+
key, if_etag: if_etag, as: @ctx.role,
|
|
21
|
+
correlation_id: @ctx.correlation_id
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
unless suppress_events
|
|
25
|
+
@bus.publish(:deleted,
|
|
26
|
+
store: @ctx.with_role(@ctx.role),
|
|
27
|
+
key: key,
|
|
28
|
+
correlation_id: @ctx.correlation_id)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
{ "protocol" => Textus::PROTOCOL, "ok" => true, "key" => key, "deleted" => true }
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -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,43 @@
|
|
|
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
|
+
@bus.publish(:put,
|
|
32
|
+
store: @ctx.with_role(@ctx.role),
|
|
33
|
+
key: key,
|
|
34
|
+
envelope: envelope,
|
|
35
|
+
correlation_id: @ctx.correlation_id)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
envelope
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -6,8 +6,8 @@ module Textus
|
|
|
6
6
|
|
|
7
7
|
def call(store)
|
|
8
8
|
key = positional.shift or raise UsageError.new("accept requires a key")
|
|
9
|
-
|
|
10
|
-
emit(
|
|
9
|
+
ctx = context_for(store)
|
|
10
|
+
emit(Textus::Composition.writes_accept(ctx).call(key))
|
|
11
11
|
end
|
|
12
12
|
end
|
|
13
13
|
end
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
ctx = context_for(store)
|
|
15
|
+
since_time = since && Textus::Application::Reads::Audit.parse_since(since, now: ctx.now)
|
|
16
|
+
rows = Textus::Composition.audit(ctx).call(
|
|
17
|
+
key: key_filter,
|
|
18
|
+
zone: zone,
|
|
19
|
+
role: role_filter,
|
|
20
|
+
verb: verb_filter,
|
|
21
|
+
since: since_time,
|
|
22
|
+
correlation_id: correlation_id,
|
|
23
|
+
limit: limit&.to_i,
|
|
24
|
+
)
|
|
25
|
+
emit({ "verb" => "audit", "rows" => rows })
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
ctx = context_for(store)
|
|
10
|
+
rows = Textus::Composition.blame(ctx).call(key: key, limit: limit&.to_i)
|
|
11
|
+
emit({ "verb" => "blame", "key" => key, "rows" => rows })
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
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
|
|
@@ -7,8 +7,8 @@ module Textus
|
|
|
7
7
|
|
|
8
8
|
def call(store)
|
|
9
9
|
key = positional.shift or raise UsageError.new("delete requires a key")
|
|
10
|
-
|
|
11
|
-
emit(
|
|
10
|
+
ctx = context_for(store)
|
|
11
|
+
emit(Textus::Composition.writes_delete(ctx).call(key, if_etag: if_etag))
|
|
12
12
|
end
|
|
13
13
|
end
|
|
14
14
|
end
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
ctx = context_for(store)
|
|
10
|
+
rows = Textus::Composition.freshness(ctx).call(prefix: prefix, zone: zone)
|
|
11
|
+
emit({ "verb" => "freshness", "rows" => rows })
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
data/lib/textus/cli/verb/get.rb
CHANGED
|
@@ -2,9 +2,15 @@ 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
|
+
ctx = context_for(store)
|
|
10
|
+
result = Textus::Composition.reads_get(ctx).call(key)
|
|
11
|
+
raise Textus::UnknownKey.new(key, suggestions: store.manifest.suggestions_for(key)) if result.nil?
|
|
12
|
+
|
|
13
|
+
emit(result)
|
|
8
14
|
end
|
|
9
15
|
end
|
|
10
16
|
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(:
|
|
27
|
-
view =
|
|
26
|
+
callable = store.registry.rpc_callable(:intake, name)
|
|
27
|
+
view = Application::Context.new(store: store, role: 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
|
data/lib/textus/cli/verb/mv.rb
CHANGED
|
@@ -8,8 +8,7 @@ module Textus
|
|
|
8
8
|
def call(store)
|
|
9
9
|
old_key = positional.shift or raise UsageError.new("mv requires <old-key> <new-key>")
|
|
10
10
|
new_key = positional.shift or raise UsageError.new("mv requires <old-key> <new-key>")
|
|
11
|
-
|
|
12
|
-
emit(store.mv(old_key, new_key, as: role, dry_run: dry_run || false))
|
|
11
|
+
emit(store.mv(old_key, new_key, as: resolved_role(store), dry_run: dry_run || false))
|
|
13
12
|
end
|
|
14
13
|
end
|
|
15
14
|
end
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
ctx = context_for(store)
|
|
8
|
+
result = Textus::Composition.policy_explain(ctx).call(key: key)
|
|
9
|
+
emit({ "verb" => "policy_explain" }.merge(result.transform_keys(&:to_s)))
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
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
|
@@ -10,20 +10,21 @@ module Textus
|
|
|
10
10
|
key = positional.shift or raise UsageError.new("put requires a key")
|
|
11
11
|
raise UsageError.new("put requires --stdin in v1") unless use_stdin
|
|
12
12
|
|
|
13
|
-
role =
|
|
13
|
+
role = resolved_role(store)
|
|
14
14
|
|
|
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
|
|
22
|
-
callable.call(config: { "bytes" => raw },
|
|
21
|
+
Timeout.timeout(Textus::Application::Refresh::Worker::FETCH_TIMEOUT_SECONDS) do
|
|
22
|
+
callable.call(config: { "bytes" => raw },
|
|
23
|
+
store: Textus::Application::Context.new(store: store, role: role), args: {})
|
|
23
24
|
end
|
|
24
25
|
rescue Timeout::Error
|
|
25
26
|
raise UsageError.new(
|
|
26
|
-
"fetch '#{fetch_name}' exceeded #{Textus::Refresh::FETCH_TIMEOUT_SECONDS}s timeout",
|
|
27
|
+
"fetch '#{fetch_name}' exceeded #{Textus::Application::Refresh::Worker::FETCH_TIMEOUT_SECONDS}s timeout",
|
|
27
28
|
)
|
|
28
29
|
end
|
|
29
30
|
basename = key.split(".").last
|
|
@@ -32,17 +33,18 @@ module Textus
|
|
|
32
33
|
"name" => basename,
|
|
33
34
|
"last_refreshed_at" => Time.now.utc.iso8601,
|
|
34
35
|
"fetched_with" => fetch_name,
|
|
35
|
-
}.merge(result[:_meta] || result["_meta"] ||
|
|
36
|
+
}.merge(result[:_meta] || result["_meta"] || {}),
|
|
36
37
|
"body" => result[:body] || result["body"] || "",
|
|
37
38
|
}
|
|
38
39
|
else
|
|
39
40
|
JSON.parse(raw)
|
|
40
41
|
end
|
|
41
42
|
|
|
42
|
-
meta = payload["_meta"] ||
|
|
43
|
+
meta = payload["_meta"] || {}
|
|
43
44
|
body = payload["body"] || ""
|
|
44
45
|
if_etag = payload["if_etag"]
|
|
45
|
-
|
|
46
|
+
ctx = Textus::Composition.context(store, role: role)
|
|
47
|
+
emit(Textus::Composition.writes_put(ctx).call(key, meta: meta, body: body, if_etag: if_etag))
|
|
46
48
|
end
|
|
47
49
|
end
|
|
48
50
|
end
|
|
@@ -6,8 +6,8 @@ module Textus
|
|
|
6
6
|
|
|
7
7
|
def call(store)
|
|
8
8
|
key = positional.shift or raise UsageError.new("refresh requires a key")
|
|
9
|
-
|
|
10
|
-
emit(Textus::
|
|
9
|
+
ctx = context_for(store)
|
|
10
|
+
emit(Textus::Composition.refresh_worker(ctx).run(key))
|
|
11
11
|
end
|
|
12
12
|
end
|
|
13
13
|
end
|
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
ctx = context_for(store)
|
|
11
|
+
result = Textus::Application::Refresh::All.call(ctx, prefix: prefix, zone: zone)
|
|
12
|
+
emit(result)
|
|
13
|
+
exit(1) unless result["ok"]
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
emit(store.reject(key, as: resolved_role(store)))
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
data/lib/textus/cli/verb.rb
CHANGED
|
@@ -57,6 +57,20 @@ module Textus
|
|
|
57
57
|
@stdout.puts(JSON.generate(payload))
|
|
58
58
|
exit_code
|
|
59
59
|
end
|
|
60
|
+
|
|
61
|
+
# Resolves the active role for this invocation. Honors the verb's
|
|
62
|
+
# `--as` flag if declared, then TEXTUS_ROLE, then the project default.
|
|
63
|
+
def resolved_role(store)
|
|
64
|
+
flag = respond_to?(:as_flag) ? as_flag : nil
|
|
65
|
+
Role.resolve(flag: flag, env: ENV, root: store.root)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Returns an Application::Context bound to the resolved role.
|
|
69
|
+
# Convenience for verbs whose only pre-call boilerplate is
|
|
70
|
+
# resolving the role and wrapping it in a context.
|
|
71
|
+
def context_for(store)
|
|
72
|
+
Textus::Composition.context(store, role: resolved_role(store))
|
|
73
|
+
end
|
|
60
74
|
end
|
|
61
75
|
end
|
|
62
76
|
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,72 @@
|
|
|
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
|
+
role: ctx.role,
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def writes_put(ctx)
|
|
49
|
+
Textus::Application::Writes::Put.new(ctx: ctx, bus: ctx.store.bus)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def writes_delete(ctx)
|
|
53
|
+
Textus::Application::Writes::Delete.new(ctx: ctx, bus: ctx.store.bus)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def writes_build(ctx)
|
|
57
|
+
Textus::Application::Writes::Build.new(ctx: ctx, bus: ctx.store.bus)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def writes_accept(ctx)
|
|
61
|
+
Textus::Application::Writes::Accept.new(ctx: ctx, bus: ctx.store.bus)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def writes_publish(ctx)
|
|
65
|
+
Textus::Application::Writes::Publish.new(ctx: ctx, bus: ctx.store.bus)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def event_bus(ctx)
|
|
69
|
+
Textus::Infra::EventBus.new(registry: ctx.store.registry)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
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
|