legion-data 1.1.5 → 1.3.7

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.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +16 -0
  3. data/.gitignore +10 -2
  4. data/.rubocop.yml +41 -18
  5. data/CHANGELOG.md +82 -15
  6. data/CLAUDE.md +199 -0
  7. data/Gemfile +11 -1
  8. data/LICENSE +201 -0
  9. data/README.md +163 -19
  10. data/exe/legionio_migrate +0 -0
  11. data/legion-data.gemspec +22 -35
  12. data/lib/legion/data/connection.rb +39 -25
  13. data/lib/legion/data/encryption/cipher.rb +49 -0
  14. data/lib/legion/data/encryption/key_provider.rb +45 -0
  15. data/lib/legion/data/encryption/sequel_plugin.rb +54 -0
  16. data/lib/legion/data/event_store/projection.rb +56 -0
  17. data/lib/legion/data/event_store.rb +112 -0
  18. data/lib/legion/data/local.rb +77 -0
  19. data/lib/legion/data/migration.rb +5 -3
  20. data/lib/legion/data/migrations/001_add_schema_columns.rb +9 -3
  21. data/lib/legion/data/migrations/002_add_nodes.rb +18 -0
  22. data/lib/legion/data/migrations/003_add_settings.rb +18 -0
  23. data/lib/legion/data/migrations/004_add_extensions.rb +23 -0
  24. data/lib/legion/data/migrations/005_add_runners.rb +21 -0
  25. data/lib/legion/data/migrations/006_add_functions.rb +21 -0
  26. data/lib/legion/data/migrations/{015_add_default_extensions.rb → 007_add_default_extensions.rb} +3 -0
  27. data/lib/legion/data/migrations/008_add_tasks.rb +23 -0
  28. data/lib/legion/data/migrations/009_add_digital_workers.rb +45 -0
  29. data/lib/legion/data/migrations/010_add_value_metrics.rb +19 -0
  30. data/lib/legion/data/migrations/011_add_extensions_registry.rb +30 -0
  31. data/lib/legion/data/migrations/012_add_apollo_tables.rb +66 -0
  32. data/lib/legion/data/migrations/013_add_relationships.rb +21 -0
  33. data/lib/legion/data/migrations/014_add_relationship_columns.rb +27 -0
  34. data/lib/legion/data/migrations/015_add_rbac_tables.rb +49 -0
  35. data/lib/legion/data/migrations/016_add_worker_health.rb +33 -0
  36. data/lib/legion/data/migrations/017_add_audit_log.rb +30 -0
  37. data/lib/legion/data/migrations/018_add_governance_events.rb +21 -0
  38. data/lib/legion/data/migrations/019_add_audit_hash_chain.rb +29 -0
  39. data/lib/legion/data/migrations/020_add_webhooks.rb +37 -0
  40. data/lib/legion/data/model.rb +5 -2
  41. data/lib/legion/data/models/apollo_access_log.rb +13 -0
  42. data/lib/legion/data/models/apollo_entry.rb +18 -0
  43. data/lib/legion/data/models/apollo_expertise.rb +12 -0
  44. data/lib/legion/data/models/apollo_relation.rb +14 -0
  45. data/lib/legion/data/models/audit_log.rb +34 -0
  46. data/lib/legion/data/models/digital_worker.rb +44 -0
  47. data/lib/legion/data/models/extension.rb +0 -0
  48. data/lib/legion/data/models/function.rb +0 -2
  49. data/lib/legion/data/models/node.rb +17 -3
  50. data/lib/legion/data/models/rbac_cross_team_grant.rb +33 -0
  51. data/lib/legion/data/models/rbac_role_assignment.rb +29 -0
  52. data/lib/legion/data/models/rbac_runner_grant.rb +21 -0
  53. data/lib/legion/data/models/relationship.rb +3 -6
  54. data/lib/legion/data/models/runner.rb +0 -0
  55. data/lib/legion/data/models/setting.rb +0 -0
  56. data/lib/legion/data/models/task.rb +0 -0
  57. data/lib/legion/data/models/task_log.rb +0 -0
  58. data/lib/legion/data/settings.rb +37 -8
  59. data/lib/legion/data/version.rb +3 -1
  60. data/lib/legion/data.rb +31 -13
  61. metadata +64 -139
  62. data/.circleci/config.yml +0 -174
  63. data/.rspec +0 -1
  64. data/Gemfile.lock +0 -85
  65. data/Rakefile +0 -55
  66. data/bin/console +0 -14
  67. data/bin/setup +0 -8
  68. data/bitbucket-pipelines.yml +0 -26
  69. data/lib/legion/data/migrations/002_add_users.rb +0 -17
  70. data/lib/legion/data/migrations/003_add_groups.rb +0 -16
  71. data/lib/legion/data/migrations/004_add_chains.rb +0 -25
  72. data/lib/legion/data/migrations/005_add_envs.rb +0 -24
  73. data/lib/legion/data/migrations/006_add_dcs.rb +0 -24
  74. data/lib/legion/data/migrations/007_add_nodes.rb +0 -26
  75. data/lib/legion/data/migrations/008_add_settings.rb +0 -18
  76. data/lib/legion/data/migrations/009_add_extensions.rb +0 -25
  77. data/lib/legion/data/migrations/010_add_runners.rb +0 -21
  78. data/lib/legion/data/migrations/011_add_functions.rb +0 -29
  79. data/lib/legion/data/migrations/012_add_tasks.rb +0 -28
  80. data/lib/legion/data/migrations/013_add_task_logs.rb +0 -23
  81. data/lib/legion/data/migrations/014_add_relationships.rb +0 -27
  82. data/lib/legion/data/migrations/016_change_task_args.rb +0 -7
  83. data/lib/legion/data/migrations/017_add_payload_task.rb +0 -7
  84. data/lib/legion/data/migrations/018_add_migration_column.rb +0 -7
  85. data/lib/legion/data/migrations/019_add_debug_to_relationships.rb +0 -7
  86. data/lib/legion/data/migrations/020_add_delay_debug_to_tasks.rb +0 -8
  87. data/lib/legion/data/models/chain.rb +0 -11
  88. data/lib/legion/data/models/datacenter.rb +0 -11
  89. data/lib/legion/data/models/environment.rb +0 -11
  90. data/lib/legion/data/models/group.rb +0 -10
  91. data/lib/legion/data/models/user.rb +0 -10
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ Sequel.migration do
4
+ up do
5
+ create_table(:webhooks) do
6
+ primary_key :id
7
+ String :url, null: false, size: 2048
8
+ String :secret, null: false, size: 255
9
+ String :event_types, text: true
10
+ String :status, default: 'active', size: 20
11
+ Integer :max_retries, default: 5
12
+ DateTime :created_at, default: Sequel::CURRENT_TIMESTAMP
13
+ DateTime :updated_at, default: Sequel::CURRENT_TIMESTAMP
14
+ end
15
+
16
+ create_table(:webhook_deliveries) do
17
+ primary_key :id
18
+ foreign_key :webhook_id, :webhooks, null: false, index: true
19
+ String :event_name, null: false, size: 255
20
+ Integer :response_status
21
+ TrueClass :success
22
+ Integer :attempt, default: 1
23
+ String :error, text: true
24
+ DateTime :delivered_at, default: Sequel::CURRENT_TIMESTAMP
25
+ end
26
+
27
+ create_table(:webhook_dead_letters) do
28
+ primary_key :id
29
+ foreign_key :webhook_id, :webhooks, null: false, index: true
30
+ String :event_name, null: false, size: 255
31
+ String :payload, text: true
32
+ Integer :attempts
33
+ String :last_error, text: true
34
+ DateTime :created_at, default: Sequel::CURRENT_TIMESTAMP
35
+ end
36
+ end
37
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Legion
2
4
  module Data
3
5
  module Models
@@ -5,7 +7,8 @@ module Legion
5
7
  attr_reader :loaded_models
6
8
 
7
9
  def models
8
- %w[user group extension chain relationship function task runner task_log datacenter environment node setting]
10
+ %w[extension function relationship task runner node setting digital_worker
11
+ apollo_entry apollo_relation apollo_expertise apollo_access_log audit_log]
9
12
  end
10
13
 
11
14
  def load
@@ -16,7 +19,7 @@ module Legion
16
19
  end
17
20
 
18
21
  def require_sequel_models(files = models)
19
- Dir["#{File.dirname(__FILE__)}models/*.rb"].each { |file| puts file }
22
+ # Dir["#{File.dirname(__FILE__)}models/*.rb"].each { |file| puts file }
20
23
  files.each { |file| load_sequel_model(file) }
21
24
  end
22
25
 
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ return unless Legion::Data::Connection.adapter == :postgres
4
+
5
+ module Legion
6
+ module Data
7
+ module Model
8
+ class ApolloAccessLog < Sequel::Model(:apollo_access_log)
9
+ many_to_one :entry, class: 'Legion::Data::Model::ApolloEntry', key: :entry_id
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ return unless Legion::Data::Connection.adapter == :postgres
4
+
5
+ module Legion
6
+ module Data
7
+ module Model
8
+ class ApolloEntry < Sequel::Model(:apollo_entries)
9
+ one_to_many :outgoing_relations, class: 'Legion::Data::Model::ApolloRelation',
10
+ key: :from_entry_id
11
+ one_to_many :incoming_relations, class: 'Legion::Data::Model::ApolloRelation',
12
+ key: :to_entry_id
13
+ one_to_many :access_logs, class: 'Legion::Data::Model::ApolloAccessLog',
14
+ key: :entry_id
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ return unless Legion::Data::Connection.adapter == :postgres
4
+
5
+ module Legion
6
+ module Data
7
+ module Model
8
+ class ApolloExpertise < Sequel::Model(:apollo_expertise)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ return unless Legion::Data::Connection.adapter == :postgres
4
+
5
+ module Legion
6
+ module Data
7
+ module Model
8
+ class ApolloRelation < Sequel::Model(:apollo_relations)
9
+ many_to_one :from_entry, class: 'Legion::Data::Model::ApolloEntry', key: :from_entry_id
10
+ many_to_one :to_entry, class: 'Legion::Data::Model::ApolloEntry', key: :to_entry_id
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Data
5
+ module Model
6
+ class AuditLog < Sequel::Model(:audit_log)
7
+ VALID_EVENT_TYPES = %w[runner_execution lifecycle_transition].freeze
8
+ VALID_STATUSES = %w[success failure denied].freeze
9
+
10
+ def validate
11
+ super
12
+ errors.add(:event_type, 'invalid') unless VALID_EVENT_TYPES.include?(event_type)
13
+ errors.add(:status, 'invalid') unless VALID_STATUSES.include?(status)
14
+ end
15
+
16
+ def parsed_detail
17
+ return nil unless detail
18
+
19
+ Legion::JSON.load(detail)
20
+ rescue StandardError
21
+ nil
22
+ end
23
+
24
+ def before_update
25
+ raise 'audit_log records are immutable and cannot be updated'
26
+ end
27
+
28
+ def before_destroy
29
+ raise 'audit_log records are immutable and cannot be deleted'
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Data
5
+ module Model
6
+ class DigitalWorker < Sequel::Model
7
+ one_to_many :tasks, key: :worker_id, primary_key: :worker_id
8
+
9
+ LIFECYCLE_STATES = %w[bootstrap active paused retired terminated].freeze
10
+ CONSENT_TIERS = %w[supervised consult notify autonomous].freeze
11
+ RISK_TIERS = %w[low medium high critical].freeze
12
+ HEALTH_STATUSES = %w[online offline unknown].freeze
13
+
14
+ def validate
15
+ super
16
+ errors.add(:lifecycle_state, 'invalid') unless LIFECYCLE_STATES.include?(lifecycle_state)
17
+ errors.add(:consent_tier, 'invalid') unless CONSENT_TIERS.include?(consent_tier)
18
+ errors.add(:risk_tier, 'invalid') if risk_tier && !RISK_TIERS.include?(risk_tier)
19
+ errors.add(:health_status, 'invalid') if health_status && !HEALTH_STATUSES.include?(health_status)
20
+ end
21
+
22
+ def active?
23
+ lifecycle_state == 'active'
24
+ end
25
+
26
+ def terminated?
27
+ lifecycle_state == 'terminated'
28
+ end
29
+
30
+ def paused?
31
+ lifecycle_state == 'paused'
32
+ end
33
+
34
+ def online?
35
+ health_status == 'online'
36
+ end
37
+
38
+ def offline?
39
+ health_status == 'offline'
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
File without changes
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'relationship'
4
-
5
3
  module Legion
6
4
  module Data
7
5
  module Model
@@ -4,9 +4,23 @@ module Legion
4
4
  module Data
5
5
  module Model
6
6
  class Node < Sequel::Model
7
- many_to_one :environment
8
- many_to_one :datacenter
9
- one_to_many :task_log
7
+ # one_to_many :task_log
8
+
9
+ def parsed_metrics
10
+ return nil unless metrics
11
+
12
+ Legion::JSON.load(metrics)
13
+ rescue StandardError
14
+ nil
15
+ end
16
+
17
+ def parsed_hosted_worker_ids
18
+ return [] unless hosted_worker_ids
19
+
20
+ Legion::JSON.load(hosted_worker_ids)
21
+ rescue StandardError
22
+ []
23
+ end
10
24
  end
11
25
  end
12
26
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Data
5
+ module Model
6
+ class RbacCrossTeamGrant < Sequel::Model
7
+ def validate
8
+ super
9
+ errors.add(:source_team, 'cannot be empty') if source_team.nil? || source_team.empty?
10
+ errors.add(:target_team, 'cannot be empty') if target_team.nil? || target_team.empty?
11
+ errors.add(:source_team, 'cannot equal target_team') if source_team == target_team
12
+ errors.add(:runner_pattern, 'cannot be empty') if runner_pattern.nil? || runner_pattern.empty?
13
+ errors.add(:actions, 'cannot be empty') if actions.nil? || actions.empty?
14
+ errors.add(:granted_by, 'cannot be empty') if granted_by.nil? || granted_by.empty?
15
+ end
16
+
17
+ def expired?
18
+ return false if expires_at.nil?
19
+
20
+ expires_at < Time.now
21
+ end
22
+
23
+ def active?
24
+ !expired?
25
+ end
26
+
27
+ def actions_list
28
+ (actions || '').split(',').map(&:strip)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Data
5
+ module Model
6
+ class RbacRoleAssignment < Sequel::Model
7
+ VALID_PRINCIPAL_TYPES = %w[worker human].freeze
8
+
9
+ def validate
10
+ super
11
+ errors.add(:principal_type, 'must be worker or human') unless VALID_PRINCIPAL_TYPES.include?(principal_type)
12
+ errors.add(:principal_id, 'cannot be empty') if principal_id.nil? || principal_id.empty?
13
+ errors.add(:role, 'cannot be empty') if role.nil? || role.empty?
14
+ errors.add(:granted_by, 'cannot be empty') if granted_by.nil? || granted_by.empty?
15
+ end
16
+
17
+ def expired?
18
+ return false if expires_at.nil?
19
+
20
+ expires_at < Time.now
21
+ end
22
+
23
+ def active?
24
+ !expired?
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Data
5
+ module Model
6
+ class RbacRunnerGrant < Sequel::Model
7
+ def validate
8
+ super
9
+ errors.add(:team, 'cannot be empty') if team.nil? || team.empty?
10
+ errors.add(:runner_pattern, 'cannot be empty') if runner_pattern.nil? || runner_pattern.empty?
11
+ errors.add(:actions, 'cannot be empty') if actions.nil? || actions.empty?
12
+ errors.add(:granted_by, 'cannot be empty') if granted_by.nil? || granted_by.empty?
13
+ end
14
+
15
+ def actions_list
16
+ (actions || '').split(',').map(&:strip)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -1,15 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'function'
4
-
5
3
  module Legion
6
4
  module Data
7
5
  module Model
8
6
  class Relationship < Sequel::Model
9
- many_to_one :chain
10
- one_to_many :task
11
- many_to_one :trigger, class: Legion::Data::Model::Function
12
- many_to_one :action, class: Legion::Data::Model::Function
7
+ many_to_one :trigger, class: 'Legion::Data::Model::Function'
8
+ many_to_one :action, class: 'Legion::Data::Model::Function'
9
+ one_to_many :tasks
13
10
  end
14
11
  end
15
12
  end
File without changes
File without changes
File without changes
File without changes
@@ -1,18 +1,52 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Legion
2
4
  module Data
3
5
  module Settings
6
+ CREDS = {
7
+ sqlite: {
8
+ database: 'legionio.db'
9
+ },
10
+ mysql2: {
11
+ username: 'legion',
12
+ password: 'legion',
13
+ database: 'legionio',
14
+ host: '127.0.0.1',
15
+ port: 3306
16
+ },
17
+ postgres: {
18
+ user: 'legion',
19
+ password: 'legion',
20
+ database: 'legionio',
21
+ host: '127.0.0.1',
22
+ port: 5432
23
+ }
24
+ }.freeze
25
+
4
26
  def self.default
5
27
  {
28
+ adapter: 'sqlite',
6
29
  connected: false,
7
30
  cache: cache,
8
31
  connection: connection,
9
32
  creds: creds,
10
33
  migrations: migrations,
11
34
  models: models,
35
+ local: local,
36
+ dev_mode: false,
37
+ dev_fallback: true,
12
38
  connect_on_start: true
13
39
  }
14
40
  end
15
41
 
42
+ def self.local
43
+ {
44
+ enabled: true,
45
+ database: 'legionio_local.db',
46
+ migrations: { auto_migrate: true }
47
+ }
48
+ end
49
+
16
50
  def self.models
17
51
  {
18
52
  continue_on_load_fail: false,
@@ -40,14 +74,9 @@ module Legion
40
74
  }
41
75
  end
42
76
 
43
- def self.creds
44
- {
45
- username: 'legion',
46
- password: 'legion',
47
- database: 'legion',
48
- host: '127.0.0.1',
49
- port: 3306
50
- }
77
+ def self.creds(adapter = nil)
78
+ adapter = (adapter || :sqlite).to_sym
79
+ CREDS.fetch(adapter, CREDS[:sqlite]).dup
51
80
  end
52
81
 
53
82
  def self.cache
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Legion
2
4
  module Data
3
- VERSION = '1.1.5'.freeze
5
+ VERSION = '1.3.7'
4
6
  end
5
7
  end
data/lib/legion/data.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'legion/data/version'
2
4
  require 'legion/data/settings'
3
5
  require 'sequel'
@@ -5,6 +7,7 @@ require 'sequel'
5
7
  require 'legion/data/connection'
6
8
  require 'legion/data/model'
7
9
  require 'legion/data/migration'
10
+ require_relative 'data/local'
8
11
 
9
12
  module Legion
10
13
  module Data
@@ -14,6 +17,7 @@ module Legion
14
17
  migrate
15
18
  load_models
16
19
  setup_cache
20
+ setup_local
17
21
  end
18
22
 
19
23
  def connection_setup
@@ -34,28 +38,42 @@ module Legion
34
38
  Legion::Data::Connection.sequel
35
39
  end
36
40
 
41
+ def local
42
+ Legion::Data::Local
43
+ end
44
+
37
45
  def setup_cache
38
46
  return if Legion::Settings[:data][:cache][:enabled]
39
47
 
40
- return unless defined?(::Legion::Cache)
48
+ nil unless defined?(::Legion::Cache)
41
49
 
42
- Legion::Data::Model::Relationship.plugin :caching, Legion::Cache, ttl: 10
43
- Legion::Data::Model::Runner.plugin :caching, Legion::Cache, ttl: 60
44
- Legion::Data::Model::Chain.plugin :caching, Legion::Cache, ttl: 60
45
- Legion::Data::Model::Datacenter.plugin :caching, Legion::Cache, ttl: 120
46
- Legion::Data::Model::Function.plugin :caching, Legion::Cache, ttl: 120
47
- Legion::Data::Model::Extension.plugin :caching, Legion::Cache, ttl: 120
48
- Legion::Data::Model::Node.plugin :caching, Legion::Cache, ttl: 10
49
- Legion::Data::Model::TaskLog.plugin :caching, Legion::Cache, ttl: 12
50
- Legion::Data::Model::Task.plugin :caching, Legion::Cache, ttl: 10
51
- Legion::Data::Model::User.plugin :caching, Legion::Cache, ttl: 120
52
- Legion::Data::Model::Group.plugin :caching, Legion::Cache, ttl: 120
53
- Legion::Logging.info 'Legion::Data connected to Legion::Cache'
50
+ # Legion::Data::Model::Relationship.plugin :caching, Legion::Cache, ttl: 10
51
+ # Legion::Data::Model::Runner.plugin :caching, Legion::Cache, ttl: 60
52
+ # Legion::Data::Model::Chain.plugin :caching, Legion::Cache, ttl: 60
53
+ # Legion::Data::Model::Function.plugin :caching, Legion::Cache, ttl: 120
54
+ # Legion::Data::Model::Extension.plugin :caching, Legion::Cache, ttl: 120
55
+ # Legion::Data::Model::Node.plugin :caching, Legion::Cache, ttl: 10
56
+ # Legion::Data::Model::TaskLog.plugin :caching, Legion::Cache, ttl: 12
57
+ # Legion::Data::Model::Task.plugin :caching, Legion::Cache, ttl: 10
58
+ # Legion::Data::Model::User.plugin :caching, Legion::Cache, ttl: 120
59
+ # Legion::Data::Model::Group.plugin :caching, Legion::Cache, ttl: 120
60
+ # Legion::Logging.info 'Legion::Data connected to Legion::Cache'
54
61
  end
55
62
 
56
63
  def shutdown
64
+ Legion::Data::Local.shutdown if defined?(Legion::Data::Local) && Legion::Data::Local.connected?
57
65
  Legion::Data::Connection.shutdown
58
66
  end
67
+
68
+ private
69
+
70
+ def setup_local
71
+ return if Legion::Settings[:data].dig(:local, :enabled) == false
72
+
73
+ Legion::Data::Local.setup
74
+ rescue StandardError => e
75
+ Legion::Logging.warn "Legion::Data::Local failed to setup: #{e.message}" if defined?(Legion::Logging)
76
+ end
59
77
  end
60
78
  end
61
79
  end