mxrb 0.1.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 +7 -0
- data/LICENSE +21 -0
- data/README.de-DE.md +108 -0
- data/README.md +172 -0
- data/README.pt-BR.md +109 -0
- data/bin/mxrb +1613 -0
- data/docs/README.md +10 -0
- data/docs/de-DE/README.md +24 -0
- data/docs/de-DE/architectural-patterns.md +26 -0
- data/docs/de-DE/architectural-standard.md +48 -0
- data/docs/de-DE/architecture.md +76 -0
- data/docs/de-DE/compiler.md +162 -0
- data/docs/de-DE/conventions.md +19 -0
- data/docs/de-DE/design-system.md +52 -0
- data/docs/de-DE/entity-dsl.md +53 -0
- data/docs/de-DE/native-runtime-quality-report.md +74 -0
- data/docs/de-DE/oql-sql.md +211 -0
- data/docs/de-DE/platform-api-integration.md +36 -0
- data/docs/de-DE/platform-operations.md +243 -0
- data/docs/de-DE/project-structure.md +32 -0
- data/docs/de-DE/ruby-first-roadmap.md +116 -0
- data/docs/de-DE/scaffolds.md +59 -0
- data/docs/de-DE/semantic-refactoring.md +56 -0
- data/docs/de-DE/team-server.md +33 -0
- data/docs/de-DE/validation-matrix.md +64 -0
- data/docs/de-DE/vetclinic-acceptance.md +24 -0
- data/docs/de-DE/writing.md +138 -0
- data/docs/en-US/README.md +24 -0
- data/docs/en-US/architectural-patterns.md +151 -0
- data/docs/en-US/architectural-standard.md +89 -0
- data/docs/en-US/architecture.md +170 -0
- data/docs/en-US/compiler.md +157 -0
- data/docs/en-US/conventions.md +82 -0
- data/docs/en-US/design-system.md +163 -0
- data/docs/en-US/entity-dsl.md +53 -0
- data/docs/en-US/native-runtime-quality-report.md +73 -0
- data/docs/en-US/oql-sql.md +201 -0
- data/docs/en-US/platform-api-integration.md +36 -0
- data/docs/en-US/platform-operations.md +244 -0
- data/docs/en-US/project-structure.md +49 -0
- data/docs/en-US/ruby-first-roadmap.md +186 -0
- data/docs/en-US/scaffolds.md +57 -0
- data/docs/en-US/semantic-refactoring.md +198 -0
- data/docs/en-US/team-server.md +32 -0
- data/docs/en-US/validation-matrix.md +257 -0
- data/docs/en-US/vetclinic-acceptance.md +23 -0
- data/docs/en-US/writing.md +655 -0
- data/docs/pt-BR/README.md +24 -0
- data/docs/pt-BR/architectural-patterns.md +25 -0
- data/docs/pt-BR/architectural-standard.md +533 -0
- data/docs/pt-BR/architecture.md +300 -0
- data/docs/pt-BR/compiler.md +162 -0
- data/docs/pt-BR/conventions.md +19 -0
- data/docs/pt-BR/design-system.md +65 -0
- data/docs/pt-BR/entity-dsl.md +116 -0
- data/docs/pt-BR/native-runtime-quality-report.md +73 -0
- data/docs/pt-BR/oql-sql.md +208 -0
- data/docs/pt-BR/platform-api-integration.md +36 -0
- data/docs/pt-BR/platform-operations.md +260 -0
- data/docs/pt-BR/project-structure.md +32 -0
- data/docs/pt-BR/ruby-first-roadmap.md +239 -0
- data/docs/pt-BR/scaffolds.md +104 -0
- data/docs/pt-BR/semantic-refactoring.md +55 -0
- data/docs/pt-BR/team-server.md +59 -0
- data/docs/pt-BR/validation-matrix.md +86 -0
- data/docs/pt-BR/vetclinic-acceptance.md +50 -0
- data/docs/pt-BR/writing.md +177 -0
- data/examples/sudoku_evaluation.rb +14 -0
- data/examples/sudoku_functional_test.rb +28 -0
- data/lib/mxrb/architecture/graph.rb +124 -0
- data/lib/mxrb/architecture/validator.rb +98 -0
- data/lib/mxrb/benchmark.rb +33 -0
- data/lib/mxrb/compare.rb +390 -0
- data/lib/mxrb/compiler/adapter.rb +96 -0
- data/lib/mxrb/compiler/artifact_document_compiler.rb +102 -0
- data/lib/mxrb/compiler/artifact_materializer.rb +53 -0
- data/lib/mxrb/compiler/client_model_materializer.rb +42 -0
- data/lib/mxrb/compiler/code_action_document_compiler.rb +64 -0
- data/lib/mxrb/compiler/code_action_materializer.rb +35 -0
- data/lib/mxrb/compiler/code_action_type_compiler.rb +32 -0
- data/lib/mxrb/compiler/combo_box_bundle_compiler.rb +314 -0
- data/lib/mxrb/compiler/compatibility_analyzer.rb +169 -0
- data/lib/mxrb/compiler/constants_materializer.rb +111 -0
- data/lib/mxrb/compiler/data_grid_bundle_compiler.rb +290 -0
- data/lib/mxrb/compiler/database_connector_action_compiler.rb +280 -0
- data/lib/mxrb/compiler/deployment_asset_copier.rb +60 -0
- data/lib/mxrb/compiler/deployment_bootstrapper.rb +208 -0
- data/lib/mxrb/compiler/deployment_materializer.rb +43 -0
- data/lib/mxrb/compiler/deployment_metadata.rb +64 -0
- data/lib/mxrb/compiler/domain_document_compiler.rb +153 -0
- data/lib/mxrb/compiler/domain_model_materializer.rb +35 -0
- data/lib/mxrb/compiler/domain_security_compiler.rb +82 -0
- data/lib/mxrb/compiler/gallery_bundle_compiler.rb +190 -0
- data/lib/mxrb/compiler/generic_widget_bundle_compiler.rb +281 -0
- data/lib/mxrb/compiler/image_bundle_compiler.rb +216 -0
- data/lib/mxrb/compiler/java_proxy_generator.rb +509 -0
- data/lib/mxrb/compiler/legacy_data_grid_compiler.rb +267 -0
- data/lib/mxrb/compiler/legacy_page_builder.rb +287 -0
- data/lib/mxrb/compiler/mda.rb +99 -0
- data/lib/mxrb/compiler/microflow_document_compiler.rb +85 -0
- data/lib/mxrb/compiler/microflow_materializer.rb +48 -0
- data/lib/mxrb/compiler/microflow_node_compiler.rb +250 -0
- data/lib/mxrb/compiler/model_package.rb +116 -0
- data/lib/mxrb/compiler/model_values.rb +67 -0
- data/lib/mxrb/compiler/nanoflow_program_compiler.rb +483 -0
- data/lib/mxrb/compiler/navigation_document_compiler.rb +93 -0
- data/lib/mxrb/compiler/packager.rb +100 -0
- data/lib/mxrb/compiler/page_bundle_builder.rb +56 -0
- data/lib/mxrb/compiler/page_bundle_compiler.rb +2004 -0
- data/lib/mxrb/compiler/page_document_compiler.rb +77 -0
- data/lib/mxrb/compiler/portable_packager.rb +356 -0
- data/lib/mxrb/compiler/project_jar_archive.rb +84 -0
- data/lib/mxrb/compiler/project_jar_builder.rb +121 -0
- data/lib/mxrb/compiler/project_materializer.rb +139 -0
- data/lib/mxrb/compiler/project_model_orderer.rb +52 -0
- data/lib/mxrb/compiler/runtime_data_types.rb +31 -0
- data/lib/mxrb/compiler/runtime_model_schema.rb +90 -0
- data/lib/mxrb/compiler/schemas/runtime-10.24.0.73019.json +1052 -0
- data/lib/mxrb/compiler/schemas/runtime-11.json +1215 -0
- data/lib/mxrb/compiler/schemas/runtime-6.10.8.json +1148 -0
- data/lib/mxrb/compiler/schemas/runtime-7.17.0.json +1144 -0
- data/lib/mxrb/compiler/schemas/runtime-7.5.0.json +1215 -0
- data/lib/mxrb/compiler/schemas/runtime-9.6.1.29396.json +868 -0
- data/lib/mxrb/compiler/schemas/system-model-10.24.0.73019.b64 +1135 -0
- data/lib/mxrb/compiler/schemas/system-model-11.12.1.b64 +815 -0
- data/lib/mxrb/compiler/schemas/system-model-6.10.8.b64 +497 -0
- data/lib/mxrb/compiler/schemas/system-model-7.17.0.b64 +519 -0
- data/lib/mxrb/compiler/schemas/system-model-7.5.0.b64 +504 -0
- data/lib/mxrb/compiler/schemas/system-model-9.6.1.29396.b64 +816 -0
- data/lib/mxrb/compiler/security_materializer.rb +104 -0
- data/lib/mxrb/compiler/settings_document_compiler.rb +45 -0
- data/lib/mxrb/compiler/settings_materializer.rb +25 -0
- data/lib/mxrb/compiler/source_model.rb +128 -0
- data/lib/mxrb/compiler/system_model_seed.rb +88 -0
- data/lib/mxrb/compiler/system_queue_materializer.rb +54 -0
- data/lib/mxrb/compiler/system_text_materializer.rb +49 -0
- data/lib/mxrb/compiler/translation_materializer.rb +95 -0
- data/lib/mxrb/compiler/web_bundle_builder.rb +155 -0
- data/lib/mxrb/compiler/web_list_data_source.rb +139 -0
- data/lib/mxrb/compiler/web_operation_compiler.rb +614 -0
- data/lib/mxrb/compiler/web_shell_materializer.rb +194 -0
- data/lib/mxrb/compiler/widget_package_extractor.rb +63 -0
- data/lib/mxrb/doctor.rb +116 -0
- data/lib/mxrb/dsl/builder.rb +1636 -0
- data/lib/mxrb/errors.rb +18 -0
- data/lib/mxrb/evaluation.rb +131 -0
- data/lib/mxrb/exporter.rb +1716 -0
- data/lib/mxrb/frontend/migrator.rb +775 -0
- data/lib/mxrb/functional.rb +307 -0
- data/lib/mxrb/github/annotator.rb +161 -0
- data/lib/mxrb/initializer.rb +257 -0
- data/lib/mxrb/integrity/validator.rb +136 -0
- data/lib/mxrb/io/bson_codec.rb +145 -0
- data/lib/mxrb/io/mpr_file.rb +558 -0
- data/lib/mxrb/io/mxunit_codec.rb +42 -0
- data/lib/mxrb/marketplace.rb +380 -0
- data/lib/mxrb/model/association.rb +98 -0
- data/lib/mxrb/model/attribute.rb +100 -0
- data/lib/mxrb/model/connector.rb +17 -0
- data/lib/mxrb/model/design_materializer.rb +133 -0
- data/lib/mxrb/model/design_migration.rb +126 -0
- data/lib/mxrb/model/design_system.rb +111 -0
- data/lib/mxrb/model/domain_model.rb +51 -0
- data/lib/mxrb/model/entity.rb +180 -0
- data/lib/mxrb/model/menu.rb +38 -0
- data/lib/mxrb/model/microflow.rb +100 -0
- data/lib/mxrb/model/module.rb +142 -0
- data/lib/mxrb/model/navigation.rb +127 -0
- data/lib/mxrb/model/page.rb +410 -0
- data/lib/mxrb/model/project.rb +187 -0
- data/lib/mxrb/model/unit.rb +48 -0
- data/lib/mxrb/module_initializer.rb +75 -0
- data/lib/mxrb/official_marketplace/content_api.rb +292 -0
- data/lib/mxrb/official_marketplace/dependency_resolver.rb +329 -0
- data/lib/mxrb/official_marketplace/lifecycle.rb +367 -0
- data/lib/mxrb/official_marketplace/module_package_importer.rb +318 -0
- data/lib/mxrb/official_marketplace/widget_package_installer.rb +414 -0
- data/lib/mxrb/official_marketplace.rb +720 -0
- data/lib/mxrb/oql/analyzer.rb +155 -0
- data/lib/mxrb/oql/index_advisor.rb +85 -0
- data/lib/mxrb/oql/plan_analyzer.rb +185 -0
- data/lib/mxrb/oql/server.rb +132 -0
- data/lib/mxrb/oql/sql_server_plan_analyzer.rb +190 -0
- data/lib/mxrb/oql/sql_server_workload_analyzer.rb +80 -0
- data/lib/mxrb/oql/workload_analyzer.rb +175 -0
- data/lib/mxrb/oql/workload_baseline.rb +60 -0
- data/lib/mxrb/oql.rb +424 -0
- data/lib/mxrb/project_lifecycle.rb +76 -0
- data/lib/mxrb/protocols/plan.rb +86 -0
- data/lib/mxrb/protocols.rb +123 -0
- data/lib/mxrb/runtime/database_workspace.rb +677 -0
- data/lib/mxrb/runtime/docker_executor.rb +41 -0
- data/lib/mxrb/runtime/docker_workspace.rb +29 -0
- data/lib/mxrb/runtime/executor.rb +169 -0
- data/lib/mxrb/runtime/java_locator.rb +26 -0
- data/lib/mxrb/runtime/native.rb +474 -0
- data/lib/mxrb/runtime/sql_server_database.rb +146 -0
- data/lib/mxrb/runtime/toolchain.rb +82 -0
- data/lib/mxrb/scaffold/cli.rb +113 -0
- data/lib/mxrb/scaffold/generator.rb +246 -0
- data/lib/mxrb/scaffold/help.rb +80 -0
- data/lib/mxrb/scaffold/page_templates.rb +54 -0
- data/lib/mxrb/scaffold/recipes.rb +198 -0
- data/lib/mxrb/scaffold/registry.rb +82 -0
- data/lib/mxrb/scaffold/templates.rb +723 -0
- data/lib/mxrb/scaffold/transaction.rb +84 -0
- data/lib/mxrb/schema/tables.rb +55 -0
- data/lib/mxrb/semantic/analyzer.rb +473 -0
- data/lib/mxrb/semantic/batch_plan.rb +68 -0
- data/lib/mxrb/semantic/domain_mutator.rb +419 -0
- data/lib/mxrb/semantic/embedder.rb +51 -0
- data/lib/mxrb/semantic/extractor.rb +290 -0
- data/lib/mxrb/semantic/index.rb +623 -0
- data/lib/mxrb/semantic/inliner.rb +216 -0
- data/lib/mxrb/semantic/mover.rb +153 -0
- data/lib/mxrb/semantic/onnx_embedder.rb +34 -0
- data/lib/mxrb/semantic/remover.rb +70 -0
- data/lib/mxrb/semantic/renamer.rb +211 -0
- data/lib/mxrb/semantic/tfidf_embedder.rb +46 -0
- data/lib/mxrb/semantic/vec_store.rb +75 -0
- data/lib/mxrb/team_server.rb +390 -0
- data/lib/mxrb/templates/project/10.24.0.73019.json +32 -0
- data/lib/mxrb/templates/project/11.12.1.json +32 -0
- data/lib/mxrb/templates/project/6.10.8.json +50 -0
- data/lib/mxrb/templates/project/7.17.0.json +50 -0
- data/lib/mxrb/templates/project/7.5.0.json +50 -0
- data/lib/mxrb/templates/project/9.6.1.29396.json +50 -0
- data/lib/mxrb/version.rb +5 -0
- data/lib/mxrb/widget_package.rb +338 -0
- data/lib/mxrb/widget_synchronizer.rb +31 -0
- data/lib/mxrb/writer.rb +3775 -0
- data/lib/mxrb.rb +188 -0
- data/marketplace/catalog.json +10 -0
- data/marketplace/modules/shared-kernel/domain/README.md +4 -0
- data/marketplace/modules/shared-kernel/module.rb +5 -0
- data/marketplace/modules/shared-kernel/mxrb-module.json +9 -0
- metadata +447 -0
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'csv'
|
|
5
|
+
require 'fileutils'
|
|
6
|
+
require 'json'
|
|
7
|
+
require 'open3'
|
|
8
|
+
require 'securerandom'
|
|
9
|
+
|
|
10
|
+
module Mxrb
|
|
11
|
+
module Runtime
|
|
12
|
+
DatabaseInfo = Data.define(
|
|
13
|
+
:project_id, :state_dir, :database_container, :runtime_container,
|
|
14
|
+
:host, :port, :database, :reader_user, :running, :model_fingerprint,
|
|
15
|
+
:runtime_url
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# Materializes a private PostgreSQL database for one MPR. The Mendix Runtime
|
|
19
|
+
# owns schema synchronization; analyst access uses a separate read-only role.
|
|
20
|
+
# rubocop:disable Metrics/ClassLength
|
|
21
|
+
class DatabaseWorkspace
|
|
22
|
+
POSTGRES_IMAGE = 'postgres:13-alpine'
|
|
23
|
+
DATABASE = 'mxrb'
|
|
24
|
+
OWNER = 'mxrb_runtime'
|
|
25
|
+
READER = 'mxrb_reader'
|
|
26
|
+
NATIVE_INPUT_DIRECTORIES = %w[
|
|
27
|
+
mprcontents deployment javasource javascriptsource userlib vendorlib
|
|
28
|
+
resources widgets theme theme-cache themesource
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
31
|
+
def initialize(project_path, state_dir: nil, port: 55_432, runtime_port: 18_080, # rubocop:disable Metrics/AbcSize, Metrics/ParameterLists
|
|
32
|
+
runner: nil, sleeper: nil)
|
|
33
|
+
@project_path = File.expand_path(project_path)
|
|
34
|
+
@project_id = Digest::SHA256.hexdigest(@project_path)[0, 12]
|
|
35
|
+
@state_dir = File.expand_path(state_dir || default_state_dir)
|
|
36
|
+
@port = Integer(port)
|
|
37
|
+
@runtime_port = Integer(runtime_port)
|
|
38
|
+
raise ArgumentError, 'database port must be between 1 and 65535' unless (1..65_535).cover?(@port)
|
|
39
|
+
raise ArgumentError, 'runtime port must be between 1 and 65535' unless (1..65_535).cover?(@runtime_port)
|
|
40
|
+
|
|
41
|
+
@runner = runner || method(:capture)
|
|
42
|
+
@sleeper = sleeper || Kernel.method(:sleep)
|
|
43
|
+
@plan = Toolchain.new(@project_path).plan
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def up(force_build: false)
|
|
47
|
+
validate!
|
|
48
|
+
prepare_state
|
|
49
|
+
refresh_runtime_package(force_build)
|
|
50
|
+
prepare_database!
|
|
51
|
+
configure_reader!
|
|
52
|
+
restart_runtime!
|
|
53
|
+
wait_for_runtime!
|
|
54
|
+
configure_reader!
|
|
55
|
+
info(running: true)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def sync = up(force_build: true)
|
|
59
|
+
|
|
60
|
+
def down
|
|
61
|
+
stop_container(runtime_container)
|
|
62
|
+
stop_container(database_container)
|
|
63
|
+
info(running: false)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def destroy(confirm: false)
|
|
67
|
+
raise ArgumentError, 'destroy requires explicit confirmation' unless confirm
|
|
68
|
+
|
|
69
|
+
remove_container(runtime_container)
|
|
70
|
+
remove_container(database_container)
|
|
71
|
+
remove_owned_resource('volume', database_volume)
|
|
72
|
+
remove_owned_resource('network', network_name)
|
|
73
|
+
FileUtils.rm_rf(@state_dir)
|
|
74
|
+
info(running: false)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def status
|
|
78
|
+
info(running: container_running?(database_container) && container_running?(runtime_container))
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Keeping the transaction envelope next to role selection makes the
|
|
82
|
+
# read-only default auditable.
|
|
83
|
+
# rubocop:disable Metrics/MethodLength
|
|
84
|
+
def query(sql, write: false)
|
|
85
|
+
raise ArgumentError, 'SQL must not be empty' if sql.to_s.strip.empty?
|
|
86
|
+
raise ArgumentError, 'SQL contains a NUL byte' if sql.include?("\0")
|
|
87
|
+
|
|
88
|
+
configure_reader! unless write
|
|
89
|
+
role = write ? OWNER : READER
|
|
90
|
+
command = [
|
|
91
|
+
'docker', 'exec', database_container,
|
|
92
|
+
'psql', '--no-psqlrc', '--set', 'ON_ERROR_STOP=1',
|
|
93
|
+
'--username', role, '--dbname', DATABASE
|
|
94
|
+
]
|
|
95
|
+
command.concat(['--command', 'BEGIN READ ONLY']) unless write
|
|
96
|
+
command.concat(['--command', sql])
|
|
97
|
+
command.concat(['--command', 'COMMIT']) unless write
|
|
98
|
+
run!(*command)
|
|
99
|
+
end
|
|
100
|
+
# rubocop:enable Metrics/MethodLength
|
|
101
|
+
|
|
102
|
+
def query_rows(sql, params: {})
|
|
103
|
+
statement = read_only_statement(sql)
|
|
104
|
+
statement, variables = bind_parameters(statement, params)
|
|
105
|
+
|
|
106
|
+
configure_reader!
|
|
107
|
+
output = run!(*query_rows_command(statement, variables))
|
|
108
|
+
CSV.parse(output, headers: true).map(&:to_h).freeze
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Uses PostgreSQL's machine-readable planner output. ANALYZE is opt-in
|
|
112
|
+
# because it executes the SELECT, although the analyst role remains in a
|
|
113
|
+
# read-only transaction and cannot mutate application data.
|
|
114
|
+
def explain(sql, analyze: false)
|
|
115
|
+
statement = read_only_statement(sql)
|
|
116
|
+
configure_reader!
|
|
117
|
+
payload = JSON.parse(run_explain(statement, analyze).strip)
|
|
118
|
+
Oql::PlanAnalyzer.new(indexes: index_catalog).analyze(payload, analyzed: analyze)
|
|
119
|
+
rescue JSON::ParserError => e
|
|
120
|
+
raise ToolchainError, "PostgreSQL returned invalid EXPLAIN JSON: #{e.message}"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def workload(limit: 20)
|
|
124
|
+
maximum = Integer(limit)
|
|
125
|
+
raise ArgumentError, 'workload limit must be between 1 and 1000' unless (1..1000).cover?(maximum)
|
|
126
|
+
|
|
127
|
+
Oql::WorkloadAnalyzer.new.analyze(
|
|
128
|
+
query_rows: workload_queries(maximum),
|
|
129
|
+
table_rows: workload_tables,
|
|
130
|
+
index_rows: workload_indexes
|
|
131
|
+
)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def index_advice(limit: 50)
|
|
135
|
+
Oql::IndexAdvisor.new.analyze(workload(limit:))
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def shell_command(write: false)
|
|
139
|
+
role = write ? OWNER : READER
|
|
140
|
+
[
|
|
141
|
+
'docker', 'exec', '-it', database_container,
|
|
142
|
+
'psql', '--no-psqlrc', '--username', role, '--dbname', DATABASE
|
|
143
|
+
].freeze
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def connection_url
|
|
147
|
+
secret = credentials.fetch('reader_password')
|
|
148
|
+
"postgresql://#{READER}:#{secret}@127.0.0.1:#{@port}/#{DATABASE}"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
private
|
|
152
|
+
|
|
153
|
+
def bind_parameters(statement, params)
|
|
154
|
+
values = params.to_h.transform_keys(&:to_s)
|
|
155
|
+
return [statement, []] if values.empty?
|
|
156
|
+
|
|
157
|
+
validate_parameter_names!(values, statement)
|
|
158
|
+
bound = statement.gsub(/(?<!:):([A-Za-z][A-Za-z0-9_]*)/) do
|
|
159
|
+
name = Regexp.last_match(1)
|
|
160
|
+
values[name].nil? ? 'NULL' : ":'mxrb_#{name}'"
|
|
161
|
+
end
|
|
162
|
+
[bound, parameter_arguments(values)]
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def validate_parameter_names!(values, statement)
|
|
166
|
+
invalid = values.keys.reject { _1.match?(/\A[A-Za-z][A-Za-z0-9_]*\z/) }
|
|
167
|
+
expected = statement.scan(/(?<!:):([A-Za-z][A-Za-z0-9_]*)/).flatten.uniq
|
|
168
|
+
parameter_error!('invalid parameter names', invalid)
|
|
169
|
+
parameter_error!('missing query parameter', expected - values.keys)
|
|
170
|
+
parameter_error!('unused query parameters', values.keys - expected)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def parameter_error!(label, names)
|
|
174
|
+
raise ArgumentError, "#{label}: #{names.join(', ')}" unless names.empty?
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def parameter_arguments(values)
|
|
178
|
+
values.reject { |_name, value| value.nil? }.flat_map do |name, value|
|
|
179
|
+
raise ArgumentError, "unsupported parameter value for #{name}" unless parameter_scalar?(value)
|
|
180
|
+
|
|
181
|
+
['--set', "mxrb_#{name}=#{value}"]
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def parameter_scalar?(value)
|
|
186
|
+
value.is_a?(String) || value.is_a?(Numeric) || value == true || value == false
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def query_rows_command(statement, variables)
|
|
190
|
+
[
|
|
191
|
+
'docker', 'exec', database_container,
|
|
192
|
+
'psql', '--no-psqlrc', '--set', 'ON_ERROR_STOP=1', *variables,
|
|
193
|
+
'--username', READER, '--dbname', DATABASE,
|
|
194
|
+
'--csv', '--quiet', '--command', "COPY (#{statement}) TO STDOUT WITH CSV HEADER"
|
|
195
|
+
]
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def prepare_database!
|
|
199
|
+
ensure_network!
|
|
200
|
+
ensure_database!
|
|
201
|
+
configure_monitoring!
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def run_explain(statement, analyze)
|
|
205
|
+
run!(
|
|
206
|
+
'docker', 'exec', database_container,
|
|
207
|
+
'psql', '--no-psqlrc', '--set', 'ON_ERROR_STOP=1',
|
|
208
|
+
'--username', READER, '--dbname', DATABASE, '--tuples-only', '--no-align', '--quiet',
|
|
209
|
+
'--command', explain_sql(statement, analyze)
|
|
210
|
+
)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def read_only_statement(sql)
|
|
214
|
+
source = sql.to_s
|
|
215
|
+
raise ArgumentError, 'SQL contains a NUL byte' if source.include?("\0")
|
|
216
|
+
|
|
217
|
+
statement = source.strip
|
|
218
|
+
raise ArgumentError, 'SQL must not be empty' if statement.empty?
|
|
219
|
+
unless statement.match?(/\A(?:SELECT|WITH)\b/i) && !statement.include?(';')
|
|
220
|
+
raise ArgumentError, 'online queries must be one read-only SELECT or WITH statement'
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
statement
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def explain_sql(statement, analyze)
|
|
227
|
+
options = ['FORMAT JSON', 'COSTS TRUE', 'VERBOSE TRUE', 'SETTINGS TRUE']
|
|
228
|
+
options.concat(['ANALYZE TRUE', 'BUFFERS TRUE', 'TIMING TRUE']) if analyze
|
|
229
|
+
"EXPLAIN (#{options.join(', ')}) #{statement}"
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def index_catalog
|
|
233
|
+
query_rows(<<~SQL)
|
|
234
|
+
SELECT schemaname, tablename, indexname, indexdef
|
|
235
|
+
FROM pg_indexes
|
|
236
|
+
WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
|
|
237
|
+
ORDER BY schemaname, tablename, indexname
|
|
238
|
+
SQL
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def refresh_runtime_package(force)
|
|
242
|
+
return unless force || stale_runtime?
|
|
243
|
+
|
|
244
|
+
remove_container(runtime_container)
|
|
245
|
+
build_runtime!
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def validate!
|
|
249
|
+
raise ArgumentError, "#{@project_path}: file not found" unless File.file?(@project_path)
|
|
250
|
+
raise ToolchainError, "Mendix Runtime #{@plan.runtime_path} is unavailable" unless @plan.available?
|
|
251
|
+
|
|
252
|
+
run!('docker', 'version', '--format', '{{.Server.Version}}')
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def prepare_state
|
|
256
|
+
FileUtils.mkdir_p(@state_dir)
|
|
257
|
+
File.chmod(0o700, @state_dir)
|
|
258
|
+
credentials
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def credentials
|
|
262
|
+
@credentials ||= if File.file?(credentials_path)
|
|
263
|
+
JSON.parse(File.read(credentials_path))
|
|
264
|
+
else
|
|
265
|
+
create_credentials
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def create_credentials
|
|
270
|
+
values = {
|
|
271
|
+
'owner_password' => SecureRandom.hex(24),
|
|
272
|
+
'reader_password' => SecureRandom.hex(24),
|
|
273
|
+
'admin_password' => SecureRandom.hex(24)
|
|
274
|
+
}
|
|
275
|
+
FileUtils.mkdir_p(@state_dir)
|
|
276
|
+
File.write(credentials_path, JSON.pretty_generate(values))
|
|
277
|
+
File.chmod(0o600, credentials_path)
|
|
278
|
+
values
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def stale_runtime?
|
|
282
|
+
!File.file?(runtime_marker) ||
|
|
283
|
+
JSON.parse(File.read(runtime_marker)).fetch('fingerprint') != model_fingerprint
|
|
284
|
+
rescue JSON::ParserError, KeyError
|
|
285
|
+
true
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def build_runtime!
|
|
289
|
+
FileUtils.rm_rf(runtime_dir)
|
|
290
|
+
FileUtils.mkdir_p(build_dir)
|
|
291
|
+
package = File.join(build_dir, 'runtime.zip')
|
|
292
|
+
FileUtils.rm_f(package)
|
|
293
|
+
build_native_package(package)
|
|
294
|
+
FileUtils.mkdir_p(runtime_dir)
|
|
295
|
+
# The package is always authoritative for this generated directory.
|
|
296
|
+
# Some unzip builds can still observe files created by a just-removed
|
|
297
|
+
# bind-mounted Runtime and prompt on stdin unless overwrite is explicit.
|
|
298
|
+
run!('unzip', '-oq', package, '-d', runtime_dir)
|
|
299
|
+
File.write(runtime_marker, JSON.generate('fingerprint' => model_fingerprint))
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def build_native_package(output)
|
|
303
|
+
Dir.mktmpdir('mxrb-db-build-', build_dir) do |root|
|
|
304
|
+
project = copy_native_input(root)
|
|
305
|
+
deployment = File.join(File.dirname(project), 'deployment')
|
|
306
|
+
compile_native(project, deployment, output)
|
|
307
|
+
end
|
|
308
|
+
rescue CompilationError => e
|
|
309
|
+
raise ToolchainError, "MXRB native database build failed: #{e.message}"
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def compile_native(project, deployment, output)
|
|
313
|
+
Compiler::DeploymentMaterializer.new(
|
|
314
|
+
project, deployment:, mendix_home: @plan.toolchain_path
|
|
315
|
+
).materialize
|
|
316
|
+
Compiler::ProjectJarBuilder.new(
|
|
317
|
+
project, deployment:, mendix_home: @plan.toolchain_path
|
|
318
|
+
).build
|
|
319
|
+
compile_web(project, deployment)
|
|
320
|
+
Compiler::PortablePackager.new(
|
|
321
|
+
project, deployment:, mendix_home: @plan.toolchain_path
|
|
322
|
+
).pack(output:, force: true)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def compile_web(project, deployment)
|
|
326
|
+
Compiler::WebBundleBuilder.new(
|
|
327
|
+
project, deployment:, mendix_home: @plan.toolchain_path
|
|
328
|
+
).build
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def copy_native_input(root)
|
|
332
|
+
destination = File.join(root, 'project')
|
|
333
|
+
FileUtils.mkdir_p(destination)
|
|
334
|
+
FileUtils.cp(@project_path, destination)
|
|
335
|
+
project_root = File.dirname(@project_path)
|
|
336
|
+
copy_native_directories(project_root, destination)
|
|
337
|
+
File.join(destination, File.basename(@project_path))
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def copy_native_directories(project_root, destination)
|
|
341
|
+
NATIVE_INPUT_DIRECTORIES.each do |name|
|
|
342
|
+
source = File.join(project_root, name)
|
|
343
|
+
next unless File.directory?(source)
|
|
344
|
+
if Dir.glob(File.join(source, '**', '*'), File::FNM_DOTMATCH).any? { File.symlink?(_1) }
|
|
345
|
+
raise ToolchainError, "native database input contains a symlink: #{source}"
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
FileUtils.cp_r(source, destination)
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def ensure_network!
|
|
353
|
+
if successful?('docker', 'network', 'inspect', network_name)
|
|
354
|
+
assert_owned_resource!('network', network_name)
|
|
355
|
+
return
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
run!('docker', 'network', 'create', '--label', ownership_label, network_name)
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def ensure_database!
|
|
362
|
+
if container_exists?(database_container)
|
|
363
|
+
assert_owned_container!(database_container)
|
|
364
|
+
run!('docker', 'start', database_container) unless container_running?(database_container)
|
|
365
|
+
else
|
|
366
|
+
create_database_container!
|
|
367
|
+
end
|
|
368
|
+
wait_for_database!
|
|
369
|
+
ensure_application_database!
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def ensure_application_database!
|
|
373
|
+
exists = run!(
|
|
374
|
+
'docker', 'exec', database_container, 'psql', '--no-psqlrc', '--tuples-only',
|
|
375
|
+
'--no-align', '--username', OWNER, '--dbname', 'postgres', '--command',
|
|
376
|
+
"SELECT 1 FROM pg_database WHERE datname = '#{DATABASE}'"
|
|
377
|
+
).strip == '1'
|
|
378
|
+
return if exists
|
|
379
|
+
|
|
380
|
+
create_application_database!
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def create_application_database!
|
|
384
|
+
command = ['docker', 'exec', database_container, 'createdb', '--username', OWNER, DATABASE]
|
|
385
|
+
output, status = run_command(*command)
|
|
386
|
+
return if status.success? || output.include?('already exists')
|
|
387
|
+
|
|
388
|
+
raise ToolchainError, "docker failed:\n#{output}"
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def create_database_container! # rubocop:disable Metrics/MethodLength
|
|
392
|
+
ensure_database_volume!
|
|
393
|
+
run!(
|
|
394
|
+
'docker', 'run', '-d', '--name', database_container,
|
|
395
|
+
'--label', ownership_label, '--network', network_name,
|
|
396
|
+
'--mount', "type=volume,source=#{database_volume},target=/var/lib/postgresql/data",
|
|
397
|
+
'-p', "127.0.0.1:#{@port}:5432",
|
|
398
|
+
'-e', "POSTGRES_DB=#{DATABASE}", '-e', "POSTGRES_USER=#{OWNER}",
|
|
399
|
+
'-e', "POSTGRES_PASSWORD=#{credentials.fetch('owner_password')}",
|
|
400
|
+
POSTGRES_IMAGE, 'postgres',
|
|
401
|
+
'-c', 'shared_preload_libraries=pg_stat_statements',
|
|
402
|
+
'-c', 'track_io_timing=on'
|
|
403
|
+
)
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def ensure_database_volume!
|
|
407
|
+
if successful?('docker', 'volume', 'inspect', database_volume)
|
|
408
|
+
assert_owned_resource!('volume', database_volume)
|
|
409
|
+
else
|
|
410
|
+
run!('docker', 'volume', 'create', '--label', ownership_label, database_volume)
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def wait_for_database! # rubocop:disable Metrics/MethodLength
|
|
415
|
+
consecutive_ready = 0
|
|
416
|
+
30.times do
|
|
417
|
+
ready = successful?(
|
|
418
|
+
'docker', 'exec', database_container,
|
|
419
|
+
'pg_isready', '--username', OWNER, '--dbname', DATABASE
|
|
420
|
+
)
|
|
421
|
+
consecutive_ready = ready ? consecutive_ready + 1 : 0
|
|
422
|
+
return if consecutive_ready >= 2
|
|
423
|
+
|
|
424
|
+
@sleeper.call(1)
|
|
425
|
+
end
|
|
426
|
+
raise ToolchainError, 'PostgreSQL did not become ready'
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def configure_monitoring!
|
|
430
|
+
ensure_statistics_preload!
|
|
431
|
+
ensure_io_timing!
|
|
432
|
+
database_owner_sql('CREATE EXTENSION IF NOT EXISTS pg_stat_statements')
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
def ensure_statistics_preload!
|
|
436
|
+
libraries = database_scalar('SHOW shared_preload_libraries').split(',').map(&:strip)
|
|
437
|
+
return if libraries.include?('pg_stat_statements')
|
|
438
|
+
|
|
439
|
+
value = (libraries.reject(&:empty?) + ['pg_stat_statements']).join(',')
|
|
440
|
+
database_owner_sql("ALTER SYSTEM SET shared_preload_libraries = '#{value}'")
|
|
441
|
+
run!('docker', 'restart', database_container)
|
|
442
|
+
wait_for_database!
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def ensure_io_timing!
|
|
446
|
+
return if database_scalar('SHOW track_io_timing') == 'on'
|
|
447
|
+
|
|
448
|
+
database_owner_sql('ALTER SYSTEM SET track_io_timing = on')
|
|
449
|
+
database_owner_sql('SELECT pg_reload_conf()')
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
def database_scalar(sql)
|
|
453
|
+
run!(
|
|
454
|
+
'docker', 'exec', database_container, 'psql', '--no-psqlrc', '--tuples-only',
|
|
455
|
+
'--no-align', '--username', OWNER, '--dbname', DATABASE, '--command', sql
|
|
456
|
+
).strip
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
def database_owner_sql(sql)
|
|
460
|
+
run!(
|
|
461
|
+
'docker', 'exec', database_container, 'psql', '--no-psqlrc',
|
|
462
|
+
'--set', 'ON_ERROR_STOP=1', '--username', OWNER, '--dbname', DATABASE,
|
|
463
|
+
'--command', sql
|
|
464
|
+
)
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
def workload_queries(limit) # rubocop:disable Metrics/MethodLength
|
|
468
|
+
query_rows(<<~SQL)
|
|
469
|
+
SELECT queryid::text, calls, total_exec_time, mean_exec_time, rows,
|
|
470
|
+
shared_blks_hit, shared_blks_read, temp_blks_written,
|
|
471
|
+
blk_read_time, blk_write_time, query
|
|
472
|
+
FROM pg_stat_statements
|
|
473
|
+
WHERE dbid = (SELECT oid FROM pg_database WHERE datname = current_database())
|
|
474
|
+
AND query NOT ILIKE '%pg_stat_statements%'
|
|
475
|
+
AND query ~* '^[[:space:]]*(SELECT|WITH)([[:space:]]|$)'
|
|
476
|
+
ORDER BY total_exec_time DESC
|
|
477
|
+
LIMIT #{limit}
|
|
478
|
+
SQL
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
def workload_tables
|
|
482
|
+
query_rows(<<~SQL)
|
|
483
|
+
SELECT schemaname, relname, seq_scan, seq_tup_read, idx_scan, n_live_tup,
|
|
484
|
+
last_analyze, last_autoanalyze
|
|
485
|
+
FROM pg_stat_user_tables
|
|
486
|
+
ORDER BY seq_tup_read DESC
|
|
487
|
+
SQL
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
def workload_indexes
|
|
491
|
+
query_rows(<<~SQL)
|
|
492
|
+
SELECT s.schemaname, s.relname, s.indexrelname, s.idx_scan,
|
|
493
|
+
s.idx_tup_read, s.idx_tup_fetch, pg_relation_size(s.indexrelid) AS index_bytes,
|
|
494
|
+
i.indisunique, i.indisprimary, pg_get_indexdef(s.indexrelid) AS indexdef
|
|
495
|
+
FROM pg_stat_user_indexes s
|
|
496
|
+
JOIN pg_index i ON i.indexrelid = s.indexrelid
|
|
497
|
+
ORDER BY index_bytes DESC
|
|
498
|
+
SQL
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
def wait_for_runtime!
|
|
502
|
+
120.times do
|
|
503
|
+
output, status = run_command('docker', 'logs', runtime_container)
|
|
504
|
+
return if status.success? && output.include?('Mendix Runtime successfully started')
|
|
505
|
+
raise ToolchainError, "Mendix Runtime stopped during synchronization:\n#{output}" \
|
|
506
|
+
unless container_running?(runtime_container)
|
|
507
|
+
|
|
508
|
+
@sleeper.call(1)
|
|
509
|
+
end
|
|
510
|
+
raise ToolchainError, 'Mendix Runtime schema synchronization timed out'
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
# The grants form one atomic security policy and intentionally stay together.
|
|
514
|
+
# rubocop:disable Metrics/MethodLength
|
|
515
|
+
def configure_reader!
|
|
516
|
+
return if @reader_configured
|
|
517
|
+
|
|
518
|
+
password = credentials.fetch('reader_password')
|
|
519
|
+
sql = <<~SQL
|
|
520
|
+
BEGIN;
|
|
521
|
+
SELECT pg_advisory_xact_lock(hashtext('mxrb.configure_reader'));
|
|
522
|
+
DO $mxrb$ BEGIN
|
|
523
|
+
CREATE ROLE #{READER} LOGIN PASSWORD '#{password}';
|
|
524
|
+
EXCEPTION WHEN duplicate_object THEN
|
|
525
|
+
ALTER ROLE #{READER} LOGIN PASSWORD '#{password}';
|
|
526
|
+
END $mxrb$;
|
|
527
|
+
ALTER ROLE #{READER} SET default_transaction_read_only = on;
|
|
528
|
+
GRANT CONNECT ON DATABASE #{DATABASE} TO #{READER};
|
|
529
|
+
GRANT USAGE ON SCHEMA public TO #{READER};
|
|
530
|
+
GRANT SELECT ON ALL TABLES IN SCHEMA public TO #{READER};
|
|
531
|
+
GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO #{READER};
|
|
532
|
+
GRANT pg_read_all_stats TO #{READER};
|
|
533
|
+
ALTER DEFAULT PRIVILEGES FOR ROLE #{OWNER} IN SCHEMA public
|
|
534
|
+
GRANT SELECT ON TABLES TO #{READER};
|
|
535
|
+
ALTER DEFAULT PRIVILEGES FOR ROLE #{OWNER} IN SCHEMA public
|
|
536
|
+
GRANT SELECT ON SEQUENCES TO #{READER};
|
|
537
|
+
COMMIT;
|
|
538
|
+
SQL
|
|
539
|
+
run!(
|
|
540
|
+
'docker', 'exec', database_container,
|
|
541
|
+
'psql', '--no-psqlrc', '--set', 'ON_ERROR_STOP=1',
|
|
542
|
+
'--username', OWNER, '--dbname', DATABASE, '--command', sql
|
|
543
|
+
)
|
|
544
|
+
@reader_configured = true
|
|
545
|
+
end
|
|
546
|
+
# rubocop:enable Metrics/MethodLength
|
|
547
|
+
|
|
548
|
+
# Runtime database settings stay adjacent so no credential or safety
|
|
549
|
+
# override can be omitted from one execution path.
|
|
550
|
+
# rubocop:disable Metrics/MethodLength
|
|
551
|
+
def restart_runtime!
|
|
552
|
+
remove_container(runtime_container)
|
|
553
|
+
secret = credentials
|
|
554
|
+
run!(
|
|
555
|
+
'docker', 'run', '-d', '--name', runtime_container,
|
|
556
|
+
'--label', ownership_label, '--network', network_name,
|
|
557
|
+
'--publish', "127.0.0.1:#{@runtime_port}:8080",
|
|
558
|
+
'--mount', bind_mount(runtime_dir, '/mendix', readonly: false),
|
|
559
|
+
'-e', 'HOME=/tmp', '-e', "M2EE_ADMIN_PASS=#{secret.fetch('admin_password')}",
|
|
560
|
+
'-e', "RUNTIME_ADMINUSER_PASSWORD=#{secret.fetch('admin_password')}",
|
|
561
|
+
'-e', 'RUNTIME_PARAMS_DATABASETYPE=POSTGRESQL',
|
|
562
|
+
'-e', "RUNTIME_PARAMS_DATABASEHOST=#{database_container}:5432",
|
|
563
|
+
'-e', "RUNTIME_PARAMS_DATABASEJDBCURL=jdbc:postgresql://#{database_container}:5432/#{DATABASE}",
|
|
564
|
+
'-e', "RUNTIME_PARAMS_DATABASENAME=#{DATABASE}",
|
|
565
|
+
'-e', "RUNTIME_PARAMS_DATABASEUSERNAME=#{OWNER}",
|
|
566
|
+
'-e', "RUNTIME_PARAMS_DATABASEPASSWORD=#{secret.fetch('owner_password')}",
|
|
567
|
+
'-e', 'RUNTIME_PARAMS_DATABASEUSESSL=false',
|
|
568
|
+
'-w', '/mendix', @plan.runtime_image, './bin/start', 'etc/Default'
|
|
569
|
+
)
|
|
570
|
+
end
|
|
571
|
+
# rubocop:enable Metrics/MethodLength
|
|
572
|
+
|
|
573
|
+
def stop_container(name)
|
|
574
|
+
return unless container_running?(name)
|
|
575
|
+
|
|
576
|
+
assert_owned_container!(name)
|
|
577
|
+
run!('docker', 'stop', name)
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
def remove_container(name)
|
|
581
|
+
return unless container_exists?(name)
|
|
582
|
+
|
|
583
|
+
assert_owned_container!(name)
|
|
584
|
+
run!('docker', 'rm', '-f', name)
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
def container_exists?(name)
|
|
588
|
+
successful?('docker', 'container', 'inspect', name)
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
def container_running?(name)
|
|
592
|
+
output, = run_command('docker', 'container', 'inspect', '--format', '{{.State.Running}}', name)
|
|
593
|
+
output.strip == 'true'
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
def assert_owned_container!(name)
|
|
597
|
+
assert_owned_resource!('container', name, '.Config.Labels')
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
def assert_owned_resource!(kind, name, labels_path = '.Labels')
|
|
601
|
+
output = run!(
|
|
602
|
+
'docker', kind, 'inspect', '--format',
|
|
603
|
+
"{{ index #{labels_path} \"mxrb.project\" }}", name
|
|
604
|
+
)
|
|
605
|
+
return if output.strip == @project_id
|
|
606
|
+
|
|
607
|
+
raise ToolchainError, "refusing to modify unowned Docker #{kind} #{name}"
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
def remove_owned_resource(kind, name)
|
|
611
|
+
return unless successful?('docker', kind, 'inspect', name)
|
|
612
|
+
|
|
613
|
+
assert_owned_resource!(kind, name)
|
|
614
|
+
run!('docker', kind, 'rm', name)
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
def successful?(*command)
|
|
618
|
+
_output, status = run_command(*command)
|
|
619
|
+
status.success?
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
def run!(*command)
|
|
623
|
+
output, status = run_command(*command)
|
|
624
|
+
raise ToolchainError, "#{command.first} failed:\n#{output}" unless status.success?
|
|
625
|
+
|
|
626
|
+
output
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
def run_command(*command)
|
|
630
|
+
result = @runner.call(*command)
|
|
631
|
+
return [result[0].to_s + result[1].to_s, result[2]] if result.length == 3
|
|
632
|
+
|
|
633
|
+
[result[0].to_s, result[1]]
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
def capture(*command)
|
|
637
|
+
Open3.capture3(*command)
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
def info(running:)
|
|
641
|
+
DatabaseInfo.new(
|
|
642
|
+
@project_id, @state_dir, database_container, runtime_container,
|
|
643
|
+
'127.0.0.1', @port, DATABASE, READER, running, model_fingerprint,
|
|
644
|
+
"http://127.0.0.1:#{@runtime_port}/"
|
|
645
|
+
)
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
def model_fingerprint
|
|
649
|
+
@model_fingerprint ||= Mxrb.open(@project_path) do |project|
|
|
650
|
+
Semantic::Index.fingerprint(project)
|
|
651
|
+
end
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
def bind_mount(source, target, readonly:)
|
|
655
|
+
values = ['type=bind', "source=#{source}", "target=#{target}"]
|
|
656
|
+
values << 'readonly' if readonly
|
|
657
|
+
values.join(',')
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
def default_state_dir
|
|
661
|
+
root = ENV['XDG_STATE_HOME'] || File.join(Dir.home, '.local', 'state')
|
|
662
|
+
File.join(root, 'mxrb', 'databases', @project_id)
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
def credentials_path = File.join(@state_dir, 'credentials.json')
|
|
666
|
+
def runtime_marker = File.join(@state_dir, 'runtime.json')
|
|
667
|
+
def runtime_dir = File.join(@state_dir, 'runtime')
|
|
668
|
+
def build_dir = File.join(@state_dir, 'build')
|
|
669
|
+
def network_name = "mxrb-#{@project_id}"
|
|
670
|
+
def database_container = "mxrb-#{@project_id}-postgres"
|
|
671
|
+
def runtime_container = "mxrb-#{@project_id}-runtime"
|
|
672
|
+
def database_volume = "mxrb-#{@project_id}-data"
|
|
673
|
+
def ownership_label = "mxrb.project=#{@project_id}"
|
|
674
|
+
end
|
|
675
|
+
# rubocop:enable Metrics/ClassLength
|
|
676
|
+
end
|
|
677
|
+
end
|