textus 0.22.0 → 0.26.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/ARCHITECTURE.md +148 -45
- data/CHANGELOG.md +102 -0
- data/README.md +1 -1
- data/SPEC.md +12 -12
- data/docs/conventions.md +10 -0
- data/lib/textus/application/caps.rb +49 -0
- data/lib/textus/application/context.rb +2 -2
- data/lib/textus/application/envelope/reader.rb +44 -0
- data/lib/textus/application/{writes/envelope_io.rb → envelope/writer.rb} +24 -50
- data/lib/textus/application/maintenance/key_delete_prefix.rb +44 -0
- data/lib/textus/application/maintenance/key_mv_prefix.rb +57 -0
- data/lib/textus/application/maintenance/migrate.rb +59 -0
- data/lib/textus/application/maintenance/rule_lint.rb +65 -0
- data/lib/textus/application/maintenance/zone_mv.rb +60 -0
- data/lib/textus/application/maintenance.rb +17 -0
- data/lib/textus/application/projection.rb +12 -10
- data/lib/textus/application/read/audit.rb +106 -0
- data/lib/textus/application/read/blame.rb +91 -0
- data/lib/textus/application/read/deps.rb +34 -0
- data/lib/textus/application/read/freshness.rb +110 -0
- data/lib/textus/application/read/get.rb +75 -0
- data/lib/textus/application/read/get_or_refresh.rb +63 -0
- data/lib/textus/application/read/list.rb +25 -0
- data/lib/textus/application/read/policy_explain.rb +47 -0
- data/lib/textus/application/read/published.rb +25 -0
- data/lib/textus/application/read/pulse.rb +101 -0
- data/lib/textus/application/read/rdeps.rb +35 -0
- data/lib/textus/application/read/schema_envelope.rb +26 -0
- data/lib/textus/application/read/stale.rb +23 -0
- data/lib/textus/application/read/uid.rb +30 -0
- data/lib/textus/application/read/validate_all.rb +32 -0
- data/lib/textus/application/{reads → read}/validator.rb +2 -2
- data/lib/textus/application/read/where.rb +26 -0
- data/lib/textus/application/use_case.rb +22 -0
- data/lib/textus/application/write/accept.rb +102 -0
- data/lib/textus/application/{writes → write}/authority_gate.rb +3 -3
- data/lib/textus/application/write/delete.rb +45 -0
- data/lib/textus/application/{writes → write}/materializer.rb +14 -15
- data/lib/textus/application/write/mv.rb +118 -0
- data/lib/textus/application/write/publish.rb +96 -0
- data/lib/textus/application/write/put.rb +49 -0
- data/lib/textus/application/write/refresh_all.rb +63 -0
- data/lib/textus/application/{refresh/orchestrator.rb → write/refresh_orchestrator.rb} +32 -8
- data/lib/textus/application/write/refresh_worker.rb +134 -0
- data/lib/textus/application/write/reject.rb +62 -0
- data/lib/textus/boot.rb +27 -29
- data/lib/textus/builder/pipeline.rb +3 -3
- data/lib/textus/cli/group/mcp.rb +9 -0
- data/lib/textus/cli/group/zone.rb +9 -0
- data/lib/textus/cli/verb/accept.rb +1 -1
- data/lib/textus/cli/verb/audit.rb +2 -2
- data/lib/textus/cli/verb/blame.rb +1 -1
- data/lib/textus/cli/verb/boot.rb +1 -1
- data/lib/textus/cli/verb/build.rb +2 -2
- data/lib/textus/cli/verb/delete.rb +1 -1
- data/lib/textus/cli/verb/deps.rb +1 -1
- data/lib/textus/cli/verb/doctor.rb +1 -1
- data/lib/textus/cli/verb/freshness.rb +1 -1
- data/lib/textus/cli/verb/get.rb +1 -1
- data/lib/textus/cli/verb/hook_run.rb +3 -4
- data/lib/textus/cli/verb/hooks.rb +11 -14
- data/lib/textus/cli/verb/key_delete.rb +24 -0
- data/lib/textus/cli/verb/list.rb +1 -1
- data/lib/textus/cli/verb/mcp_serve.rb +17 -0
- data/lib/textus/cli/verb/migrate.rb +18 -0
- data/lib/textus/cli/verb/mv.rb +11 -3
- data/lib/textus/cli/verb/published.rb +1 -1
- data/lib/textus/cli/verb/pulse.rb +1 -1
- data/lib/textus/cli/verb/put.rb +8 -6
- data/lib/textus/cli/verb/rdeps.rb +1 -1
- data/lib/textus/cli/verb/refresh.rb +1 -1
- data/lib/textus/cli/verb/refresh_stale.rb +1 -1
- data/lib/textus/cli/verb/reject.rb +1 -1
- data/lib/textus/cli/verb/rule_explain.rb +1 -1
- data/lib/textus/cli/verb/rule_lint.rb +18 -0
- data/lib/textus/cli/verb/schema.rb +1 -1
- data/lib/textus/cli/verb/uid.rb +1 -1
- data/lib/textus/cli/verb/where.rb +1 -1
- data/lib/textus/cli/verb/zone_mv.rb +19 -0
- data/lib/textus/cli/verb.rb +4 -4
- data/lib/textus/doctor/check/audit_log.rb +2 -2
- data/lib/textus/doctor/check/handler_allowlist.rb +2 -2
- data/lib/textus/doctor/check/hooks.rb +4 -3
- data/lib/textus/doctor/check/illegal_keys.rb +2 -2
- data/lib/textus/doctor/check/intake_registration.rb +2 -2
- data/lib/textus/doctor/check/manifest_files.rb +2 -2
- data/lib/textus/doctor/check/protocol_version.rb +2 -2
- data/lib/textus/doctor/check/refresh_locks.rb +2 -2
- data/lib/textus/doctor/check/rule_ambiguity.rb +2 -2
- data/lib/textus/doctor/check/schema_parse_error.rb +1 -1
- data/lib/textus/doctor/check/schema_violations.rb +1 -1
- data/lib/textus/doctor/check/schemas.rb +2 -2
- data/lib/textus/doctor/check/sentinels.rb +2 -2
- data/lib/textus/doctor/check/templates.rb +2 -2
- data/lib/textus/doctor/check/unowned_schema_fields.rb +1 -1
- data/lib/textus/doctor/check.rb +5 -3
- data/lib/textus/doctor.rb +24 -27
- data/lib/textus/domain/authorizer.rb +4 -4
- data/lib/textus/{application → domain}/policy/predicates/accept_authority_signed.rb +2 -2
- data/lib/textus/{application → domain}/policy/predicates/schema_valid.rb +1 -1
- data/lib/textus/{application → domain}/policy/promotion.rb +1 -1
- data/lib/textus/domain/staleness/generator_check.rb +2 -2
- data/lib/textus/domain/staleness/intake_check.rb +2 -2
- data/lib/textus/domain/staleness.rb +1 -1
- data/lib/textus/hooks/builtin.rb +14 -14
- data/lib/textus/hooks/context.rb +13 -13
- data/lib/textus/hooks/error_log.rb +32 -0
- data/lib/textus/hooks/{bus.rb → event_bus.rb} +41 -53
- data/lib/textus/hooks/loader.rb +29 -3
- data/lib/textus/hooks/rpc_registry.rb +77 -0
- data/lib/textus/infra/audit_subscriber.rb +6 -7
- data/lib/textus/infra/refresh/detached.rb +1 -1
- data/lib/textus/key/path.rb +7 -3
- data/lib/textus/manifest/data.rb +78 -0
- data/lib/textus/manifest/entry/base.rb +4 -4
- data/lib/textus/manifest/entry/derived.rb +4 -5
- data/lib/textus/manifest/entry/validators/events.rb +1 -1
- data/lib/textus/manifest/policy.rb +48 -0
- data/lib/textus/manifest/resolver.rb +14 -14
- data/lib/textus/manifest/rules.rb +1 -1
- data/lib/textus/manifest.rb +53 -111
- data/lib/textus/mcp/errors.rb +32 -0
- data/lib/textus/mcp/server.rb +127 -0
- data/lib/textus/mcp/session.rb +31 -0
- data/lib/textus/mcp/tool_schemas.rb +71 -0
- data/lib/textus/mcp/tools.rb +129 -0
- data/lib/textus/mcp.rb +6 -0
- data/lib/textus/schema/tools.rb +14 -10
- data/lib/textus/session.rb +84 -0
- data/lib/textus/store.rb +14 -9
- data/lib/textus/version.rb +1 -1
- data/lib/textus.rb +8 -1
- metadata +61 -36
- data/lib/textus/application/reads/audit.rb +0 -94
- data/lib/textus/application/reads/blame.rb +0 -82
- data/lib/textus/application/reads/deps.rb +0 -26
- data/lib/textus/application/reads/freshness.rb +0 -88
- data/lib/textus/application/reads/get.rb +0 -67
- data/lib/textus/application/reads/get_or_refresh.rb +0 -51
- data/lib/textus/application/reads/list.rb +0 -17
- data/lib/textus/application/reads/policy_explain.rb +0 -39
- data/lib/textus/application/reads/published.rb +0 -17
- data/lib/textus/application/reads/pulse.rb +0 -63
- data/lib/textus/application/reads/rdeps.rb +0 -27
- data/lib/textus/application/reads/schema_envelope.rb +0 -18
- data/lib/textus/application/reads/stale.rb +0 -15
- data/lib/textus/application/reads/uid.rb +0 -23
- data/lib/textus/application/reads/validate_all.rb +0 -24
- data/lib/textus/application/reads/where.rb +0 -18
- data/lib/textus/application/refresh/all.rb +0 -52
- data/lib/textus/application/refresh/worker.rb +0 -116
- data/lib/textus/application/writes/accept.rb +0 -89
- data/lib/textus/application/writes/delete.rb +0 -33
- data/lib/textus/application/writes/mv.rb +0 -105
- data/lib/textus/application/writes/publish.rb +0 -81
- data/lib/textus/application/writes/put.rb +0 -37
- data/lib/textus/application/writes/reject.rb +0 -50
- data/lib/textus/infra/event_bus.rb +0 -27
- data/lib/textus/operations.rb +0 -176
data/lib/textus/cli/verb/put.rb
CHANGED
|
@@ -8,7 +8,7 @@ module Textus
|
|
|
8
8
|
option :use_stdin, "--stdin"
|
|
9
9
|
option :fetch_name, "--fetch=NAME"
|
|
10
10
|
|
|
11
|
-
def call(store)
|
|
11
|
+
def call(store)
|
|
12
12
|
key = positional.shift or raise UsageError.new("put requires a key")
|
|
13
13
|
raise UsageError.new("put requires --stdin in v1") unless use_stdin
|
|
14
14
|
|
|
@@ -17,15 +17,17 @@ module Textus
|
|
|
17
17
|
raw = @stdin.read
|
|
18
18
|
payload =
|
|
19
19
|
if fetch_name
|
|
20
|
-
callable = store.bus.rpc_callable(:resolve_intake, fetch_name)
|
|
21
20
|
result =
|
|
22
21
|
begin
|
|
23
|
-
Timeout.timeout(Textus::Application::
|
|
24
|
-
|
|
22
|
+
Timeout.timeout(Textus::Application::Write::RefreshWorker::FETCH_TIMEOUT_SECONDS) do
|
|
23
|
+
store.rpc.invoke(:resolve_intake, fetch_name,
|
|
24
|
+
caps: nil,
|
|
25
|
+
config: { "bytes" => raw },
|
|
26
|
+
args: {})
|
|
25
27
|
end
|
|
26
28
|
rescue Timeout::Error
|
|
27
29
|
raise UsageError.new(
|
|
28
|
-
"fetch '#{fetch_name}' exceeded #{Textus::Application::
|
|
30
|
+
"fetch '#{fetch_name}' exceeded #{Textus::Application::Write::RefreshWorker::FETCH_TIMEOUT_SECONDS}s timeout",
|
|
29
31
|
)
|
|
30
32
|
end
|
|
31
33
|
basename = key.split(".").last
|
|
@@ -44,7 +46,7 @@ module Textus
|
|
|
44
46
|
meta = payload["_meta"] || {}
|
|
45
47
|
body = payload["body"] || ""
|
|
46
48
|
if_etag = payload["if_etag"]
|
|
47
|
-
result =
|
|
49
|
+
result = store.session(role: role).put(key, meta: meta, body: body, if_etag: if_etag)
|
|
48
50
|
emit(result.to_h_for_wire)
|
|
49
51
|
end
|
|
50
52
|
end
|
|
@@ -7,7 +7,7 @@ module Textus
|
|
|
7
7
|
|
|
8
8
|
def call(store)
|
|
9
9
|
key = positional.shift or raise UsageError.new("policy explain requires a KEY")
|
|
10
|
-
result =
|
|
10
|
+
result = session_for(store).policy_explain(key: key)
|
|
11
11
|
emit({ "verb" => "policy_explain" }.merge(result.transform_keys(&:to_s)))
|
|
12
12
|
end
|
|
13
13
|
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class CLI
|
|
3
|
+
class Verb
|
|
4
|
+
class RuleLint < Verb
|
|
5
|
+
command_name "lint"
|
|
6
|
+
parent_group Group::Rule
|
|
7
|
+
|
|
8
|
+
option :against, "--against=FILE"
|
|
9
|
+
|
|
10
|
+
def call(store)
|
|
11
|
+
path = against or raise UsageError.new("rule lint --against=FILE required")
|
|
12
|
+
yaml = File.read(path)
|
|
13
|
+
emit(session_for(store).rule_lint(candidate_yaml: yaml).to_h)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
data/lib/textus/cli/verb/uid.rb
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
class CLI
|
|
3
|
+
class Verb
|
|
4
|
+
class ZoneMv < Verb
|
|
5
|
+
command_name "mv"
|
|
6
|
+
parent_group Group::Zone
|
|
7
|
+
|
|
8
|
+
option :as_flag, "--as=ROLE"
|
|
9
|
+
option :dry_run, "--dry-run"
|
|
10
|
+
|
|
11
|
+
def call(store)
|
|
12
|
+
from = positional.shift or raise UsageError.new("zone mv requires <from> <to>")
|
|
13
|
+
to = positional.shift or raise UsageError.new("zone mv requires <from> <to>")
|
|
14
|
+
emit(session_for(store).zone_mv(from: from, to: to, dry_run: dry_run || false).to_h)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
data/lib/textus/cli/verb.rb
CHANGED
|
@@ -100,12 +100,12 @@ module Textus
|
|
|
100
100
|
# Convenience for verbs whose only pre-call boilerplate is
|
|
101
101
|
# resolving the role and wrapping it in a context.
|
|
102
102
|
def context_for(store)
|
|
103
|
-
|
|
103
|
+
store.session(role: resolved_role(store)).ctx
|
|
104
104
|
end
|
|
105
105
|
|
|
106
|
-
# Returns
|
|
107
|
-
def
|
|
108
|
-
|
|
106
|
+
# Returns a Session instance bound to the resolved role.
|
|
107
|
+
def session_for(store)
|
|
108
|
+
store.session(role: resolved_role(store))
|
|
109
109
|
end
|
|
110
110
|
end
|
|
111
111
|
end
|
|
@@ -3,8 +3,8 @@ module Textus
|
|
|
3
3
|
class Check
|
|
4
4
|
class AuditLog < Check
|
|
5
5
|
def call
|
|
6
|
-
path = File.join(
|
|
7
|
-
Textus::Infra::AuditLog.new(
|
|
6
|
+
path = File.join(root, "audit.log")
|
|
7
|
+
Textus::Infra::AuditLog.new(root).verify_integrity.map do |v|
|
|
8
8
|
{
|
|
9
9
|
"code" => "audit.parse_error",
|
|
10
10
|
"level" => "warning",
|
|
@@ -7,12 +7,12 @@ module Textus
|
|
|
7
7
|
class HandlerAllowlist < Check
|
|
8
8
|
def call
|
|
9
9
|
out = []
|
|
10
|
-
|
|
10
|
+
manifest.data.entries.each do |mentry|
|
|
11
11
|
next unless mentry.is_a?(Textus::Manifest::Entry::Intake)
|
|
12
12
|
|
|
13
13
|
handler = mentry.handler
|
|
14
14
|
|
|
15
|
-
allow =
|
|
15
|
+
allow = manifest.rules.for(mentry.key).handler_allowlist
|
|
16
16
|
next if allow.nil?
|
|
17
17
|
next if allow.allows?(handler)
|
|
18
18
|
|
|
@@ -4,15 +4,16 @@ module Textus
|
|
|
4
4
|
class Hooks < Check
|
|
5
5
|
def call
|
|
6
6
|
out = []
|
|
7
|
-
dir = File.join(
|
|
7
|
+
dir = File.join(root, "hooks")
|
|
8
8
|
return out unless File.directory?(dir)
|
|
9
9
|
|
|
10
10
|
Dir.glob(File.join(dir, "*.rb")).sort.each do |f| # rubocop:disable Lint/RedundantDirGlobSort
|
|
11
|
-
|
|
11
|
+
events = Textus::Hooks::EventBus.new
|
|
12
|
+
rpc = Textus::Hooks::RpcRegistry.new
|
|
12
13
|
Textus.drain_hook_blocks
|
|
13
14
|
begin
|
|
14
15
|
load(f)
|
|
15
|
-
Textus.drain_hook_blocks.each { |b| b.call(
|
|
16
|
+
Textus.drain_hook_blocks.each { |b| b.call(Textus::Hooks::Loader::Dsl.new(events: events, rpc: rpc)) }
|
|
16
17
|
end
|
|
17
18
|
rescue StandardError, ScriptError => e
|
|
18
19
|
out << {
|
|
@@ -4,10 +4,10 @@ module Textus
|
|
|
4
4
|
class IllegalKeys < Check
|
|
5
5
|
def call
|
|
6
6
|
out = []
|
|
7
|
-
|
|
7
|
+
manifest.data.entries.each do |entry|
|
|
8
8
|
next unless entry.nested?
|
|
9
9
|
|
|
10
|
-
base = File.join(
|
|
10
|
+
base = File.join(root, "zones", entry.path)
|
|
11
11
|
next unless File.directory?(base)
|
|
12
12
|
|
|
13
13
|
index_fn = entry.respond_to?(:index_filename) ? entry.index_filename : nil
|
|
@@ -6,7 +6,7 @@ module Textus
|
|
|
6
6
|
|
|
7
7
|
def call
|
|
8
8
|
declared = collect_declared_handlers
|
|
9
|
-
registered =
|
|
9
|
+
registered = rpc.names(:resolve_intake).to_set
|
|
10
10
|
|
|
11
11
|
out = (declared - registered).map do |name|
|
|
12
12
|
{
|
|
@@ -35,7 +35,7 @@ module Textus
|
|
|
35
35
|
|
|
36
36
|
def collect_declared_handlers
|
|
37
37
|
set = Set.new
|
|
38
|
-
|
|
38
|
+
manifest.data.entries.each do |mentry|
|
|
39
39
|
set << mentry.handler.to_sym if mentry.is_a?(Textus::Manifest::Entry::Intake)
|
|
40
40
|
end
|
|
41
41
|
set
|
|
@@ -3,10 +3,10 @@ module Textus
|
|
|
3
3
|
class Check
|
|
4
4
|
class ManifestFiles < Check
|
|
5
5
|
def call
|
|
6
|
-
|
|
6
|
+
manifest.data.entries.each_with_object([]) do |entry, out|
|
|
7
7
|
next if entry.nested?
|
|
8
8
|
|
|
9
|
-
path = Textus::Key::Path.resolve(
|
|
9
|
+
path = Textus::Key::Path.resolve(manifest.data, entry)
|
|
10
10
|
next if File.exist?(path)
|
|
11
11
|
|
|
12
12
|
out << {
|
|
@@ -23,10 +23,10 @@ module Textus
|
|
|
23
23
|
}]
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
# Doctor check interface:
|
|
26
|
+
# Doctor check interface: root is the .textus/ directory itself,
|
|
27
27
|
# so manifest.yaml lives directly inside it.
|
|
28
28
|
def call
|
|
29
|
-
path = File.join(
|
|
29
|
+
path = File.join(root, "manifest.yaml")
|
|
30
30
|
return [] unless File.exist?(path)
|
|
31
31
|
|
|
32
32
|
doc = YAML.safe_load_file(path, aliases: false) || {}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module Textus
|
|
2
2
|
module Doctor
|
|
3
3
|
class Check
|
|
4
|
-
# Lists per-key refresh lock files under <
|
|
4
|
+
# Lists per-key refresh lock files under <root>/.locks/ whose
|
|
5
5
|
# recorded PID is no longer running. These are forensic artifacts only:
|
|
6
6
|
# Refresh::Lock uses flock(2), which the kernel releases on process
|
|
7
7
|
# death, so stale files do not block subsequent acquires. The check
|
|
@@ -9,7 +9,7 @@ module Textus
|
|
|
9
9
|
# (e.g. a refresh path that crashes repeatedly).
|
|
10
10
|
class RefreshLocks < Check
|
|
11
11
|
def call
|
|
12
|
-
dir = File.join(
|
|
12
|
+
dir = File.join(root, ".locks")
|
|
13
13
|
return [] unless File.directory?(dir)
|
|
14
14
|
|
|
15
15
|
Dir.glob(File.join(dir, "*.lock")).filter_map { |path| inspect_lock(path) }
|
|
@@ -7,7 +7,7 @@ module Textus
|
|
|
7
7
|
# leaving the operator with no signal that a schema is broken.
|
|
8
8
|
class SchemaParseError < Check
|
|
9
9
|
def call
|
|
10
|
-
dir = File.join(
|
|
10
|
+
dir = File.join(root, "schemas")
|
|
11
11
|
return [] unless File.directory?(dir)
|
|
12
12
|
|
|
13
13
|
Dir.glob(File.join(dir, "*.yaml")).each_with_object([]) do |path, out|
|
|
@@ -3,7 +3,7 @@ module Textus
|
|
|
3
3
|
class Check
|
|
4
4
|
class SchemaViolations < Check
|
|
5
5
|
def call
|
|
6
|
-
res =
|
|
6
|
+
res = @session.validate_all
|
|
7
7
|
res["violations"].map do |v|
|
|
8
8
|
fix = v["expected"] &&
|
|
9
9
|
"field '#{v["field"]}' should be written by '#{v["expected"]}' (last writer: #{v["last_writer"]})"
|
|
@@ -4,10 +4,10 @@ module Textus
|
|
|
4
4
|
class Schemas < Check
|
|
5
5
|
def call
|
|
6
6
|
out = []
|
|
7
|
-
|
|
7
|
+
manifest.data.entries.each do |entry|
|
|
8
8
|
next if entry.schema.nil?
|
|
9
9
|
|
|
10
|
-
sp = File.join(
|
|
10
|
+
sp = File.join(root, "schemas", "#{entry.schema}.yaml")
|
|
11
11
|
next if File.exist?(sp)
|
|
12
12
|
|
|
13
13
|
out << {
|
|
@@ -3,10 +3,10 @@ module Textus
|
|
|
3
3
|
class Check
|
|
4
4
|
class Sentinels < Check
|
|
5
5
|
def call
|
|
6
|
-
dir = File.join(
|
|
6
|
+
dir = File.join(root, "sentinels")
|
|
7
7
|
return [] unless File.directory?(dir)
|
|
8
8
|
|
|
9
|
-
repo_root = File.dirname(
|
|
9
|
+
repo_root = File.dirname(root)
|
|
10
10
|
Dir.glob(File.join(dir, "**", "*#{Textus::Domain::Sentinel::SUFFIX}")).flat_map do |sentinel_path|
|
|
11
11
|
inspect_sentinel(sentinel_path, repo_root)
|
|
12
12
|
end
|
|
@@ -4,11 +4,11 @@ module Textus
|
|
|
4
4
|
class Templates < Check
|
|
5
5
|
def call
|
|
6
6
|
out = []
|
|
7
|
-
|
|
7
|
+
manifest.data.entries.each do |entry|
|
|
8
8
|
template = entry.respond_to?(:template) ? entry.template : nil
|
|
9
9
|
next if template.nil?
|
|
10
10
|
|
|
11
|
-
tp = File.join(
|
|
11
|
+
tp = File.join(root, "templates", template)
|
|
12
12
|
next if File.exist?(tp)
|
|
13
13
|
|
|
14
14
|
out << {
|
data/lib/textus/doctor/check.rb
CHANGED
|
@@ -14,8 +14,8 @@ module Textus
|
|
|
14
14
|
.downcase
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
def initialize(
|
|
18
|
-
@
|
|
17
|
+
def initialize(session)
|
|
18
|
+
@session = session
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def call
|
|
@@ -24,7 +24,9 @@ module Textus
|
|
|
24
24
|
|
|
25
25
|
protected
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
def root = @session.read_caps.root
|
|
28
|
+
def manifest = @session.read_caps.manifest
|
|
29
|
+
def rpc = @session.rpc
|
|
28
30
|
end
|
|
29
31
|
end
|
|
30
32
|
end
|
data/lib/textus/doctor.rb
CHANGED
|
@@ -30,7 +30,7 @@ module Textus
|
|
|
30
30
|
|
|
31
31
|
module_function
|
|
32
32
|
|
|
33
|
-
def run(
|
|
33
|
+
def run(session, checks: nil)
|
|
34
34
|
selected_keys = checks ? Array(checks).map(&:to_s) : ALL_CHECKS
|
|
35
35
|
unknown = selected_keys - ALL_CHECKS
|
|
36
36
|
unless unknown.empty?
|
|
@@ -40,8 +40,8 @@ module Textus
|
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
selected = CHECKS.select { |c| selected_keys.include?(c.name_key) }
|
|
43
|
-
issues = selected.flat_map { |c| c.new(
|
|
44
|
-
issues.concat(run_registered_checks(
|
|
43
|
+
issues = selected.flat_map { |c| c.new(session).call }
|
|
44
|
+
issues.concat(run_registered_checks(session))
|
|
45
45
|
|
|
46
46
|
summary = LEVELS.to_h { |l| [l, issues.count { |i| i["level"] == l }] }
|
|
47
47
|
{
|
|
@@ -52,30 +52,27 @@ module Textus
|
|
|
52
52
|
}
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
-
def run_registered_checks(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
out.concat(result.map { |h| h.transform_keys(&:to_s) })
|
|
63
|
-
else
|
|
64
|
-
out << fail_issue(name, code: "doctor_check.bad_return",
|
|
65
|
-
message: "doctor_check '#{name}' returned #{result.class} (expected Array)",
|
|
66
|
-
fix: "return an array of issue hashes from the doctor_check block")
|
|
67
|
-
end
|
|
68
|
-
rescue Timeout::Error
|
|
69
|
-
out << fail_issue(name, code: "doctor_check.timeout",
|
|
70
|
-
message: "doctor_check '#{name}' exceeded #{DOCTOR_CHECK_TIMEOUT_SECONDS}s",
|
|
71
|
-
fix: "shorten the check or split it into smaller checks")
|
|
72
|
-
rescue StandardError => e
|
|
73
|
-
out << fail_issue(name, code: "doctor_check.failed",
|
|
74
|
-
message: "#{e.class}: #{e.message}",
|
|
75
|
-
fix: "fix the :validate hook in .textus/hooks/")
|
|
76
|
-
end
|
|
55
|
+
def run_registered_checks(session)
|
|
56
|
+
session.rpc.names(:validate).flat_map { |name| invoke_registered_check(session, name) }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def invoke_registered_check(session, name)
|
|
60
|
+
result = Timeout.timeout(DOCTOR_CHECK_TIMEOUT_SECONDS) do
|
|
61
|
+
session.rpc.invoke(:validate, name, caps: session.write_caps)
|
|
77
62
|
end
|
|
78
|
-
|
|
63
|
+
return result.map { |h| h.transform_keys(&:to_s) } if result.is_a?(Array)
|
|
64
|
+
|
|
65
|
+
[fail_issue(name, code: "doctor_check.bad_return",
|
|
66
|
+
message: "doctor_check '#{name}' returned #{result.class} (expected Array)",
|
|
67
|
+
fix: "return an array of issue hashes from the doctor_check block")]
|
|
68
|
+
rescue Timeout::Error
|
|
69
|
+
[fail_issue(name, code: "doctor_check.timeout",
|
|
70
|
+
message: "doctor_check '#{name}' exceeded #{DOCTOR_CHECK_TIMEOUT_SECONDS}s",
|
|
71
|
+
fix: "shorten the check or split it into smaller checks")]
|
|
72
|
+
rescue StandardError => e
|
|
73
|
+
[fail_issue(name, code: "doctor_check.failed",
|
|
74
|
+
message: "#{e.class}: #{e.message}",
|
|
75
|
+
fix: "fix the :validate hook in .textus/hooks/")]
|
|
79
76
|
end
|
|
80
77
|
|
|
81
78
|
def fail_issue(name, code:, message:, fix:)
|
|
@@ -88,6 +85,6 @@ module Textus
|
|
|
88
85
|
}
|
|
89
86
|
end
|
|
90
87
|
|
|
91
|
-
private_class_method :run_registered_checks, :fail_issue
|
|
88
|
+
private_class_method :run_registered_checks, :invoke_registered_check, :fail_issue
|
|
92
89
|
end
|
|
93
90
|
end
|
|
@@ -11,24 +11,24 @@ module Textus
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def can_write?(zone, role:)
|
|
14
|
-
@manifest.permission_for(zone.to_s).allows_write?(role)
|
|
14
|
+
@manifest.policy.permission_for(zone.to_s).allows_write?(role)
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def can_read?(zone, role:)
|
|
18
|
-
@manifest.permission_for(zone.to_s).allows_read?(role)
|
|
18
|
+
@manifest.policy.permission_for(zone.to_s).allows_read?(role)
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def authorize_write!(mentry, role:)
|
|
22
22
|
return if can_write?(mentry.zone, role: role)
|
|
23
23
|
|
|
24
|
-
writers = @manifest.zone_writers(mentry.zone)
|
|
24
|
+
writers = @manifest.policy.zone_writers(mentry.zone)
|
|
25
25
|
raise WriteForbidden.new(mentry.key, mentry.zone, writers: writers)
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def authorize_read!(mentry, role:)
|
|
29
29
|
return if can_read?(mentry.zone, role: role)
|
|
30
30
|
|
|
31
|
-
readers = @manifest.zone_readers[mentry.zone]
|
|
31
|
+
readers = @manifest.policy.zone_readers[mentry.zone]
|
|
32
32
|
readers = nil if readers == :all
|
|
33
33
|
raise ReadForbidden.new(mentry.key, mentry.zone, readers: readers)
|
|
34
34
|
end
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
module Textus
|
|
2
|
-
module
|
|
2
|
+
module Domain
|
|
3
3
|
module Policy
|
|
4
4
|
module Predicates
|
|
5
5
|
# Promotion predicate: the role driving the promotion must have
|
|
@@ -20,7 +20,7 @@ module Textus
|
|
|
20
20
|
role_str = role&.to_s
|
|
21
21
|
return true if role_str.nil? || role_str.empty?
|
|
22
22
|
|
|
23
|
-
kind = manifest.role_kind(role_str)
|
|
23
|
+
kind = manifest.policy.role_kind(role_str)
|
|
24
24
|
return true if kind == :accept_authority
|
|
25
25
|
|
|
26
26
|
@reason = "role '#{role_str}' has kind '#{kind.inspect}', expected ':accept_authority'"
|
|
@@ -19,7 +19,7 @@ module Textus
|
|
|
19
19
|
src = mentry.source
|
|
20
20
|
return [] unless src.is_a?(Textus::Manifest::Entry::Derived::External)
|
|
21
21
|
|
|
22
|
-
path = Textus::Key::Path.resolve(@manifest, mentry)
|
|
22
|
+
path = Textus::Key::Path.resolve(@manifest.data, mentry)
|
|
23
23
|
return [stale_row(mentry, path, "derived entry has never been generated")] unless File.exist?(path)
|
|
24
24
|
|
|
25
25
|
parsed = Entry.for_format(mentry.format).parse(File.binread(path), path: path)
|
|
@@ -63,7 +63,7 @@ module Textus
|
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
def check_filesystem_source(src, gen_time)
|
|
66
|
-
abs = File.absolute_path?(src) ? src : File.join(File.dirname(@manifest.root), src)
|
|
66
|
+
abs = File.absolute_path?(src) ? src : File.join(File.dirname(@manifest.data.root), src)
|
|
67
67
|
if File.directory?(abs)
|
|
68
68
|
Dir.glob(File.join(abs, "**", "*")).each do |fp|
|
|
69
69
|
next unless File.file?(fp)
|
|
@@ -13,10 +13,10 @@ module Textus
|
|
|
13
13
|
def rows_for(mentry)
|
|
14
14
|
return [] unless mentry.is_a?(Textus::Manifest::Entry::Intake)
|
|
15
15
|
|
|
16
|
-
ttl = @manifest.
|
|
16
|
+
ttl = @manifest.rules.for(mentry.key).refresh&.ttl_seconds
|
|
17
17
|
return [] unless ttl
|
|
18
18
|
|
|
19
|
-
path = Textus::Key::Path.resolve(@manifest, mentry)
|
|
19
|
+
path = Textus::Key::Path.resolve(@manifest.data, mentry)
|
|
20
20
|
return [row(mentry, path, "never refreshed")] unless File.exist?(path)
|
|
21
21
|
|
|
22
22
|
meta = Entry.for_format(mentry.format).parse(File.binread(path), path: path)["_meta"]
|