legionio 1.4.48 → 1.4.50
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 +17 -0
- data/lib/legion/api/middleware/tenant.rb +36 -0
- data/lib/legion/api/tenants.rb +48 -0
- data/lib/legion/cli/graph_command.rb +42 -0
- data/lib/legion/graph/builder.rb +43 -0
- data/lib/legion/graph/exporter.rb +54 -0
- data/lib/legion/tenant_context.rb +27 -0
- data/lib/legion/tenants.rb +52 -0
- data/lib/legion/version.rb +1 -1
- metadata +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9f4884c77ed8ea44e74bdc3c9860dac014693cc90a3029fb06f1bc9e5a7edc27
|
|
4
|
+
data.tar.gz: 8da5570f871da743755e0c1c0fc1f81a8a5085061a6e27b5a8d9c2befe2400e0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: be184813bae9bba912f9d190a06dbb9e3bc0a5b9c317019743fcaf1787de5c8aaad28fec85dcd14e1ffe27115cb80be3ea0970dd183cc002042aac1f2a12a7f1
|
|
7
|
+
data.tar.gz: e844ec3f09dd2f5bc6d8c908f884def2a6e4d71aa0ec5178d0e1ec1440c3a4cfe9d5d58996736acca92c67b7bced45ed8e8d2ffbc00a5f37a1ac4c6c4fb3ca37
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Legion Changelog
|
|
2
2
|
|
|
3
|
+
## [1.4.50] - 2026-03-17
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `Legion::Graph::Builder`: builds task relationship graph from relationships table with chain/worker filtering
|
|
7
|
+
- `Legion::Graph::Exporter`: renders graphs to Mermaid and DOT (Graphviz) formats
|
|
8
|
+
- `legion graph show`: CLI command with `--format mermaid|dot`, `--chain`, `--worker`, `--output`, `--limit` options
|
|
9
|
+
|
|
10
|
+
## [1.4.49] - 2026-03-17
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `Legion::TenantContext`: thread-local tenant context propagation (set, clear, with block)
|
|
14
|
+
- `Legion::Tenants`: tenant CRUD, suspension, and quota enforcement
|
|
15
|
+
- `Middleware::Tenant`: extracts tenant_id from JWT/header, sets TenantContext per request
|
|
16
|
+
- `GET/POST /api/tenants`: tenant listing and provisioning endpoints
|
|
17
|
+
- `POST /api/tenants/:id/suspend`: tenant suspension
|
|
18
|
+
- `GET /api/tenants/:id/quota/:resource`: quota check endpoint
|
|
19
|
+
|
|
3
20
|
## [1.4.48] - 2026-03-17
|
|
4
21
|
|
|
5
22
|
### Added
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
class API < Sinatra::Base
|
|
5
|
+
module Middleware
|
|
6
|
+
class Tenant
|
|
7
|
+
SKIP_PATHS = %w[/api/health /api/ready /api/openapi.json /metrics].freeze
|
|
8
|
+
|
|
9
|
+
def initialize(app, opts = {})
|
|
10
|
+
@app = app
|
|
11
|
+
@opts = opts
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call(env)
|
|
15
|
+
return @app.call(env) if skip_path?(env['PATH_INFO'])
|
|
16
|
+
|
|
17
|
+
tenant_id = extract_tenant(env)
|
|
18
|
+
Legion::TenantContext.set(tenant_id) if tenant_id
|
|
19
|
+
@app.call(env)
|
|
20
|
+
ensure
|
|
21
|
+
Legion::TenantContext.clear
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def skip_path?(path)
|
|
27
|
+
SKIP_PATHS.any? { |sp| path.start_with?(sp) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def extract_tenant(env)
|
|
31
|
+
env['legion.tenant_id'] || env['HTTP_X_TENANT_ID']
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../tenants'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
class API < Sinatra::Base
|
|
7
|
+
module Routes
|
|
8
|
+
module Tenants
|
|
9
|
+
def self.registered(app)
|
|
10
|
+
app.get '/api/tenants' do
|
|
11
|
+
tenants = Legion::Tenants.list
|
|
12
|
+
json_response(data: tenants)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
app.post '/api/tenants' do
|
|
16
|
+
params = parsed_body
|
|
17
|
+
result = Legion::Tenants.create(
|
|
18
|
+
tenant_id: params['tenant_id'],
|
|
19
|
+
name: params['name'],
|
|
20
|
+
max_workers: params['max_workers'] || 10
|
|
21
|
+
)
|
|
22
|
+
status result[:error] ? 409 : 201
|
|
23
|
+
json_response(data: result)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
app.get '/api/tenants/:tenant_id' do
|
|
27
|
+
tenant = Legion::Tenants.find(params[:tenant_id])
|
|
28
|
+
halt 404, json_response(error: 'not_found') unless tenant
|
|
29
|
+
json_response(data: tenant)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
app.post '/api/tenants/:tenant_id/suspend' do
|
|
33
|
+
result = Legion::Tenants.suspend(tenant_id: params[:tenant_id])
|
|
34
|
+
json_response(data: result)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
app.get '/api/tenants/:tenant_id/quota/:resource' do
|
|
38
|
+
result = Legion::Tenants.check_quota(
|
|
39
|
+
tenant_id: params[:tenant_id],
|
|
40
|
+
resource: params[:resource].to_sym
|
|
41
|
+
)
|
|
42
|
+
json_response(data: result)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'thor'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module CLI
|
|
7
|
+
class GraphCommand < Thor
|
|
8
|
+
namespace 'graph'
|
|
9
|
+
|
|
10
|
+
desc 'show', 'Display task relationship graph'
|
|
11
|
+
option :chain, type: :string, desc: 'Filter by chain ID'
|
|
12
|
+
option :worker, type: :string, desc: 'Filter by worker ID'
|
|
13
|
+
option :format, type: :string, default: 'mermaid', enum: %w[mermaid dot]
|
|
14
|
+
option :output, type: :string, desc: 'Write to file'
|
|
15
|
+
option :limit, type: :numeric, default: 100
|
|
16
|
+
def show
|
|
17
|
+
require 'legion/graph/builder'
|
|
18
|
+
require 'legion/graph/exporter'
|
|
19
|
+
|
|
20
|
+
graph = Legion::Graph::Builder.build(
|
|
21
|
+
chain_id: options[:chain],
|
|
22
|
+
worker_id: options[:worker],
|
|
23
|
+
limit: options[:limit]
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
rendered = case options[:format]
|
|
27
|
+
when 'dot' then Legion::Graph::Exporter.to_dot(graph)
|
|
28
|
+
else Legion::Graph::Exporter.to_mermaid(graph)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
if options[:output]
|
|
32
|
+
File.write(options[:output], rendered)
|
|
33
|
+
say "Written to #{options[:output]}", :green
|
|
34
|
+
else
|
|
35
|
+
say rendered
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
default_task :show
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Graph
|
|
5
|
+
module Builder
|
|
6
|
+
class << self
|
|
7
|
+
def build(chain_id: nil, worker_id: nil, limit: 100) # rubocop:disable Lint/UnusedMethodArgument
|
|
8
|
+
return { nodes: {}, edges: [] } unless db_available?
|
|
9
|
+
|
|
10
|
+
ds = Legion::Data.connection[:relationships].limit(limit)
|
|
11
|
+
ds = ds.where(chain_id: chain_id) if chain_id
|
|
12
|
+
|
|
13
|
+
nodes = {}
|
|
14
|
+
edges = []
|
|
15
|
+
|
|
16
|
+
ds.each do |rel|
|
|
17
|
+
trigger = rel[:trigger] || "node_#{rel[:id]}_from"
|
|
18
|
+
action = rel[:action] || "node_#{rel[:id]}_to"
|
|
19
|
+
|
|
20
|
+
nodes[trigger] ||= { label: trigger, type: 'trigger' }
|
|
21
|
+
nodes[action] ||= { label: action, type: 'action' }
|
|
22
|
+
edges << {
|
|
23
|
+
from: trigger,
|
|
24
|
+
to: action,
|
|
25
|
+
label: rel[:runner_function] || '',
|
|
26
|
+
chain_id: rel[:chain_id]
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
{ nodes: nodes, edges: edges }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def db_available?
|
|
36
|
+
defined?(Legion::Data) && Legion::Data.connection&.table_exists?(:relationships)
|
|
37
|
+
rescue StandardError
|
|
38
|
+
false
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Graph
|
|
5
|
+
module Exporter
|
|
6
|
+
class << self
|
|
7
|
+
def to_mermaid(graph)
|
|
8
|
+
lines = ['graph TD']
|
|
9
|
+
node_ids = {}
|
|
10
|
+
counter = 0
|
|
11
|
+
|
|
12
|
+
graph[:nodes].each do |key, node|
|
|
13
|
+
counter += 1
|
|
14
|
+
id = "N#{counter}"
|
|
15
|
+
node_ids[key] = id
|
|
16
|
+
lines << " #{id}[#{node[:label]}]"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
graph[:edges].each do |edge|
|
|
20
|
+
from = node_ids[edge[:from]]
|
|
21
|
+
to = node_ids[edge[:to]]
|
|
22
|
+
next unless from && to
|
|
23
|
+
|
|
24
|
+
lines << if edge[:label] && !edge[:label].empty?
|
|
25
|
+
" #{from} -->|#{edge[:label]}| #{to}"
|
|
26
|
+
else
|
|
27
|
+
" #{from} --> #{to}"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
lines.join("\n")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def to_dot(graph)
|
|
35
|
+
lines = ['digraph legion_tasks {', ' rankdir=LR;']
|
|
36
|
+
|
|
37
|
+
graph[:nodes].each do |key, node|
|
|
38
|
+
label = node[:label].gsub('"', '\\"')
|
|
39
|
+
shape = node[:type] == 'trigger' ? 'box' : 'ellipse'
|
|
40
|
+
lines << " \"#{key}\" [label=\"#{label}\" shape=#{shape}];"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
graph[:edges].each do |edge|
|
|
44
|
+
label = edge[:label] && !edge[:label].empty? ? " [label=\"#{edge[:label]}\"]" : ''
|
|
45
|
+
lines << " \"#{edge[:from]}\" -> \"#{edge[:to]}\"#{label};"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
lines << '}'
|
|
49
|
+
lines.join("\n")
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module TenantContext
|
|
5
|
+
class << self
|
|
6
|
+
def current
|
|
7
|
+
Thread.current[:legion_tenant_id]
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def set(tenant_id)
|
|
11
|
+
Thread.current[:legion_tenant_id] = tenant_id
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def clear
|
|
15
|
+
Thread.current[:legion_tenant_id] = nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def with(tenant_id)
|
|
19
|
+
prev = current
|
|
20
|
+
set(tenant_id)
|
|
21
|
+
yield
|
|
22
|
+
ensure
|
|
23
|
+
set(prev)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Tenants
|
|
5
|
+
class << self
|
|
6
|
+
def create(tenant_id:, name: nil, max_workers: 10, max_queue_depth: 10_000, **)
|
|
7
|
+
return { error: 'tenant_exists' } if find(tenant_id)
|
|
8
|
+
|
|
9
|
+
Legion::Data.connection[:tenants].insert(
|
|
10
|
+
tenant_id: tenant_id,
|
|
11
|
+
name: name || tenant_id,
|
|
12
|
+
max_workers: max_workers,
|
|
13
|
+
max_queue_depth: max_queue_depth,
|
|
14
|
+
status: 'active',
|
|
15
|
+
created_at: Time.now.utc,
|
|
16
|
+
updated_at: Time.now.utc
|
|
17
|
+
)
|
|
18
|
+
{ created: true, tenant_id: tenant_id }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def find(tenant_id)
|
|
22
|
+
Legion::Data.connection[:tenants].where(tenant_id: tenant_id).first
|
|
23
|
+
rescue StandardError
|
|
24
|
+
nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def suspend(tenant_id:, **)
|
|
28
|
+
Legion::Data.connection[:tenants]
|
|
29
|
+
.where(tenant_id: tenant_id)
|
|
30
|
+
.update(status: 'suspended', updated_at: Time.now.utc)
|
|
31
|
+
{ suspended: true, tenant_id: tenant_id }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def list(**)
|
|
35
|
+
Legion::Data.connection[:tenants].all
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def check_quota(tenant_id:, resource:, **)
|
|
39
|
+
tenant = find(tenant_id)
|
|
40
|
+
return { allowed: true } unless tenant
|
|
41
|
+
|
|
42
|
+
case resource
|
|
43
|
+
when :workers
|
|
44
|
+
count = Legion::Data.connection[:digital_workers].where(tenant_id: tenant_id).count
|
|
45
|
+
{ allowed: count < tenant[:max_workers], current: count, limit: tenant[:max_workers] }
|
|
46
|
+
else
|
|
47
|
+
{ allowed: true }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
data/lib/legion/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: legionio
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.4.
|
|
4
|
+
version: 1.4.50
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -333,6 +333,7 @@ files:
|
|
|
333
333
|
- lib/legion/api/middleware/auth.rb
|
|
334
334
|
- lib/legion/api/middleware/body_limit.rb
|
|
335
335
|
- lib/legion/api/middleware/rate_limit.rb
|
|
336
|
+
- lib/legion/api/middleware/tenant.rb
|
|
336
337
|
- lib/legion/api/nodes.rb
|
|
337
338
|
- lib/legion/api/oauth.rb
|
|
338
339
|
- lib/legion/api/openapi.rb
|
|
@@ -341,6 +342,7 @@ files:
|
|
|
341
342
|
- lib/legion/api/schedules.rb
|
|
342
343
|
- lib/legion/api/settings.rb
|
|
343
344
|
- lib/legion/api/tasks.rb
|
|
345
|
+
- lib/legion/api/tenants.rb
|
|
344
346
|
- lib/legion/api/token.rb
|
|
345
347
|
- lib/legion/api/transport.rb
|
|
346
348
|
- lib/legion/api/validators.rb
|
|
@@ -415,6 +417,7 @@ files:
|
|
|
415
417
|
- lib/legion/cli/function.rb
|
|
416
418
|
- lib/legion/cli/gaia_command.rb
|
|
417
419
|
- lib/legion/cli/generate_command.rb
|
|
420
|
+
- lib/legion/cli/graph_command.rb
|
|
418
421
|
- lib/legion/cli/init/config_generator.rb
|
|
419
422
|
- lib/legion/cli/init/environment_detector.rb
|
|
420
423
|
- lib/legion/cli/init_command.rb
|
|
@@ -511,6 +514,8 @@ files:
|
|
|
511
514
|
- lib/legion/extensions/helpers/transport.rb
|
|
512
515
|
- lib/legion/extensions/hooks/base.rb
|
|
513
516
|
- lib/legion/extensions/transport.rb
|
|
517
|
+
- lib/legion/graph/builder.rb
|
|
518
|
+
- lib/legion/graph/exporter.rb
|
|
514
519
|
- lib/legion/guardrails.rb
|
|
515
520
|
- lib/legion/ingress.rb
|
|
516
521
|
- lib/legion/isolation.rb
|
|
@@ -566,6 +571,8 @@ files:
|
|
|
566
571
|
- lib/legion/service.rb
|
|
567
572
|
- lib/legion/supervision.rb
|
|
568
573
|
- lib/legion/telemetry.rb
|
|
574
|
+
- lib/legion/tenant_context.rb
|
|
575
|
+
- lib/legion/tenants.rb
|
|
569
576
|
- lib/legion/version.rb
|
|
570
577
|
- lib/legion/webhooks.rb
|
|
571
578
|
- scripts/rollout-ci-workflow.sh
|